mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 00:52:40 +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:
@@ -135,6 +135,8 @@ func runMigrations(d *sql.DB) error {
|
||||
`ALTER TABLE adventure_characters ADD COLUMN pet_morning_defense INTEGER NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE adventure_characters ADD COLUMN auto_babysit INTEGER NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE adventure_characters ADD COLUMN streak_decayed INTEGER NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE coop_dungeon_runs ADD COLUMN last_resolved_day INTEGER NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE coop_dungeon_members ADD COLUMN member_payout INTEGER`,
|
||||
}
|
||||
for _, stmt := range columnMigrations {
|
||||
if _, err := d.Exec(stmt); err != nil {
|
||||
@@ -1375,18 +1377,19 @@ CREATE INDEX IF NOT EXISTS idx_tip_audit_action ON holdem_tip_audit(action, stre
|
||||
|
||||
-- Co-op Dungeon (party multi-day runs)
|
||||
CREATE TABLE IF NOT EXISTS coop_dungeon_runs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
tier INTEGER NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'open', -- open, active, complete, wiped, cancelled
|
||||
leader_id TEXT NOT NULL,
|
||||
current_day INTEGER NOT NULL DEFAULT 0,
|
||||
total_days INTEGER NOT NULL,
|
||||
base_difficulty INTEGER NOT NULL, -- per-floor failure % at zero modifier
|
||||
gold_pool INTEGER NOT NULL DEFAULT 0, -- accumulated funding
|
||||
reward_total INTEGER NOT NULL DEFAULT 0, -- final reward (set on completion)
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
locked_at DATETIME,
|
||||
completed_at DATETIME
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
tier INTEGER NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'open', -- open, active, complete, wiped, cancelled
|
||||
leader_id TEXT NOT NULL,
|
||||
current_day INTEGER NOT NULL DEFAULT 0,
|
||||
total_days INTEGER NOT NULL,
|
||||
base_difficulty INTEGER NOT NULL, -- per-floor failure % at zero modifier
|
||||
gold_pool INTEGER NOT NULL DEFAULT 0, -- accumulated funding
|
||||
reward_total INTEGER NOT NULL DEFAULT 0, -- final reward (set on completion)
|
||||
last_resolved_day INTEGER NOT NULL DEFAULT 0, -- crash-resume guard: highest day whose floor outcome is final
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
locked_at DATETIME,
|
||||
completed_at DATETIME
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_coop_runs_status ON coop_dungeon_runs(status);
|
||||
|
||||
@@ -1397,6 +1400,7 @@ CREATE TABLE IF NOT EXISTS coop_dungeon_members (
|
||||
total_contributed INTEGER NOT NULL DEFAULT 0,
|
||||
is_liability INTEGER NOT NULL DEFAULT 0,
|
||||
daily_funding TEXT NOT NULL DEFAULT '{}', -- JSON: {"1":"standard","2":"all_in",...}
|
||||
member_payout INTEGER, -- NULL until reward credited; idempotency claim
|
||||
PRIMARY KEY (run_id, user_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_coop_members_user ON coop_dungeon_members(user_id);
|
||||
|
||||
Reference in New Issue
Block a user