mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 08:52:42 +00:00
Co-op audit fixes: lock scheduler, idempotent payouts, deterministic ties
Audit pass addressed concurrency races, crash-recovery double-pay risks, and non-determinism in tie tallying. CRITICAL — concurrency - coopResolveFloor now acquires advUserLock for every party member (sorted by UserID) before mutating state, so concurrent !coop fund / vote / giftvote commands serialize against the scheduler. CRITICAL — crash idempotency - Add coop_dungeon_runs.last_resolved_day and coop_dungeon_members.member_payout columns (with migration entries) for crash-resume tracking. - Skip resolution only if last_resolved_day >= day AND status is terminal; otherwise re-enter and finish via idempotent operations. - On resume detection (event already has outcome), reuse the saved roll instead of re-rolling — prevents different outcomes on retry. - claimCoopMemberPayout / claimCoopBetPayout: atomic UPDATE...WHERE col IS NULL pattern. Each member/bet can only be paid once; retries are silent no-ops. - createCoopEvent now INSERT OR IGNORE — idempotent on retry. - last_resolved_day is set as the FINAL write per floor. HIGH — bugs - Lazy-create floor event in resolver if missing — covers crash window between lockCoopRun and the original createCoopEvent. - coopTallyVote sorts tied winners alphabetically before fallback (was non-deterministic via Go map iteration). - Removed dead day-1 wipe combat-action refund block — refund check was always false because midnight reset clears state before resolution. LOW - Log slog.Error on malformed JSON in parseCoopVotes / parseCoopFundingMap instead of silently dropping data. Tests - Tally test extended to assert determinism across 50 runs (catches map-iteration leaks). - Added coverage for new tie-with-leader-vote-outside-winners case. - Idempotency-guard sanity check on LastResolvedDay vs CurrentDay. Race detector: clean. Full suite: green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -255,6 +255,21 @@ func setCoopBetPayout(runID int, userID id.UserID, payout int) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// claimCoopBetPayout atomically claims a bet for payout: returns true only if
|
||||
// this call wins the race (the row had payout IS NULL and we set it). Used to
|
||||
// prevent double-credit on retry after a mid-resolution crash.
|
||||
func claimCoopBetPayout(runID int, userID id.UserID, payout int) (bool, error) {
|
||||
d := db.Get()
|
||||
res, err := d.Exec(`UPDATE coop_dungeon_bets SET payout = ?
|
||||
WHERE run_id = ? AND player_id = ? AND payout IS NULL`,
|
||||
payout, runID, string(userID))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
return n > 0, nil
|
||||
}
|
||||
|
||||
func loadMostRecentBettableCoopRun() (*CoopRun, error) {
|
||||
runs, err := queryCoopRuns(`status IN ('open','active') ORDER BY id DESC LIMIT 1`)
|
||||
if err != nil || len(runs) == 0 {
|
||||
@@ -278,15 +293,22 @@ func (p *AdventurePlugin) coopResolveBets(run *CoopRun, won bool) string {
|
||||
}
|
||||
winners, total, rake, payouts := coopParimutuelPayouts(bets, winningPosition)
|
||||
|
||||
// Idempotent: claim each bet's payout slot (UPDATE...WHERE payout IS NULL)
|
||||
// before crediting. If the claim fails, this bet was already paid by a
|
||||
// prior resolution attempt — skip the credit. Prevents double-pay on
|
||||
// crash-restart.
|
||||
for _, b := range winners {
|
||||
payout := payouts[b.PlayerID]
|
||||
claimed, err := claimCoopBetPayout(run.ID, b.PlayerID, payout)
|
||||
if err != nil || !claimed {
|
||||
continue
|
||||
}
|
||||
if payout > 0 {
|
||||
p.euro.Credit(b.PlayerID, float64(payout), "coop_bet_payout")
|
||||
}
|
||||
_ = setCoopBetPayout(run.ID, b.PlayerID, payout)
|
||||
}
|
||||
for _, b := range bets {
|
||||
if b.Position != winningPosition {
|
||||
if b.Position != winningPosition && !b.Payout.Valid {
|
||||
_ = setCoopBetPayout(run.ID, b.PlayerID, 0)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user