Skip to content

Commit 98bfafd

Browse files
fix(tables): seek pending fold tables by index and document the sweep result
1 parent 6a92ca3 commit 98bfafd

2 files changed

Lines changed: 35 additions & 15 deletions

File tree

‎apps/sim/lib/table/row-changes.integration.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,10 @@ describe('table row-change log against real PostgreSQL', () => {
152152
}
153153
})()
154154

155-
await Promise.all(appenders)
155+
const appended = await Promise.allSettled(appenders)
156156
appending = false
157157
await Promise.all([folder, reader])
158+
for (const result of appended) if (result.status === 'rejected') throw result.reason
158159

159160
const total = writers * appendsPerWriter
160161
expect(await readCurrentRowsVersion(tableId)).toBe(total)

‎apps/sim/lib/table/row-changes.ts‎

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@
1717
*/
1818

1919
import { db } from '@sim/db'
20-
import { userTableDefinitions, userTableRowChanges } from '@sim/db/schema'
20+
import { userTableDefinitions } from '@sim/db/schema'
2121
import { createLogger } from '@sim/logger'
22-
import { and, eq, notInArray, sql } from 'drizzle-orm'
22+
import { and, eq, sql } from 'drizzle-orm'
2323
import { setTableTxTimeouts } from '@/lib/table/tx'
2424

2525
const logger = createLogger('TableRowChanges')
2626

27-
/** Tables folded per sweep query; the sweep loops until the log is empty or its budget runs out. */
27+
/** Tables per pending-table page; the sweep pages until the log is empty or its budget runs out. */
2828
const FOLD_SWEEP_PAGE_SIZE = 500
2929

3030
/**
@@ -100,36 +100,55 @@ export async function foldTableRowChanges(tableId: string): Promise<boolean> {
100100
})
101101
}
102102

103+
/** Outcome of one fold sweep. */
103104
export interface FoldSweepResult {
105+
/** Tables whose log rows were folded into their definition row. */
104106
folded: number
107+
/** Tables left for the next sweep because another transaction held their definition row. */
105108
skipped: number
109+
/** Whether the sweep stopped at its budget with tables still unvisited. */
106110
budgetExhausted: boolean
107111
}
108112

109113
/**
110-
* Folds each table with unfolded log rows once, until none is left or `budgetMs` elapses. Rows a
111-
* table logs after its fold wait for the next sweep.
114+
* The next page of distinct table ids in the log after `afterTableId`, by loose index scan: each
115+
* step seeks the next id through the `table_id` index, so the cost follows the number of tables,
116+
* not the number of log rows a busy table has piled up.
117+
*/
118+
async function nextPendingTableIds(afterTableId: string): Promise<string[]> {
119+
const rows = await db.execute<{ table_id: string }>(sql`
120+
WITH RECURSIVE pending AS (
121+
(SELECT table_id FROM user_table_row_changes
122+
WHERE table_id > ${afterTableId} ORDER BY table_id LIMIT 1)
123+
UNION ALL
124+
SELECT (SELECT c.table_id FROM user_table_row_changes c
125+
WHERE c.table_id > p.table_id ORDER BY c.table_id LIMIT 1)
126+
FROM pending p WHERE p.table_id IS NOT NULL
127+
)
128+
SELECT table_id FROM pending WHERE table_id IS NOT NULL LIMIT ${FOLD_SWEEP_PAGE_SIZE}`)
129+
return rows.map((row) => row.table_id)
130+
}
131+
132+
/**
133+
* Folds each table with unfolded log rows once, in table-id order, until none is left or
134+
* `budgetMs` elapses. Rows a table logs after its fold wait for the next sweep.
112135
*/
113136
export async function foldPendingTableRowChanges(budgetMs: number): Promise<FoldSweepResult> {
114137
const deadline = Date.now() + budgetMs
115-
const visited = new Set<string>()
138+
let afterTableId = ''
116139
let folded = 0
117140
let skipped = 0
118141

119142
for (;;) {
120-
const pending = await db
121-
.selectDistinct({ tableId: userTableRowChanges.tableId })
122-
.from(userTableRowChanges)
123-
.where(visited.size > 0 ? notInArray(userTableRowChanges.tableId, [...visited]) : undefined)
124-
.limit(FOLD_SWEEP_PAGE_SIZE)
125-
if (pending.length === 0) return { folded, skipped, budgetExhausted: false }
143+
const tableIds = await nextPendingTableIds(afterTableId)
144+
if (tableIds.length === 0) return { folded, skipped, budgetExhausted: false }
126145

127-
for (const { tableId } of pending) {
146+
for (const tableId of tableIds) {
128147
if (Date.now() >= deadline) {
129148
logger.warn('Table row-change fold sweep ran out of budget', { folded, skipped, budgetMs })
130149
return { folded, skipped, budgetExhausted: true }
131150
}
132-
visited.add(tableId)
151+
afterTableId = tableId
133152
if (await foldTableRowChanges(tableId)) folded++
134153
else skipped++
135154
}

0 commit comments

Comments
 (0)