Skip to content

Loot-roll promotion can double-promote, and insert_roll_rows has no uniqueness on (corpse_guid, slot) #63

Closed

Description

@Mathih13

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):

shard.pending_local_rolls()          // read
  -> realm.realm_loot_op(START, ..)  // promote
  -> shard.clear_promoted_loot_roll  // clear

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).

Activity

  1. added
    bugSomething isn't working
    P3someday / nice-to-have
    on Aug 11, 2026
  2. Mathih13 commented on Sep 7, 2026

    @Mathih13
    ContributorAuthor

    This was generated by AI during triage.

    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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3someday / nice-to-havebugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions