Skip to content

Commit 28290e2

Browse files
committed
fix(provenance): clear the rows that went unknown after the first repair
0005 is finished and will not run again — the runner records a name in script_migrations and never offers it back, which is the contract a run-once repair wants. But it cleared the backlog that existed at the instant it ran, and the writer that produced that backlog kept running until the fix in this branch. Nothing heals such a row in place, so each one goes on reporting on every later read; a few dozen of them account for thousands of log lines a week. A second entry rather than deleting the first's tracking row: the registry is append-only, and a repair that ran twice should say so twice. It shares 0005's walk rather than restating it — the parent-first lock ordering and the status re-check under that lock are subtleties worth having once — and is idempotent, so it costs one empty query if there is nothing left to repair.
1 parent a721a76 commit 28290e2

5 files changed

Lines changed: 85 additions & 20 deletions

packages/db/script-migrations-paused-billing-attribution.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,7 @@ describe('script migration registry', () => {
442442
'0003_backfill_workspace_storage_usage',
443443
'0004_backfill_fork_kb_file_ownership',
444444
'0005_repair_unknown_table_row_provenance',
445+
'0006_repair_unknown_table_row_provenance_second_pass',
445446
])
446447
})
447448
})

packages/db/script-migrations/0005_repair_unknown_table_row_provenance.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import type { Sql } from 'postgres'
55
import { describe, expect, it, vi } from 'vitest'
66
import { repairUnknownTableRowProvenance } from './0005_repair_unknown_table_row_provenance'
7+
import { repairUnknownTableRowProvenanceSecondPass } from './0006_repair_unknown_table_row_provenance_second_pass'
78

89
function normalizeSql(value: string): string {
910
return value.replace(/\s+/g, ' ').trim()
@@ -105,3 +106,31 @@ describe('0005 repair unknown table row provenance', () => {
105106
expect(cursors).toEqual(['', 'row-1'])
106107
})
107108
})
109+
110+
/**
111+
* 0005 is finished — the runner records a name and never offers it again — but it cleared only the
112+
* backlog that existed when it ran, and the writers producing that backlog kept running. The second
113+
* pass exists to clear what accumulated since, and shares the first's implementation because the
114+
* lock ordering and the status re-check are subtleties worth having once.
115+
*/
116+
describe('0006 second pass', () => {
117+
it('repairs on the same walk as the first pass rather than restating it', async () => {
118+
const { sql, statements, cursors } = createSqlHarness([['row-1'], []])
119+
120+
await repairUnknownTableRowProvenanceSecondPass.up(sql)
121+
122+
expect(cursors).toEqual(['', 'row-1'])
123+
const lockIndex = statements.findIndex((statement) => statement.includes('FOR UPDATE'))
124+
const deleteIndex = statements.findIndex((statement) =>
125+
statement.startsWith('DELETE FROM user_table_row_secret_provenance')
126+
)
127+
expect(deleteIndex).toBeGreaterThan(lockIndex)
128+
expect(statements[deleteIndex]).toContain("AND status = 'unknown'")
129+
})
130+
131+
it('is a distinct entry so a repair that ran twice is recorded twice', () => {
132+
expect(repairUnknownTableRowProvenanceSecondPass.name).not.toBe(
133+
repairUnknownTableRowProvenance.name
134+
)
135+
})
136+
})

