Skip to content

Coordinate the World and Map session phases from the canonical producer (#787) - #792

Merged
alseif0x merged 9 commits into
3.4.3from
787-p3-map-driven-session-pass
Sep 12, 2026
Merged

Coordinate the World and Map session phases from the canonical producer (#787)#792
alseif0x merged 9 commits into
3.4.3from
787-p3-map-driven-session-pass

Conversation

@alseif0x

Copy link
Copy Markdown
Owner

C++ runs both session passes from the thread that owns the session:
World::Update calls UpdateSessions(diff) (World.cpp:2704) and then
MapManager::Update(diff), whose maps drive their players' sessions
(World.cpp:2748, Map.cpp:669-680). RustyCore sessions own themselves, so
that order and that ownership have to travel explicitly. This PR makes them
travel, and closes the correctness gaps two review rounds found in the process.

Exactly-once. The tick plan is neither Clone nor Copy and resume
consumes it, behind manager state Idle → AwaitingSessions(epoch) → Resuming(epoch) → Idle. A second begin answers Busy without advancing the
shared timer, so the refused diff is still owed to the next accepted tick. Maps
carry an incarnation, so one recreated under a reused MapKey during the pass
receives neither the phases that follow a dynamic-tree phase it never ran nor
the removal recorded for its predecessor — it does still receive
DelayedUpdate, as C++ visits every surviving map (MapManager.cpp:314-317).

Revocation, quiescence and the barrier. Each request carries a permit whose
single atomic transition decides between the session claiming it and the
coordinator revoking it at its deadline. A revoked pass provably never ran; a
claimed one is waited for, with the stall reported on every deadline. When a
claimed pass cannot account for its effects, the phase stops at that
participant and the permit travels to the producer, which holds a barrier across
steps: no phase issued and no tick admitted until it resolves, so neither
respawns nor DelayedUpdate run on that uncertainty.

Admission. The request freezes coordinator, epoch, phase, permit,
PlayerRegistration, PlayerHandle, MapKey, map incarnation, residence
revision and the effective diff, and the session revalidates all of them before
its first effect. A per-phase watermark refuses an earlier producer, a retired
step and a replay of one already served — things a fresh permit and the player's
identity cannot distinguish. A→B→A is caught by the revision.

Composition. Phases travel on a per-session rail instead of the command
mailbox a pass drains, consumed in the task that owns the WorldSession; the
autonomous per-session clock is gone. The producer drives the world phase for
every ready session, character screen included, and admits the map tick only
once that phase is quiescent, so Map→World inside one iteration is no longer
expressible. Registration is not readiness. At shutdown the session takes the
handover: it withdraws readiness, refuses queued phases before any effect and
drains its own control mailbox, so KickAll and the UpdateSessions(1) flush
are observed even without a live producer. A claimed phase is never cancelled
mid-effect; cancellation is honoured at the phase boundary.

Tails. The time sync moves to the map-pass tail with the admitted diff and
the logout decision to the end of the world pass, after the packet loop and the
query callbacks (WorldSession.cpp:488-503), so a LogoutCancel queued in the
same step is seen first. Warden has no update call on this path; that is
recorded as absent, not implemented.

Evidence. validation-v2 final --base origin/3.4.3 --architecture --timings passed at 306269f4 on a clean tree: manifest
20260912T192723.798686Z-2980204-final.json, 252 s, peak child RSS 2.6 GiB,
cargo_jobs=1. Suites: wow-map 735, wow-world 3865, world-server 579,
zero failures each; architecture check, self-test and the syntax-only ownership
ratchet pass with the regenerated baseline whose delta was reviewed.

Not claimed, and why this is not yet acceptance. Runtime/DB/relogin QA has
not been run, and composition evidence is still missing for a producer that dies
during shutdown and for finalization after the acknowledgement. Separately, 148
of 397 registrations are map-eligible and 54 of them await something
persistence-shaped: that is a candidate inventory, not 54 defects. What matters
is the narrower set whose wait needs a session or phase this barrier is holding;
QuestLogRemoveQuest is classified against C++ (QuestHandler.cpp:439,
Player.cpp:15575, Player.cpp:20138, WorkerPool.cpp:533) and stays with its
existing owner unless this coordination makes it a hard blocker.

Closes #787

🤖 Generated with Claude Code

Alseif0x and others added 6 commits September 12, 2026 15:17
Bounded source review at Rust aee29a6 and C++ a5f8da2e for the coordinated
map-driven session pass decided on the issue.

What C++ does, read at the exact ranges: World.cpp:2704 runs UpdateSessions
before :2748 runs MapManager::Update inside the same world tick, with different
diffs; WorldSession.cpp:488-540 splits the tails by filter (time sync only in
the map pass, query callbacks in both, warden/logout/socket cleanup only in the
world pass), so driving the whole Rust driver twice is excluded;
LockedQueue.h:82-95 reads the front and does not pop when the check refuses it,
so selection is FIFO, head-only and stops at an ineligible head; and
MapReference.cpp:22-28 inserts with insertFirst, so the map walk is reverse
insertion order rather than sorted GUIDs.

What RustyCore has: the ProcessingPlace classification and both filters already
exist (wow-handler lib.rs:38, processing.rs:41) and are unconsumed; one session
driver per task with its own diff (session_factory.rs:142-215); a canonical map
tick holding three synchronous mutexes across the whole update (runtime/map.rs
1596-1634); a MapManager::Update shape that already matches C++
(manager/state_2.rs:426-518) whose MapUpdater runs inline with no async barrier
(:604-647); and an existing request/acknowledgement command with a per-request
response channel (mailbox/protocol.rs:163-278, shutdown.rs:95-150) whose shape
the map-phase request reuses but whose timeout semantics it may not.

The contract fixes order, the split point and guard release, serial session
execution with justified membership order, exactly-once resumption with the
saved diff, filtered FIFO selection with control-channel progress, identity
revalidation, failure semantics where an acknowledgement is not completion and a
timeout is not permission, the await audit, and the explicit limit that the
legacy creature runtime is untouched. It also lists the regressions the macro
owes.

The plan's stale claim that no ProcessingPlace contract exists is corrected in
place with the evidence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The branch carries the tick split, the C++ membership order and the
phase-filtered session pass, but the coordinated contract is not satisfied:
the acknowledgement deadline does not revoke an admitted operation, the
session composition still drives its own clock, the request does not
revalidate admission identities or consume the effective diff, and the tick
plan is clonable so exactly-once is unenforced.

Also corrects the earlier premise that leaving the world makes every Map
packet ineligible: Inplace is eligible in both phases regardless of
residence, as in C++.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
C++ runs both session passes from the thread that owns the session:
World::Update calls UpdateSessions(diff) (World.cpp:2704) and then
MapManager::Update(diff), whose maps drive their players' sessions
(World.cpp:2748, Map.cpp:669-680). RustyCore sessions own themselves, so the
order and the ownership have to travel explicitly. This closes the four
correctness gaps the review found in the first attempt.

Exactly-once: MapTickPlanLikeCpp is no longer Clone and resume consumes it by
value, backed by manager state Idle -> AwaitingSessions(epoch) ->
Resuming(epoch) -> Idle. A second begin answers Busy without advancing the
shared timer, so a refused diff is still owed to the next accepted tick. Maps
carry an incarnation, so one recreated under a reused MapKey during the pass
receives neither the phases that follow a dynamic-tree phase it never ran nor
the removal the plan recorded for its predecessor.

Revocation and quiescence: every request carries a permit whose single atomic
transition decides between the session claiming it and the coordinator revoking
it. The deadline ends only a pass that never started; a claimed one is waited
for and its delay reported, because continuing would overlap live effects. A
claimed pass whose end cannot be observed abandons the tick instead of resuming
over it. Completion is always an explicit transition, never a destructor, and
interruption after start is its own terminal state.

Admission: the request freezes coordinator, epoch, phase, permit,
PlayerRegistration, PlayerHandle, MapKey, map incarnation and residence
revision, and the session revalidates all of them before its first effect, so a
replacement incarnation or an away-and-back transfer is refused rather than
served. The effective diff feeds the map pass tail (WorldSession.cpp:488-497)
even when no packet was dispatched.

Composition: phases travel on a per-session rail instead of the command mailbox
a pass drains, and are consumed in the task that already owns the WorldSession.
The autonomous per-session clock is gone; the producer issues World for every
ready session, including those on the character screen, and admits the map tick
only once that phase is quiescent. Map->World within one iteration is therefore
no longer expressible. Registration is not readiness: the owning task publishes
that flag when it parks on the rail and withdraws it when it leaves.

Acceptance is not claimed here: the affected suites pass on this tree
(wow-map 735, wow-world 3858, world-server 577, all with zero failures), but
the remaining contract scenarios, the audit of handlers that await other steps,
shutdown exercise and runtime/DB/relogin QA are still open.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…udit (#787)

The clock trace now says what the producer owns: the world phase of every ready
session before the map tick is admitted, and the map-phase requests with their
completion boundary. The session clock entry stops claiming a clock of its own:
its diff arrives in the request, and its only remaining wake re-reads the
shutdown gate.

The three new WorldSession members are assigned to reviewed families rather
than absorbed by a count. The phase rail joins the delivery family, with the
reason it is not the command mailbox recorded there; the coordination flag joins
admission/dispatch as an explicit migration boundary. The family totals are
recomputed from the regenerated syntax baseline, whose delta is exactly those
two fields plus the signatures and bodies this macro changed; the stale
expected_unique_field_count of 648 is corrected to the 645 the baseline
actually contains.

The checkpoint records what the implementation does, the suite numbers measured
on this tree, and a finite audit: of 397 registrations, 148 are map-eligible and
54 of those await something persistence-shaped inside the handler. That set is
where the permit bounds overlap but not progress, and it is named as open work
rather than treated as covered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An admitted pass whose effects cannot be accounted for no longer lets the tick
walk on: the phase stops at that participant, and its permit travels to the
producer, which holds a barrier across steps until the pass reaches a terminal
state. Abandoning a plan was never a clean state, so respawns and DelayedUpdate
do not run on that uncertainty.

The shutdown handover has one owner. At the gate the session withdraws its
readiness, refuses every queued phase before any effect — answering the
producer rather than making it wait out a deadline — and drains its own control
mailbox, so KickAll and the UpdateSessions(1) flush are observed even without a
live producer. A claimed phase is still never cancelled mid-effect; cancellation
is honoured at the phase boundary instead, which is a deliberate change from the
previous force-cancel behaviour.

Provenance is now checked rather than assumed: a per-phase watermark of the
producer and step refuses an earlier producer, a retired step and a replay of
one already served, none of which a fresh permit or the player's identity can
distinguish. The logout decision moves to the end of the world pass, after the
packet loop and the query callbacks (WorldSession.cpp:498-503), so a
LogoutCancel queued in the same step is seen first. unload_all clears the map
incarnations with the maps.

The checkpoint's obligation over the 54 map-eligible handlers that await is
corrected: they are a candidate inventory, not 54 defects or 54 rewrites. What
this macro must fix is the narrower set whose wait needs a session or phase this
barrier is holding; an external wait that can progress on its own needs an
error/stop policy and evidence, not a callback for symmetry. QuestLogRemoveQuest
is classified against C++ (QuestHandler.cpp:439, Player.cpp:15575,
Player.cpp:20138, WorkerPool.cpp:533) and left with its existing owner unless
this coordination makes it a hard blocker.

Suites after the corrections: wow-map 735, wow-world 3865, world-server 579,
zero failures each; architecture check, self-test and the syntax-only ratchet
pass with the reviewed baseline. Final gate and runtime QA remain open.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The bounded login/save/relogin gate passes for the candidate with restitution:
bot_status 0, login_save_relog_verified true, the original live build restored
and serving. It drives a full live session — auth, character enumeration, player
login, instance socket, world entry, known spells, stream drain, normal logout
on the realm route, offline row with a newer logout_time and the six-family
projection retained across two authentications — entirely through the
coordinated producer, since the session no longer has a clock of its own.

The reputation failure that appeared first was investigated instead of accepted.
origin/3.4.3 without this macro performs the same AT_WAR write from the same
starting state, so the divergence belongs to the deployed 2026-09-07 build,
which predates the branch. The branch matches C++, where LoadFromDB sets AtWar
for a hostile rank and keeps needSave when the computed flags differ from the
row (ReputationMgr.cpp:766-783). The eight-run sequence that isolates this is
recorded, and no database row was edited by hand to reach it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@alseif0x
alseif0x merged commit d14a9a6 into 3.4.3 Sep 12, 2026
5 checks passed
@alseif0x
alseif0x deleted the 787-p3-map-driven-session-pass branch September 12, 2026 23:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ARCH.CORE] Introduce the opcode ProcessingPlace contract and the map-driven session update (P3)

1 participant