You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Loot-roll promotion can double-promote, and insert_roll_rows has no uniqueness on (corpse_guid, slot) #63
A read-modify-write race with no module-side dedupe behind it. Latent until a second gateway exists, but the module half is worth fixing on its own merits.
The gateway's loot-roll relay (gateway/src/stdb/connection.rs:1201, 200 ms poll) promotes a world shard's staging roll onto realm-core in three non-atomic steps (gateway/src/world/loot.rs:151-178, promote_one):
Two gateways polling the same shard can both read the same un-cleared staging roll before either clears it, and both promote.
The module does not dedupe.realm_loot_op(START) dispatches to insert_roll_rows (module/src/loot.rs), which is a bare auto-inc insert with no uniqueness on (corpse_guid, slot):
let roll = ctx.db.game_loot_roll().insert(LootRoll{id:0, corpse_guid, slot, ...});for&guid in recipients { votes.insert(LootRollVote{id:0,roll_id: roll.id, ...});}
So a double promotion creates two game_loot_roll rows for the same corpse and slot, each with its own vote set — the party's votes split between two rolls and neither resolves correctly.
It is not only the 200 ms tick: flush_pending_promotions (world/loot.rs:196-215) also runs synchronously from party::run before every LEAVE/UNINVITE, across every connected world shard, so a party op on gateway A races gateway B's timer.
Settlement is fine, and deliberately so — do not "fix" it. settle_roll_grant checks withheld before granting, pinned by a module tripwire (settle_roll_grant_checks_withheld_before_granting), which is what makes the gateway's fan-to-every-shard settle safe. The only multi-gateway cost there is N x the calls, because won_watermark is a thread-local &mut u64 (connection.rs:603).
Where to start. A uniqueness constraint on (corpse_guid, slot) in game_loot_roll is the narrowest fix and makes the second promotion fail loudly instead of silently duplicating — but it is a schema change, so check the migration rules in docs/danger-zones.md first. Alternatively have promotion claim the staging row.
Context: the gateway scaling and hot-swap write-up §4 (private working origin).
Done when: two concurrent realm_loot_op(START) calls for the same (corpse_guid, slot) result in exactly one game_loot_roll row with one complete vote set — verified by SQL against realm-core, not by log inspection.
Migrated 2026-08-11 from the private tracker (spacetime-core#307).
PR #439 is merged. Its private Standalone test sends concurrent real START requests and checks committed rows through SQL: one Loot Roll and one complete three-member vote set for the corpse and slot. A recorded NEED vote and the first roll metadata survive a repeated START. Other corpse and slot pairs stay independent. All seven current-head checks passed, with no unresolved review threads.
This satisfies the concurrent-promotion acceptance here. A delayed START after a resolved roll has been deleted is a separate lifecycle problem tracked in #441; settlement and schema are unchanged.
Activity
Mathih13 commentedon Sep 7, 2026
PR #439 is merged. Its private Standalone test sends concurrent real START requests and checks committed rows through SQL: one Loot Roll and one complete three-member vote set for the corpse and slot. A recorded NEED vote and the first roll metadata survive a repeated START. Other corpse and slot pairs stay independent. All seven current-head checks passed, with no unresolved review threads.
This satisfies the concurrent-promotion acceptance here. A delayed START after a resolved roll has been deleted is a separate lifecycle problem tracked in #441; settlement and schema are unchanged.
Verification: #439 (comment)