fix(streaming): stop replaying committed recenter retirements
Root cause: pending-only live projection buckets were misclassified as landblock presentation owners during origin recentering. That manufactured a second full cleanup receipt for a generation whose first receipt was still advancing; the duplicate guard threw and the broad retry path replayed the already-committed detach 243 times. Keep pending live projections through the spatial identity map without issuing another receipt, and fail fast when a receipt-ledger invariant occurs after detachment. Evidence: docs/research/2026-08-02-collision-throughput-handoff/p1-retirement-receipt-loop.md. Release suite, lifecycle gate, and nine-stop soak pass.
This commit is contained in:
parent
c65559d8f8
commit
01f4791e95
9 changed files with 309 additions and 9 deletions
|
|
@ -1349,8 +1349,14 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
|||
if (id != 0u)
|
||||
AddId(id);
|
||||
}
|
||||
foreach (uint id in _pendingByLandblock.Keys)
|
||||
AddId(id);
|
||||
// A pending-only bucket owns live spatial projections, not a loaded
|
||||
// landblock presentation generation. Those projections are retained
|
||||
// below through _projectionLocations, but there is no terrain,
|
||||
// collision, static-script, or renderer owner to retire. In
|
||||
// particular, DetachLandblock deliberately parks surviving live
|
||||
// projections here while its existing exact retirement receipt is
|
||||
// still advancing. Emitting another full receipt during a recenter
|
||||
// would give the same retired generation two cleanup owners.
|
||||
foreach (uint id in _pendingRenderIdsByLandblock.Keys)
|
||||
AddId(id);
|
||||
foreach (uint id in _pendingNearTierLandblocks)
|
||||
|
|
|
|||
|
|
@ -322,8 +322,25 @@ public sealed class LandblockPresentationPipeline
|
|||
{
|
||||
GpuWorldRecenterRetirement detached =
|
||||
_state.DetachAllForOriginRecenter();
|
||||
Exception? adoptionFailure =
|
||||
_retirements.AdoptDetachedFull(detached.Landblocks);
|
||||
Exception? adoptionFailure;
|
||||
try
|
||||
{
|
||||
adoptionFailure =
|
||||
_retirements.AdoptDetachedFull(detached.Landblocks);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
// Spatial detachment is already committed. A receipt-ledger
|
||||
// invariant failure cannot be retried by detaching the same
|
||||
// generation again; doing so was the 243-frame origin-recenter
|
||||
// exception loop. Surface the committed edge so the controller
|
||||
// fails fast instead of pretending the operation is resumable.
|
||||
throw new StreamingMutationException(
|
||||
"Origin-recenter retirement receipt adoption failed after " +
|
||||
"the spatial generation detached.",
|
||||
mutationCommitted: true,
|
||||
error);
|
||||
}
|
||||
Exception? failure = (detached.ObserverFailure, adoptionFailure) switch
|
||||
{
|
||||
(null, null) => null,
|
||||
|
|
|
|||
|
|
@ -1433,6 +1433,14 @@ public sealed class StreamingController
|
|||
transaction.PreparationCommitted = true;
|
||||
return true;
|
||||
}
|
||||
catch (StreamingMutationException error) when (error.MutationCommitted)
|
||||
{
|
||||
// The old spatial generation is already gone. Re-entering this
|
||||
// transaction would replay the detach against a new state, so a
|
||||
// committed receipt/adoption invariant is terminal and must be
|
||||
// surfaced to the caller.
|
||||
throw;
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
Console.WriteLine(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue