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:
Erik 2026-08-02 20:53:11 +02:00
parent c65559d8f8
commit 01f4791e95
9 changed files with 309 additions and 9 deletions

View file

@ -160,6 +160,112 @@ public sealed class LandblockPresentationPipelineTests
Assert.Equal([0x2223FFFFu], enqueued);
}
[Fact]
public void OriginRecenter_PendingOnlyLiveProjectionKeepsItsExistingRetirementOwner()
{
const uint oldLandblockId = 0x2424FFFFu;
var live = Entity(1u, serverGuid: 0x70000004u);
var state = new GpuWorldState();
state.AddLandblock(new LoadedLandblock(
oldLandblockId,
new LandBlock(),
Array.Empty<WorldEntity>()));
state.PlaceLiveEntityProjection(oldLandblockId, live);
bool holdCleanup = true;
var retirements = new LandblockRetirementCoordinator(
state,
ticket => ticket.RunOnce(
LandblockRetirementStage.EntityLighting,
() =>
{
if (holdCleanup)
{
throw new InvalidOperationException(
"injected retained cleanup");
}
}),
_ => LandblockRetirementStage.EntityLighting);
retirements.BeginFull(oldLandblockId);
Assert.Equal(1, retirements.PendingCount);
Assert.Equal(1, state.PendingLiveEntityCount);
var origin = new LiveWorldOriginState();
Assert.True(origin.TryInitialize(0x24, 0x24));
var controller = new StreamingController(
enqueueLoad: static (_, _) => { },
enqueueUnload: static _ => { },
drainCompletions: static _ => Array.Empty<LandblockStreamResult>(),
applyTerrain: static (_, _) => { },
state,
nearRadius: 0,
farRadius: 0,
retirementCoordinator: retirements,
workBudgetOptions: GenerousWorkBudget());
var recenter = new StreamingOriginRecenterCoordinator(controller, origin);
Assert.False(recenter.Begin(0x25, 0x25, isSealedDungeon: false));
Assert.True(Converge(recenter, controller, 0x24, 0x24));
Assert.Equal((0x25, 0x25), (origin.CenterX, origin.CenterY));
Assert.Equal(1, retirements.PendingCount);
Assert.Equal(1, state.PendingLiveEntityCount);
holdCleanup = false;
controller.Tick(0x25, 0x25);
Assert.Equal(0, retirements.PendingCount);
}
[Fact]
public void OriginRecenter_CommittedReceiptInvariantFailsFastInsteadOfReplayingDetach()
{
const uint landblockId = 0x2626FFFFu;
var state = new GpuWorldState();
state.AddLandblock(new LoadedLandblock(
landblockId,
new LandBlock(),
Array.Empty<WorldEntity>()));
var retirements = new LandblockRetirementCoordinator(
state,
ticket => ticket.RunOnce(
LandblockRetirementStage.EntityLighting,
static () => throw new InvalidOperationException(
"injected retained cleanup")),
_ => LandblockRetirementStage.EntityLighting);
retirements.BeginFull(landblockId);
Assert.Equal(1, retirements.PendingCount);
// Directly violate the production publication fence so the recenter
// obtains a genuinely conflicting receipt after its spatial commit.
// The invariant must surface once; it must not become retry work.
state.AddLandblock(new LoadedLandblock(
landblockId,
new LandBlock(),
Array.Empty<WorldEntity>()));
var origin = new LiveWorldOriginState();
Assert.True(origin.TryInitialize(0x26, 0x26));
var controller = new StreamingController(
enqueueLoad: static (_, _) => { },
enqueueUnload: static _ => { },
drainCompletions: static _ => Array.Empty<LandblockStreamResult>(),
applyTerrain: static (_, _) => { },
state,
nearRadius: 0,
farRadius: 0,
retirementCoordinator: retirements,
workBudgetOptions: GenerousWorkBudget());
var recenter = new StreamingOriginRecenterCoordinator(controller, origin);
Assert.False(recenter.Begin(0x27, 0x27, isSealedDungeon: false));
StreamingMutationException error = Assert.Throws<StreamingMutationException>(
() => controller.Tick(0x26, 0x26));
Assert.True(error.MutationCommitted);
Assert.IsType<InvalidOperationException>(error.InnerException);
Assert.False(state.IsLoaded(landblockId));
}
[Fact]
public void OriginRecenter_DetachesFullTwentyFiveByTwentyFiveWindowAtomically()
{