Releases: dangra/durable
Release list
v0.10.0
Modules
github.com/dangra/durable@v0.10.0github.com/dangra/durable/contrib/durableotel@v0.10.0
Migration from v0.9.0
durable owes no compatibility to earlier releases; this release breaks the generated API, the handler contract, the proto options, and the cancel model.
- Regenerate every pipeline proto (#99). The
durable.v1.stepanddurable.v1.pipelineoptions are now extensions 1374 and 1375, durable's registered numbers; a descriptor compiled against 51841/51842 is invisible to the generator and the runtime. - Steps nest in their pipeline message (#95). A step is a message declared directly inside the pipeline message, in topology order; the
stepslist option is gone. Generated step types arePipeline_Step. - One handler interface per pipeline (#93, #96).
NewXxx(h XxxHandlers)takes one implementation with a method per step,Unwind<Step>per unwinding step, andReduceOutput/ReduceFailurewhen the pipeline declares outputs. Per-step handler types andXxxFuncadapters are gone; handler methods receiveXxxInvocation, adurable.TypedInvocation[*XxxInput]. - Per-pipeline middleware (#97).
NewXxx(h, xxxpb.WithMiddleware(mw...))installs middleware on one pipeline, composed inside the engine chain once at Bind; hand-rolled definitions setpipelinedef.Config.Middleware. - Cancellation (#92, #98). A dormant or parked operation resolves as canceled at once with no handler run; a running attempt's context is canceled with a
*durable.PreemptedErrorcause and anything but success resolves it as canceled; reduction is uncancellable; a park cascades to its targets only withdurable.WithCancelCascade().CancelRequested,FailFastOnCancel, andYieldare removed. - Shutdown interrupts, it does not fail (#89). An attempt cut by
Stopthat returns an ordinary error records no last error and no retry backoff; observers seeAttemptInterrupted. - Run classes and unbounded workers (#87, #88).
run_classon a pipeline plusengine.WithRunClassbound started nonterminal Runs with an eligibility-ordered line; workers are unbounded unlessWithConcurrencyis set.
What's Changed
- Run classes: a run-scoped counting bound with an eligibility-ordered line by @dangra in #87
- Unbounded workers by default by @dangra in #88
- Run-class test: wait for every queued Run before reading Stats by @dangra in #91
- Shutdown interrupts an attempt; it does not fail it by @dangra in #89
- Cancellation: resolve the pending operation, cut the running one, cascade to children by @dangra in #92
- Generated pipelines: one handler interface, one typed invocation by @dangra in #93
- Steps nest in their pipeline message; the steps list is gone by @dangra in #95
- Generated handlers: Reduce is ReduceOutput by @dangra in #96
- A cancel request racing an attempt's registration still cuts it by @dangra in #98
- Per-pipeline middleware, composed once at Bind by @dangra in #97
- Extension numbers 1374 and 1375, durable's registered allocation by @dangra in #99
- Release v0.10.0 by @dangra in #100
Full Changelog: v0.9.0...v0.10.0
v0.9.0
v0.9.0: the terminal stage, blobs in their place, and reads shaped to what they answer
Twelve changes since v0.8.0, on one theme: what a run costs the store and the engine to keep, read, and finish. Four reshape contracts and set this release's migration section.
-
The terminal stage (#74, #76). A run's terminality commit replaces its whole nonterminal stage — input, operation history, cursor, failure, cancel — with one terminal record, in the same atomic step. What a finished run keeps is its identity, outcome, output, commit time, failure, cancel request, and failed unwinds; everything else is released at terminality, not at retention. bbolt's layout is rebuilt around it: one append-only
activebucket keyed by run and tag with operations in resolution order, the cursor on its own, aterminalrow per finished run, anexpiryindex so reap walks its victims and decodes nothing, and astagedbucket that frees released stages in batches.ListRunsleaves the store contract,InputByteson a terminal run returnsErrRunTerminal, and every reserved proto field is deleted outright. -
Blobs in their place, and cached (#78, #81). An input, state, or output over half a page lives in a nested bucket beside its row, so writing or deleting it touches its own pages; smaller ones stay inside their records. Fat-state disk bytes drop about 65%. The store keeps in-memory copies of the blobs of runs in flight and of the large outputs of terminal runs, two bounded LRUs (64 MiB and 16 MiB by default) set through
bbolt.Openoptions or the URI query. Input, State, and Output slices are declared immutable and shareable: stores retain and return shared memory, and bytes per op fall 65–75% in fat-input scenarios. -
Reads shaped to what they answer (#83, #85).
GetRunHeadreturns a run without its blobs or history, and it is whatStatus,Wait, the lookups, await bookkeeping, and recovery read — polling many idle runs no longer churns the blob cache, and cold-run polling is about six times faster. The reconcile loop reads the full record once per dispatch and carries it; a cancel goes through the engine, which marks the run dirty so the loop re-reads. A straight run costs three store reads instead of eleven, and allocations per op drop 25–39% across the population scenarios.Waitregisters its watcher before its single read. -
Cancel tightened (#85). A cancel request after the first is a true no-op. It used to re-preempt whatever attempt was running, including the retry the contract says continues cooperatively.
-
The perf suite as an instrument (#75, #77, #79, #80, #82). Recovery and SupersedeCycle sized past a single leaf per bucket, a shape matrix of none, slim, and fat for input, state, and output, output scenarios that read their output, a cold working-set scenario, and reported
polls/secandreads/run.
Breaking changes and migration
- bbolt on-disk layout changed with no migration: a database written by v0.8.0 or earlier is not readable by this driver. Discard it.
- Terminal runs release their input:
Run.InputBytesand the generated typedInput()returndurable.ErrRunTerminalonce the run is terminal. Read the input beforeWaitreturns, or read the output, which the reducer folded it into. - Immutable blobs: the slices
InputBytes,OutputBytes, and a store'sRunRecordreturn are shared with the store and must not be modified. The generated typed accessors still return caller-owned messages. - Store SPI (
store/driver):ListRunsis gone;GetRunHeadis new and required;ListNonterminalreturns heads;RunRecord.CompactTerminalis the shared terminal-stage rule;Cloneshares blob slices.store/memkeepsRuns()as a test enumeration. - bbolt driver:
Open(path, opts ...Option)withWithBlobCacheandWithOutputCache; URI query keysblob_cacheandoutput_cachewithK/M/G/KiB/MiB/GiBunits,0disabling; the database is opened withNoFreelistSync. - Storage proto:
RunMetadrops its input,Terminalis new,Cursor.awaiting_run_idand every reserved field are removed; buf'sRESERVED_MESSAGE_NO_DELETEandFIELD_NO_DELETE_UNLESS_NUMBER_RESERVEDare excepted for the internal schema.
contrib/durableotel v0.9.0 is released in lockstep.
Modules
github.com/dangra/durable@v0.9.0github.com/dangra/durable/contrib/durableotel@v0.9.0
What's Changed
- perf: size Recovery and SupersedeCycle past a single leaf per bucket by @dangra in #75
- Terminal stage: release input and step states when a run ends by @dangra in #74
- proto: drop every reserved and deprecated field by @dangra in #76
- perf: fat state and fat output scenarios by @dangra in #77
- perf: the shape matrix by @dangra in #79
- perf: drop the FatState and FatOutput composites by @dangra in #80
- bbolt: large inputs, states, and outputs in buckets of their own by @dangra in #78
- perf: the output shapes read their output after Wait by @dangra in #82
- bbolt: cache the blobs of the runs in flight; Input, State, and Output are immutable and shareable by @dangra in #81
- store: GetRunHead, the read for observation by @dangra in #83
- Drop the perf.test binary committed by mistake by @dangra in #84
- engine: the reconcile loop reads the full record once per dispatch by @dangra in #85
- Release v0.9.0 by @dangra in #86
Full Changelog: v0.8.0...v0.9.0
v0.8.0
v0.8.0: mutexes, operation records, one Failure, and the failure reducer
Eight changes since v0.7.0. Four reshape contracts and set this release's migration section; four are additive.
-
Mutexes replace exclusion groups (#67). A pipeline names any number of
mutexes; a run holds each on itsResourceIDfrom admission to terminality, and two pipelines exclude each other exactly when they share a name. Exclusion is pairwise, so a snapshot pipeline can exclude both a deploy and a backup while deploys and backups run together. Still an admission rule resolved atStart, never persisted, so changes converge on their own. -
Operation records (#70). A step's forward execution and its unwind are two rows, each written once when it resolves, carrying status, attempts, the committed state (forward only), the permanent failure that resolved it, and its resolution order. An unwind never rewrites the forward row's state, and nothing in the bbolt driver is read back to be rewritten.
Result.UnwindFailuresis gone; the per-step facts live in the store. -
One
Failuretype (#71).FailureRecord,RootFailure, andUnwindFailurecollapse intoFailure; the role is where the value sits. A run's failure isResult.Failure,Status.Failure(set from the moment the run starts unwinding), andInvocation.Failure(). The aggregate handler-side struct and the unwind-failure list are gone from the handler contract. -
Failure reducer (#72). A pipeline may declare
failure_output; aFailureReducerfolds the input, the committed states, the run'sFailure, and the per-stepUnwindFailure(step)into it when the unwind completes, and the typedResultexposes it asFailureOutput(). The snapshots example reports the object a jammed storage backend left behind. -
Additive: generated
NewXxxInvocation(core)andXxxReducer.Reduce(view)sodurabletest.NewInvocationdrives generated handlers and reducers directly (#64); durableotel unwind spans carrydurable.run_failure.step,.kind, and.reason(#65);WithTextLimitbounds every free text the engine records, default 4096 bytes (#68); option names follow what they configure (#66).
Breaking changes and migration
- Protos:
exclusion_group: "x"ismutexes: "x"(field 5 reserved). Hand-written configs:ExclusionGroup: "x"isMutexes: []string{"x"}. - Options:
engine.WithRetentionisWithRetentionPolicy;durableotel.WithBaggageisWithBaggagePropagation.WithObserverandWithScheduleAnnotatorare variadic; single-argument calls compile unchanged. - Failure types:
durable.RootFailure,UnwindFailure, andFailureRecordaredurable.Failure.res.RootFailureisres.Failure;inv.Failure().Root.Xisinv.Failure().X;inv.Failure().UnwindFailureshas no replacement on the invocation, andResult.UnwindFailuresis gone. Read per-step unwind failures in a failure reducer throughUnwindFailure(step). - Typed invocations gain
Failure()andLogger(); third-party fakes ofdurable.Invocationmust addFailure(), and fakes ofdurable.ReduceViewmust addFailure()andUnwindFailure(StepID). - Store SPI (
store/driver):OperationRecord,StepRecord{Forward, Unwind},OpWrite{StepID, Phase, Record},Transition.Ops;Transition.UnwindFailureandRunRecord.UnwindFailures(the field) are gone,RunRecord.RootFailureisFailure, andRunRecord.UnwindFailures()is a derived view. - bbolt on-disk layout changed with no migration: a database written by v0.7.0 or earlier is not readable by this driver. Discard it.
- Storage proto: the
StepRecordandFailuresmessages are replaced byOperationRecord; buf'sMESSAGE_NO_DELETEis excepted for the internal schema.
contrib/durableotel v0.8.0 is released in lockstep.
Modules
github.com/dangra/durable@v0.8.0github.com/dangra/durable/contrib/durableotel@v0.8.0
What's Changed
- codegen: typed invocation constructors and Reducer.Reduce for engine-free tests by @dangra in #64
- durableotel: unwind spans carry the failure being unwound by @dangra in #65
- Option names follow what they configure by @dangra in #66
- Exclusion groups become mutexes by @dangra in #67
- Bound the free text the engine records on a run by @dangra in #68
- Operation records: one row per step phase, carrying its own failure by @dangra in #70
- One Failure type; Status carries the Run's failure from the start of unwind by @dangra in #71
- Failure reducer: a typed account of a failed run by @dangra in #72
- Release v0.8.0 by @dangra in #73
Full Changelog: v0.7.0...v0.8.0
v0.7.0
v0.7.0: the lookup API, exclusion as an admission rule, and Invocation.Failure
Three contract changes and one new example, all shaped by what the engine, handler authors, and the flyd pilot need; dashboard-style listing APIs are gone.
-
Run lookup (#57, #58). A bound pipeline has three read methods:
Schedule,GetRun(id), andGetActiveRun(resource).Engine.GetRun(id)finds a run across pipelines for callers holding only a RunID, withStatusnaming the pipeline for the typed upgrade.GetActiveRunis one indexed store read. The listing methods (ListActiveRuns,GetRuns, and their engine-level counterparts) are removed. -
Exclusion groups are an admission rule (#59). A run's slot is always
(PipelineID, ResourceID); the store never persists a group. Membership is resolved from the bound definitions atEngine.Startand checked atScheduleagainst every member's slot in one transaction. Adding, removing, or renaming a group converges by itself: in-flight runs finish, new admissions follow the current definitions from the firstSchedule. This fixes a drift where a renamed group could admit two runs of one pipeline on one resource. -
Recovery walks the slot index (#60).
ListNonterminalin the bbolt driver costs the runs in flight rather than the retained history. -
Unwind handlers read the failure from the invocation (#61). The
durable.Failureparameter is gone;Invocation.Failure() *Failureis non-nil exactly inPhaseUnwind, so middleware sees it too. Typed invocations gainFailure()andLogger()(#62). -
examples/snapshots(#62) is a pipeline assembled entirely from the generatedhttp.HandlerFunc-style adapters: closures over a dependency struct, no handler types.
Breaking changes and migration
-
Unwind handlers: delete the third parameter and read
inv.Failure()where it was used.// before func (h *reserve) Unwind(ctx context.Context, inv pb.ReserveInvocation, f durable.Failure) error // after func (h *reserve) Unwind(ctx context.Context, inv pb.ReserveInvocation) error { root := inv.Failure().Root // never nil during unwind
pipelinedef.Step.UnwindFuncand the generatedXxxFuncs.UnwindFuncchange the same way.durabletest.InvocationConfiggains aFailurefield. -
Bound pipelines:
Run(id)isGetRun(id),ActiveRun(resource)isGetActiveRun(resource);Active(),Runs(resource), and the engine-level listings are removed. Keep a RunID and poll it withEngine.GetRun. -
Store SPI (
store/driver):CreateRun(ctx, rec, excluding []PipelineID)takes the exclusion set;GetActiveRunID(ctx, pipeline, resource)is new;RunRecord.Groupis gone. Third-party drivers must check every excluded pipeline's slot atomically with their own. -
bbolt databases: proto field 4 of the run meta (
slot_group) is reserved and ignored; a v0.6.0 database opens unchanged. A run created under a persisted group keeps running and is admitted against by the new rule.
contrib/durableotel v0.7.0 is released in lockstep.
Modules
github.com/dangra/durable@v0.7.0github.com/dangra/durable/contrib/durableotel@v0.7.0
What's Changed
- Bound pipeline lookups: GetRun, GetActiveRun, ListActiveRuns, GetRuns by @dangra in #57
- Drop the listing APIs; GetActiveRun becomes one indexed read by @dangra in #58
- Exclusion groups are an admission rule, never a persisted slot by @dangra in #59
- bbolt: ListNonterminal walks the slot index by @dangra in #60
- Unwind handlers read the failure through Invocation.Failure by @dangra in #61
- examples/snapshots: a pipeline assembled from handler func adapters by @dangra in #62
- Release v0.7.0 by @dangra in #63
Full Changelog: v0.6.0...v0.7.0
v0.6.0
v0.6.0: stores under store/, opened by URI
The storage side takes the database/sql shape (#55):
-
store.Open(uri)opens a store from a URI through drivers that register their scheme ininit.storeitself knows no driver: blank-import the ones you want and nothing else is linked into the binary.import ( "github.com/dangra/durable/store" _ "github.com/dangra/durable/store/bbolt" ) st, err := store.Open("bbolt:///var/lib/app/durable.db") eng := engine.New(st)
-
store/bboltis the persistent driver (bbolt:///abs/path,bbolt:/abs/path,bbolt:rel/path; no host, no options yet, a query string is an error). -
store/memis the in-memory driver (mem:), promoted from a test double to a real store for CLI tools and other programs whose runs are ephemeral. It remains the executable reference the bbolt driver is differentially fuzzed against. -
store/driveris the SPI backends implement.
Breaking changes and migration
github.com/dangra/durable/storedriverisgithub.com/dangra/durable/store/driver(packagedriver).github.com/dangra/durable/bboltstoreisgithub.com/dangra/durable/store/bbolt(packagebbolt; alias one side if you also importgo.etcd.io/bbolt).bbolt.Open(path)is unchanged, or usestore.Open("bbolt:...").durabletest.NewMemStore()ismem.New()ingithub.com/dangra/durable/store/mem, orstore.Open("mem:").durabletestkeeps the fake Clock and the fake Invocation.- No wire or behavior change: a v0.5.0 bbolt database opens unchanged.
contrib/durableotel v0.6.0 is released in lockstep.
Modules
github.com/dangra/durable@v0.6.0github.com/dangra/durable/contrib/durableotel@v0.6.0
What's Changed
- Stores under store/: driver SPI, bbolt and mem drivers, Open by URI by @dangra in #55
- Release v0.6.0 by @dangra in #56
Full Changelog: v0.5.0...v0.6.0
v0.5.0
v0.5.0: the package layout release
The module is now split by audience, and every import arrow points from the more specific package to the more general one (#47):
durableis the handler contract and nothing else:Invocation,Failure,Fail,AwaitRun/AwaitAll/AwaitAny,HandlerandMiddlewarewith their result classifiers, the step references, theScheduleoptions a step uses to fan out children, and aliases of the shared vocabulary. Handler files import only this package.engineis the wiring side:engine.New, theWith*options,Start/Stop/Stats,Bind,Pipeline,Run,Result,Status.pipelinedefis the type-erased definition generated code builds andEngine.Bindvalidates.kernelis the shared leaf (identities, phases, outcomes, parks, failure records);storedriverandobserveno longer depend on the root.durabletest.NewInvocationis a fakeInvocationfor unit-testing handlers without an engine or a store.
Breaking changes and migration
durable.NewEngineisengine.New;Engine, theWith*options,Pipeline,Run,Result,Status,RunState,Clock,RetryPolicy,RetentionPolicy,ScheduleAnnotatorare inengine.ErrEngineNotStarted/ErrEngineStartedareengine.ErrNotStarted/engine.ErrStarted.InvocationandReduceVieware interfaces. Handler and middleware signatures takedurable.Invocationby value, not*durable.Invocation.NewDefinition,DefinitionConfig,StepConfigarepipelinedef.New,pipelinedef.Config,pipelinedef.Step;def.Bind(engine)isengine.Bind(def), which now validates and returns an error instead of the constructor panicking.StepRefandStateStepRefhave exported fields (Step,New);NewStepRef/NewStateStepRefarepipelinedef.StepRef/pipelinedef.StateStepRef.- Generated code must be regenerated with this version's
protoc-gen-durable(buf generate). - Middleware classifiers gained
AwaitTimeout,FailureCause, andFailureReasonbesideAwaitRequestandFailureInfo.
contrib/durableotel v0.5.0 is released in lockstep. The specification is at Draft 1.3.
Modules
github.com/dangra/durable@v0.5.0github.com/dangra/durable/contrib/durableotel@v0.5.0
What's Changed
- Spec Draft 1.2: parks, cancellation, drain, observability, API surface by @dangra in #46
- Extract the shared vocabulary: durable/kernel by @dangra in #48
- Invocation and ReduceView become interfaces by @dangra in #49
- Move the engine to its own package: durable/engine by @dangra in #50
- Extract the codegen adapter surface: durable/pipelinedef by @dangra in #51
- Docs: describe the v0.5 package layout (spec Draft 1.3) by @dangra in #52
- Schedule options and child-run errors belong to the handler contract by @dangra in #53
- Release v0.5.0 by @dangra in #54
Full Changelog: v0.4.0...v0.5.0
v0.4.0
Modules
github.com/dangra/durable@v0.4.0github.com/dangra/durable/contrib/durableotel@v0.4.0
What's Changed
- durableotel: require durable v0.3.0 for release by @dangra in #35
- Run.Wait: fail fast with ErrRunInProgress inside a handler by @dangra in #36
- Multi-target parks and deadlines: AwaitAll, AwaitAny, WithAwaitTimeout by @dangra in #38
- Await gate: push completions to parkers; fan-in benchmark by @dangra in #39
- Tests: shared harness, engine_test.go split by concern by @dangra in #40
- Freeze the pipeline registry at Start with internal/frozen by @dangra in #41
- Lockstep releases: VERSION, scripts/release.sh, tag-release workflow by @dangra in #42
- Release v0.4.0-rc.1 by @dangra in #43
- release.sh: wait for in-flight check runs before tagging by @dangra in #44
- Release v0.4.0 by @dangra in #45
Full Changelog: v0.3.0...v0.4.0
v0.4.0-rc.1
Modules
github.com/dangra/durable@v0.4.0-rc.1github.com/dangra/durable/contrib/durableotel@v0.4.0-rc.1
What's Changed
- Add CI workflow and dependabot config by @dangra in #1
- Add performance regression suite gated in CI by @dangra in #2
- Add CoordinationMix perf scenarios; exact-deterministic write gates by @dangra in #3
- Logging story: engine lifecycle events and Invocation.Logger by @dangra in #4
- Metrics story: Observer lifecycle hooks and Engine.Stats by @dangra in #5
- Extract keyed wait primitives into internal/watchset by @dangra in #6
- Extract the concurrency-class gate into internal/tokenpool by @dangra in #7
- Extract the run dispatcher into internal/dispatcher by @dangra in #8
- Tidy-ups: bboltstore groupCommit helper, ULID generation to ids.go by @dangra in #9
- Read the frozen pipelines map without locking in processRun by @dangra in #10
- Fuzz the extracted primitives; hold them at 100% coverage in CI by @dangra in #11
- Differentially fuzz the stores; harden identifier and text contracts by @dangra in #12
- Model-stress crash-restart convergence by @dangra in #13
- Unit-test perfcompare: the gate that guards the gates by @dangra in #14
- Annotations: durable trace propagation and run metadata by @dangra in #15
- Add contrib/durableotel: OpenTelemetry integration as a contrib module by @dangra in #16
- durableotel: log correlation — stamp trace_id/span_id onto slog records by @dangra in #17
- Close observability gaps found against flyd's FSM library by @dangra in #18
- durableotel: opt-in W3C Baggage relay through the Run by @dangra in #19
- Declare propagation once: engine-wide ScheduleAnnotator by @dangra in #20
- Perf gate: wall-clock alarms require deterministic corroboration by @dangra in #21
- README: drop the layout table, add the observability story by @dangra in #22
- Godoc examples for the feature surface by @dangra in #23
- Add the guided tour: docs/tour.md by @dangra in #24
- Add the flagship example: examples/release-train by @dangra in #25
- CI: split the fuzz smoke into its own parallel job by @dangra in #26
- durableotel: require durable v0.2.0 for release by @dangra in #27
- Tour: ctx.Err() is an ordinary error by @dangra in #28
- Cancellation causes, verified yield attribution, and FailFastOnCancel by @dangra in #29
- Tour follow-up + FailFastExcept: per-step cooperative escape hatch by @dangra in #30
- Graceful drain: WithDrainTimeout on Engine.Stop by @dangra in #31
- Extract the store SPI: durable/storedriver by @dangra in #32
- Extract the lifecycle event surface: durable/observe by @dangra in #33
- doc.go: the API in five groups by @dangra in #34
- durableotel: require durable v0.3.0 for release by @dangra in #35
- Run.Wait: fail fast with ErrRunInProgress inside a handler by @dangra in #36
- Multi-target parks and deadlines: AwaitAll, AwaitAny, WithAwaitTimeout by @dangra in #38
- Await gate: push completions to parkers; fan-in benchmark by @dangra in #39
- Tests: shared harness, engine_test.go split by concern by @dangra in #40
- Freeze the pipeline registry at Start with internal/frozen by @dangra in #41
- Lockstep releases: VERSION, scripts/release.sh, tag-release workflow by @dangra in #42
- Release v0.4.0-rc.1 by @dangra in #43
New Contributors
Full Changelog: https://github.com/dangra/durable/commits/v0.4.0-rc.1