feat: publish assignment rosters atomically

This commit is contained in:
Josh Creek
2026-09-01 08:08:00 +01:00
parent f520584368
commit 63f4f11aa8
3 changed files with 48 additions and 1 deletions
+38
View File
@@ -86,6 +86,44 @@ func SaveAssignment(ctx context.Context, db *sql.DB, assignment DurableAssignmen
return nil
}
// SaveAssignments publishes a complete signed roster atomically. Assignment
// readiness is a match boundary: exposing only some players would let the
// control plane tell different participants incompatible stories after a
// transient database failure.
func SaveAssignments(ctx context.Context, db *sql.DB, assignments []DurableAssignment) error {
if db == nil || len(assignments) == 0 {
return fmt.Errorf("invalid assignment batch")
}
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
seen := make(map[string]struct{}, len(assignments))
for _, assignment := range assignments {
if err := validateDurableAssignment(assignment); err != nil {
return err
}
key := assignment.MatchID + "\x00" + assignment.PlayerID
if _, ok := seen[key]; ok {
return fmt.Errorf("duplicate assignment in batch")
}
seen[key] = struct{}{}
result, err := tx.ExecContext(ctx, AssignmentUpsertSQL, assignment.MatchID, assignment.PlayerID, assignment.AllocationID, assignment.ServerID, assignment.Slot, assignment.Region, assignment.ClientBuild, assignment.ProtocolVersion, assignment.Transport, assignment.Endpoint, assignment.JoinAuthorisation, assignment.ManifestDigest, assignment.ExpiresAt, assignment.Revision)
if err != nil {
return err
}
changed, err := result.RowsAffected()
if err != nil {
return err
}
if changed != 1 {
return fmt.Errorf("assignment persistence conflict")
}
}
return tx.Commit()
}
func GetAssignment(ctx context.Context, db *sql.DB, playerID, matchID string, now time.Time) (DurableAssignment, error) {
if db == nil || playerID == "" || matchID == "" || now.IsZero() {
return DurableAssignment{}, fmt.Errorf("invalid assignment recovery arguments")
+9
View File
@@ -29,3 +29,12 @@ func TestAssignmentStoreRejectsInvalidRecoveryAndManifestInputs(t *testing.T) {
t.Fatal("out-of-range slot accepted")
}
}
func TestAssignmentStoreRejectsInvalidBatches(t *testing.T) {
if err := SaveAssignments(nil, nil, nil); err == nil {
t.Fatal("nil database/empty batch accepted")
}
if err := SaveAssignments(nil, nil, []DurableAssignment{{MatchID: "match-1", PlayerID: "player-1"}}); err == nil {
t.Fatal("invalid assignment batch accepted")
}
}