ADR-037: Canonical engine deprecation and removal¶
Status¶
Accepted — implemented for 0.7 (#370, roadmap decision D2). Removal itself lands in 0.8.
Context¶
The canonical (row-mutating) engine has existed since 0.1 as the original storage model. Since queue-storage became the default and supported substrate (ADR-019, 0.6.0), canonical's only remaining production role is as the source of the staged 0.5→0.6 transition: a place existing work drains from while queue-storage takes over.
Keeping it alive is not free. Every feature pays a dual-engine tax — implementation, tests,
and docs — and #360 documented how that tax
ships defects: the queue-storage cancel_by_unique_key branch referenced a column that does
not exist and passed CI because the harness only exercised canonical
(#359). The dual-engine test matrix added for
0.7 catches this class, but it doubles integration cost for as long as both engines are
claimable.
Decision¶
- 0.7 migrate gate.
awa migrate(and library callers ofawa_model::migrations::run) refuses to apply pending migrations unless the cluster's storage transition is finalized (state = 'active') or the install is fresh. "Fresh" mirrors exactly whatawa.storage_auto_finalize_if_freshaccepts at worker startup — an unprepared canonical state with no jobs and no runtimes seen in the last 90 seconds — so the migrate gate and the startup auto-finalize door admit the same clusters. The refusal applies migrations atomically-in-intent: nothing is applied on refusal, and the error names the exact finalize steps (prepare → enter-mixed-transition → finalize --wait) and the upgrade guides. - Formal deprecation in 0.7. Any runtime whose effective storage resolves to canonical logs a startup warning naming the transition steps and this ADR. Release notes and the 0.6 to 0.7 upgrade guide state the deprecation.
- Removal in 0.8. Canonical claim, execution, and trigger paths are deleted. Upgrades step 0.5 → 0.6 (finalize) → 0.7 → 0.8. A read-only drain/inspection check may remain so 0.8 can still refuse legibly rather than misbehave against a canonical remnant.
- Model coverage.
correctness/storage/AwaStorageTransition.tlagains aMigrate07action gated byMigrate07GateOpen, with the invariantMigrate07OnlyOnQuiescedCanonical: a 0.7 migration never lands while canonical work exists or a canonical-only runtime is live. A finalized cluster may retain an idlecanonical_drain_onlyruntime because supported writes already route exclusively to queue storage. TheAwaStorageTransitionMigrate07Ungated.cfgexpected-counterexample config witnesses why the gate is load-bearing.
Consequences¶
Positive
- 0.7 features (per-key control, dependencies, backpressure, tracing storage) need exactly one storage implementation, one test leg, one docs story.
- The #360 dual-engine matrix becomes a bounded compatibility suite: full matrix until 0.8 removes canonical, then it retires into the forward-compat matrix (#367).
- Operators get one unambiguous instruction at the boundary, and the refusal is a no-op — it never leaves a half-migrated cluster.
Negative
- Clusters that ignored the 0.6 transition cannot jump straight to 0.7; they must run a 0.6
binary long enough to finalize. This is deliberate (stepping-stone upgrades), but it is a
hard stop for anyone tracking
mainfrom a canonical cluster. - The "effectively fresh" arm shares auto-finalize's 90-second runtime-staleness window: in principle an old canonical worker could start immediately after the gate passes and write canonical work into a newer schema. The window is identical to the one 0.6 already accepts at worker startup, and the forward-compat matrix (#367) is the backstop.
Alternatives considered¶
- Gate only migrations numbered above the 0.6 ceiling (v40+). Would let a 0.7 binary bring a 0.5 schema up to v39 before refusing. Rejected: it splits the operator experience into "migrated some, then stopped", and the applied prefix duplicates what the supported 0.6 binary already does. Refusing up front keeps intent atomic.
- Auto-finalize during migrate. Rejected: finalize is an operator decision with
documented one-way-door semantics (#180);
awa migratesilently crossing it would violate that contract. The fresh-install arm is the only auto-promotion, and it already exists at worker startup. - Keep canonical indefinitely behind a feature flag. Rejected: the dual-engine tax is per-feature and permanent; #359 shows it ships bugs even with good intentions.
Relationship to other ADRs¶
Completes ADR-019's supersession of the pre-0.6 storage model; bounds the #360 dual-engine test matrix; interacts with the staged-transition decisions recorded on #180 (explicit finalize, no reverse migrator). The maintenance-only role (ADR-028) and callback ingress (ADR-027) deployment shapes assume queue-storage-only runtimes from 0.8.