Repository navigation
Conversation
|
@CHKM001 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! 🚀 |
|
This has gone conflicting — #250 landed a couple of hours ago and moved the ingest layer onto a single Worth knowing before you start, so the rebase is quick rather than a surprise: No rush and nothing wrong with the work — rebase on main and push, and I will pick it straight up. |
2f6f35d to
7665c04
Compare
|
DONE! |
7665c04 to
33ccd66
Compare
|
done |
33ccd66 to
03971be
Compare
tsc --noEmit clean vitest 91 files, 1052 passing (up from 86 / 962 on main) #251 read and scale token decimals from the chain #307 standardise empty-result vs not-found status codes #318 validate the live service against the published OpenAPI #324 content-derived strong ETag and honour conditional requests #326 cover vwap and the aggregator #388 persist permanent webhook delivery failures BREAKING, and deliberate: #307 changes routes that returned 404 for "no data" to 200 with an explicit reason. The rule is now 404 = the resource does not exist (pair not watched, key not found), 200 + empty = the query matched nothing. Our dropped-test guard surfaced it as - src/__tests__/basket.test.ts :: returns 404 when an asset has no price data which turned out to be renamed rather than deleted, to "returns 200 with error and reason when an asset has no price data". That is the guard working as intended: it made a behaviour change a decision instead of an accident. Any client branching on 404 for empty results needs updating; #427 (API versioning) is open and is where this belongs long-term. Three fixes applied on merge: - #388 and #318 each added a console.* call to non-test source, which the repo's own logging-hygiene test forbids. Both now use createLogger, which is what delivery.ts was already doing two lines above the offending call. - #326's test asserted CONFIDENCE_HIGH/MEDIUM_THRESHOLD_SECONDS, which do not exist. The constants are real and the values match, but the shipped names are HIGH_/MEDIUM_CONFIDENCE_MAX_AGE_SECONDS, from #545. Updated the test to the shipped names rather than renaming source to fit a stale test. - npx prisma generate, for #388's WebhookDeliveryFailure model. NOT merged: #384 (periodic WebSocket ping) does not compile -- it reads config.ws and never adds it to src/config.ts, and it drops a fanout test. #254 conflicts. Claude-Session: https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB
Miracle656
left a comment
There was a problem hiding this comment.
This needs a rebase, and that is the only thing wrong with it — so I want to be clear up front that I am not asking you to redesign anything.
The work itself is complete and wired
I checked the thing I check first on a feature like this, which is whether it is actually switched on. It is, everywhere it needs to be:
src/ingest/venues/sdex.ts recordVenueSuccess / recordVenueFailure / recordVenueReadSuccess
src/ingest/venues/amm.ts same
src/ingest/venues/soroswap.ts same
src/ingest/venues/aquarius.ts same
src/api/rest.ts:116 venueHeartbeats: getAllHeartbeats().filter(h => h.network === network)
src/api/graphql.ts:10 getStaleVenues
src/jobs/aggregateRefresh.ts:8 getStaleVenues
All four adapters report, /status exposes the heartbeats per network, and both the GraphQL layer and the aggregate refresh consult getStaleVenues() so a quiet venue is excluded from the price rather than silently contributing a stale number. That last part is what #219 is really about — "indistinguishable from a venue with no trades" is only fixed if something downstream acts on the distinction — and you did it. 495 lines of tests on a 226-line module is the right ratio for something that decides whether a venue counts toward a published price.
Why I am not hand-merging it
Nine of the twenty-one files conflict, and one of them is src/aggregator/vwap.ts. main has changed the shape of exactly the functions you extend, in 3d69182 and the feat/248-decimal-strings-in-api work behind it:
// your base
async function calculateVWAP(…, source?: 'SDEX' | 'AMM'): Promise<number>
// main now
async function calculateVWAP(…, source?: 'SDEX' | 'AMM'): Promise<{ num: number; raw: string }>and getAggregatedPrice's single return type has been split into AggregatedPriceNumeric and AggregatedPriceString, because the API now serves exact decimal strings alongside numbers — prices were being rounded through IEEE-754 doubles on the way out.
Your change threads an excludedSources: string[] = [] parameter through those same functions. Combining the two is not a merge, it is re-threading your parameter through signatures that no longer exist, in the code that computes published prices. I am not willing to do that by hand on your behalf: a mistake there is a silently wrong price, not a failing test, and you know what you intended far better than I can infer it from a conflict.
The same applies to src/api/schemas.ts and src/api/rest.ts, where /status has grown status, health, process, data and adminConfigured blocks since you branched (#273, #508), and your venueHeartbeats addition lands in the middle of that.
What a rebase needs to account for
calculateVWAPreturns{ num, raw }. YourexcludedSourcesfilter goes into the same SQL condition list, but every caller now destructures. Decide whether excluding a venue affects therawstring too (it should — same rows, same filter).getAggregatedPriceis split into numeric and string variants.excludedVenuesbelongs on both, or on neither./statushas new required fields.statusResponseSchemahasadditionalProperties: false, sovenueHeartbeatshas to be declared there (you do declare it — just re-apply against the current list).openapi.json,schema.graphqland the README tables are all CI-guarded generated artifacts now. After rebasing, runnpm run openapi:gen,npm run readme:gen, andnpx --yes openapi-typescript@7.13.0 openapi.yaml -o clients/typescript/src/schema.d.ts. Do not hand-editopenapi.json— the guard compares it byte-for-byte against the generator.- #471 landed a per-cycle ingest deadline.
ingestPairnow takes anAbortSignaland adapters check it at write-unit boundaries. A cycle abandoned on its deadline should probably not count as a venue failure — it is our own timeout, not the venue going quiet — sorecordVenueFailureon that path is worth a thought. console.*is rejected in non-test source bysrc/__tests__/logger.test.ts; usecreateLogger.
Ping me when it is rebased and I will review it properly — on the merits this is one of the better PRs open against the repo, and I would rather have it in than have it rot. If the rebase turns out to be painful, splitting the heartbeat module and its adapter wiring from the excludedSources plumbing into vwap.ts would give you two PRs that each land cleanly, and the first one has no conflicts at all.
2952d09 to
c327cdb
Compare
c327cdb to
033e591
Compare
|
Status update, and an apology for the moving target. When I reviewed this a couple of days ago I asked for a rebase because the branch conflicted heavily with It is not any more, and that is on the volume of work landing around it rather than on anything you did. Since this morning
Merging -- yours
WHERE ${conditions.join('\n AND ')}
-- main
WHERE pair_key = $1
AND network = $2
AND COALESCE(observed_at, timestamp) > NOW() - ($3 || ' minutes')::interval
${sourceFilter}Both are right; reconciling them means deciding how venue exclusion composes with the observation-time predicate inside the published-price query. I am not going to guess at that on your behalf — it is the query behind every price Lens serves, and you designed the exclusion mechanism. The work itself still looks good. I said so last time and nothing has changed: the heartbeat is recorded by all four adapters, A rebase now should be smaller than it sounds — most of the nine conflicts are generated artifacts ( That leaves Two things to watch for while you are in there, both of which bit other PRs today:
Ping me when it is up and I will prioritise it — this has been ready once already and I would rather it landed than kept losing races. |
Miracle656
left a comment
There was a problem hiding this comment.
Scanned clean. A per-venue heartbeat with stale-price exclusion is worth having — a venue that stops updating but keeps returning its last price is worse than one that errors, because the staleness is invisible to every consumer downstream. Excluding on a heartbeat rather than trusting the row is the right shape.
The branch is 79 commits behind main, and merging it as-is would revert shipped work.
Against current main the diff removes roughly 6,000 lines from src/ that are not yours — files that landed after you branched, which git removes because your branch predates them. The largest are src/reconciliation/dispersion.ts (312, #448), src/__tests__/venueReconciliation.test.ts (385) and src/__tests__/observationAndSourceTime.test.ts (335, #240). Among the rest is src/routes/facilitator.ts, whose x402 spend ceiling landed this morning — reverting that one would be a silent regression on a money path.
None of that is a judgement on your code. Please rebase on current main and re-request.
Two things that moved and will matter: every documented path in openapi.yaml now carries a /v1 prefix (routes are still registered bare; rewriteUrl resolves the segment), and npx tsc --noEmit plus npx vitest run both pass on main right now — so anything they report after your rebase is genuinely yours.
One thing to check while rebasing: main now distinguishes observed_at from the legacy timestamp column (#240), and they are deliberately different clocks. A staleness check has to be explicit about which one it means — observed_at is when we saw the price, which is almost certainly what a heartbeat wants. There is a guard test, src/__tests__/windowBoundaries.test.ts, that reads the operator out of the SQL a route sends; worth a look as a model for asserting this kind of thing.
Track last successful read, last successful write, and consecutive read failures per (venue, network). Prices whose venue has not written within its staleness bound are excluded from aggregation, and the exclusion is surfaced on /price (excludedVenues) and /status (venueHeartbeats) plus three Prometheus gauges on /metrics. Also apply the exclusion in the refresh worker and GraphQL getPrice so cached and precomputed aggregates cannot blend a dead venue's last price.
033e591 to
b5c33f2
Compare
PR #219 Per-venue heartbeat with stale-price exclusion
What this does
Adds a per-venue heartbeat keyed by
(venue, network)that tracks lastsuccessful read, last successful write, and consecutive read failures,
and excludes a venue from price aggregation once its last written price is older
than its staleness bound (default 5 min). The exclusion is surfaced, not silent.
The read/write split is what makes a dead adapter distinguishable from a
merely quiet one: a quiet venue keeps a recent
lastSuccesswith zerofailures while its
lastWriteages; a dead venue ages on both withconsecutiveFailuresclimbing. Staleness is measured on write age (the ageof the price), so a quiet venue's stale price is excluded either way.
Changes
src/venueHeartbeat.ts— in-process heartbeat registry.src/aggregator/vwap.ts) —calculateVWAP/getAggregatedPricetake an
excludedSourceslist and thread it through every query (source NOT IN (…)), including the AMM spot average, volume breakdown, source count andlast-trade age. Exclusion applies to the exact decimal-string (
raw) outputstoo, not just the numeric ones.
cached payload:
GET /price(src/api/rest.ts), the refresh worker(
src/jobs/aggregateRefresh.ts), and GraphQL (src/api/graphql.ts) — eachreturns
excludedVenues.SDEX,AMM,aquarius_amm,soroswap_amm):a successful read with no new trades records a read success (quiet, not dead);
a successful write records both; a read error records a failure.
fetchAquariusPoolsnow throws on a transport/non-2xx error and returns[]only for a genuinely empty result, so a dead API is no longer conflated with a
pair that has no pool. A cycle abandoned on its own deadline (feat(ingest): bound every ingester cycle with a deadline, abandoned-cycle metric, and overlap-proof abandonment (#351) #471) is not
recorded as a venue failure.
GET /status→venueHeartbeats(filtered to the requestednetwork);
GET /metrics→venue_last_success_timestamp,venue_last_write_timestamp,venue_consecutive_failures(labelsvenue,network; cardinality bounded by config).src/api/schemas.ts,openapi.yaml/openapi.json,schema.graphql,README.md,docs/ingest-metrics.md.Acceptance criteria
was excluded (live
/price, refresh worker, and GraphQL)./statusand/metricsboth expose it.CI notes
tests-removed-intentionallylabel. This PRremoves
aquariusIngester.test.ts :: "returns an empty array on a non-ok response"and replaces it with"throws on a non-ok response so the caller can record a read failure". The throw-vs-[]change is the core of the dead-vs-quiet distinction, so the old assertion can no longer be true — the removal is
deliberate.
A venue that goes quiet is indistinguishable from a venue with no trades #219: it defines
components/responses/BadRequestinopenapi.yaml(andregenerates
openapi.json+clients/typescript/src/schema.d.ts). That$refwas dangling on
main(from the/ratesendpoint in feat: ingest Blend lending pool state, rates, and derived utilisation #453) and brokeclient:genfor every open PR. Included only to get CI green — happy to splitit into its own commit/PR if you'd prefer it separate.
Testing
tsc --noEmit,client:gen+client:typecheckclean; committed client is insync (
git diff --exit-code -- clients/typescript).write, quiet-vs-dead staleness, per-network isolation), the
venue_last_write_timestampgauge,/statusexposure, stale-driven exclusionfrom
/price(assertingexcludedVenuesand theNOT INSQL), and therefresh worker dropping a stale venue from both the cached payload and the
precomputed buckets.
Closes #219