fix: atomic goal updates in sync — transaction + bulk INSERT
Previously each match goal sync did: DELETE (auto-commit) → N individual INSERTs (each auto-commit). During those ~50ms readers saw 0 goals for the match — the inconsistency window. Now: collectGoals() builds the rows in memory, replaceGoals() wraps the DELETE + single bulk VALUES INSERT in a transaction. Under Postgres READ COMMITTED, readers see the old goals until commit and the full new set after — never an empty window. Also drop sync pool from max:5 → max:2; the job is fully sequential and was holding idle connections unnecessarily. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+35
-13
@@ -37,7 +37,7 @@ function parseScore(score: RawScore | undefined) {
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const client = postgres(DATABASE_URL!, { max: 5 })
|
||||
const client = postgres(DATABASE_URL!, { max: 2 })
|
||||
const db = drizzle(client)
|
||||
|
||||
const teamCache = new Map<string, number>()
|
||||
@@ -94,17 +94,32 @@ async function run() {
|
||||
return (rows[0] as { id: number }).id
|
||||
}
|
||||
|
||||
async function syncGoals(matchId: number, teamId: number, rawGoals: RawGoal[], isOwnGoalTeamId: number) {
|
||||
for (const g of rawGoals) {
|
||||
if (!g.name) continue
|
||||
type GoalRow = { teamId: number; name: string; minute: number | null; offset: number; penalty: boolean; owngoal: boolean }
|
||||
|
||||
function collectGoals(teamId: number, rawGoals: RawGoal[], isOwnGoalTeamId: number): GoalRow[] {
|
||||
return rawGoals.flatMap(g => {
|
||||
if (!g.name) return []
|
||||
const minute = g.minute != null ? parseInt(String(g.minute)) : null
|
||||
const scoringTeamId = g.owngoal ? isOwnGoalTeamId : teamId
|
||||
await db.execute(sql`
|
||||
return [{ teamId: g.owngoal ? isOwnGoalTeamId : teamId, name: g.name,
|
||||
minute: isNaN(minute!) ? null : minute, offset: g.offset ?? 0,
|
||||
penalty: g.penalty ?? false, owngoal: g.owngoal ?? false }]
|
||||
})
|
||||
}
|
||||
|
||||
async function replaceGoals(matchId: number, rows: GoalRow[]) {
|
||||
await db.transaction(async tx => {
|
||||
await tx.execute(sql`DELETE FROM goals WHERE match_id = ${matchId}`)
|
||||
if (rows.length > 0) {
|
||||
// Single bulk INSERT — readers see old goals until commit, never an empty window
|
||||
const vals = rows.map(g =>
|
||||
sql`(${matchId}, ${g.teamId}, ${g.name}, ${g.minute}, ${g.offset}, ${g.penalty}, ${g.owngoal})`
|
||||
)
|
||||
await tx.execute(sql`
|
||||
INSERT INTO goals (match_id, team_id, player_name, minute, minute_offset, is_penalty, is_own_goal)
|
||||
VALUES (${matchId}, ${scoringTeamId}, ${g.name}, ${isNaN(minute!) ? null : minute},
|
||||
${g.offset ?? 0}, ${g.penalty ?? false}, ${g.owngoal ?? false})
|
||||
VALUES ${sql.join(vals, sql`, `)}
|
||||
`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
console.log('\nSyncing 2026...')
|
||||
@@ -153,9 +168,11 @@ async function run() {
|
||||
const score = parseScore(m.score)
|
||||
const matchId = await upsertMatch(2026, m.round ?? 'Unknown', m.group ?? null, m.date ?? null, m.time ?? null, t1Id, t2Id, score, false)
|
||||
if (m.goals1?.length || m.goals2?.length) {
|
||||
await db.execute(sql`DELETE FROM goals WHERE match_id = ${matchId}`)
|
||||
if (m.goals1?.length) await syncGoals(matchId, t1Id, m.goals1, t2Id)
|
||||
if (m.goals2?.length) await syncGoals(matchId, t2Id, m.goals2, t1Id)
|
||||
const goalRows = [
|
||||
...(m.goals1?.length ? collectGoals(t1Id, m.goals1, t2Id) : []),
|
||||
...(m.goals2?.length ? collectGoals(t2Id, m.goals2, t1Id) : []),
|
||||
]
|
||||
await replaceGoals(matchId, goalRows)
|
||||
}
|
||||
matchCount++
|
||||
goalCount += (m.goals1?.length ?? 0) + (m.goals2?.length ?? 0)
|
||||
@@ -188,8 +205,13 @@ async function run() {
|
||||
const t2Id = await upsertTeam(m.team2)
|
||||
const score = parseScore(m.score)
|
||||
const matchId = await upsertMatch(2026, m.round ?? 'Qualifier', null, m.date ?? null, m.time ?? null, t1Id, t2Id, score, true)
|
||||
if (m.goals1?.length) await syncGoals(matchId, t1Id, m.goals1, t2Id)
|
||||
if (m.goals2?.length) await syncGoals(matchId, t2Id, m.goals2, t1Id)
|
||||
if (m.goals1?.length || m.goals2?.length) {
|
||||
const goalRows = [
|
||||
...(m.goals1?.length ? collectGoals(t1Id, m.goals1, t2Id) : []),
|
||||
...(m.goals2?.length ? collectGoals(t2Id, m.goals2, t1Id) : []),
|
||||
]
|
||||
await replaceGoals(matchId, goalRows)
|
||||
}
|
||||
}
|
||||
console.log(` Quali playoffs: ${qualiData.matches.length} matches`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user