mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-15 06:22:03 +00:00
feat: add result transaction SQL boundary
This commit is contained in:
+1
-1
@@ -1202,7 +1202,7 @@ the local/CI/community transport, not a silent production fallback.
|
|||||||
| 8.22 `[D:8.21]` | **IN PROGRESS.** Pure Go ranked profile exposes the first ten games as provisional and keeps casual ratings outside the API | `RankedIsProvisional` covers the 0–9/10 boundary; authoritative tier derivation and UI remain |
|
| 8.22 `[D:8.21]` | **IN PROGRESS.** Pure Go ranked profile exposes the first ten games as provisional and keeps casual ratings outside the API | `RankedIsProvisional` covers the 0–9/10 boundary; authoritative tier derivation and UI remain |
|
||||||
| 8.23 `[D:8.21]` | **IN PROGRESS.** Pure Go ranked-only season rollover compresses 25% toward 1500, clamps RD to 200–350, preserves volatility/history and is idempotent by season ID | `ApplySeasonRollover` covers compression, floor/cap and duplicate replay; PostgreSQL transaction locking and 12-week scheduler remain |
|
| 8.23 `[D:8.21]` | **IN PROGRESS.** Pure Go ranked-only season rollover compresses 25% toward 1500, clamps RD to 200–350, preserves volatility/history and is idempotent by season ID | `ApplySeasonRollover` covers compression, floor/cap and duplicate replay; PostgreSQL transaction locking and 12-week scheduler remain |
|
||||||
| 8.24 `[D:8.9,8.20,8.21]` | **IN PROGRESS.** Pure Go ranked connection policy binds match/server/player/team/slot/protocol, supports 60 s reclaim with server-owned generations, fences old connections, and applies the rolling 7-day 5 m/15 m/1 h/24 h abandon ladder | `server/domain/reconnect.go` covers repeated backend-independent reclaim, binding rejection, old-generation fencing, grace boundary and deterministic cooldown audit ordering; signed authorisations, persistent lease fencing, join transport and full match/result integration remain |
|
| 8.24 `[D:8.9,8.20,8.21]` | **IN PROGRESS.** Pure Go ranked connection policy binds match/server/player/team/slot/protocol, supports 60 s reclaim with server-owned generations, fences old connections, and applies the rolling 7-day 5 m/15 m/1 h/24 h abandon ladder | `server/domain/reconnect.go` covers repeated backend-independent reclaim, binding rejection, old-generation fencing, grace boundary and deterministic cooldown audit ordering; signed authorisations, persistent lease fencing, join transport and full match/result integration remain |
|
||||||
| 8.25 `[D:8.10,8.24]` | **IN PROGRESS.** Pure Go result policy binds match/server/workload identity, hashes canonical payloads, makes identical retries idempotent, leaves conflicts inert, separates integrity eligibility, validates annotation signatures/digests, and exposes 5 m alert/30 m review delivery thresholds | `server/domain/result.go` and adversarial fixtures cover binding, duplicate/conflict, annotation forgery, commit and delivery-health invariants; production credential verification, Agones annotation persistence/reconciliation, PostgreSQL atomic rating/outbox transaction and integrity-classification adapters remain |
|
| 8.25 `[D:8.10,8.24]` | **IN PROGRESS.** Pure Go result policy binds match/server/workload identity, hashes canonical payloads, makes identical retries idempotent, leaves conflicts inert, separates integrity eligibility, validates annotation signatures/digests, and exposes 5 m alert/30 m review delivery thresholds; Go store SQL now defines conflict-safe receipt insert, deterministic match/rating locks and atomic completion/outbox boundaries | `server/domain/result.go` plus `server/store/result_sql.go` and adversarial fixtures cover binding, duplicate/conflict, annotation forgery, commit, lock ordering and delivery-health invariants; production credential verification, Agones annotation persistence/reconciliation, live PostgreSQL execution and integrity-classification adapters remain |
|
||||||
|
|
||||||
#### 8D — Agones, allocation and regional scaling
|
#### 8D — Agones, allocation and regional scaling
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
// ResultReceiptInsertSQL intentionally uses DO NOTHING. The adapter must
|
||||||
|
// select the existing receipt afterward and compare its digest; an identical
|
||||||
|
// retry is acknowledged, while a different payload is a conflict with no
|
||||||
|
// update side effect.
|
||||||
|
const ResultReceiptInsertSQL = `INSERT INTO result_receipts
|
||||||
|
(result_id, match_id, result_nonce, payload_digest, integrity_state, received_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
|
ON CONFLICT DO NOTHING`
|
||||||
|
|
||||||
|
const ResultReceiptSelectSQL = `SELECT result_id, match_id, result_nonce, payload_digest,
|
||||||
|
integrity_state, received_at, committed_at
|
||||||
|
FROM result_receipts
|
||||||
|
WHERE match_id = $1
|
||||||
|
FOR UPDATE`
|
||||||
|
|
||||||
|
// ResultCommitLockSQL establishes the match lock before participant/rating
|
||||||
|
// locks. Rating rows are then locked in lexical player-ID order by the
|
||||||
|
// adapter, ensuring every concurrent result computes from one snapshot.
|
||||||
|
const ResultCommitLockSQL = `SELECT match_id, playlist, state
|
||||||
|
FROM matches
|
||||||
|
WHERE match_id = $1 AND server_id = $2
|
||||||
|
FOR UPDATE`
|
||||||
|
|
||||||
|
const ResultMatchCompleteSQL = `UPDATE matches
|
||||||
|
SET state = 'COMPLETED', revision = revision + 1, completed_at = $2
|
||||||
|
WHERE match_id = $1 AND state = 'RESULT_PENDING'`
|
||||||
|
|
||||||
|
const ResultOutboxSQL = `INSERT INTO outbox
|
||||||
|
(event_id, aggregate_type, aggregate_id, revision, event_type, payload)
|
||||||
|
VALUES ($1, 'match', $2, $3, 'match_completed', $4)`
|
||||||
|
|
||||||
|
const RatingLockSQL = `SELECT player_id, rating, deviation, volatility, ranked_games, revision
|
||||||
|
FROM ratings
|
||||||
|
WHERE player_id = ANY($1)
|
||||||
|
ORDER BY player_id
|
||||||
|
FOR UPDATE`
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestResultSQLPreservesReceiptConflictAndAtomicCommitBoundaries(t *testing.T) {
|
||||||
|
checks := map[string][]string{
|
||||||
|
ResultReceiptInsertSQL: {"ON CONFLICT DO NOTHING", "payload_digest", "integrity_state"},
|
||||||
|
ResultReceiptSelectSQL: {"FOR UPDATE", "committed_at"},
|
||||||
|
ResultCommitLockSQL: {"server_id = $2", "FOR UPDATE"},
|
||||||
|
ResultMatchCompleteSQL: {"state = 'RESULT_PENDING'", "revision = revision + 1"},
|
||||||
|
ResultOutboxSQL: {"match_completed", "aggregate_id", "revision"},
|
||||||
|
RatingLockSQL: {"ORDER BY player_id", "FOR UPDATE"},
|
||||||
|
}
|
||||||
|
for query, fragments := range checks {
|
||||||
|
for _, fragment := range fragments {
|
||||||
|
if !contains(query, fragment) {
|
||||||
|
t.Fatalf("query %q missing %q", query, fragment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(value, fragment string) bool {
|
||||||
|
for i := 0; i+len(fragment) <= len(value); i++ {
|
||||||
|
if value[i:i+len(fragment)] == fragment {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user