Skip to content

Dual-network correctness harness: prove two loops never cross-contaminate #172

Description

@Miracle656

Background

#159, #160 and #161 shipped the network column, per-network RPC clients and one indexer loop per network. Every acceptance criterion was met and 290 unit tests pass.

None of it has ever run against two networks at once. The unit tests assert that the right arguments are passed; nothing asserts that two live loops, writing to one database, keep their rows apart.

That gap matters more than usual here, because every failure mode in this area is silent. network carries DEFAULT 'testnet', so a write that forgets to set it compiles, typechecks, passes CI and inserts a row — just under the wrong chain. Three such writes were found by reading during #161 (upsertHostFnLogs, the INGEST_WORKERS > 1 parallel path, fetchNftMetadata's passphrase). Reading is not a repeatable control.

What to build

An integration harness that runs two indexer loops concurrently against one database with two independently controlled event sources, and asserts isolation.

Key files

  • tests/integration/ — existing suite, vitest.integration.config.ts
  • src/indexer.tsstartIndexer(network), startAllIndexers(), _resetIndexerLoops()
  • src/rpc.tsgetRpc(network), _resetRpcClients()
  • src/indexer/sources/index.tsSourceSwitcherConfig.network, the injection seam

Suggested execution

The loops are infinite, so drive pollOnce via a controllable source rather than startIndexer directly, or run startIndexer with a stub source and stop it between assertions.

  1. Stand up one Postgres with the Add a network column across all Prisma models #159 migration applied.
  2. Build two fake event sources returning disjoint, identifiable events — e.g. testnet emits contractId CTEST…, mainnet CMAIN…, at deliberately different ledger heights (mainnet far behind testnet, which is the real-world shape).
  3. Run both loops to completion over a fixed range.
  4. Assert isolation across every table that carries network.

Acceptance criteria

Each must fail if the corresponding scoping is removed — please verify that by actually reverting it, not by inspection.

  • Every row written by the testnet loop has network='testnet', and vice versa, across TokenTransfer, HostFnLog, NftTransfer, NftMetadata, AccountSummary
  • IndexerState holds one row per network with different lastIndexedLedger values; neither loop advances the other's cursor
  • The same eventId present on both networks produces two rows, not one (this is the collision @@unique([network, eventId]) exists to allow — assert it directly)
  • AccountSummary for one address active on both networks yields two rows with independent totals, and neither total includes the other chain's amounts
  • rollbackToLedger(n, 'testnet') deletes no mainnet rows, even when mainnet has rows above ledger n
  • pruneOldTransfers('testnet') deletes no mainnet rows
  • Killing one loop mid-run leaves the other's cursor and row counts untouched
  • /status reports both networks with the correct, differing lastIndexedLedger
  • The harness runs in CI

Notes

  • INGEST_WORKERS > 1 takes a different code path (pollParallel). Cover it — it is where one of the three untagged writes lived.
  • Do not assert only that queries succeed. A query missing its network filter still succeeds; it just returns both chains. Assert the row counts and the tags.
  • _resetIndexerLoops() and _resetRpcClients() exist for exactly this.

Drips Wave · Complexity: Advanced · 200 points


Required: Before submitting, join the contributor Telegram so your work can be tracked and counted toward the Stellar Wave: https://t.me/+fxHXq8f1SwlkZDBk

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions