fix(runtime): name why a placement is parked and fail closed when it cannot resolve
Fixes #284 (plan S1).
A first-entry placement that could not be prepared returned
RetrySetupUnavailable and was re-Advanced every pump forever. Nothing counted
it, nothing named its cause, and nothing distinguished "waiting for something
that will arrive" from "waiting for something that never can". That is why
#281's 43 test failures presented as four unrelated symptoms across App and
Runtime instead of one cause, and why a stuck entity in the live client simply
never appears with no log line to follow.
Worse, the two causes were conflated: 670f307c's missing-world-frame park
reported itself as RetrySetupUnavailable, sending anyone diagnosing it to the
prepared-asset pipeline rather than to the absent local-player Create that
actually publishes the frame.
- RetryWorldFrameUnavailable splits the two causes. Call sites now ask
IsRetryable() instead of comparing against one reason, so a future retry
reason cannot be silently reclassified as a hard rejection - the exact way
this class of bug hides.
- The operation retains its RuntimeSetPositionParkReason, and
RuntimeSetPositionOwnershipSnapshot reports parked work by cause
(ParkedAwaitingSetupCollisionCount / ParkedAwaitingWorldFrameCount /
ParkedPlacementCount), so parked placements appear wherever ledgers are
already asserted.
- ObserveLocalPlayerCreate records the accepted local-player Create even when
it carries no landblock - precisely the case where no frame is ever
published - and ThrowIfWorldFrameUnreachable makes that contradiction
terminal. Waiting is legitimate only while that Create is outstanding; after
it, no later pump can supply the frame. Same shape as 01f4791e, which made a
violated receipt-ledger invariant terminal rather than resumable.
This is observability plus fail-fast. There is no timeout, no retry cap, and
no grace period anywhere in it; retryable work still retries exactly as before
and no placement behaviour changed.
The parked counts are deliberately NOT folded into IsConverged: #277 documents
a far Create legitimately parking for a whole session, so a parked entry at
teardown is not automatically a defect. Wiring them into the connected gates
is carried with #277's service-window conversion, where "legitimately parked"
becomes definable.
Runtime 1,012/1,012. Complete Release solution: 10,834 passed / 4 skipped /
0 failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
95ebc03af4
commit
97d11e6c7f
9 changed files with 273 additions and 19 deletions
|
|
@ -2404,6 +2404,7 @@ public sealed class RuntimeSetPositionStateTests
|
|||
"InheritedLostDeadline",
|
||||
"EnteringWorldFromCelllessResidence",
|
||||
"DormantLocalActivation",
|
||||
"ParkReason",
|
||||
"PreparedCommandAwaitingWithdrawalAck",
|
||||
"InPool",
|
||||
];
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using AcDream.Core.Net;
|
|||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Runtime.Entities;
|
||||
using AcDream.Runtime.Physics;
|
||||
|
||||
namespace AcDream.Runtime.Tests.Physics;
|
||||
|
||||
|
|
@ -148,6 +149,80 @@ public sealed class RuntimeWorldFrameTests
|
|||
Assert.Equal(-192f, sourceX);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #284: waiting for the world frame is legitimate only while the
|
||||
/// local-player Create is still outstanding. The frame is published once
|
||||
/// per session from that Create, so once it has been accepted WITHOUT
|
||||
/// publishing one - a local-player CreateObject carrying no landblock -
|
||||
/// no later pump can ever supply it, and every remote placement would
|
||||
/// retry forever in silence. That is a violated invariant, not a wait.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ALocalPlayerCreateWithNoLandblock_MakesTheFrameUnreachable()
|
||||
{
|
||||
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||
BindGeneration(lifetime);
|
||||
|
||||
// Before the local player is seen at all, waiting is legitimate.
|
||||
lifetime.Physics.ThrowIfWorldFrameUnreachable(CenterCell);
|
||||
|
||||
lifetime.Physics.ObserveLocalPlayerCreate(0u);
|
||||
|
||||
InvalidOperationException error =
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
lifetime.Physics.ThrowIfWorldFrameUnreachable(CenterCell));
|
||||
Assert.Contains("world frame is unreachable", error.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnAcceptedLocalPlayerCreate_LeavesTheFrameReachable()
|
||||
{
|
||||
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||
BindGeneration(lifetime);
|
||||
|
||||
lifetime.Physics.ObserveLocalPlayerCreate(CenterCell);
|
||||
|
||||
// The frame exists, so nothing is unreachable and nothing throws.
|
||||
lifetime.Physics.ThrowIfWorldFrameUnreachable(CenterCell);
|
||||
Assert.True(lifetime.Physics.TryGetWorldFrameOffset(
|
||||
CenterCell,
|
||||
out _,
|
||||
out _));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #284: a missing world frame used to report itself as
|
||||
/// RetrySetupUnavailable, sending anyone diagnosing a parked placement to
|
||||
/// the asset pipeline rather than the absent local-player Create. Both
|
||||
/// remain retryable - only the reported cause differs.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ParkReasons_AreDistinctAndBothRetryable()
|
||||
{
|
||||
Assert.True(RuntimeSetPositionMoverPreparationStatus
|
||||
.RetrySetupUnavailable.IsRetryable());
|
||||
Assert.True(RuntimeSetPositionMoverPreparationStatus
|
||||
.RetryWorldFrameUnavailable.IsRetryable());
|
||||
Assert.False(RuntimeSetPositionMoverPreparationStatus
|
||||
.RejectedAuthority.IsRetryable());
|
||||
Assert.False(RuntimeSetPositionMoverPreparationStatus
|
||||
.InvalidData.IsRetryable());
|
||||
Assert.False(RuntimeSetPositionMoverPreparationStatus
|
||||
.Prepared.IsRetryable());
|
||||
|
||||
Assert.Equal(
|
||||
RuntimeSetPositionParkReason.AwaitingSetupCollision,
|
||||
RuntimeSetPositionMoverPreparationStatus.RetrySetupUnavailable
|
||||
.ParkReason());
|
||||
Assert.Equal(
|
||||
RuntimeSetPositionParkReason.AwaitingWorldFrame,
|
||||
RuntimeSetPositionMoverPreparationStatus.RetryWorldFrameUnavailable
|
||||
.ParkReason());
|
||||
Assert.Equal(
|
||||
RuntimeSetPositionParkReason.None,
|
||||
RuntimeSetPositionMoverPreparationStatus.Prepared.ParkReason());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AZeroCellIdNeitherPublishesNorResolves()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue