Repository navigation
br-service-engine 0.3.2
0.3.2 - 2026-09-23
A patch: migrate upgrades a database that the 0.2.0 engine migrated, and every
failure the engine answers reaches the client with a code.
Adopter note: nothing to do. Bump the pin. A database migrated by engine
0.2.0 (svc-runners 0.1.0 on the dev stage, svc-accounts) now upgrades on the next
migrate; before this release migrate on 0.3.0 or 0.3.1 exited 1 there with
VersionMismatch(9113000023) (sqlx migrate info: installed (different checksum)).
Fixed
-
migrateadopts the checksum a released engine applied for a migration that
was later edited. v0.3.0 (e667eb9) removed the four-line header comment of
9113000023_mirror_stream_identity.sql, which v0.2.0 had applied; sqlx hashes
the whole file, so every database 0.2.0 migrated refused the 0.3.x engine set
(ignore_missingdoes not cover a changed checksum). Audit of every engine
migration acrossv0.1.0,v0.2.0,v0.3.0,v0.3.1andmain(and every
commit onmain):9113000023is the only file whose bytes changed —Migration v0.1.0 v0.2.0 v0.3.0 / v0.3.1 / main 9113000001–9113000022shipped same bytes same bytes 9113000023_mirror_stream_identity.sql— SHA-384 74808ac1…daae46fSHA-384 ea7d3984…d84720fd9113000024,9113000025— — added schema/released.rsholds a data table,EDITED_AFTER_RELEASE: &[(version, &[released SHA-384])], with the one entry9113000023 → [v0.2.0's checksum].schema::migratetakes the sqlx migrator's own advisory
lock on one connection, rewrites in one transaction every stored checksum that
the table registers for its version to the embedded file's checksum, logs each
atinfowith the version and the old and new checksums, then runs the
migrator on the same locked connection. An unknown checksum is never touched
and still fails asEngineError::Migrate(VersionMismatch(v)), exactly as
before. Only the versions in the table — all inside the engine's reserved band —
are read or written, so library and service rows on the shared
_sqlx_migrationsledger are never touched. A failed run closes its connection
instead of pooling one that still holds the session advisory lock. -
Every failure reaches the client with a code (constitution principle 22).
A mutation whose transaction did not begin or commit answered an uncoded
mutation failed: database; a query, a subscription attach or a page that
failed answered an uncoded message too. Now everything the client cannot fix —
a database or NATS fault, a begin or commit failure, an engine wiring fault, a
handler error whoseMutationFault::reason()isNone— isINTERNAL
(extensions.code), with the fixed messageinternal errorand no database
detail. The few failures a client can act on keep a specific code:NOT_FOUND
for a page on a session the caller does not hold or a window it never attached,
CONFLICTfor an attach under a session id a live session holds,
UNAUTHENTICATEDfor an attach whose principal no longer exists. A failure to
load the principal's facts is a500with the sameINTERNALbody, no longer a
401carrying the error text. Refusals that carry a reason are unchanged: same
code, samemutation refused: …message. -
The engine's logs keep the whole cause chain.
EngineError::Dbdisplayed
database,Migratemigrations,Serviceservice(andCronError::{Db, Job},RelayError::{Db, Relay}likewise), so every%errorlog and every
adopter that logged an engine error saw the label, not the cause. These pure
wrappers are now#[error(transparent)]: theirDisplayis the wrapped error's
and the chain does not repeat. Every engine log site that logged an error by
Displaynow logs its wholesource()chain, as do the conversions to text the
engine stores or forwards (dispatch errors and dead-letter reasons, object
storage and NATS details). Where the engine converts a failure for the client it
logs the chain aterrorfirst: an engine fault in the mutation pipeline with
its context, a handler fault without a reason with the mutation name (and the
fault's own chain when it returnsSome(self)from the new
MutationFault::as_error), a query, attach or page fault with its context.
Added
graphql::{INTERNAL_CODE, INTERNAL_MESSAGE, NOT_FOUND_CODE, CONFLICT_CODE, UNAUTHENTICATED_CODE},graphql::internal_error(context, &cause)(logs the
chain, answersINTERNAL) andgraphql::internal_fault(detail)for a fault
with no error value behind it.MutationFault::as_error, a provided method (defaultNone): a fault that is a
std::error::ErrorreturnsSome(self)so the engine logs its chain.error::describe(&dyn Error) -> String, thetop: cause: rootrendering the
engine logs, for a service that logs an engine error or keeps it as text.- CI guard
.github/scripts/check-released-migrations.sh, run in the
fmt-clippy-testjob (checkout nowfetch-depth: 0): it exports the engine
migrations of everyv*tag and fails when a released file is gone from HEAD or
differs from it without its released SHA-384 registered in
EDITED_AFTER_RELEASE. Every tag is checked, not only the last, so a registered
checksum cannot be dropped silently. s241(conformance, in-crate, real Postgres): a store migrated by the real
v0.2.0 engine files plus a library and a service set upgrades through the
migration chain, its engine ledger then equals a fresh store's and its library
and service rows are untouched, and a secondmigratechanges nothing; a forged
unknown checksum still fails withVersionMismatchand is left as it was; a
fresh store migrates as before. The first scenario fails with
Migrate(VersionMismatch(9113000023))on 0.3.1.
Changed
- The
DisplayofEngineError::{Db, Migrate, Service}is now the wrapped
error's message, database text included. A service that forwards an engine
error's text to its clients should answerINTERNALinstead; both platform
adopters already do (svc-runners renders its code, svc-accounts' reasonless
faults now reach the client asINTERNAL). example-service'sAppFaultis the reference shape:NotFoundcarries the
reasonNOT_FOUND(a refusal the client can act on, no longer an uncoded
failure),Storekeeps the engine error's whole chain as text (rendered with
error::describe, so the type keeps its auto traits) andas_errorreturns
Some(self); itsReactionFaultkeeps the chain the same way.- The engine now owns one lowercase-hex encoder (
hex::lower); the four copies in
blob signing, blob digests, seal hashes and NATS subject tokens use it, and the
unwrapin the subject-token copy is gone.