fix(events): three defects the live rig found, two of them data loss
The whole-rig walk (ServUO + sidecar + website) against a real two-phase event.
- **A WS reconnect would have orphaned every live resource.** The backfill
replays the last several `server.hello` frames in order — this rig saw three,
each with a different `bootId` — so every replayed frame reads as a restart,
and the intermediate ones compare a resource stamped with the CURRENT boot
against a boot that ended hours ago. The row is then `orphaned`: a live crier
line core will never take down again, lost to nothing worse than the website
reconnecting. Gated on `!fromBackfill`, the rule the engagement fan-out and
the SSE broadcast beside it already state. The website-was-down case is not
missed — core asks every module at its own boot.
- **The shard explains its refusals and the run log dropped the explanation.**
A 403 body reads `{"reason":"admin write plane disabled"}`; `legError` looks
for `data.message`, finds nothing, and reports "sidecar responded 403". For a
staff member clicking a button that is survivable. For an event that ran at
four in the morning the run log is the only place anyone will learn why.
- **The "not retried" clause explained the wrong thing on a permanent status.**
A 403 will not succeed on any attempt, so telling an operator it was not
retried "because a repeat would announce twice" points them at a policy
decision instead of at the switch they have to flip. The clause is now added
only where a retry was genuinely given up, and 403/404 join the statuses the
keyed verbs treat as terminal.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -103,7 +103,7 @@ async function resolveShardName(shard, deps) {
|
||||
|
||||
// Apply the state-change side effect for a kind (if any). Returns a promise.
|
||||
async function applyStateChange(event, deps) {
|
||||
const { shardState, uoLinkConfig, eventsReconcile, log } = deps
|
||||
const { shardState, uoLinkConfig, eventsReconcile, fromBackfill, log } = deps
|
||||
switch (event.kind) {
|
||||
case 'server.hello': {
|
||||
const incoming = event.bootId || null
|
||||
@@ -117,7 +117,7 @@ async function applyStateChange(event, deps) {
|
||||
}
|
||||
if (incoming) state.bootId = incoming
|
||||
await uoLinkConfig.recordStatus({ pluginConnected: true, bootId: incoming, lastEventAt: event.t })
|
||||
if (restarted) {
|
||||
if (restarted && !fromBackfill) {
|
||||
// EVENTS.md F: core has no concept of the game being up, so the module
|
||||
// says when a ledger of live shard resources has become a claim about a
|
||||
// world that no longer exists. This is that moment, and a changed
|
||||
@@ -130,8 +130,16 @@ async function applyStateChange(event, deps) {
|
||||
// row. Asking first would have every resource compared against the boot
|
||||
// that has just ended, and every one of them would look live.
|
||||
//
|
||||
// Fire-and-forget by the contract: core logs what it orphaned, and there
|
||||
// is nothing an ingest handler could correctly do with the answer.
|
||||
// **And never on a backfill replay**, which is the same rule the
|
||||
// engagement fan-out and the SSE broadcast state below and is far more
|
||||
// expensive to break here. A reconnect replays the last several
|
||||
// `server.hello` frames in order — this rig saw three, each with a
|
||||
// different `bootId` — so every replayed frame looks like a restart, and
|
||||
// the intermediate ones would compare a resource stamped with the CURRENT
|
||||
// boot against a boot that ended hours ago. The row is then `orphaned`:
|
||||
// a live crier line core will never take down again, lost to nothing
|
||||
// worse than the website reconnecting. The website-was-down case is not
|
||||
// missed by skipping these — core asks every module at its own boot.
|
||||
eventsReconcile()
|
||||
}
|
||||
return
|
||||
@@ -315,6 +323,10 @@ function resolveDeps(deps) {
|
||||
// reconcile must be able to see the call without a live event engine behind
|
||||
// it.
|
||||
eventsReconcile: deps.eventsReconcile || (() => coreEvents.reconcile()),
|
||||
// Not injectable — it is the caller's statement about this frame rather than
|
||||
// a dependency. It reaches `applyStateChange` because the reconcile below is
|
||||
// the one state change that must not act on a replay; see the note there.
|
||||
fromBackfill: Boolean(deps.fromBackfill),
|
||||
log: deps.log || defaultLog,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user