Repository navigation
feat(ingest): add historical backfill CLI and persistent gap detectio… - #385
Conversation
Divinity-Labs#241) - Add `IngestionGap` model to track gaps per network, venue/source, and pair with statuses 'open', 'filled', and 'permanent'. - Implement idempotent gap detection with cadence-based hole detection and automatic merging of overlapping/adjacent intervals. - Add backfill service and CLI script (`npm run backfill`) to refetch trade history via Horizon for SDEX and AMM, writing through the idempotent `upsertPricePoints` ingestion path. - Mark gaps for non-history snapshot sources (Soroswap, Aquarius) as 'permanent' without failing noisily. - Declare `incomplete: true` and overlapping `gaps` in time-window API responses (`/prices/history`, `/candles/*`, `/price/*/*history`, `/price/twap/*`, `/price/vwap/*`) when windows overlap open or permanent gaps. - Update OpenAPI specifications (`openapi.yaml`, `openapi.json`), route schemas, README documentation, and add test suite covering detection, backfill, and API responses.
|
@Noobb-byte Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
Resolve conflicts with main and fix CI: - Keep both sides of the import/script/schema conflicts (package.json, prisma/schema.prisma, rest.ts, candles.ts, price.ts) - Drop duplicate `network` declarations that broke tsc (TS2451) - Add prisma migration for the ingestion_gaps table now that main uses prisma migrate (keeps the drift check green) - Replace console.* in backfillService with the structured logger to satisfy the logging hygiene test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Not merging yet — and the reason is a bug in What happenedYour branch passed cleanly against It passes in isolation and fails in a full run, which is what made it worth chasing rather than dismissing as flake. The cause
const observedAt = p.observedAt ?? p.timestamp ?? new Date()
const timestamp = p.timestamp ?? observedAt
eventId: p.eventId || synthesizeEventId(network, { ...p, timestamp })A point with neither Your backfill is simply the first thing to re-ingest identical data deliberately, so it is the first thing to notice. Please do not try to work around it here — #620 is where it gets fixed, and a workaround in the backfill would hide a problem that affects ordinary ingestion too. Two things I did resolve, so you do not have to redo them
What I would likeRebase once #620 lands, and keep that idempotency test exactly as it is. It is the most valuable assertion in the PR: it states the contract in a sentence, it proved itself by catching a regression from a different PR entirely, and a weaker version — asserting only that the second run "succeeded" — would have passed while silently duplicating rows. One note for the rebase: run the full |
Resolve conflicts with main: - db.ts: take main's upsertPricePoints (observedAt/sourceTime and the Divinity-Labs#620 fix) and keep deriving the row id from the event identity - history/candles: keep main's cursor pagination alongside the gap declaration (gaps cover the full requested window) - openapi.yaml: restore incomplete/gaps on the candles response; regenerate openapi.json, README tables and the TypeScript client - package.json, schema.prisma, README: keep both sides - backfillService: follow toid's move to ../ingest/toid (Divinity-Labs#255) The idempotency test is unchanged. It still fails intermittently in a full run because SDEX trade points carry a wall-clock timestamp since Divinity-Labs#394, which is a bug in main rather than in this branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged into Why the block mattered. The backfill refetches points through One fix on merge, and it was your harness rather than your code. Your own idempotency test — "second run inserts 0 points and changes nothing" — was failing. The cause is in the const exists = storedPoints.some(
p => p.id === id && p.timestamp.getTime() === item.timestamp.getTime()
)That models the composite primary key @@unique([network, source, eventId], map: "price_point_identity")and The fake now checks both constraints, and all 13 backfill tests pass. The feature was idempotent the whole time; the test was measuring the wrong index. Worth taking from that: when a fake stands in for a database, the constraints it simulates are part of the contract under test. A fake that models the primary key but not the uniqueness rule your feature depends on will fail you in exactly the case the feature exists for. One thing I did not change, and filed instead: #625. The backfill modules read tsc clean, 1,791 tests pass. |
Summary
Implements historical backfill capability and persistent ingestion gap detection for Lens:
IngestionGapPrisma model to store ingestion holes per(network, source, pair)with statuses'open','filled', and'permanent'.detectGaps) and merging logic (recordGaps) that combines overlapping or adjacent intervals into single continuous spans without duplicate records. Handles pair inversions (e.g.XLM/USDCandUSDC/XLM) and splits partially filled gaps.src/backfill/backfillService.tsandscripts/backfill.ts(npm run backfill) to refetch trade history for history-capable sources (SDEX,AMM) via Horizon. All refetched data writes through the same idempotentupsertPricePointspath as live ingestion, preventing duplicate points on re-runs.soroswap_amm,aquarius_amm) automatically record or transition gaps to'permanent'and exit cleanly without failing noisily./prices/history,/candles/*,/price/*/*history,/price/twap/*,/price/vwap/*) declareincomplete: truealong with overlappinggaps: [...]whenever the requested window intersectsopenorpermanentgaps. Windows that are clean or contain onlyfilledgaps omit these fields.openapi.yamlandopenapi.json), REST response schemas, andREADME.mdwith gap cadences, CLI command usage, and incomplete response formats.Related issue
closes #241
Type of change
Checklist
npx tsc --noEmitpassesnpm run buildpasses