Repository navigation
feat(ingest): bound every ingester cycle with a deadline, abandoned-cycle metric, and overlap-proof abandonment (#351) - #471
Open
felixumeh wants to merge 2 commits into
Conversation
`clearAllMocks` clears calls but leaves implementations in place, so the rejection stub from one case leaked into the next under shuffled test order and poisoned ingestPool's dispatch assertion. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
…nity-Labs#351) A single slow upstream could stretch an ingester cycle indefinitely: the loop never crashed, so nothing restarted it, and every health signal still reported the ingester healthy while it silently stopped producing. Each cycle now races a configurable per-ingester deadline. On expiry the cycle aborts, ingester_abandoned_cycles_total{venue,network} is incremented, and the next cycle starts only after the abandoned one has unwound, so two cycles against the same venue can never overlap. Abort checkpoints sit before a write unit and never inside one, so an abandoned cycle skips whole, replayable units (skipDuplicates makes the refetch idempotent) instead of committing a partial write. Closes Divinity-Labs#351 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
|
@felixumeh 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! 🚀 |
5 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #351
Summary of Changes
Bounds every ingester cycle with a deadline so one slow upstream can no longer stall an ingester while every health signal still reports the service as healthy. Issues below are fixed by this PR:
runCycleWithDeadline()in the newsrc/ingest/cycleDeadline.ts. A cycle that exceeds its deadline is aborted at its next unit checkpoint, counted in the newingester_abandoned_cycles_totalmetric labelledvenue+network, logged, and abandoned — the loop resumes after teardown of work already in flight instead of awaiting the abandoned cycle's remaining work.All five acceptance criteria from the issue:
AbortSignalfires and every checkpoint stops further pairs/pools from starting, so the abandoned cycle stops at its in-flight unit rather than grinding through the rest of the round.INGEST_CYCLE_DEADLINE_MSwith per-venue (INGEST_CYCLE_DEADLINE_MS_SOROSWAP) and per-venue-per-network (INGEST_CYCLE_DEADLINE_MS_SOROSWAP_MAINNET) overrides, precedence documented ingetCycleDeadlineMs()and.env.example; default 60s,0explicitly disables the deadline. Follows the paired-var convention used everywhere else insrc/config.ts.ingester_abandoned_cycles_total{venue,network}(prom-client Counter insrc/metrics.ts); cardinality isvenues x networks, bounded by the adapter registry, documented indocs/ingest-metrics.mdalongside the existing metrics, with apromqlquery example.upsertPricePointsis onecreateMany(skipDuplicates); the SDEX/AMM page upsert and its cursor advance are checked before the sequence as a whole, never between the two statements;snapshotPool, Soroswap's per-pool write and the snapshot appender are each either skipped whole or run whole. Skipping costs only a refetch from the same cursor, andskipDuplicatesmakes the replay idempotent.src/__tests__/ingestCycleDeadline.test.tsdrives the realstartVenueAdapterloop with a 250ms upstream under a 50ms deadline and asserts ≥3 cycles start,maxActive === 1(no two cycles ever overlap), and ≥2 abandonments land on the counter withvenue/networklabels.Guarantees in detail
runCycleWithDeadline()never resolves while the previous cycle's promise is unsettled — abandonment awaits unwind, not work. The unwind is prompt because the signal is passed tofetchWithTimeout({ signal })(the house pattern fromsrc/fetch.ts/src/alerts/delivery.ts) and checked at every unit boundary. If a single in-flight call ignores even its own timeout, the loop still refuses to start an overlapping cycle and escalates with a loud[cycle-deadline] … still has not unwounderror after a 10s grace (CYCLE_UNWIND_GRACE_MS), rather than silently double-running a venue.clearTimeout, sostartVenueAdapter's exponential backoff andrestartIngesterfault isolation behave exactly as before; only deadline expiry counts as an abandonment (metric, no error backoff), so a slow venue is not also punished with retry delay.ingestPair(pair, network, signal?)adds an optional third parameter; all four adapters, bothwriteVenuePricePointsand the snapshot loop thread it through; existing custom adapters and tests compile and run unmodified.startVenueAdapter(adapter, network, { stopSignal })lets a loop stop at a cycle boundary — used by the new test; unset in production where loops legitimately run until process exit.Files changed
src/ingest/cycleDeadline.tsrunCycleWithDeadline(),getCycleDeadlineMs()precedence chain,CycleDeadlineExceeded, grace-window loggingsrc/metrics.tsingester_abandoned_cycles_totalcounter (venue,network)src/ingest/venues/runner.tswriteVenuePricePointspre-write checkpoint;VenueAdapterLoopOptionssrc/ingest/venues/types.tsingestPairgains optionalsignalsrc/ingest/venues/{sdex,amm,soroswap,aquarius}.tsingestPair→ outbound fetches (fetchWithTimeout({ signal })) and checkpoints before each write unitsrc/ingesters/snapshot.tsINSERT … ON CONFLICT).env.exampleINGEST_CYCLE_DEADLINE_MS[_<NETWORK>]documented (drift guard)docs/ingest-metrics.md.changeset/ingest-cycle-deadline.mdsrc/__tests__/ingestCycleDeadline.test.tsThe branch also carries one pre-existing test-hygiene commit (
test: reset upsertPricePoints mock between soroswap ingester cases) —clearAllMocksretains implementations, so a rejection stub could leak into the next test under a shuffled run.Verification (all run locally, mirroring
.github/workflows/ci.yml)npm ci→ up to date, exit 0prisma generate→ exit 0npx tsc --noEmit→ exit 0, zero errorsnpm run build→ exit 0 (emitsdist/)npm test→ exit 0 — 67 files passed, 673 tests passed, 1 skipped (includes the 8 new Ingester loops have no upper bound on a single cycle #351 tests;.env.exampledrift guard and OpenAPI sync green)ingestCycleDeadline.test.ts→ 8/8 pass with live[cycle-deadline] … abandoned after exceeding its 50ms deadlinelogs proving the behavior end-to-endNotes for reviewers
main; happy to reconcile whichever direction you prefer — therunCycleWithDeadline()seam is small and would slot under whatever scheduling feat(ingest): add schema verification, cycle deadlines, lag telemetry, and /status health breakdown #463 lands.fetchWithTimeout-style per-call deadlines (fix: enforce deadline timeouts on all outbound fetch calls #379); the cycle deadline ensures we refuse to begin work for an abandoned cycle and to overlap cycles regardless.main(per the issue text) and is not part of PR CI; unaffected here.npm testincludes one intentionally skipped integration e2e test (tests/integration/e2e.test.ts), pre-existing and unrelated.