Jun 09, 4-5 PM (16)
Jun 09, 5-6 PM (18)
Jun 09, 6-7 PM (18)
Jun 09, 7-8 PM (19)
Jun 09, 8-9 PM (16)
Jun 09, 9-10 PM (16)
Jun 09, 10-11 PM (28)
Jun 09, 11-12 AM (10)
Jun 10, 12-1 AM (11)
Jun 10, 1-2 AM (16)
Jun 10, 2-3 AM (11)
Jun 10, 3-4 AM (19)
Jun 10, 4-5 AM (5)
Jun 10, 5-6 AM (2)
Jun 10, 6-7 AM (46)
Jun 10, 7-8 AM (82)
Jun 10, 8-9 AM (18)
Jun 10, 9-10 AM (59)
Jun 10, 10-11 AM (46)
Jun 10, 11-12 PM (134)
Jun 10, 12-1 PM (49)
Jun 10, 1-2 PM (33)
Jun 10, 2-3 PM (32)
Jun 10, 3-4 PM (28)
Jun 10, 4-5 PM (36)
Jun 10, 5-6 PM (12)
Jun 10, 6-7 PM (12)
Jun 10, 7-8 PM (38)
Jun 10, 8-9 PM (11)
Jun 10, 9-10 PM (10)
Jun 10, 10-11 PM (20)
Jun 10, 11-12 AM (7)
Jun 11, 12-1 AM (10)
Jun 11, 1-2 AM (2)
Jun 11, 2-3 AM (0)
Jun 11, 3-4 AM (2)
Jun 11, 4-5 AM (8)
Jun 11, 5-6 AM (12)
Jun 11, 6-7 AM (34)
Jun 11, 7-8 AM (106)
Jun 11, 8-9 AM (37)
Jun 11, 9-10 AM (20)
Jun 11, 10-11 AM (105)
Jun 11, 11-12 PM (25)
Jun 11, 12-1 PM (38)
Jun 11, 1-2 PM (39)
Jun 11, 2-3 PM (15)
Jun 11, 3-4 PM (20)
Jun 11, 4-5 PM (5)
Jun 11, 5-6 PM (7)
Jun 11, 6-7 PM (26)
Jun 11, 7-8 PM (90)
Jun 11, 8-9 PM (11)
Jun 11, 9-10 PM (5)
Jun 11, 10-11 PM (25)
Jun 11, 11-12 AM (5)
Jun 12, 12-1 AM (8)
Jun 12, 1-2 AM (2)
Jun 12, 2-3 AM (2)
Jun 12, 3-4 AM (4)
Jun 12, 4-5 AM (7)
Jun 12, 5-6 AM (15)
Jun 12, 6-7 AM (46)
Jun 12, 7-8 AM (19)
Jun 12, 8-9 AM (28)
Jun 12, 9-10 AM (22)
Jun 12, 10-11 AM (29)
Jun 12, 11-12 PM (42)
Jun 12, 12-1 PM (24)
Jun 12, 1-2 PM (26)
Jun 12, 2-3 PM (22)
Jun 12, 3-4 PM (38)
Jun 12, 4-5 PM (23)
Jun 12, 5-6 PM (19)
Jun 12, 6-7 PM (26)
Jun 12, 7-8 PM (12)
Jun 12, 8-9 PM (17)
Jun 12, 9-10 PM (5)
Jun 12, 10-11 PM (30)
Jun 12, 11-12 AM (6)
Jun 13, 12-1 AM (6)
Jun 13, 1-2 AM (2)
Jun 13, 2-3 AM (0)
Jun 13, 3-4 AM (3)
Jun 13, 4-5 AM (0)
Jun 13, 5-6 AM (3)
Jun 13, 6-7 AM (7)
Jun 13, 7-8 AM (5)
Jun 13, 8-9 AM (6)
Jun 13, 9-10 AM (14)
Jun 13, 10-11 AM (12)
Jun 13, 11-12 PM (2)
Jun 13, 12-1 PM (23)
Jun 13, 1-2 PM (21)
Jun 13, 2-3 PM (8)
Jun 13, 3-4 PM (1)
Jun 13, 4-5 PM (4)
Jun 13, 5-6 PM (4)
Jun 13, 6-7 PM (3)
Jun 13, 7-8 PM (3)
Jun 13, 8-9 PM (7)
Jun 13, 9-10 PM (16)
Jun 13, 10-11 PM (19)
Jun 13, 11-12 AM (24)
Jun 14, 12-1 AM (18)
Jun 14, 1-2 AM (0)
Jun 14, 2-3 AM (0)
Jun 14, 3-4 AM (0)
Jun 14, 4-5 AM (2)
Jun 14, 5-6 AM (0)
Jun 14, 6-7 AM (2)
Jun 14, 7-8 AM (3)
Jun 14, 8-9 AM (0)
Jun 14, 9-10 AM (1)
Jun 14, 10-11 AM (2)
Jun 14, 11-12 PM (10)
Jun 14, 12-1 PM (8)
Jun 14, 1-2 PM (4)
Jun 14, 2-3 PM (8)
Jun 14, 3-4 PM (2)
Jun 14, 4-5 PM (1)
Jun 14, 5-6 PM (1)
Jun 14, 6-7 PM (0)
Jun 14, 7-8 PM (11)
Jun 14, 8-9 PM (1)
Jun 14, 9-10 PM (13)
Jun 14, 10-11 PM (29)
Jun 14, 11-12 AM (23)
Jun 15, 12-1 AM (8)
Jun 15, 1-2 AM (10)
Jun 15, 2-3 AM (4)
Jun 15, 3-4 AM (4)
Jun 15, 4-5 AM (1)
Jun 15, 5-6 AM (4)
Jun 15, 6-7 AM (6)
Jun 15, 7-8 AM (41)
Jun 15, 8-9 AM (26)
Jun 15, 9-10 AM (11)
Jun 15, 10-11 AM (34)
Jun 15, 11-12 PM (25)
Jun 15, 12-1 PM (40)
Jun 15, 1-2 PM (26)
Jun 15, 2-3 PM (21)
Jun 15, 3-4 PM (24)
Jun 15, 4-5 PM (21)
Jun 15, 5-6 PM (13)
Jun 15, 6-7 PM (13)
Jun 15, 7-8 PM (7)
Jun 15, 8-9 PM (26)
Jun 15, 9-10 PM (20)
Jun 15, 10-11 PM (22)
Jun 15, 11-12 AM (39)
Jun 16, 12-1 AM (10)
Jun 16, 1-2 AM (5)
Jun 16, 2-3 AM (1)
Jun 16, 3-4 AM (9)
Jun 16, 4-5 AM (6)
Jun 16, 5-6 AM (1)
Jun 16, 6-7 AM (11)
Jun 16, 7-8 AM (81)
Jun 16, 8-9 AM (18)
Jun 16, 9-10 AM (28)
Jun 16, 10-11 AM (10)
Jun 16, 11-12 PM (31)
Jun 16, 12-1 PM (36)
Jun 16, 1-2 PM (48)
Jun 16, 2-3 PM (33)
Jun 16, 3-4 PM (27)
Jun 16, 4-5 PM (11)
3,127 commits this week Jun 09, 2026 - Jun 16, 2026
feat: replace 'apply indexes' with a native solution (#746)
# feat: replace 'apply indexes' with a native solution

Closes #736.

Moves the required Rosetta index lifecycle (create, validate, track
progress)
from the external `index-applier` sidecar container into `yaci-indexer`
itself.
After blockchain sync reaches tip, the indexer automatically creates all
16
Rosetta-required indexes, gates readiness on completion, and exposes
per-index
lifecycle state via a new actuator endpoint.

---

## Architecture: Before vs After

```
BEFORE
──────────────────────────────────────────────────────────────
  ┌─────────────────┐     ┌──────────────────┐
  │  yaci-indexer   │     │  index-applier   │
  │                 │     │  (sidecar)       │
  │  sync chain     │     │                  │
  │  expose data    │     │  apply-indexes   │
  │                 │     │  .sh             │
  └─────────────────┘     └──────────────────┘
          │                        │
          │                        │  psql + shell loop
          └──────────┬─────────────┘
                     ▼
              PostgreSQL DB
              (16 indexes)

AFTER
──────────────────────────────────────────────────────────────
  ┌───────────────────────────────────────────┐
  │              yaci-indexer                 │
  │                                           │
  │  sync chain                               │
  │  expose data                              │
  │  PgIndexService                           │
  │    └─ creates indexes when synced         │
  │    └─ gates readiness probe               │
  │    └─ exposes /actuator/rosetta-indexes   │
  └───────────────────────────────────────────┘
                     │
                     │  JDBC (autoCommit=true)
                     ▼
              PostgreSQL DB
              (16 indexes)
```

---

## Lifecycle State Machine

```mermaid
stateDiagram-v2
    [*] --> PENDING : startup (init)

    PENDING --> PENDING : sync not yet at tip
    PENDING --> APPLYING : node reaches tip\n(compareAndSet guard)

    APPLYING --> READY : all 16 indexes created\nsuccessfully
    APPLYING --> FAILED : ≥1 index failed\n(remaining still attempted)

    READY --> [*]
    FAILED --> [*]

    note right of PENDING
        Re-queries pg_index on startup.
        Skips to READY if all indexes
        already exist and are valid.
    end note

    note right of APPLYING
        Single background thread.
        Continue-on-failure: all 16
        indexes are always attempted.
    end note

    note right of FAILED
        Per-index errorMessage available
        via /actuator/rosetta-indexes.
        Recovery requires pod restart.
    end note
```

---

## Index Creation Flow

```mermaid
flowchart TD
    A([triggerIndexing called]) --> B{state ==\nPENDING?}
    B -- No --> Z([return — no-op])
    B -- Yes --> C[CAS: PENDING → APPLYING]
    C --> D[submit to RosettaIndexBuilder thread]

    D --> E[for each of 16 indexes]

    E --> F[queryIndexState from pg_index\nschema-filtered]

    F --> G{state?}

    G -- READY --> N[skip — already done]
    G -- INVALID or BUILDING --> H[DROP INDEX IF EXISTS\nschema.indexname]
    G -- MISSING --> I[CREATE INDEX CONCURRENTLY\nautoCommit=true]
    H --> I

    I --> J{success?}
    J -- Yes --> K[mark READY\nupdate lastProgressAt]
    J -- No --> L[mark FAILED\nlog error\nfailedCount++]

    K --> M{more indexes?}
    L --> M

    N --> M
    M -- Yes --> E
    M -- No --> O{failedCount == 0?}

    O -- Yes --> P[state = READY]
    O -- No --> Q[state = FAILED\nlog warning]
```

---

## Health Probe Behaviour

```mermaid
flowchart LR
    subgraph Readiness [Readiness Probe — /actuator/health/readiness]
        R1{connection\nalive?} -- No --> RD1[DOWN\nConnection]
        R1 -- Yes --> R2{synced\nto tip?}
        R2 -- No --> RD2[DOWN\nSyncing]
        R2 -- Yes --> R3{index lifecycle\n== READY?}
        R3 -- No --> RD3[DOWN\nApplying Indexes]
        R3 -- Yes --> RU[UP]
    end

    subgraph Liveness [Liveness Probe — /actuator/health/liveness]
        L1{state ==\nAPPLYING?} -- No --> LU[UP]
        L1 -- Yes --> L2{lastProgressAt\n< 15 min ago?}
        L2 -- Yes --> LU
        L2 -- No --> LD[DOWN\nStalled]
    end
```

> **Note**: FAILED state → Readiness DOWN, Liveness **UP**. This is
intentional —
> a stalled build warrants a pod restart (liveness kill), but a
permanently failed
> index does not. Operators diagnose via `/actuator/rosetta-indexes`.

---

## Actuator Endpoint

`GET /actuator/rosetta-indexes`

```json
{
  "overallState": "APPLYING",
  "lastProgressAt": "2026-04-22T10:31:05Z",
  "totalRequired": 16,
  "totalReady": 9,
  "totalMissing": 6,
  "totalFailed": 1,
  "indexes": [
    { "name": "idx_address_utxo_amounts_gin", "state": "READY",   "errorMessage": null },
    { "name": "idx_tx_input_tx_hash",         "state": "BUILDING", "errorMessage": null },
    { "name": "idx_block_hash",               "state": "FAILED",   "errorMessage": "permission denied for table block" },
    { "name": "idx_stake_address_hash_raw",   "state": "MISSING",  "errorMessage": null }
  ]
}
```

---

## Key Implementation Details

### Why `autoCommit=true`?
`CREATE INDEX CONCURRENTLY` is rejected by PostgreSQL inside a
transaction block.
The service uses a raw JDBC connection with explicit `autoCommit=true`
to guarantee
no transaction wrapping.

### Why continue-on-failure?
The 16 indexes span independent tables. All indexes are always attempted
so a single
permission error on one table does not block the remaining 15. Per-index
failure
detail is available via `/actuator/rosetta-indexes`.

### Schema-qualified DROP
Both `pg_index` queries and `DROP INDEX` statements are scoped to
`current_schema()`
to avoid cross-schema matches in multi-schema PostgreSQL deployments.

---

## Infrastructure Changes

| Area | Before | After |
|------|--------|-------|
| Docker Compose | `index-applier` sidecar container | Removed —
`yaci-indexer` handles indexes natively |
| `apply-indexes.sh` | Shell script (psql + yq loop) | Deleted |
| Postgres Dockerfile | Bundled `jq`, `yq`, `apply-indexes.sh` | Removed
(no longer needed) |
| Helm chart | `index-applier` Job + ConfigMap templates | Removed —
`indexApplier` section dropped |
| Helm profile resources | `indexApplier` blocks in entry/mid/advanced |
Removed |
| Healthcheck retries | 360 × 10 s = 1 hour | 1080 × 10 s = **3 hours**
(mainnet GIN build time) |
| Documentation | References to `index-applier` throughout | Updated to
reflect native approach |

---

## Package & Class Naming (yaci-indexer)

Classes were renamed for clarity as part of this PR:

| Old | New |
|-----|-----|
| `indexmanagement` package | `indexes` package |
| `RosettaIndexLifecycleService` | `IndexService` |
| `PostgreSQLRosettaIndexLifecycleService` | `PgIndexService` |
| `NoOpRosettaIndexLifecycleService` | `NoOpIndexService` |
| `RosettaIndexConfig` | `IndexCatalog` |
| `RosettaIndexesEndpoint` | `IndexesEndpoint` |
| `RosettaIndexProgressSnapshot` | `IndexProgress` |
| `RosettaIndexStallIndicator` | `IndexStallIndicator` |

---

## API module — cleanup

Removed dead `getIndexCommands()` from `RosettaIndexConfig` in the api
module.
The api never creates indexes — that is `PgIndexService`'s
responsibility.
The method was unused and has been deleted.

---

## Test Coverage

39 unit tests across 5 classes:

| Class | Scenarios |
|-------|-----------|
| `PgIndexServiceTest` | Init states, sync trigger, INVALID drop,
BUILDING drop, continue-on-failure, failure handling, double-trigger
guard |
| `IndexesEndpointTest` | APPLYING snapshot, READY (all pre-exist)
snapshot |
| `IndexStallIndicatorTest` | Stall detection, terminal state
distinction |
| `YaciSyncHealthIndicatorTest` | Readiness states including
APPLYING_INDEXES |
| `IndexCatalogParityTest` | Both module YAML files are byte-identical |

> `CREATE INDEX CONCURRENTLY` with `autoCommit=true` must be verified
against a real
> PostgreSQL instance — unit mocks cannot catch transaction-block
rejections.
refactor(common): cleanup manual implementation of serde::Deserialize
Following adjustements from the auto-derived impl re-used from serde:
- leverage `SignedEntityTypeDiscriminantMessage` to avoid defining +
  implementing deserialize on a an enum which define only the variants
  names
- use imports instead of absolute path
- rename visitors based on their implented symbol for clarity
- remove most magic strings by using `stringify!`
- use `ok_or` instead of `match` for error handling when visiting tuple
  variants
fix(codegen): use canonical keys for expect decoders
Include the full type identity in generated expect-decoder cache keys instead
of using only the local type name plus flattened generic argument names.

This prevents decoder reuse between distinct types that share a local name
across modules, such as mod_a.ActionType and mod_b.ActionType, and also avoids
collisions between different generic shapes with the same flattened name.