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:
+38
-16
@@ -37,7 +37,7 @@ function parseScore(score: RawScore | undefined) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
const client = postgres(DATABASE_URL!, { max: 5 })
|
const client = postgres(DATABASE_URL!, { max: 2 })
|
||||||
const db = drizzle(client)
|
const db = drizzle(client)
|
||||||
|
|
||||||
const teamCache = new Map<string, number>()
|
const teamCache = new Map<string, number>()
|
||||||
@@ -94,17 +94,32 @@ async function run() {
|
|||||||
return (rows[0] as { id: number }).id
|
return (rows[0] as { id: number }).id
|
||||||
}
|
}
|
||||||
|
|
||||||
async function syncGoals(matchId: number, teamId: number, rawGoals: RawGoal[], isOwnGoalTeamId: number) {
|
type GoalRow = { teamId: number; name: string; minute: number | null; offset: number; penalty: boolean; owngoal: boolean }
|
||||||
for (const g of rawGoals) {
|
|
||||||
if (!g.name) continue
|
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 minute = g.minute != null ? parseInt(String(g.minute)) : null
|
||||||
const scoringTeamId = g.owngoal ? isOwnGoalTeamId : teamId
|
return [{ teamId: g.owngoal ? isOwnGoalTeamId : teamId, name: g.name,
|
||||||
await db.execute(sql`
|
minute: isNaN(minute!) ? null : minute, offset: g.offset ?? 0,
|
||||||
INSERT INTO goals (match_id, team_id, player_name, minute, minute_offset, is_penalty, is_own_goal)
|
penalty: g.penalty ?? false, owngoal: g.owngoal ?? false }]
|
||||||
VALUES (${matchId}, ${scoringTeamId}, ${g.name}, ${isNaN(minute!) ? null : minute},
|
})
|
||||||
${g.offset ?? 0}, ${g.penalty ?? false}, ${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 ${sql.join(vals, sql`, `)}
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('\nSyncing 2026...')
|
console.log('\nSyncing 2026...')
|
||||||
@@ -153,9 +168,11 @@ async function run() {
|
|||||||
const score = parseScore(m.score)
|
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)
|
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) {
|
if (m.goals1?.length || m.goals2?.length) {
|
||||||
await db.execute(sql`DELETE FROM goals WHERE match_id = ${matchId}`)
|
const goalRows = [
|
||||||
if (m.goals1?.length) await syncGoals(matchId, t1Id, m.goals1, t2Id)
|
...(m.goals1?.length ? collectGoals(t1Id, m.goals1, t2Id) : []),
|
||||||
if (m.goals2?.length) await syncGoals(matchId, t2Id, m.goals2, t1Id)
|
...(m.goals2?.length ? collectGoals(t2Id, m.goals2, t1Id) : []),
|
||||||
|
]
|
||||||
|
await replaceGoals(matchId, goalRows)
|
||||||
}
|
}
|
||||||
matchCount++
|
matchCount++
|
||||||
goalCount += (m.goals1?.length ?? 0) + (m.goals2?.length ?? 0)
|
goalCount += (m.goals1?.length ?? 0) + (m.goals2?.length ?? 0)
|
||||||
@@ -188,8 +205,13 @@ async function run() {
|
|||||||
const t2Id = await upsertTeam(m.team2)
|
const t2Id = await upsertTeam(m.team2)
|
||||||
const score = parseScore(m.score)
|
const score = parseScore(m.score)
|
||||||
const matchId = await upsertMatch(2026, m.round ?? 'Qualifier', null, m.date ?? null, m.time ?? null, t1Id, t2Id, score, true)
|
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.goals1?.length || m.goals2?.length) {
|
||||||
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
console.log(` Quali playoffs: ${qualiData.matches.length} matches`)
|
console.log(` Quali playoffs: ${qualiData.matches.length} matches`)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user