Streaming daemon: Phase 2 — hot store, lifecycle + live ingestion (closes #816, #808) - #820
Conversation
3d12c9e to
84ff8c2
Compare
df8ec80 to
17b5c39
Compare
84ff8c2 to
419f7ec
Compare
17b5c39 to
145c1cc
Compare
04f9931 to
7f8e58f
Compare
e15575a to
bc56b0a
Compare
7f8e58f to
f3431cd
Compare
bc56b0a to
440443b
Compare
fa0083f to
cbc80ab
Compare
440443b to
c4944fb
Compare
…hanges Rebased onto the updated #820 and propagated #817's API changes into the Phase 2 live-ingestion/daemon layer: - window -> tx-hash index rename + key prefix index: -> txhash_index: (TxHashIndexCoverage.Index, Catalog.txhashIndex), Catalog.Get/Has -> get/has, config sections regrouped (cfg.Retention/Layout/Storage/Ingestion), pins via PinLayout. - daemon.go merge: kept #821's live-ingestion wiring (LifecycleConfig + Core) and deduped the HotProbe line (#821's Phase-2 wiring already set it, so #820's HotProbe fix is redundant here). - removed the #819 cold-only catch-up E2E (TestRunDaemon_CatchUpMaterializes...) + its someTxBackend/oneTxLCMBytes helpers: #821's daemon now requires Boundaries.Core and runs a continuous live loop, so a cold-only "catch up then return" test can't fit — and TestE2E_DaemonLifecycle covers it end to end. Mechanical propagation only; build/vet/test -short green (the heavy lifecycle E2E stays -short-gated).
cbc80ab to
ae91d20
Compare
c4944fb to
aeca6a0
Compare
Rebased the live-ingestion capstone onto the reorganized #820 and propagated: - qualify moved symbols (geometry./catalog.) in daemon.go, startup.go, e2e_test.go - window->tx-hash-index + RetentionGate->RetentionFloor renames; cat.layout->Layout(), cat.Has->public HotState shim, .IndexFilePath->.TxHashIndexFilePath - config regroup: cfg.Streaming.CaptiveCoreConfig -> cfg.Ingestion.CaptiveCoreConfig - restored #821's daemon_test.go (drops the cold-only catch-up test the full daemon supersedes; adds the supervise/backend-tip/boundaries tests) + the HotProbe/Core wiring - avoided the txhash_txhash_index find-replace corruption (was only in the dropped restack) build + vet + go test -short green EXCEPT the lifecycle E2E, whose generated TOML still uses the pre-regroup [streaming]/[backfill] schema (follow-up; per maintainer the stack will be re-rebased).
14aa4c8 to
aafbe0d
Compare
Relocate the one-write protocol ordering helper (mark -> create -> barrier -> flip) from backfill/process.go to catalog_protocol.go, where the protocol's states and mark/flip steps already live, and export it as catalog.OneWrite. processChunk and buildTxhashIndex now call it across the package boundary. It is a zero-dependency pure function and catalog never imports backfill, so there is no import cycle; #820's hot-tier openHotTierForChunk adopts it as the third caller by import alone, with no later relocation. Addresses the #818 review thread that asked to establish the shared helper here rather than deferring the move to #820.
| // sliding floor (the fixed earliest-ledger floor alone applies). | ||
| RetentionChunks uint32 | ||
|
|
||
| // OpRetryAttempts / OpRetryBackoff bound the per-op retry the discard/prune |
There was a problem hiding this comment.
OpRetryAttempts / OpRetryBackoff have no production wiring: run() builds lifecycle.Config from ExecConfig + RetentionChunks only, and no TOML field reaches these, so production unconditionally runs the WithLifecycleDefaults constants — only tests can set them. Either plumb a config knob (the design's config schema carried the retry attempts/backoff) or demote them to constants; as they stand they advertise a configurability that doesn't exist.
There was a problem hiding this comment.
Sweeping for more of this class turned up five siblings:
backfill.ExecConfig.RetryBackoff— same shape exactly:MaxRetriesis TOML-wired right next to it, but no production code setsRetryBackoff; onlyexecute_test.godoes. (Its default is also applied twice —WithDefaultsandretryBackOff()— same constant today, nothing linking them.)ExecConfig.Build.BuildOpts— nobody sets it, not even tests;cfg.BuildOpts...at txindex.go:101 always expands empty. Fully dead knob.ColdWriterOptions{}— both cold ingesters pass the zero value, whose own doc says batch workloads should set non-zero — and the batch freeze/backfill path is the only production consumer, so a full-history backfill always runs serial zstd with no background writeback. The inline "driver-level tuning is a follow-up via Config" note has no issue home — worth folding into fullhistory: halve cold per-ledger extraction (one ExtractLedgerEvents walk) + thin the ColdIngester seam #836 or filing.[backfill].workersdefaults to GOMAXPROCS independently in two packages (config.go:168, execute.go:54) with nothing linking them; the second site is unreachable today but diverges silently if either changes.waitForCoverage's documented zero-value fallback (backend.go:146-150) is unreachable — its only caller passes the same constants the callee falls back to.
Adjacent nit: logging.format accepts any string and silently means text unless it's exactly "json".
There was a problem hiding this comment.
All landed in 8217374, direction: demote rather than plumb — none of these have an operator asking for them yet, and TOML can be added when one does. (a) OpRetryAttempts/OpRetryBackoff → unexported opRetryAttempts/opRetryBackoff, documented as a test seam. (b) ExecConfig.RetryBackoff → unexported, and the double default is gone — retryBackOff() is now the single applier of defaultRetryBackoff (removed from WithDefaults). (c) BuildOpts deleted, field and expansion. (d) ColdWriterOptions{} tuning note now points at #836 explicitly (commented there too). (e) workers: both sites now call a shared backfill.DefaultWorkers() — one source, no silent divergence. (f) waitForCoverage's unreachable fallback deleted; params stay (tests pass explicit values). (g) logging.format now rejects unknown values at validation instead of silently meaning text.
| } | ||
|
|
||
| // Batch is durable — now and only now apply the events mirror/offsets update. | ||
| applyEvents() |
There was a problem hiding this comment.
applyEvents() runs outside all five phases, so the claim both doc sites make — "the phases sum to the per-ledger total" (the Phase doc and the hot_phase_duration_seconds help) — isn't quite true: the ledger's cost is extract + queues + commit + apply, and the apply share lands in no metric. It's usually small, but the mirror update clones each touched term's bitmap copy-on-write, and popular terms' bitmaps grow all chunk long, so the cost rises exactly where a stall would be hardest to diagnose — the phase histograms would all look fast while live cadence lags. The pre-unification LedgerPhases doc carried a "(minus the tiny post-commit mirror apply)" qualifier that the rework dropped while keeping the claim. Cheapest honest fix: a sixth PhaseApply stamped around this call — it only runs on success, so the emission contract stays clean. Restoring the qualifier in both doc sites is the fallback, at the cost of keeping the blind spot.
There was a problem hiding this comment.
Fixed in 8217374 with the sixth phase: PhaseApply stamped around the mirror apply, so the phases genuinely sum to the per-ledger total and a growing copy-on-write bitmap apply now shows up exactly where you'd look for a late-chunk stall. It's post-commit and success-only, so the failure-emission contract is untouched (Failed can never be PhaseApply). Enum, sink array, help text, and the emission tests all updated; TestPrometheusSink_Smoke and TestHotService_EmitsEveryPhaseOnSuccess now cover six phases.
|
|
||
| // NewRetentionFloor pins the floor for one (through, retentionChunks, earliest) | ||
| // snapshot. A shortened retentionChunks raises the floor at once. | ||
| func NewRetentionFloor(through, retentionChunks, earliest uint32) RetentionFloor { |
There was a problem hiding this comment.
NewRetentionFloor has zero production callers: the tick builds the gate via EffectiveRetentionFloor + RetentionFloorAt (compute once, share with both scans), and startup uses EffectiveRetentionFloor directly. Only tests use this constructor, and as a second construction path it can drift from the shape that superseded it. Tests can compose the two calls; suggest deleting it.
There was a problem hiding this comment.
Deleted in 8217374. Tests now compose RetentionFloorAt(EffectiveRetentionFloor(...)) at all call sites — the same shape the production tick builds, so there's no second construction path to drift.
| next := closed + 1 | ||
| // Handoff fence: close the write handle BEFORE the next chunk's key is | ||
| // created (that key is what makes THIS chunk complete to a tick, which may | ||
| // then freeze and discard its hot DB — no writer may hold it then). |
There was a problem hiding this comment.
rocksdb.Store.Close returns nil on every path — a flush failure is logged and swallowed by design (data stays durable in the WAL) — and hotchunk.DB.Close just forwards it. So this boundary close-error branch, and the deferred close's error propagation below, are unreachable. Related doc casualty: run()'s header says it "returns nil only on a clean shutdown", but run() never returns nil at all (ingestion's nil is converted to an error, every other path errors), and supervise classifies clean shutdown by ctx.Err(), so its err == nil arm is equally dead. Worth either making Close's contract honest or trimming the dead branches and fixing the two docs — as written, a reader budgets for error paths that cannot happen.
There was a problem hiding this comment.
Trimmed in 8217374 (took the trim-the-branches option — Close's log-and-swallow is deliberate, the WAL keeps the data durable, so the contract to make honest was the callers'). The boundary close-error branch and the deferred close's error propagation are gone (_ = hotDB.Close() with a one-line note); the loop dropped its named error return. run()'s header no longer claims a nil return — it states that a clean shutdown surfaces as a ctx-canceled error classified by supervise, whose dead err == nil arm is also gone. The load-bearing pair (the stream-ended-unexpectedly error and the nil-to-error guard) stays, now pinned by tests from the coverage thread.
|
|
||
| // CompleteThrough maps a signed chunk index to its "complete through" last ledger: | ||
| // c < 0 ⇒ PreGenesisLedger; c >= 0 ⇒ chunk.ID(c).LastLedger(). | ||
| func CompleteThrough(c int64) uint32 { |
There was a problem hiding this comment.
CompleteThrough is the design pseudocode's retired name — the doc rewrite split that concept into lastCompleteChunk (which LastCompleteChunkAt implements) plus a plain chunk→ledger conversion, chunkLastLedger. This function is the second half, so name it for what it does: ChunkLastLedger(c int64), the exact companion of ChunkFirstLedger below. "Complete through" describes one caller's reading of the result, not the conversion itself — and the call sites get clearer for it (lastCommitted != geometry.ChunkLastLedger(c)).
There was a problem hiding this comment.
Renamed in 8217374: geometry.ChunkLastLedger(c), doc rewritten as the plain chunk→ledger conversion companion to ChunkFirstLedger; all call sites updated (lastCommitted != geometry.ChunkLastLedger(c) reads as intended now).
| // run is the daemon's startup: backfill to the tip, then serve reads (injected). | ||
| // Returns nil only on clean shutdown; any other return is restartable | ||
| // (ErrFirstStartNoTip on a first start with no reachable backend). | ||
| // run is the daemon's startup, in two steps: (1) BACKFILL to the tip, then |
There was a problem hiding this comment.
A stale-comment sweep of the final diff — after this many rewrite rounds, these now contradict the code they sit on:
startup.go:21-25— therun()header still describes the pre-reorder choreography ("…the live ingestion loop (which opens the resume chunk's hot DB itself)"); the body now opens it beforeServeReadsand hands it in.ingest/metrics.go:202-204— the NOTE says "there is no full-history ingest daemon startup path yet"; this PR built it (buildSinksin daemon.go wiresNewPrometheusSinkinto both tiers).hotloop.go:203-204— "both surfaced as the cursor's error element": the cursor is deleted; it's the stream's error element.- "watermark" (renamed project-wide to last committed ledger) survives at
hotloop.go:35,catalog/catalog.go:153,lifecycle/progress.go:147,observability/observability.go:17, andpkg/stores/hotchunk/hotchunk.go:90-93(twice). backfill/backend.go:142promises "a fatal 'backend tip query' error … (a broken backend is not retried)" andbackfill/execute.go:236says "an unproducible chunk fatals" — nothing fatals: the task retries underwithRetries, then the plan cancels and supervise restarts.geometry/paths.go:117citesRunCold, which no longer exists (the cold entry point isWriteColdChunk);geometry/paths.go:14andbackfill/process.go:4citedesign-docs/full-history-streaming-workflow.md, which isn't in the tree.- "fold" at
daemon.go:65("fold+prune") andlifecycle/lifecycle.go:122("freeze + index fold") — the index path rebuilds from scratch (Rebuildmetric,buildThenSweep); fold is retired design language. e2e_test.go:330,464— "doorbell" is the deleted mechanism; it'sBoundarySignalnow.- Review-history narration that stops meaning anything once this merges:
ingest/driver.go:17-19("Close no longer emits…"),ingest/events.go:175("matching the old…"),config.go:27-28,catalog/catalog.go:185-186,geometry/txhash_index.go:17-19("It was once…") — state the current contract, drop the history.
There was a problem hiding this comment.
Full sweep landed in 8217374 — every listed site: run()'s header now describes the open-before-serve choreography; the metrics NOTE acknowledges buildSinks; hotloop's cursor prose is stream prose; all five "watermark" stragglers (plus the test identifiers flagged in the residue thread) now say last-committed; the false "fatal / not retried" claims at backend.go/execute.go now describe withRetries + pass-fail + supervised restart; paths.go cites WriteColdChunk and the citations to the nonexistent workflow doc are dropped (nothing under design-docs/ matches — didn't want to re-point at a guess); fold→rebuild at both sites; doorbell→BoundarySignal; and the review-history narration in driver.go/events.go/config.go/catalog.go/txhash_index.go states the current contract with the history dropped.
| } | ||
|
|
||
| func (s *testSink) HotIngest(dataType string, _ time.Duration, items int, err error) { | ||
| func (s *testSink) HotPhase(phase hotchunk.Phase, _ time.Duration, items int, err error) { |
There was a problem hiding this comment.
Six invariants from this review's fixes have no test pin — each of these regressions would pass CI today:
- Failed-phase narrowing: the only failure ever driven through
HotServiceis a closed DB, whereFailedkeeps itsPhaseCommitdefault — no test triggers an extract or queue failure and asserts the attribution, andFailed's zero value ISPhaseExtract, so a forgotten assignment on an extract path is structurally invisible. - Partial durations on failure: this
testSink.HotPhasediscards the duration argument and nothing anywhere readsrep.Phases[p].Dur, so a straight revert of the partial-duration stamping passes. - The last-committed gauge: no ingestion-loop test wires a metrics recorder (the per-ledger emission is unobserved), and no lifecycle test would notice the tick re-emitting a chunk-aligned value — the exact regression the gauge split fixed can return silently.
- The handoff fence:
recordingBoundaryrecords chunk ids only; nothing observes close-before-next-key or publish-after-open. A publisher fake that, insidePublish(closed), attemptshotchunk.OpenExisting(closed)(fails on the RocksDB LOCK if the writer still held it) and readsHotState(next)would pin both edges. - runOps ctx-abort mid-backoff: the cancel test cancels before the first op, so the
backoff.WithContextwiring is never exercised — dropping it would block shutdown for (attempts−1)×backoff per failing op and every test stays green. - The nil-return pair: no test covers a stream that ends cleanly (hotloop's "ingestion stream ended unexpectedly") or
run()'s nil-guard, so both could be deleted together and a graceful stream end would hangg.Waitwith nothing red.
There was a problem hiding this comment.
All six pinned in 8217374 (one is sharpened by the AddEntriesToBatch thread — txhash attribution is now unrepresentable by construction, so the reachable narrowing set is extract/events/commit): (1) TestHotService_ExtractFailureLandsOnExtractPhase + TestHotService_EventsQueueFailureLandsOnEventsPhase — the latter asserts a non-zero Failed, the discriminator you called out for the zero-value trap. (2) TestHotService_FailedPhaseCarriesPartialDuration — testSink now records the duration arg; failed and completed phases both assert Dur>0. (3) TestRunIngestionLoop_LastCommittedGaugeAdvancesPerLedger asserts the exact per-ledger sequence, and TestRunLifecycleTick_DoesNotReEmitLastCommitted pins the tick to RetentionFloor only. (4) TestRunIngestionLoop_HandoffFenceClosesBeforeNextKey — your suggested shape: the publisher fake re-opens the closed chunk read-write inside Publish (LOCK-fenced) and asserts HotState(next)==ready. (5) TestRunOps_CtxCancelDuringBackoffReturnsPromptly — 30s backoff, cancel mid-sleep, asserts prompt ctx return; a dropped backoff.WithContext hangs it visibly. (6) TestRunIngestionLoop_CleanStreamEndIsError + TestRun_IngestionCleanEndSurfacesErrorNotHang. One honest note on (6): the run()-level test pins the property (graceful end → error, no g.Wait hang) but can't reach the literal nil-guard line — it's unreachable through the real loop, which is exactly what (6a) enforces.
There was a problem hiding this comment.
applySharedTableOptions (rocksdb.go:674-690) installs one shared bloom filter across every CF's table options — but grocksdb's SetFilterPolicy is a documented move ("this op is move, fp is no longer usable"): it nils the filter's C pointer after installing it. So only the FIRST CF in the loop gets the filter; every later iteration passes NULL and silently installs no filter. The if s.filter != nil guard doesn't catch it because the Go wrapper stays non-nil after its C pointer is gone. With hotchunk's CF order, the 12-bits/key bloom lands only on ledgers — sequential 4-byte keys, where a bloom is useless — and never on txhash, the random-point-lookup CF that is the filter's entire justification (its doc: every false positive at no-compaction SST counts costs a disk seek). Silent and perf-only, so no test can catch it.
This also corrects #838's premise: the tuning isn't over-applied to all CFs — the bloom is mis-applied to exactly one, the wrong one. #838's per-CF options would fix this incidentally (each CF constructing its own filter), but the PR shouldn't ship a filterless txhash CF in the meantime: the one-line interim fix is constructing a fresh NewBloomFilter per CF in the loop.
There was a problem hiding this comment.
Fixed in 8217374 with your interim shape: a fresh NewBloomFilter per CF constructed inside the loop, so txhash actually gets the 12-bits/key filter. Verified the move in grocksdb v1.10.7 source (SetFilterPolicy does opts.cFp = fp.c; fp.c = nil) — which is also why the old s.filter != nil guard couldn't see it. The shared filter field is gone; ownership now rides with each CF's table options (see the teardown reply for who frees what). #838's per-CF options can subsume this cleanly.
There was a problem hiding this comment.
Two more instances of the same default-CF/API-semantics class as the flush finding:
logOpenState(rocksdb.go:696-699) readsrocksdb.cur-size-active-mem-table,num-files-at-level0, andtotal-sst-files-sizevia DB-levelGetProperty, which resolves against the default CF — always empty here. So the[ROCKSDB:OPEN]line's memtable/L0/SST figures are permanently ~zero and its stated diagnostic purpose (big WAL → replay, high L0 → pending compaction) can never fire; only the WAL figure is real. Fix:GetPropertyCFsummed overs.cfHandles.- Teardown: the per-CF
BlockBasedTableOptionscreated inapplySharedTableOptionsare never destroyed (grocksdb'sOptions.Destroydoesn't free them), ands.filter.Destroy()in Close is a no-op once the move has nil'd the pointer. A small C allocation leaks on every open — recurring, since the daemon opens a DB at every chunk boundary and every freeze.
There was a problem hiding this comment.
Both fixed in 8217374. logOpenState now sums memtable/L0/SST figures per-CF via GetPropertyCF (WAL stays DB-level), so the diagnostic line can actually fire. Teardown: verified in grocksdb source that SetBlockBasedTableFactory copies the BBTO rep (caller keeps ownership) and Options.Destroy never frees it — the Store now retains the per-CF BBTOs and destroys them in Close and on the open-failure path; the no-op filter.Destroy() is deleted (post-move it was rocksdb_filterpolicy_destroy(nil)). The moved-in filter policy is freed via its owning BBTO. One residual noted so nobody re-hunts it: grocksdb's BBTO keeps a cFp field it never frees — a few bytes per CF per open, unreachable through the wrapper's API.
There was a problem hiding this comment.
The applyEvents finding generalizes — holding every metric's help text to its literal claim against the actual timed window:
phase_duration_seconds{phase="freeze"}: the timer starts afterresolve()(execute.go:243 vs 247), but the claim is "plan-and-execute". Resolve is range-proportional catalog I/O — two metastore reads per chunk over the whole range plus per-window coverage scans — so the steady-state tick, whose plan is empty, scans[floor, lastChunk]every tick while Freeze observes ~0. The "plan" half is invisible.phase="backfill_pass": the pass's own definition includes sampling the network tip (startup.go:170-173), but the timer wraps onlyrunBackfill— on a flaky bulk backend the retried tip call (up to attempts×interval of sleep) escapes. One-line fix now; full-history: frontfill-only deployment cannot bootstrap (no tip source on first start) #833's tipSampler will restructure this path anyway.cold_chunk_duration_seconds: the type doc says "times from the first Ingest" but the timer starts at construction; the window includes drain's source-stream time (bulk download can dominate) which the help attributes to "cold ingesters' ingests plus their Finalizes"; and Close — part of the claimed lifetime — runs after the emit. The wide window looks intended (the bucket comment says so) — the doc text is what needs fixing.pruned_artifacts_total: the help's "(below the retention floor)" is false for three of the four counted categories (transient index keys from any window, in-retention.bindemotions, redundant txhash keys in finalized windows) — noting this amends the earlier rename we settled in thepruned_ops_totalthread; the name is right, the parenthetical isn't.phase="rebuild": help says "one index rebuild's wall-clock" but the window wrapswithRetries— up to MaxRetries+1 full attempts plus exponential sleeps in one sample.- Discard/Prune on failure: both emit only after
runOpsfully succeeds (lifecycle.go:142-145, 163-166), so a mid-sweep failure permanently loses counts for ops that already retired DBs or swept artifacts — they don't reappear in the next scan. The family's own convention is the opposite ("reported even on failure" at both freeze and rebuild).
There was a problem hiding this comment.
All six in 8217374. Freeze: timer moved before resolve(), so "plan-and-execute" is now literally what's timed and the steady-state tick's range-proportional scan shows up. backfill_pass: timer now starts before the tip sample, so retried tip calls are in the window. cold_chunk: kept the wide window (it's intended) and fixed the doc to say construction-to-emit including source-stream drain. pruned_artifacts_total: parenthetical replaced with the four real categories. rebuild: help now states the sample spans all retry attempts plus backoff sleeps. Discard/Prune on failure: real fix, not a doc fix — runOps returns the completed-op count and the prune scan returns per-op artifact weights, so a mid-sweep failure meters the ops that actually retired DBs/swept artifacts before the error surfaces, matching the family's reported-even-on-failure convention.
There was a problem hiding this comment.
AddEntriesToBatch is a loop of error-less Puts ending in return nil — it structurally cannot fail (CF errors are latched inside BatchWriter and surfaced by Store.Batch itself). That makes hotchunk's queue-tx-hashes failure branch dead and Failed == PhaseTxhash unrepresentable — unlike its two siblings, which have real error paths (zstd encode; the events facade's four returns). Either drop the error from this signature and the dead branch with it, or leave a one-word note that the error exists for signature symmetry. Related: this sharpens the test-gap thread's first item — one of the three narrowing branches isn't just unpinned, it can't fire.
There was a problem hiding this comment.
Dropped the error in 8217374 — signature is now func(...) with a doc stating it cannot fail (Put latches, Store.Batch surfaces), and hotchunk's dead queue-tx-hashes branch went with it, making Failed==PhaseTxhash unrepresentable by construction rather than merely unexercised. PhaseTxhash survives as a duration phase. The narrowing tests from the coverage thread pin the two attributions that remain reachable (extract, events).
| } | ||
|
|
||
| // ChunkID returns the chunk this DB is bound to. | ||
| func (d *DB) ChunkID() chunk.ID { return d.chunkID } |
There was a problem hiding this comment.
Sweep residue in two small classes:
- Test-only exports missing the seam doc their siblings carry:
DB.Ledgers()andDB.ChunkID()(hotchunk.go:127-130) have zero production callers —Txhash()/Events()carry the explicit "Wire the v2 stores into the API handlers and support both v1 and v2 #772 read seam" doc, these two don't; same foreventstore.HotStore.ChunkID(). To be clear about history: the earlier accessor thread deleted the ledger/txhash facade accessors and kept eventstore's chunkID field as load-bearing — these are the accessors that ruling didn't cover. Alsoeventstore.HotStore.All's doc claims "used by the freeze loop" — the freeze path this PR built never calls it (it's test-only now), andcatalog.AllArtifacts+NewArtifactSetare production-unreachable (the resolver builds sets per-kind; pre-existing, but worth a doc fix or a fullhistory/streaming: split the streaming package into purpose-named packages #824 line). - Retired vocabulary survives only in test identifiers:
mustDeriveWatermark/wmBeforeRestart(e2e_test.go),seedWatermark/TestRunIngestionLoop_RestartResumesFromWatermark(hotloop_test.go), and two...Folds...test names — production identifiers are clean apart from the already-flaggedCompleteThrough. Bycatch:rocksdb/encoding.go'sEncodeUint64/DecodeUint64are dead tree-wide (pre-PR file).
There was a problem hiding this comment.
Landed in 8217374. Seam docs: DB.Ledgers()/DB.ChunkID() and eventstore.HotStore.ChunkID() now carry the same #772 read-seam doc as their siblings; HotStore.All's doc no longer claims the freeze loop uses it (states it's the Reader full-scan, test-only until #772); catalog.AllArtifacts/NewArtifactSet are doc-marked as test-only seams. Retired vocabulary: mustDeriveWatermark→mustDeriveLastCommitted, wmBeforeRestart→lastCommittedBeforeRestart, seedWatermark→seedLastCommitted, TestRunIngestionLoop_RestartResumesFromWatermark→…FromLastCommitted, and both "Folds" test names renamed to rebuild/covers vocabulary. Bycatch: EncodeUint64/DecodeUint64 confirmed dead tree-wide and deleted.
There was a problem hiding this comment.
One residue the identifier renames didn't reach: ~28 "watermark" mentions survive in test comments and assertion strings (e2e_test, startup_test, hotloop_test, and five more files) — including the e2e narration of the resume semantics, which is exactly where retired vocabulary re-seeds itself. Mechanical find-replace to "last committed ledger" / lastCommitted.
There was a problem hiding this comment.
Done in fab9b92 — swept all residual watermark mentions from test comments and assertion strings, and renamed the deriveWatermark test helper (and its TestDeriveWatermark* cases) to deriveLastCommitted. Prose now reads "last committed ledger" / "last committed seq"; the "single watermark" phrasings became "committed frontier" (the term already used in progress.go) to avoid tautologies like "the last committed seq is the last committed seq". grep -ni watermark over the fullhistory tree is now 0.
- rocksdb: flush all CFs on close (FlushCFs), per-CF bloom filter (SetFilterPolicy is a move), per-CF property sums in the open log, destroy per-CF table options on close; drop dead EncodeUint64/DecodeUint64 - hot ingest: sixth PhaseApply phase around the mirror apply; AddEntriesToBatch cannot fail - drop its error and the dead txhash failure branch; remove the redundant closed-check in ledger iterate - knobs: unexport test-seam retry knobs (lifecycle op retry, backfill RetryBackoff), delete dead BuildOpts, share one DefaultWorkers source, drop waitForCoverage's unreachable fallback, validate logging.format, delete NewRetentionFloor - lifecycle: meter Discard/Prune counts even when a sweep fails mid-way - metrics: align each help text with the actual timed window (freeze plan+execute, backfill_pass incl. tip sample, cold chunk lifetime, pruned categories, rebuild retries) - rename geometry.CompleteThrough -> ChunkLastLedger; trim unreachable hot-DB close-error branches; stale-comment sweep (watermark -> last committed, fold -> rebuild, doorbell -> BoundarySignal, dead citations) - tests: pin failed-phase attribution, partial phase durations, last-committed gauge ownership, boundary handoff fence, runOps ctx-abort mid-backoff, clean-stream-end error
- geometry/paths.go: name the cold entry point (backfill.WriteColdChunk) in the TxHashRawRoot rationale - backfill/backend.go: replace the last 'fatal / don't retry' wording with the actual poll-abort -> pass failure -> supervised restart flow
|
Final round from the review — six small items worth landing in this PR. The first three are in pre-existing files, same rationale as the flush/bloom fixes: this PR's daemon is what makes them bite.
|
|
|
||
| // The one snapshot every stage shares. earliest and the retention gate are read | ||
| // and computed ONCE here (not re-derived per scan), then passed to both scans. | ||
| through := lastChunk.LastLedger() |
There was a problem hiding this comment.
through is retired design vocabulary that outlived its source — it was named from CompleteThrough(...), which this PR just renamed to ChunkLastLedger, leaving a bare preposition. Two fixes, neither a rename-in-place: in LastCommittedLedger, through is the value the function returns, so call it lastCommitted; in the tick, it exists only to compare chunk completeness in the ledger domain — c.LastLedger() <= through is c <= lastChunk (LastLedger is monotonic), so pass lastChunk down, compare in the chunk domain, and convert at the single site that needs a ledger (EffectiveRetentionFloor). The log field renames with it.
There was a problem hiding this comment.
Done in fab9b92. In LastCommittedLedger, through (the returned value) → lastCommitted. In the tick, through is gone entirely: eligibleDiscardOps now takes lastChunk and compares in the chunk domain (c <= lastChunk, via LastLedger monotonicity), and the only ledger-domain conversion is EffectiveRetentionFloor(lastChunk.LastLedger(), …). The debug log field renamed to last_chunk.
Cold txhash pipeline: - openDirect goes through os.OpenFile so .bin fds get O_CLOEXEC and a captive-core child spawned mid-merge can't pin unlinked files. - scanBinHeader and ReadColdBin share one overflow-safe header check (coldBinCount) that divides the trusted file size instead of multiplying the untrusted header count; drops the dead size<0 branch. - Delete the duplicated bin* constant/format-doc family; cold_index and cold_merge use cold_bin.go's ColdKeySize/coldBin* directly. Comments/vocab: - geometry/paths.go: backfill.WriteColdChunk -> ingest.WriteColdChunk. - hotloop.go: drop stale supervise speculation on the fall-through error. - lifecycle: rename the retired "through" vocabulary — lastCommitted in LastCommittedLedger, and the tick/discard scan compares in the chunk domain against lastChunk (converting to a ledger only at EffectiveRetentionFloor). - Sweep residual "watermark" mentions from test prose and the deriveWatermark test helper. Tests: - ledger hot_store post-close ErrStoreClosed iterate assertion already present (no change needed). - Add TestBuildColdIndex_HeaderOverflowRejected for the count overflow.
|
All six addressed in fab9b92:
|
PR #820 — review summaryPhase 2 of the streaming full-history daemon: live captive-core ingestion + the hot→cold freeze/discard/prune lifecycle (closes #816, #808). It went through ~5 rounds of review (primarily @tamirms). Concise recap of the back-and-forth and every change that landed. Design alignment (first pass — design comments #14–#39)
Round 3 — polish: dead code, metric-name fix, stale docs; added Round 4 — lifecycle tick cleanups + gauge correctness (don't regress last-committed from the chunk-aligned value); deleted dead seams; pinned missing tests; golangci fixes. Round 5 — unified hot metrics into one phase-keyed family; deleted Final round
Deferred (tracked, no behavior change): Status: vet + package tests + golangci-lint ( |
|
Two last things before this wraps:
|
- cold_bin.go: drop the now-unused //nolint:gosec on coldBinCount's size division (gosec doesn't flag it). - lifecycle_helpers_test.go: assert.NoError -> require.NoError in assertQuiescent (testifylint require-error). - cold_merge.go: gofumpt blank lines between the streamReader accessors that gofmt wrapped when the constant rename lengthened k0().
|
Both handled:
|
Closes #816, #808 — the complete Phase 2 (hot tier + lifecycle + live ingestion) in one PR.
Phase 2 introduces the hot tier + lifecycle machinery + live ingestion on top of Phase 1's source-blind cold pipeline:
Hot storage — the per-chunk
hotchunkDBtransient/readystate machine; a read-only view serves both the freeze source and the watermark refineringest.HotServicecommits each ledger as one atomic synced WriteBatch across all CFs (decision (a)) — onefsync, a single per-chunkMaxCommittedSeq, no per-store frontiers, no fan-outExtractLedgerEventswalk per ledger feeds both the tx-hash and events CFs (event-ID assignment order unchanged)Backfill integration — hot source by path (no probe seam)
backfillSource's hot branch opens the chunk's hot DB read-only straight from itsgeometry.Layoutpath (hotchunk.OpenReadOnly) and yields aledgerbackend.LedgerStreamreadychunk whose DB is missing/gutted fails the must-exist open — an ordinary restartable error, never auto-healed into a fresh empty DB (no watermark regression)Progress — derived watermark
LastCommittedLedger(cat, logger)maxes the cold term (highest fully-durable chunk) vs the highestreadyhot DB'sMaxCommittedSeq(one read-only open, which replays any synced WAL after an ungraceful crash) vs the earliest-pin floor — all in the signed domain, never stored; restart re-derives from durable stateLifecycle + live ingestion (the folded-in layer 2)
run()transitions from backfill to a serve+ingest steady state: start captive core (injectedCoreOpener) → serve reads (injected) → run the ingestion loop and the lifecycle loop as a joinederrgroup.WithContextpair (whichever returns first tears down the other; both joined beforerunreturns — the single-lifecycle-goroutine invariant across supervisor restarts)RawLedgersstream, commits each ledger, and at each chunk boundary closes the filled DB → opens the next → publishes the completed chunk (the handoff fence)lifecycle.Loop): freeze → index-aware discard → prune, driven by a latest-cellBoundarySignal— a slow lifecycle can never fall behind, since one tick over[floor, latest]subsumes every skipped boundarybackfill.RunBackfill— the same path catch-up usessuperviseis the single clean-vs-restart decision point (a canceled ctx is a clean shutdown; anything else is a warn + backoff restart). There is no fatal-and-exit class: genuine volume loss presents as a supervised crash-loop, upheld by the must-exist hot-DB open rather than by a hard exitDaemon wiring
captiveCoreOpenerbuilds a fresh captive-coreLedgerStreamper run (NewCaptiveCoreStream); captive-core config unification and the read-serving cutover are deferred to Wire the v2 stores into the API handlers and support both v1 and v2 #772 (injectCore/ServeReadsto run today)Observability
HotLedgerTotal(duration, err)per ledger + per-typeHotItemsvolume + per-phase timings (extract / ledgers / txhash / events / commit)ColdIngestis emitted only on a terminal step (a Finalize, or an Ingest error) — never fromClose— so a rolled-back or sibling-abandoned ingester leaves no phantom-success sampleFolded-in cleanup
RunHot/HotStoresstream-drain orchestration and theHotProbe/HotChunk/ErrHotVolumeLostprobe machinery (verified zero production callers)Verification:
go build+go vet+go test(incl. the full-lifecycle E2E: ingest → freeze → fold → discard → cold+hot lookup → restart → prune) green on./cmd/stellar-rpc/internal/fullhistory/...(cgo RocksDB toolchain).golangci-lintruns in CI.Follow-ups: #772 (captive-core config unification + read-serving cutover), #835 (
LastCommittedLedger(cat)signature cleanup), #836 (halve cold-path extraction / thin theColdIngesterseam).