packages/db/script-migrations/0005_repair_unknown_table_row_provenance.ts

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -107,26 +107,28 @@ async function repairUnknownProvenancePage(
107107
* past the page instead makes each pass finite and the whole walk terminate on the only condition
108108
* that means finished: a page with no candidates left in it.
109109
*/
110+
export async function runUnknownTableRowProvenanceRepair(sql: Sql): Promise<void> {
111+
let repaired = 0
112+
let skipped = 0
113+
let afterRowId = ''
114+
for (;;) {
115+
const page = await repairUnknownProvenancePage(
116+
sql,
117+
UNKNOWN_PROVENANCE_REPAIR_BATCH_SIZE,
118+
afterRowId
119+
)
120+
if (page.candidates === 0 || page.lastRowId === null) break
121+
repaired += page.repaired
122+
skipped += page.candidates - page.repaired
123+
afterRowId = page.lastRowId
124+
console.log(` repaired ${repaired} unknown table row(s)`)
125+
}
126+
console.log(
127+
`Unknown table row provenance repair complete: ${repaired} row(s) repaired, ${skipped} left to a concurrent writer.`
128+
)
129+
}
130+
110131
export const repairUnknownTableRowProvenance: ScriptMigration = {
111132
name: '0005_repair_unknown_table_row_provenance',
112-
async up(sql: Sql): Promise<void> {
113-
let repaired = 0
114-
let skipped = 0
115-
let afterRowId = ''
116-
for (;;) {
117-
const page = await repairUnknownProvenancePage(
118-
sql,
119-
UNKNOWN_PROVENANCE_REPAIR_BATCH_SIZE,
120-
afterRowId
121-
)
122-
if (page.candidates === 0 || page.lastRowId === null) break
123-
repaired += page.repaired
124-
skipped += page.candidates - page.repaired
125-
afterRowId = page.lastRowId
126-
console.log(` repaired ${repaired} unknown table row(s)`)
127-
}
128-
console.log(
129-
`Unknown table row provenance repair complete: ${repaired} row(s) repaired, ${skipped} left to a concurrent writer.`
130-
)
131-
},
133+
up: runUnknownTableRowProvenanceRepair,
132134
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { Sql } from 'postgres'
2+
import { runUnknownTableRowProvenanceRepair } from './0005_repair_unknown_table_row_provenance'
3+
import type { ScriptMigration } from './types'
4+
5+
/**
6+
* Clears the rows that went `unknown` after 0005 had already run.
7+
*
8+
* 0005 was correct and is finished: the runner records a name in `script_migrations` and never
9+
* offers it again, which is exactly the contract a run-once repair wants. But it cleared the
10+
* backlog that existed at the instant it ran, and the writers that produced that backlog kept
11+
* running afterwards — a table write whose block could not project one input latched the run's
12+
* registry, so its rows were stored unrecorded, for as long as that bug was live.
13+
*
14+
* Nothing heals such a row in place; a partial cell update keeps it unknown and only a full replace
15+
* carrying complete provenance clears it. So each one goes on reporting on every later read, which
16+
* is why a few dozen rows account for thousands of log lines a week. A second pass is the whole
17+
* remedy.
18+
*
19+
* A new entry rather than deleting 0005's tracking row: the registry is append-only, and a repair
20+
* that ran twice should say so twice. Ordered after the fix that stopped producing these — repairing
21+
* while the writer still creates them only refills the backlog.
22+
*
23+
* Shares 0005's implementation rather than restating it. The walk locks the parent row before the
24+
* sidecar to match the application writer's order, and re-checks `status` under that lock so a
25+
* concurrently committed exact sidecar is never deleted — subtleties worth having once, not twice.
26+
* Idempotent, so it costs one empty query when there is nothing left to repair.
27+
*/
28+
export const repairUnknownTableRowProvenanceSecondPass: ScriptMigration = {
29+
name: '0006_repair_unknown_table_row_provenance_second_pass',
30+
up: (sql: Sql) => runUnknownTableRowProvenanceRepair(sql),
31+
}

packages/db/script-migrations/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { backfillPausedBillingAttribution } from './0002_backfill_paused_billing
44
import { backfillWorkspaceStorageUsage } from './0003_backfill_workspace_storage_usage'
55
import { backfillForkKnowledgeBaseFileOwnership } from './0004_backfill_fork_kb_file_ownership'
66
import { repairUnknownTableRowProvenance } from './0005_repair_unknown_table_row_provenance'
7+
import { repairUnknownTableRowProvenanceSecondPass } from './0006_repair_unknown_table_row_provenance_second_pass'
78
import type { ScriptMigration } from './types'
89

910
export type { ScriptMigration } from './types'
@@ -19,6 +20,7 @@ export const scriptMigrations: readonly ScriptMigration[] = [
1920
backfillWorkspaceStorageUsage,
2021
backfillForkKnowledgeBaseFileOwnership,
2122
repairUnknownTableRowProvenance,
23+
repairUnknownTableRowProvenanceSecondPass,
2224
]
2325

2426
/**

0 commit comments

Comments
 (0)