Repository navigation
br-service-engine 0.4.0
0.4.0 - 2026-09-24
The breaking release after 0.3.4. Paging, the last gesture bound to one pod,
becomes subscription arguments, so any replica serves any subscription.
PassportPrincipal::from_passport no longer sees the database, so a database
fault can never answer 401. The rough edges of the 0.3.1–0.3.4 rollouts are
fixed: a Recreate rollout that changes versions no longer restarts the new pod,
and an error chain renders each cause once. Good practice is built into the
primitives: a batched read is required, one aggregate, a parent's children and
hand SQL are read behind the view's gate or under RLS, a cohort window is read
through the store's cohort index and bounded, never loaded and filtered in
memory, a one-key view fetch reads its one row, a failure a service builds is
logged, and a mirror detects a change on any column. No engine service is in
production; every break has one migration line below. It ships with chart
br-engine-service 1.1.1 (next section); the compatibility entry for this
release is chart 1.1.1 ↔ engine 0.4.0.
Changed (breaking)
- Paging is subscription arguments. The window a session renders is what
the projector'spopulatereturns for the subscription's arguments (size, a
beforecursor, a filter), carried in the view's typedQuery. A window change
is a new subscription and a new session: itsResetat revision 1 carries the
whole new window, including a write committed during the switch. The engine
mints everySessionId, and nothing ties a window to a pod: no session
affinity, no cross-pod relay, any replica count. Thepagemutation, the
paging runtime and the client-chosen session are removed. window_capacitybounds a window at attach, never after. An attach whose
population holds more thanwindow_capacitykeys (default 10,000,
EngineConfig::with_window_capacity) is refused before the render with the new
AttachError::WindowTooLarge { projector, keys_read, capacity }, answered as
WINDOW_TOO_LARGE(WINDOW_TOO_LARGE_CODE); the message names the capacity
only.keys_readis the countpopulatereturned before the refusal:
window_capacity + 1when it binds its ceiling, never the collection's size.
An attach of several windows populates and admits them all before it renders
any. A
whole-collection view (type Query = ()) is refused once its collection
outgrows the capacity. Through 0.3.4 the capacity bounded the pages one session
held and evicted the oldest page.- A one-shot window fetch is bounded like an attach.
Query::fetch_window,
fetch_window_jsonandfetch_view_windowpopulate under the attach ceiling
(window_capacity + 1) and refuse a window abovewindow_capacitywith
WINDOW_TOO_LARGEbefore any row is loaded or rendered; through 0.3.4 they read
and rendered the whole window, however large. The read before the refusal is
bounded whenpopulatebinds its ceiling (cohort_window,full_eda::keys,
cx.limit_all(),cx.limit(&page),ceiling.limit()); apopulatethat
ignores it still reads its whole population before it is refused. - A raw one-key fetch is bounded like a window fetch.
Query::fetchand
fetch_jsondecide membership from the projector's default window populated
under the same ceiling, and refuse withWINDOW_TOO_LARGEwhen that window
holds more thanwindow_capacitykeys, since a truncated window cannot decide
membership. The client has no window argument to narrow, so the message says
that the service must read the key by its row, and the engine logs onewarn
per such refusal naming the projector, the keys read and the capacity (the fix
isfetch_view). Through 0.3.4 they read the whole default population
(KeyCeiling::NONE) on every one-key call. CohortIndex::keys_in_cohorts(conn, cohorts, ceiling: KeyCeiling)takes the
read bound and bindsceiling.limit()as itsLIMIT.view::cohort_window
passes the populate's ceiling, so a whole-collection cohort attach reads at most
window_capacity + 1keys before it is refused; in a debug build
cohort_windowpanics when a store returns more keys than the ceiling.full_eda::keys::<T, P>(cx: &Populate<'_, P>)takes the populate context
instead of a pool and reads at mostcx.limit_all()keys.PassportPrincipal::from_passport(passport) -> Result<Self, PrincipalRejected>
is pure and synchronous: noPgPool, noBoxFuture. The passport alone
decides the one401a principal can cause, with its exact message. Every
database fact goes through aregister_principal_factloader, run on every
POST /graphqland/graphql/wsupgrade and again at each principal refresh
(the refresh never calledfrom_passport, so a fact read there went stale on a
live session). A loader fault answers the coded500INTERNALwith no detail.
A loader has no rejecting outcome: a denial read from local data isFORBIDDEN
or an affordancereasonCode, never a401.max_body_bytesmoves toEngineConfig(with_max_body_bytes,
config::DEFAULT_MAX_BODY_BYTES, same 16 MiB default, still fed by no
environment variable). It bounds every authenticatedPOST /graphqlbody.
EngineConfig::validateis the one judge of the body bounds;
MultipartConfig::validateis removed.MultipartConfig::max_file_bytesis an
Option<u64>: unset, the part bound is 8 MiB ormax_body_byteswhen lower;
set abovemax_body_bytes, boot refuses it and names both settings.- Faults are errors.
MutationFaultandReactionErrorrequire
std::error::Error + Send + 'static, andMutationFault::as_erroris removed.
A mutation fault without a reason is logged aserror::describe(&fault); a
reaction fault's dispatch and dead-letter reason isdescribe(&fault), source
chain included.gate::Reasonis an error whoseDisplayis its code, so
type Error = Reasonstill works. MutationErroris a refusal or a failure, with no public field.
MutationError::refused(reason, detail)takes aReason;
MutationError::internal(detail)is the failure, and it logsdetailat
errorwhen it builds it, so a failure a service builds and forwards with?
keeps its cause in the log; the engine's own failures are logged once where
they occur.reason(),code()andrefusal_detail()answerSomefor a
refusal only. A failure writes only
mutation failedand keeps its internal text as itssource(), which no
accessor hands out: a resolver that forwards it with?answersINTERNAL
with no database or engine text, anddescribe(&error)renders the chain for a
log.?in a resolver converts only an error that says what the client gets.
The engine turns on async-graphql'scustom-error-conversion, and Cargo's
feature unification turns it on for every crate of the build, so the blanket
From<T: Display>intoasync_graphql::Erroris gone.?on agate::Reason
(whatGate::requirereturns) answers its code; on aMutationErrorits
refusal code withmutation refused: <detail>, orINTERNALfor a failure; on
anAttachErrorWINDOW_TOO_LARGE,UNAUTHENTICATEDorINTERNAL; on a
WindowSizeOutOfRangeWINDOW_SIZE_INVALID.?on any other error
(EngineError,sqlx,serde_json,std::io, …) no longer compiles: a fault
goes throughOrInternal::or_internal(context). A bareStringor
&'static strstill converts (async-graphql keeps those two conversions) and
reaches the client with no code: build a refusal withcoded_errorand a fault
withor_internal. Through 0.3.4 a bare?sent
a refusal without its code, and a fault'sDisplaytext (a database message
included) to the client uncoded. The engine does not compile without the
feature: its conversions conflict with the blanket one.- The schema-version claim is internal. The
schema_versionmodule is
crate-private (claim_schema_version,refresh_schema_version,
version_conflict_reasonleave the API); the claim and its handover run only
insideEngine::boot, the refresh only in the beat.EngineConfig::validate
refuses abeatthat is not belowschema_version_liveness, and
establish_transportvalidates its config before any database work. - One batched read per store, one way to read a row.
Persistence::read_manyis required and has no per-key default, so a store
without a batched read does not compile.loadleaves the trait: it is
PersistenceExt::load, implemented once for every store as the one-key
read_many, so the mutation path, render and the gated reads cannot read a row
two ways. - A mirror projection receives its whole batch:
Project::project(cx, keys: Vec<K>)gets every key its lead transaction
touches, one live change's keys or every key of a rescan (boot, periodic
reconcile, leader takeover), so a rescan of N keys costs the statements a
rescan of one costs. Projection::replace_rows(RowScope, rows)overKnownRowrows is the mirror
kit's one row write. It detects a change on any column: one multi-row upsert
per ~30,000 bound values writes a row only when a value column differs, one
NOT EXISTSdelete removes the scoped rows the call did not name, and it stages
exactly the rows inserted, changed or deleted. Rows compare by typed key
values.Projection::replace,upsert,retire,replace_one,remove, the
KnownandKnownScopetraits andWritten::Insertedare removed; hand SQL
goes throughProjection::connplusimpact_*. The change test is
IS DISTINCT FROMon every value column, so a value column needs an equality
operator: a document column isjsonb, neverjson(ajsoncolumn fails
every write, as the 0.3.4 upsert did).KnownRowdeclaresKEYandPRINCIPAL.KEYnames the key columns
key()returns, in order; a write whose key names other columns is refused, and
the impact key is the key columns joined by/(foreign_keyis no longer a
trait method).PRINCIPALis required:Some(PrincipalColumn { column, deps })
for a table a principal fact loader reads, and every row the kit stages then
also stagesPrincipalFactsChangedfor exactly that principal.- A resolver gets no raw pool.
GraphqlState::pg()andChunkReader::pool()
are crate-private: a resolver reads through a view,Query::load_visible,
Query::behind, orQuery::read_under_rls. Query::downloadgates on the row's visibility (load_visible: the
view'svisible, under RLS when the view declares it) instead of membership of
the view's default window, so a row on any page of a paged view downloads.Query::fetch_view::<View>(key)reads one row. It isload_visiblethen
the view'sproject, so it no longer runs the view'spopulatefor its default
window underKeyCeiling::NONEto decide membership: a one-key fetch costs one
row read on any collection size, and a visible row outside the default window
(on a later page, or outside a defaultQueryfilter) is answered.
View::Outno longer needsDeserializeOwnedfor it. The raw projector's
Query::fetch/fetch_jsonkeep the membership test, under the attach
ceiling (above).Visibilityhas no in-memory window builder.Visibility::windowand
Visibility::visible_keysare removed: a cohort view populates through
view::cohort_windowover its store'sCohortIndex, which reads only the
caller's keys with the ceiling as itsLIMIT, so no engine primitive loads a
table to filter it in memory. The window is live by default
(Visibility::LIVE), and a live window repopulates on a principal-fact change
only whenVisibility::DEPSnames the fact's bit; theKeyswindow
Visibility::windowreturned repopulated on every principal change.
check_window_matches_visibilitycompares a window with the declaration's
visible, so it follows an overriddenvisible(Unrestrictedincluded).Projector::populate/ErasedProjector::populatetake aKeyCeiling
after the window arguments (the attach read bound, below).EngineErroris split by capability. The chunk and seal variants move to
AccumulatorError(carried asEngineError::Accumulator), the GraphQL schema
composition and root-prefix variants toCompositionError
(EngineError::Composition); both convert withFrom.Config,Db,
Service,Encode,Decode,Bloband the other variants stay flat.
Added
Query::load_visible::<View>(key): one aggregate, loaded under RLS when the
view declaresRLS, kept only if the view'svisiblegate (itsVisibilityby
default) admits it. A hidden row and an absent key both answerNone, so a
caller cannot probe which keys exist.Query::read_under_rls(|conn| …): hand SQL in aREPEATABLE READ READ ONLYtransaction with the principal's RLS context applied through the
registeredRlsApplier, rolled back at the end; every statement reads one
snapshot. A write fails; a fault answersINTERNALwith the cause in the
log; with noRlsApplierthe read is refused, never run without the context.Query::behind::<View>(&key).read(|parent, conn| …)(Behind): one
REPEATABLE READ READ ONLYtransaction (under RLS when the view declares it)
loads the parent and runs the closure only when the view'svisibleadmits it;
the gate and the closure read one snapshot. A hidden parent and an absent key
both answerNoneand the closure never runs. The view is the one type
argument; the closure's types are inferred.RowScope:RowScope::any_of(column, keys)for a batch (a typed
= ANY($1); an empty list matches no row and costs no statement),
RowScope::by(col(..))with.and(col(..)), and the written-out
RowScope::whole_table(). No scope widens to the whole table by mistake.WindowSize,Page<K>,KeyCeiling,Populate::limit(&page)and
Populate::limit_all(). A paged
view takessize: WindowSize(a GraphQLInt; below 1 it is refused with
WINDOW_SIZE_INVALID/WINDOW_SIZE_INVALID_CODEbefore the resolver runs) and
carries aPage<K>(abeforecursor and a size) in itsQuery;
page.population(keys)gives an open head without a cursor and a fixed ordered
page behind one.populatebindscx.limit(&page): at attach the page size
clamped towindow_capacity + 1, so an over-capacity attach reads at most one
key past the capacity; a one-shot window fetch reads under the same ceiling,
and a repopulation and a repair run underKeyCeiling::NONE. A populate with
no page (a whole-collection read) bindsPopulate::limit_all(), the ceiling
itself, so its refused attach is bounded the same way.WindowSize::new(u32) -> Result<WindowSize, WindowSizeOutOfRange>(WindowSizeOutOfRangeis
re-exported at the crate root) andWindowSize::MAX: u32, equal to
i32::MAX, the largest GraphQLInt. This shape is the contract: the size is a
plain GraphQLInt, the window arguments travel in the view's typedQueryas
aPage<K>, and a size below 1 isWINDOW_SIZE_INVALID.- Windows over capacity are counted:
service_engine_windows_over_capacity_total{projector, outcome}
(metrics::WINDOWS_OVER_CAPACITY_TOTAL, listed inmetrics::ALL; the new
labelmetrics::LABEL_PROJECTORbesideLABEL_OUTCOME) counts a refused
attach, one-shot window fetch or raw one-key fetch (refused; an attach or a
window fetch is not logged, since its client can fix it, and a raw one-key
fetch logs itswarn, since only the service can) and a live window that grew
past the capacity (kept, onewarnnaming the session, the projector, the
size and the capacity). No shipped alert watches it: a client that asks for a
sizeabove the capacity countsrefusedwhile the service is sound. OrInternal(graphql::OrInternal, re-exported at the crate root), until
now crate-private:result.or_internal(context)logs the context and the whole
cause chain and answersINTERNAL.impl From<_> for async_graphql::Error
forReason,MutationError,AttachErrorandWindowSizeOutOfRange(above).PersistenceExt,AccumulatorError,CompositionErrorandBehind
re-exported at the crate root;gate::ReasonimplementsDisplay(its code)
andstd::error::Error;SessionRuntime::pending_sessions()(test-support).- README section Adopter SQL and reads: bound parameters only; one primitive
per engine requirement; one mirror per table set with a sum-type key; per-row
keys written as one batch; no clock reads in a projection.
Changed
- Schema-version handover on a
Recreaterollout. A pod that finds another
(engine, service) version with a fresh heartbeat no longer exits at once. It
re-checks once perbeatand claims as soon as that heartbeat is older than
schema_version_liveness; it refuses withEngineError::SchemaVersionConflict
(loud, both versions named) only if the heartbeat is still fresh at first
conflict +schema_version_liveness+ onebeat(31 s by default, inside the
chart's 150 s startup budget). Each re-check logs awarnwith both versions
and the time left and sets that reason on readiness. Ages use the database
now(), and booting pods still claim one at a time under the advisory lock. A
stopping pod does not expire its own heartbeat. error::describewrites a repeated cause once and is the engine's only
renderer. Asource()segment is skipped when its parent's text equals it or
ends with:followed by it, so sqlx'serror returned from database: <text>
and an async-natskind: sourceerror render their cause once; a mid-phrase
match (cannot open configoverconfig) keeps both. Every engine log site and
every failure text the engine keeps goes through it.EngineError::Httpno
longer repeats its source inDisplay.- A live window is never ended for its size; crossing
window_capacity
after attach is logged and counted (above). Ending it would make a busy view
flap: end, re-subscribe, a largeReset, grow, end again. - An attach's stream exists from the start of the attach, so an attach the
client abandons mid-snapshot (a burst of window changes) is dropped at the next
render pass orgc;session_ttlnow bounds only an attach that is still not
live. FullEda<T>::read_manyreads every requested snapshot and every later event
in two statements and replays each key; a read of keys with no snapshot stops
before the event log, so the key-reuse check before a full-EDA create costs one
statement.
Fixed
- Failure texts keep their cause: the dead-letter ops-view staging text keeps
the sqlx cause of a staging failure, and a published-language relay failure
keeps its NATS error's source. - The full-EDA row reads (
read_many,keys,erase) return a decode fault as
an error instead of panicking. - An attach whose principal refresh faults while it connects (the
PrincipalResolveror a fact loader fails) is refused with the new
AttachError::PrincipalRefreshFailed, answeredINTERNAL; through 0.3.4 it was
PrincipalRevoked, answeredUNAUTHENTICATEDas if the principal no longer
existed. Only a resolver answeringNonestill revokes the attach.
Removed
- The 0.3.4 known limitation (one replica per paging service): paging no longer
needs the session's pod. - The
pagefunction (service_engine::pagenow names the module of the window
arguments),attach_with_session,PageReport,
AttachRequest::session/with_session,SessionRuntime::page,
impl From<Uuid> for SessionId,EngineError::{NoLiveSession, NoSuchWindow},
AttachError::DuplicateSession,CONFLICT_CODE,NOT_FOUND_CODE(no engine or
known adopter use left). No SDL root field takes a session argument. graphql::mutation_error(also re-exported at the crate root):?on a
MutationErrorgives the same coded error.
Adopter migration
- Paging: a paged view takes
size: WindowSizeand abeforecursor on its
delta subscription, carries them in itsQueryas aPage<K>
(WindowSpec::view::<V>(&query, rls)), bindscx.limit(&page)and
page.cursor()inpopulateand returnspage.population(keys). Delete the
pagemutation, everypage/attach_with_sessioncall and everysession
argument, and the matches onAttachError::DuplicateSession,
EngineError::NoLiveSession/NoSuchWindow,CONFLICT_CODEand
NOT_FOUND_CODE. A client changes its window by re-subscribing with new
variables, keeps its list on screen until the newReset, and does not refetch. - Capacity: map
AttachError::WindowTooLarge/WINDOW_TOO_LARGE. A
whole-collection view (type Query = ()) is refused at attach above
window_capacity: bound it with asizeor a narrower filter argument, or
raiseEngineConfig::with_window_capacity, and bindcx.limit_all()as the
LIMITof every read that has no page, so a refused attach reads at most one
key past the capacity. populate: a rawprojector::Projector::populateor
ErasedProjector::populatetakes aKeyCeilingafter the window arguments; an
implementation bindsceiling.limit()as theLIMITof the read that has no
page (one whose population is not read from the database ignores it), a direct
caller passesKeyCeiling::NONE.- One-shot window fetch: a resolver that calls
fetch_window,
fetch_window_jsonorfetch_view_windowon a window that can outgrow
window_capacitybounds it with its arguments; the refusal already reaches the
client asWINDOW_TOO_LARGE. - Raw one-key fetch: a resolver that calls
Query::fetchorfetch_jsonon
a projector whose default window can outgrowwindow_capacitymoves to a
view::Projectorandfetch_view, which reads the one row (a read-only table
gets a store whosesaveandcreaterefuse, as the roster library does). keys_in_cohorts: add theceiling: KeyCeilingparameter and bind
ceiling.limit()as theLIMITof the query (… WHERE tenant_id = ANY($1) LIMIT $2).full_eda::keys: pass thePopulatecontext instead of a pool:
full_eda::keys::<T, _>(cx).from_passport: delete the_pgargument and the
Box::pin(async move { … })wrapper; move any database read into a fact loader
(engine.register_principal_fact(..)); a rejection derived from database data
becomesFORBIDDENor an affordancereasonCode, not a401.- Body bounds: replace
MultipartConfig::with_max_body_bytes(n)with
EngineConfig::with_max_body_bytes(n)and
config::DEFAULT_MULTIPART_MAX_BODY_BYTESwith
config::DEFAULT_MAX_BODY_BYTES; drop any call toMultipartConfig::validate. max_file_bytes: a read ofMultipartConfig::max_file_bytesgets an
Option; a service that lowered the part bound only so a lower body bound could
boot removes itswith_max_file_bytescall.- Faults: derive
thiserror::Erroron everyMutationFaultand
ReactionErrortype (or implementstd::error::Error); aReactionErrortype
must now also beSend. Keep the wrapped error as#[from]/#[source]
instead of a pre-renderedString, write only your own context inDisplay,
and delete anyas_erroroverride. MutationError: build it withMutationError::refused(reason, detail)(a
Reason, no longer anOption) orMutationError::internal(detail); read
reason(),code()andrefusal_detail()instead of the fields; return it
from a resolver with?(or throughexecute/ack). Delete a log written
just beforeMutationError::internal: the constructor logs the detail.?in resolvers: keep?on aReason, aMutationError, an
AttachError, aWindowSizeOutOfRangeor anasync_graphql::Error. Where
?no longer compiles (anEngineErrorfromWindowSpec::viewor
WindowParams::encode, asqlx,serde_jsonorstd::ioerror), write
.or_internal("what failed")?withuse service_engine::OrInternal. Replace
.map_err(mutation_error)with?, acoded_error(code, code)over a
Reasonwith?on theReason(Gate::require()?), and a hand-rolled
"log and answer INTERNAL" helper withor_internal. A service's own refusal
type may implementFrom<ItsRefusal> for async_graphql::Errorover
coded_error.- Schema version: remove any import of
service_engine::schema_version; boot
the engine instead. KeepENGINE_BEAT_MSbelowschema_version_liveness
(defaults 1 s and 30 s), or raisewith_schema_version_liveness. A test that
expectsSchemaVersionConflictkeeps the first version beating for the whole
wait (a shortwith_schema_version_liveness/with_beatkeeps it fast). - Persistence: write
read_manyfor every store as one statement per call
(WHERE id = ANY($1)), never one per key; delete anyloadoverride (it no
longer compiles); bringPersistenceExtinto scope to callStore::load. - Mirror batch:
fn project<'a>(&'a self, cx: Projection<'a>, keys: Vec<K>);
build the rows for every key, then write each table once with
cx.replace_rows(RowScope::any_of("<key column>", keys), rows). AProject<()>
takes_keys: Vec<()>; a sum-type key splits the batch by variant. For tables
linked by a cascading foreign key: upsert the present parents, replace the
children over every key, then retire the absent parents. - Mirror writes: replace
Projection::replace/upsert/retire/
replace_one/removeand theKnown/KnownScopeimpls withKnownRow
rows written throughreplace_rows, or with set-based hand SQL oncx.conn()
pluscx.impact_foreign(..). MatchWritten::Changed/Written::Unchanged
(or callis_effective). KnownRow: addconst KEY(the key column names, inkey()order) and
const PRINCIPAL(None, orSome(PrincipalColumn { column, deps })for a
table a principal fact loader reads); deleteforeign_keyand any hand-written
refresh of every principal in scope.- Resolver reads: a resolver that read through
GraphqlState::pg()reads
through a view,Query::load_visible,Query::behindor
Query::read_under_rls; accumulated state is read through
ChunkReader::state::<A>(key), never a raw pool. Visibility::window/visible_keys: implement
CohortIndex::keys_in_cohortson the view's store (one query on the cohort
columns,ceiling.limit()as itsLIMIT) and return
view::cohort_window::<Self>(cx)frompopulate; aVisibilitywith
LIVE = falsekeeps a closedKeyswindow. A liveVisibilitywhose
membershipsread a principal fact declares that fact's bit inDEPS(the
bit its mutation passes toimpact_principal_facts), or a membership change no
longer repopulates an open window. A filter that is not a cohort goes in that
SQL or in a view of its own. A test that compared a window with the
declaration keepscheck_window_matches_visibility.fetch_view: none for a view whosepopulatederives from its
Visibility; a view whose defaultpopulateis narrower than itsvisible
moves that rule intovisible(or its RLS policy) before a one-key fetch relies
on it.- Mirror columns: a
jsonvalue column of aKnownRowtable becomes
jsonb. - Downloads: none for a view whose
populatederives from itsVisibility;
a view that declaresUnrestrictedbut filters by principal inpopulatemoves
that filter into itsVisibilityor RLS policy first. - Errors: match
EngineError::Accumulator(AccumulatorError::…)and
EngineError::Composition(CompositionError::…)where a match named a chunk,
seal, schema-composition or root-prefix variant. - A service that kept its own de-duplicating chain renderer drops it for
error::describe.
Reference service and battery
- example-service:
examplePageCards,CardPageand
exampleCardPageDeltas(session, …)are removed;exampleCardDeltas(boardId, size: Int!, before: UUID)pages by arguments. Its principal builds from the
passport alone.AppFaultandReactionFaultderivethiserror::Errorand
keep their source (so they are no longerUnwindSafe); a reply finish or cancel
whoselast_seqis abovei64::MAXis the newReactionFault::Malformed,
dead-lettered on its first delivery instead of retried as a store fault.
CardStoreno longer overridesload. The ledger andOrgBoardsRlswindows
and the roster library view read at most the populate's ceiling. The boards and
replies views populate throughcohort_window:BoardStore(onorg_id,id
andis_public, with a partial index on public boards) andReplyStore(on
org_id) implementCohortIndex, the unusedBoardFilterQueryis removed,
and both windows are live (a new row in the caller's cohorts reaches an open
session); the boardVisibilitydeclares its membership fact inDEPS, so a
granted or revoked membership still reaches a live session.exampleBoard,
exampleCard,exampleLedgerandexampleReplyread one row. A scenario
proves thatexampleBoardslists exactly the org, public and member cohorts.
Its subscription resolvers name the window-encoding fault withor_internal. - example-lib-roster:
RosterUsers<P>is aview::Projectorover the new
RosterStore(the read ofroster.known_persons;saveandcreaterefuse,
the host's directory mirror being the only writer),Unrestrictedwith the new
RosterIsOpenreason, so<prefix>_personreads its one row through
fetch_viewon a roster of any size; the subscription union names
ViewProjector<RosterUsers<P>>and the slice registers it with
register_view. A scenario fetches a person from a roster over
window_capacity. - Battery:
s162,s163,s167ands183are retired (their 0.3 paging
meanings are gone). New:s243(a fact-loader fault answers500INTERNAL
onPOST /graphqland the/graphql/wsupgrade, a rejected passport keeps its
401),s244–s247(a wider window is a new session, a page behind the head,
the capacity refusal at attach only and the attach read ceiling, a burst of
window changes, a write during a switch),s250–s257(the batched full-EDA
read,load_visible,read_under_rls,replace_rowschange detection and
whole-table choice, the gated read of a parent's children, a whole-table
replace within a statement timeout, a rescan written in as many statements for
many keys as for one),s258(an attach whose principal refresh faults while
it connects isPrincipalRefreshFailed, one whose principal no longer exists
isPrincipalRevoked), andbb17(two replicas: window changes on either pod,
a write through one reaching a subscriber on the other, the dropped session on
the pod the client left,WINDOW_SIZE_INVALID, no root field naming a
session).s136ands187cover the handover wait and the refusal of a
version that keeps beating;s006covers the abandoned and the stuck attach.
The multipart scenario that shareds241with the migrate scenario is renumbered
s248.s259covers a whole-collection cohort attach and a one-shot window
fetch overwindow_capacity: each asks the store forwindow_capacity + 1keys
and is refused before any row is read.s260proves that a one-key
fetch_viewon a collection overwindow_capacityreads its one row and asks
the store for no population, and answers a row outside the caller's cohorts
like an absent key; and that a rawfetch/fetch_jsonasks the store for
window_capacity + 1keys, is refused withWINDOW_TOO_LARGEabove the
capacity before any row is read, and answers a member at the capacity.s252
ands254prove that the statements ofread_under_rls, and the gate and the
closure ofbehind(..).read(..), read one snapshot: a row committed between
them is not seen.s115proves that aGate::require()?refusal answers its
reason code on a query and on a subscription open. - Conformance crate:
sample::graphql::base_configis public,
boot_graphql_service_with,boot_counted_service(withsample::counted, a
cohort view whose store records the ceilings it is asked for and the rows it
reads, itssampleCountedItemone-key view fetch and its
sampleCountedRawItem/sampleCountedRawItemJsonraw one-key fetches),
sample::latch::AdvisoryLatch,
AssignmentPage::whole(),sample::gated::all_assignment_keysand
sample::visible(VisibleAssignments, moved fromsample::gated, and its
closed-windowSnapshotAssignmentVisibility) are added. Every samplepopulate
that reads a table binds the ceiling (ceiling.limit()orcx.limit_all()) as
itsLIMIT, and the cohort-filtered sample views read through
AssignmentStore::keys_in_cohorts, never a table filtered in memory.
sample::graphql::SAMPLE_REFUSEDand thesampleRefusedPeek/
sampleRefusedStreamroot fields refuse with a bare?; the upload and
widget sample resolvers name their faults withor_internal.
CI
rustfmt --checkruns oncrates/example-service/src/slices/*/mod.rsbeside
cargo fmt --check: the slice modules are declared insidecompose_service!,
whichcargo fmtnever expands, so their files were never format-checked (they
are formatted in this release).cargo semver-checksruns no check on a breaking bump (v0.3.4→0.4.0is
major in semver terms), so its gate passes vacuously. On such a bump the job now
also lists every break of each gated crate, report-only, as if the bump were a
minor one. For this release it reports 15 lint classes onservice-engine, each
break named under Changed (breaking) or Removed with its line in Adopter
migration, and 3 onexample-service, named under Reference service and
battery. The listing does not see a changed parameter or return type, so a
break of that kind is found by review, not by the job.