mirror of
https://github.com/gastownhall/beads.git
synced 2026-09-14 20:17:24 +08:00
4e7cc88180
* feat(journalops): the mutation journal becomes the third facade namespace The durable mutation journal's read side is now a role. `journalops` is a leaf package beside `issueops` and `memoryops` holding Row, Page, TruncatedError/TruncatedCode and `Journal`, whose doc comment is the specification: since-exclusive, seq-ascending, limit 0 uncapped with the wire's cap named as the handler's, a head that comes with its rows and never decreases, a caught-up checkpoint, and a typed truncation naming a resumable window. NOTHING CHANGES BEHAVIOR. The four names in `internal/storage` become ALIASES of the leaf — `type EventsJournalCursor = journalops.Journal` and its three siblings — so every implementation, every errors.As site and every caller compiles unchanged. The alias direction is what avoids the import cycle: the leaf imports context and fmt and cannot name internal/storage, so the canon goes down and the alias goes up. Outside storage.go and the conformance tier, the only non-test edits in this change are COMMENTS. The role brings the apparatus a role owes: - backend/conformance/journal_contract.go, six cases over a fixture carrying the role, the activation switch, a prune, and a mutation kit that is exhaustive against the engine's closed op vocabulary. - Wirings on all three legs (dolt, embeddeddolt, uow), so #5499's entrypoint-scoped lock passes with every leg running every case. - journalops added to the conformance census's facadePackages. That is the #5459 demand side, and the role that needed it: `Journal` is handed out by NO accessor — a front door type-asserts for it — so only a source parse can see it. issueops.Importer is the precedent. EventsJournalAccessor and EventsJournalConfigurer stay in internal/storage. Retention and activation are operator surface, and a server documented to publish the journal and never retain it must not hold a delete. Net deletion on the bespoke side: TestEventsJournalPageReportsTheHead is subsumed by the contract and is gone. TestEventsJournalRestartAndRetentionBoundary stays, because it observes ReadEventsInTx — the accessor's second read body, deliberately not on the role — and a mutation of one is invisible to the other. Mutation-measured, per case, on the embedded leg: an inclusive checkpoint, a head derived from the page, an untyped truncation, a head taken from MAX(seq) of surviving rows, and a dropped engine-only op each redden exactly the arm named for them, and a fixture whose activation hook is a no-op reddens all six. Reading the head BEFORE the rows leaves all six green — that promise is declared unpinnable in the contract header rather than faked. Refs ga-fvya1, ga-2yaqp.4. * docs(journalops): fix three comment-precision findings from review All comments, plus one test-only compile-time pin block. 1. journal_facade_test.go told a misassembled causal story: it used the CURSOR de-aliasing as its example and then attributed the breakage to errors.As. Those are two different failures. Measured, both: de-aliasing the interface builds the WHOLE TREE clean and breaks nothing behavioral — what it costs is the claim that one role has one name — while the error's dangerous shape is not the bare defined type (which loses Error() and fails to build) but the COPY-PASTE TWIN, which compiles in silence and stops crossing errors.As for everything holding the leaf's spelling. Rewritten to say that straight. 2. The seq commit-order unpinnable declaration cited "the dolt package's own journal tests" for the server-mode half. Those are single-threaded. The actual pin is internal/storage/domain/db/journal_seq_test.go — TestEventsJournal_CommitOrderedGaplessSeq drives two OVERLAPPING transactions on independent connections, and TestEventsJournal_ConcurrentWritersGaplessNoDup a racing batch. Cited correctly, and the single-threaded ones named as NOT the pin. 3. EveryMutationKindLandsARow's runs-last ordering is load-bearing but held by convention: a reorder compiles, passes, and silently gives up the only coverage that a journal keeps recording after a full prune. It was stated in the dispatch table and the three leg wirings; it is now stated at the case itself, where somebody editing a leg file is actually looking. And the compile-time half of the alias pin, which fires before any test runs: a pointer conversion per name. The pointer form is not decoration — for the three structs a plain assignment would do, but for the INTERFACE it would be blind, since interface-to-interface assignment goes by method set and a de-aliased cursor still accepts a journalops.Journal. Watched fail: the de-aliased interface, which nothing else in the tree catches. --------- Co-authored-by: Review Sim <ci@beads.test>
64 lines
3.1 KiB
Go
64 lines
3.1 KiB
Go
package journalops
|
|
|
|
import "fmt"
|
|
|
|
// TruncatedCode is the stable machine-readable code a consumer matches on when
|
|
// its checkpoint has fallen below the retained journal window.
|
|
//
|
|
// It is the wire spelling of *TruncatedError, for the front doors that cannot
|
|
// hand back a Go type: the HTTP problem document carries it, and a client that
|
|
// speaks JSON dispatches on this string exactly as an in-process caller
|
|
// dispatches on errors.As. Both name one condition, which is why the string
|
|
// lives beside the error rather than in a handler.
|
|
const TruncatedCode = "events_journal_truncated"
|
|
|
|
// TruncatedError reports that a sequential read cannot resume from the caller's
|
|
// checkpoint because the records it needs next were pruned.
|
|
//
|
|
// Without it, `WHERE seq > since` cannot distinguish "nothing new" from "your
|
|
// prefix is gone": a consumer resuming past a prune would either see an empty
|
|
// success and stall forever, or silently skip to the current floor and lose
|
|
// every record in between. Both are silent data loss, so the read fails loudly
|
|
// instead and hands back the window it can actually serve.
|
|
//
|
|
// Floor is the lowest seq still retained, or Head+1 when the journal holds no
|
|
// rows at all. Head is the highest seq the counter has ever assigned; it never
|
|
// decreases under a prune, so Floor > Head means "fully pruned, caught up to
|
|
// Head". A consumer that receives this must decide explicitly — resume from
|
|
// Floor-1 and accept the gap, or rebuild from scratch — and the engine does not
|
|
// decide for it.
|
|
//
|
|
// It is returned as a POINTER, which is what errors.As matches against, and
|
|
// implementations return it rather than wrapping it in a message of their own
|
|
// making. A caller reads the FIELDS; the string below is for humans.
|
|
type TruncatedError struct {
|
|
// Since is the checkpoint the reported window begins after, which is the
|
|
// caller's own checkpoint in every case except one.
|
|
//
|
|
// When the rows the read can serve start above the caller's checkpoint —
|
|
// the ordinary "your prefix was pruned" case — Since IS that checkpoint and
|
|
// Floor is the first row still retained.
|
|
//
|
|
// When the prefix is intact but the retained window has an interior hole
|
|
// (a restored or hand-edited table; bd's own prune cannot produce one),
|
|
// Since is instead the last seq the engine could serve contiguously from
|
|
// the caller's checkpoint, and Floor is where the next intact island
|
|
// starts. A batch with BOTH shapes reports the prefix one; the interior
|
|
// hole is reported on the next read, once the caller has resumed past the
|
|
// first. Every gap is surfaced, one resume at a time.
|
|
//
|
|
// Since therefore never reports a value BELOW what the caller presented, so
|
|
// echoing it back can never make a consumer re-read records it already has.
|
|
Since int64
|
|
// Floor is the lowest retained seq (Head+1 when nothing is retained).
|
|
Floor int64
|
|
// Head is the highest seq ever assigned.
|
|
Head int64
|
|
}
|
|
|
|
func (e *TruncatedError) Error() string {
|
|
return fmt.Sprintf(
|
|
"events journal truncated: checkpoint %d is below the retained window [%d..%d]; records %d..%d were pruned",
|
|
e.Since, e.Floor, e.Head, e.Since+1, e.Floor-1)
|
|
}
|