feat(runtime): bridge executor completion to the placement stream
Cutover slice C0 (docs/plans/2026-08-02-placement-cutover.md): the seam work that lets C3 flip hosts onto a complete receipt stream instead of growing one mid-cutover. The executor's Released exit now publishes an acknowledge-only ExecutorCompleted receipt through the one placement projection stream — registered before observer dispatch, correlated to the full execution receipt, reaped exactly once on acknowledgement/ discard/session-clear, and counted in the convergence ledger. All three production placement sinks acknowledge-and-ignore the new kind via early returns proven behavior-preserving for every existing kind; without them the first such receipt at cutover would permanently wedge the exact-head FIFO behind sinks that return false. Provably inert today: the publisher has no production caller. Execute's live inputs now derive from Runtime's own owners bound at GameRuntime construction: UsePositionFromServer is retail's exact autonomy_level != 2 (CommandInterpreter::UsePositionFromServer 0x006B3B40, startup-only knob), and PlayerDistance uses the live movement controller's position with a null-safe fallback to the caller struct — never a fabricated origin. TryPrepareAndSubmitAuthoredPlacement chains the prepared-collision Setup read through PrepareMover to submission with zero validation-semantics changes. TryCommitParent and CommitWithdrawal gain the sibling cancellation flow (residence + ordinary placement family); TryCommitParent deliberately omits LeaveWorld — retail's set_parent performs its single gated leave_world (0x00515A90) and a second would have no counterpart. Not fully dormant: the two cancellation fixes change Runtime paths production already calls (today as no-op-adjacent hardening, since nothing upstream begins a residence yet); everything else is reachable only by tests. Reviewed: retail-conformance PASS + architecture/ adversarial PASS after one fix round (sink wedge, completion-receipt lifecycle, null-controller distance). Runtime 921/921; complete Release solution 10,716 passed / 4 intentional skips. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
27e05b99e4
commit
67f63e85e5
14 changed files with 1639 additions and 11 deletions
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Immutable;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
|
|
@ -44,7 +45,18 @@ public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
|
|||
int DeferredAcceptedRelationCount = 0,
|
||||
/// <summary>Round 5 R5-3: mirrors StreamDispatchFailureCount/HasLastStreamDispatchFailure for the executor's contained-replay failure surface. Diagnostic only - like its stream precedent, NOT gated by <see cref="IsConverged"/>.</summary>
|
||||
long ReplayFailureCount = 0,
|
||||
bool HasLastReplayFailure = false)
|
||||
bool HasLastReplayFailure = false,
|
||||
/// <summary>
|
||||
/// F2: outstanding <see cref="RuntimeInitialCreateContinuationExecutor"/>
|
||||
/// token-to-receipt correlation entries - one per completed drain whose
|
||||
/// ExecutorCompleted receipt a host has not yet acknowledged. Gated by
|
||||
/// <see cref="IsConverged"/>, mirroring
|
||||
/// <see cref="RuntimeSetPositionOwnershipSnapshot.PendingProjectionAcknowledgementCount"/>'s
|
||||
/// existing "unacknowledged receipt is outstanding debt" shape for the
|
||||
/// SAME underlying receipt stream - unlike ReplayFailureCount above,
|
||||
/// this is NOT a diagnostic-only counter.
|
||||
/// </summary>
|
||||
int PendingCompletionReceiptCount = 0)
|
||||
{
|
||||
public bool IsConverged =>
|
||||
IsDisposed
|
||||
|
|
@ -65,6 +77,7 @@ public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
|
|||
&& PendingMoveCount == 0
|
||||
&& InitialCreateResidenceLeaseCount == 0
|
||||
&& InitialCreateExecutorProgressCount == 0
|
||||
&& PendingCompletionReceiptCount == 0
|
||||
&& StreamSubscriberCount == 0
|
||||
&& PlacementStreamSubscriberCount == 0
|
||||
&& PendingDispatchCount == 0
|
||||
|
|
@ -152,6 +165,13 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
// residence state referencing the executor type directly.
|
||||
InitialCreateResidences.BindRetirementNotification(
|
||||
key => InitialCreateExecution.DiscardProgress(key));
|
||||
// F2: reaps the executor's completion-receipt correlation entry
|
||||
// exactly when a host acknowledges the ExecutorCompleted receipt it
|
||||
// correlates - mirrors the residence-retirement binding immediately
|
||||
// above.
|
||||
Physics.SetPosition.BindExecutorCompletionAcknowledgement(
|
||||
(key, sequence) =>
|
||||
InitialCreateExecution.ForgetCompletionReceipt(key, sequence));
|
||||
Placements = new RuntimePlacementProjectionChannel(
|
||||
Events,
|
||||
Physics.SetPosition);
|
||||
|
|
@ -197,6 +217,13 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
// residence state referencing the executor type directly.
|
||||
InitialCreateResidences.BindRetirementNotification(
|
||||
key => InitialCreateExecution.DiscardProgress(key));
|
||||
// F2: reaps the executor's completion-receipt correlation entry
|
||||
// exactly when a host acknowledges the ExecutorCompleted receipt it
|
||||
// correlates - mirrors the residence-retirement binding immediately
|
||||
// above.
|
||||
Physics.SetPosition.BindExecutorCompletionAcknowledgement(
|
||||
(key, sequence) =>
|
||||
InitialCreateExecution.ForgetCompletionReceipt(key, sequence));
|
||||
Placements = new RuntimePlacementProjectionChannel(
|
||||
Events,
|
||||
Physics.SetPosition);
|
||||
|
|
@ -242,6 +269,13 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
// residence state referencing the executor type directly.
|
||||
InitialCreateResidences.BindRetirementNotification(
|
||||
key => InitialCreateExecution.DiscardProgress(key));
|
||||
// F2: reaps the executor's completion-receipt correlation entry
|
||||
// exactly when a host acknowledges the ExecutorCompleted receipt it
|
||||
// correlates - mirrors the residence-retirement binding immediately
|
||||
// above.
|
||||
Physics.SetPosition.BindExecutorCompletionAcknowledgement(
|
||||
(key, sequence) =>
|
||||
InitialCreateExecution.ForgetCompletionReceipt(key, sequence));
|
||||
Placements = new RuntimePlacementProjectionChannel(
|
||||
Events,
|
||||
Physics.SetPosition);
|
||||
|
|
@ -292,7 +326,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
_disposed,
|
||||
parents.DeferredAcceptedRelationCount,
|
||||
InitialCreateExecution.ReplayFailureCount,
|
||||
InitialCreateExecution.LastReplayFailure is not null);
|
||||
InitialCreateExecution.LastReplayFailure is not null,
|
||||
InitialCreateExecution.PendingCompletionReceiptCount);
|
||||
}
|
||||
|
||||
public void BindEventContext(
|
||||
|
|
@ -306,6 +341,25 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
InitialCreateExecution.BindGeneration(generation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// C0-2: forwards to <see cref="RuntimeInitialCreateContinuationExecutor.BindLiveInputs"/>,
|
||||
/// the same fan-out shape <see cref="BindEventContext"/> already uses for
|
||||
/// generation binding. Separate from <see cref="BindEventContext"/>
|
||||
/// because <c>GameRuntime</c> constructs <c>RuntimeCharacterState</c>/
|
||||
/// <c>RuntimeLocalPlayerMovementState</c> (the real source owners) AFTER
|
||||
/// this lifetime, so the live-input bind necessarily happens at a later
|
||||
/// point in <c>GameRuntime</c>'s construction sequence than the
|
||||
/// generation bind.
|
||||
/// </summary>
|
||||
public void BindLiveInputs(
|
||||
Func<bool> usePositionFromServer,
|
||||
Func<Vector3?> localPlayerPosition)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
InitialCreateExecution.BindLiveInputs(
|
||||
usePositionFromServer, localPlayerPosition);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Owns the presentation-free half of retail's CreateObject lifetime
|
||||
/// transaction. An attached graphical host may synchronously retire the
|
||||
|
|
@ -963,6 +1017,28 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
}
|
||||
|
||||
Entities.RefreshSnapshot(canonical, accepted);
|
||||
// C0-4(a): this method had NO cancellation choke-point at all,
|
||||
// leaving a residence lease or ordinary SetPosition operation
|
||||
// dangling once ordinary placement traffic goes live - fixed with
|
||||
// the SAME exactly-once ForgetInitialCreateResidence ->
|
||||
// Physics.SetPosition.Forget -> PreferCancellation sequence every
|
||||
// other commit in this family (CommitPositionChannelUpdate,
|
||||
// TryApplyPickup, CommitAcceptedParentCellless, TryAcceptDelete)
|
||||
// uses for THIS part of the job.
|
||||
// F4 (deliberate, NOT an oversight): unlike CommitPositionChannelUpdate,
|
||||
// this method does NOT also call Physics.CollisionReports.LeaveWorld.
|
||||
// Retail set_parent (0x00515A90, lines 283832-283833) performs its
|
||||
// single leave_world call gated behind the SAME add_child branch this
|
||||
// method's staged/deferred-replay commit represents (App's
|
||||
// EquippedChildRenderController realize sequence, the executor's own
|
||||
// ParentRelationReplay) - a second LeaveWorld here would double-leave-
|
||||
// world with no retail counterpart.
|
||||
RuntimePlacementCancellationReceipt initialCancellation =
|
||||
ForgetInitialCreateResidence(canonical);
|
||||
RuntimePlacementCancellationReceipt ordinaryCancellation =
|
||||
Physics.SetPosition.Forget(canonical);
|
||||
RuntimePlacementCancellationReceipt cancellation =
|
||||
PreferCancellation(initialCancellation, ordinaryCancellation);
|
||||
Entities.AdvanceParentCommit(canonical);
|
||||
ulong parentCommitVersion = canonical.ParentCommitVersion;
|
||||
return AcknowledgeProjectionAndPublish(
|
||||
|
|
@ -970,7 +1046,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
() => acknowledgeProjection?.Invoke(canonical),
|
||||
RuntimeEntityChange.Updated,
|
||||
() => canonical.ParentCommitVersion
|
||||
== parentCommitVersion);
|
||||
== parentCommitVersion,
|
||||
cancellation);
|
||||
}
|
||||
|
||||
public bool CommitAcceptedParentCellless(
|
||||
|
|
@ -1407,9 +1484,21 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
if (!Entities.IsCurrent(canonical))
|
||||
return false;
|
||||
|
||||
RuntimePlacementCancellationReceipt cancellation =
|
||||
// C0-4(b): this cancelled the initial-create residence but never the
|
||||
// ORDINARY placement family (Physics.SetPosition.Forget), unlike
|
||||
// TryApplyPickup/CommitAcceptedParentCellless/TryAcceptDelete, which
|
||||
// all cancel both. WithdrawLiveEntityProjectionToCellless routes
|
||||
// through here, so a live ordinary SetPosition/lost-cell watch could
|
||||
// dangle across a withdrawal-to-cellless once ordinary placement
|
||||
// traffic goes live. Fixed symmetrically with the same
|
||||
// Forget/PreferCancellation pair every sibling withdrawal uses.
|
||||
RuntimePlacementCancellationReceipt initialCancellation =
|
||||
ForgetInitialCreateResidence(canonical);
|
||||
Physics.CollisionReports.LeaveWorld(canonical);
|
||||
RuntimePlacementCancellationReceipt ordinaryCancellation =
|
||||
Physics.SetPosition.Forget(canonical);
|
||||
RuntimePlacementCancellationReceipt cancellation =
|
||||
PreferCancellation(initialCancellation, ordinaryCancellation);
|
||||
Entities.SuspendObjectClock(canonical);
|
||||
Entities.SetFullCell(canonical, 0u, 0u);
|
||||
ulong spatialVersion = canonical.SpatialAuthorityVersion;
|
||||
|
|
|
|||
|
|
@ -274,7 +274,26 @@ internal sealed class RuntimeInitialCreateContinuationExecutor
|
|||
_applyAcceptedSpawn;
|
||||
private readonly Dictionary<RuntimeEntityKey, Progress> _progress = [];
|
||||
private readonly HashSet<RuntimeEntityKey> _executing = [];
|
||||
/// <summary>
|
||||
/// C0-1: correlates a published
|
||||
/// <see cref="RuntimePlacementProjectionKind.ExecutorCompleted"/>
|
||||
/// receipt back to the full execution receipt/trace, keyed by the SAME
|
||||
/// public Entity/Sequence identity every other Kind uses (the receipt's
|
||||
/// own <c>Token.Entity</c>/<c>Token.Sequence</c>). Overwritten (never
|
||||
/// accumulated) per entity key - an entity cannot have two drains
|
||||
/// completing concurrently (<see cref="Execute"/>'s own <see cref="_executing"/>
|
||||
/// reentrancy guard), so only the most recent completion for a key is
|
||||
/// ever meaningful; the exact-sequence check in
|
||||
/// <see cref="TryGetCompletionReceipt"/> rejects a stale lookup against a
|
||||
/// superseded completion under a reused key.
|
||||
/// </summary>
|
||||
private readonly Dictionary<RuntimeEntityKey,
|
||||
(ulong Sequence, RuntimeInitialCreateExecutionReceipt Receipt)>
|
||||
_completionReceipts = [];
|
||||
private Func<RuntimeGenerationToken>? _generation;
|
||||
private Func<bool>? _usePositionFromServer;
|
||||
private Func<Vector3?>? _localPlayerPosition;
|
||||
private bool _liveInputsBound;
|
||||
|
||||
internal RuntimeInitialCreateContinuationExecutor(
|
||||
RuntimeEntityDirectory entities,
|
||||
|
|
@ -308,6 +327,157 @@ internal sealed class RuntimeInitialCreateContinuationExecutor
|
|||
_generation = generation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// C0-2: binds Runtime's own live-input sources so no host ever computes
|
||||
/// <see cref="RuntimeInitialCreateExecutionInputs.UsePositionFromServer"/>/
|
||||
/// <see cref="RuntimeInitialCreateExecutionInputs.PlayerDistance"/>
|
||||
/// itself. Optional/nullable exactly like <see cref="_generation"/> is
|
||||
/// NOT (that one throws when unbound) - here an unbound source is a
|
||||
/// legitimate, permanent state for bare-lifetime tests, which keep
|
||||
/// constructing the executor without a <see cref="GameRuntime"/> and
|
||||
/// keep driving <see cref="Execute"/> with an explicit caller-supplied
|
||||
/// <see cref="RuntimeInitialCreateExecutionInputs"/> override (see
|
||||
/// <see cref="ResolveInputs"/>). <see cref="GameRuntime"/> binds the real
|
||||
/// owners - <c>RuntimeCharacterState.UsePositionFromServer</c> and the
|
||||
/// live <c>RuntimeLocalPlayerMovementState.Controller</c> position -
|
||||
/// once both exist (they are constructed AFTER
|
||||
/// <see cref="RuntimeEntityObjectLifetime"/>/this executor, so this bind
|
||||
/// cannot happen at the executor's own constructor time the way
|
||||
/// <see cref="BindGeneration"/> does; it happens alongside
|
||||
/// <c>BindEventContext</c> in <c>GameRuntime</c>'s construction
|
||||
/// sequence). Throws if called twice, matching every other Bind* seam on
|
||||
/// this class/its siblings (<see cref="BindGeneration"/>,
|
||||
/// <c>RuntimeEntityObjectEventStream.BindContext"/>,
|
||||
/// <c>RuntimePlacementProjectionChannel.BindGeneration</c>).
|
||||
/// F3: <paramref name="localPlayerPosition"/> itself returns
|
||||
/// <c>Vector3?</c>, not <c>Vector3</c> - a BOUND source with no live
|
||||
/// controller yet (the login-window drain, before
|
||||
/// <c>RuntimeLocalPlayerMovementState.Controller</c> exists) must yield
|
||||
/// null, not a fabricated <c>Vector3.Zero</c>. <see cref="ResolveInputs"/>
|
||||
/// falls back to the caller-supplied struct's PlayerDistance whenever
|
||||
/// this source is unbound OR returns null - the SAME fallback rule
|
||||
/// either way, never a synthetic origin-point distance that could
|
||||
/// misclassify a remote entity as implausibly far (>96 m) during that
|
||||
/// window.
|
||||
/// </summary>
|
||||
internal void BindLiveInputs(
|
||||
Func<bool> usePositionFromServer,
|
||||
Func<Vector3?> localPlayerPosition)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(usePositionFromServer);
|
||||
ArgumentNullException.ThrowIfNull(localPlayerPosition);
|
||||
if (_liveInputsBound)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The initial-create continuation executor's live-input sources are already bound.");
|
||||
}
|
||||
_usePositionFromServer = usePositionFromServer;
|
||||
_localPlayerPosition = localPlayerPosition;
|
||||
_liveInputsBound = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// C0-2: resolves the EFFECTIVE inputs for one <see cref="Execute"/>
|
||||
/// call. A bound source always wins; the caller-supplied
|
||||
/// <paramref name="inputs"/> struct is the test-override shape (its own
|
||||
/// doc comment still describes production usage now that this method
|
||||
/// exists) and is used verbatim only for whichever field has no bound
|
||||
/// source - a bare-lifetime test that never calls
|
||||
/// <see cref="BindLiveInputs"/> gets EXACTLY the caller-supplied values,
|
||||
/// preserving every existing test's behavior unchanged.
|
||||
/// <see cref="RuntimeInitialCreateExecutionInputs.PlayerDistance"/> uses
|
||||
/// the SAME world-space basis as today's legacy remote path
|
||||
/// (<c>LiveEntityNetworkUpdateController.cs</c>'s
|
||||
/// <c>MaxPhysicsDistance</c>/<c>dist</c> computation, cutover-routes.md
|
||||
/// route 4: <c>Vector3.Distance(worldPos, localPlayerPos)</c> where
|
||||
/// <c>localPlayerPos</c> is the live physics-CONTROLLER position, never a
|
||||
/// record snapshot) - here, <c>Vector3.Distance</c> between THIS
|
||||
/// entity's own currently-accepted position (the exact field
|
||||
/// <c>BeginAcceptedPlacementCore</c>/<c>CanonicalSetupTableId</c> already
|
||||
/// trust: <c>Snapshot.Physics?.Position ?? Snapshot.Position</c>) and the
|
||||
/// bound local-player controller position. Computed ONCE per
|
||||
/// <see cref="Execute"/> call, matching the one-shot-per-call granularity
|
||||
/// <paramref name="inputs"/> already had before this slice (retail
|
||||
/// recomputes <c>player_distance</c> per wire packet; refining this
|
||||
/// executor to per-continuation freshness is out of C0-2's scope).
|
||||
/// </summary>
|
||||
private RuntimeInitialCreateExecutionInputs ResolveInputs(
|
||||
RuntimeEntityRecord canonical,
|
||||
in RuntimeInitialCreateExecutionInputs inputs)
|
||||
{
|
||||
bool usePositionFromServer = _usePositionFromServer is { } source
|
||||
? source()
|
||||
: inputs.UsePositionFromServer;
|
||||
float playerDistance = inputs.PlayerDistance;
|
||||
// F3: an unbound source AND a bound-but-null live position (no
|
||||
// controller yet) both fall back to the caller-supplied struct
|
||||
// identically - never fabricate Vector3.Zero as a stand-in.
|
||||
if (_localPlayerPosition?.Invoke() is { } localPlayerPosition
|
||||
&& (canonical.Snapshot.Physics?.Position
|
||||
?? canonical.Snapshot.Position) is { } accepted)
|
||||
{
|
||||
var target = new Vector3(
|
||||
accepted.PositionX, accepted.PositionY, accepted.PositionZ);
|
||||
playerDistance = Vector3.Distance(target, localPlayerPosition);
|
||||
}
|
||||
return new RuntimeInitialCreateExecutionInputs(
|
||||
usePositionFromServer, playerDistance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// C0-1: reaches the full execution receipt/trace correlated with an
|
||||
/// observed <see cref="RuntimePlacementProjectionKind.ExecutorCompleted"/>
|
||||
/// receipt, purely via that receipt's own public
|
||||
/// <c>Token.Entity</c>/<c>Token.Sequence</c> identity - the same identity
|
||||
/// every other placement Kind is acknowledged by. Returns false for a
|
||||
/// superseded/stale sequence under a reused entity key.
|
||||
/// </summary>
|
||||
internal bool TryGetCompletionReceipt(
|
||||
in RuntimePlacementProjectionToken token,
|
||||
out RuntimeInitialCreateExecutionReceipt receipt)
|
||||
{
|
||||
if (_completionReceipts.TryGetValue(
|
||||
token.Entity,
|
||||
out (ulong Sequence, RuntimeInitialCreateExecutionReceipt Receipt) entry)
|
||||
&& entry.Sequence == token.Sequence)
|
||||
{
|
||||
receipt = entry.Receipt;
|
||||
return true;
|
||||
}
|
||||
receipt = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// F2: reaps exactly one completion-receipt correlation entry, bound as
|
||||
/// <see cref="RuntimeSetPositionState.BindExecutorCompletionAcknowledgement"/>'s
|
||||
/// notification callback - fired the moment a host acknowledges the
|
||||
/// Kind ExecutorCompleted receipt this entry correlates, never before.
|
||||
/// The exact-sequence check rejects removing a NEWER completion's entry
|
||||
/// under a reused key (mirrors <see cref="TryGetCompletionReceipt"/>'s
|
||||
/// own currency check).
|
||||
/// </summary>
|
||||
internal void ForgetCompletionReceipt(RuntimeEntityKey key, ulong sequence)
|
||||
{
|
||||
if (_completionReceipts.TryGetValue(
|
||||
key,
|
||||
out (ulong Sequence, RuntimeInitialCreateExecutionReceipt Receipt) entry)
|
||||
&& entry.Sequence == sequence)
|
||||
{
|
||||
_completionReceipts.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// F2: folded into <see cref="RuntimeEntityObjectOwnershipSnapshot"/>/
|
||||
/// <c>IsConverged</c> - an unacknowledged completion receipt is
|
||||
/// outstanding host debt, mirroring
|
||||
/// <see cref="RuntimeSetPositionOwnershipSnapshot.PendingProjectionAcknowledgementCount"/>'s
|
||||
/// existing "must be zero to converge" shape for the SAME underlying
|
||||
/// receipt stream.
|
||||
/// </summary>
|
||||
internal int PendingCompletionReceiptCount => _completionReceipts.Count;
|
||||
|
||||
internal int ProgressCount => _progress.Count;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -389,6 +559,15 @@ internal sealed class RuntimeInitialCreateContinuationExecutor
|
|||
/// </summary>
|
||||
internal void DiscardProgress(RuntimeEntityKey key)
|
||||
{
|
||||
// F2: reap this key's completion-receipt correlation entry
|
||||
// unconditionally - DiscardProgress owns cleanup of every piece of
|
||||
// state IT introduces, and this cache is exactly that (see
|
||||
// _completionReceipts's own doc comment). Independent of whether
|
||||
// _progress still tracks this key: a completed drain has ALREADY
|
||||
// removed its own Progress entry before this correlation entry was
|
||||
// ever added (see ExecuteCore's Released case), so this is the
|
||||
// ONLY choke point that reaps it outside of a normal acknowledge.
|
||||
_completionReceipts.Remove(key);
|
||||
if (!_progress.Remove(key, out Progress? progress))
|
||||
return;
|
||||
if (progress.PendingContinuationPlacement.IsValid)
|
||||
|
|
@ -421,6 +600,9 @@ internal sealed class RuntimeInitialCreateContinuationExecutor
|
|||
_physics.SetPosition.PublishCancellation(cancellation);
|
||||
}
|
||||
_progress.Clear();
|
||||
// F2: bulk-reap every completion-receipt correlation entry - a full
|
||||
// session clear must not carry any of this cache across a reset.
|
||||
_completionReceipts.Clear();
|
||||
}
|
||||
|
||||
internal RuntimeInitialCreateExecutionStatus Execute(
|
||||
|
|
@ -458,6 +640,11 @@ internal sealed class RuntimeInitialCreateContinuationExecutor
|
|||
out RuntimeInitialCreateExecutionReceipt receipt)
|
||||
{
|
||||
receipt = default;
|
||||
// C0-2: resolve ONCE per Execute call - a bound Runtime source always
|
||||
// wins over the caller-supplied test-override struct (see
|
||||
// ResolveInputs's own doc comment for the exact fallback rule).
|
||||
RuntimeInitialCreateExecutionInputs effectiveInputs =
|
||||
ResolveInputs(canonical, inputs);
|
||||
|
||||
// An existing Progress for a DIFFERENT (older or ABA-reused) lease
|
||||
// id is discarded here, and THIS exact call fails closed - an old
|
||||
|
|
@ -561,7 +748,7 @@ internal sealed class RuntimeInitialCreateContinuationExecutor
|
|||
return Abandon(canonical, key);
|
||||
|
||||
RuntimeInitialCreateExecutionStatus applyStatus =
|
||||
ApplyContinuation(canonical, token, key, continuation, inputs, progress);
|
||||
ApplyContinuation(canonical, token, key, continuation, effectiveInputs, progress);
|
||||
// Round 3 B2: every apply method below now rebaselines
|
||||
// itself immediately after its own canonical mutation and
|
||||
// BEFORE its own publish (mutate -> rebaseline -> publish),
|
||||
|
|
@ -592,14 +779,36 @@ internal sealed class RuntimeInitialCreateContinuationExecutor
|
|||
switch (release)
|
||||
{
|
||||
case RuntimeInitialCreateResidenceExecutorReleaseStatus.Released:
|
||||
receipt = new RuntimeInitialCreateExecutionReceipt(
|
||||
{
|
||||
var completedReceipt = new RuntimeInitialCreateExecutionReceipt(
|
||||
key,
|
||||
residenceReceipt.FullCellId,
|
||||
residenceReceipt.TeleportHookPhase,
|
||||
progress.Trace.ToImmutable(),
|
||||
progress.ReplayedDeferredChildCount);
|
||||
receipt = completedReceipt;
|
||||
_progress.Remove(key);
|
||||
// C0-1: bridge the executor's own completion onto the
|
||||
// SAME ordered placement receipt stream every
|
||||
// Place/Withdraw/Discard uses (canonical is still
|
||||
// current here - nothing between the last continuation
|
||||
// apply and ConsumeExecuted's Released outcome mutates
|
||||
// it). Correlate the full trace via the fresh token's
|
||||
// Entity/Sequence identity - see TryGetCompletionReceipt.
|
||||
// F2: registration happens INSIDE PublishExecutorCompletion's
|
||||
// beforePublish callback (before the synchronous observer
|
||||
// dispatch), not after this call returns - a subscriber
|
||||
// reading the correlation back from inside its own
|
||||
// OnPlacement callback must already find it. receipt is
|
||||
// copied to a local (completedReceipt) because an `out`
|
||||
// parameter cannot be captured by a lambda.
|
||||
_physics.SetPosition.PublishExecutorCompletion(
|
||||
canonical,
|
||||
beforePublish: token =>
|
||||
_completionReceipts[key] =
|
||||
(token.Sequence, completedReceipt));
|
||||
return RuntimeInitialCreateExecutionStatus.Completed;
|
||||
}
|
||||
case RuntimeInitialCreateResidenceExecutorReleaseStatus.Revised:
|
||||
// A new continuation arrived mid-drain (Enqueue bumps the
|
||||
// completed entry's Adoption.Revision in place). Re-fetch
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue