Skip to content

Commit 0a985c7

Browse files
committed
fix(tables): scope dispatch liveness to its rows, not just its groups
The previous round narrowed the cell-liveness probe to the dispatch's groups on the reasoning that two active dispatches over the same groups cannot coexist, because starting a run cancels prior work on its scope. That reasoning was wrong. `cancelPriorRuns` in `workflow-columns` requires `isManualRun`, so auto-fired runs never cancel anything, and the per-row path is explicitly a no-op for dispatch cancellation. Same-group coexistence is ordinary. A dispatch that names rows now only accepts liveness from those rows, which covers the auto-fired and row-scoped runs that reach this state. What remains is two table-wide dispatches over the same groups, where nothing in the row execution says whose work it is; closing that needs a `dispatch_id` column on `table_row_executions` threaded through six write sites, including the shared cell-write path every cell task uses. That residue is a delay rather than a permanent mask — the live dispatch's cells stop updating when it finishes, and the next sweep after a quiet window reclaims the abandoned row.
1 parent b347f91 commit 0a985c7

2 files changed

Lines changed: 33 additions & 7 deletions

File tree

apps/sim/lib/table/dispatcher.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -858,13 +858,24 @@ export async function cancelStaleDispatches(
858858
* partial `(table_id, status)` index that already covers exactly these three
859859
* statuses.
860860
*
861-
* Narrowed to the dispatch's OWN groups, because `table_row_executions` has no
862-
* dispatch column. Table-scoped, a live dispatch's cells would read as
863-
* evidence that an abandoned dispatch beside it was still working, and the
864-
* abandoned one would never be reclaimed — the stuck overlay this sweep exists
865-
* to clear, now permanent. Two active dispatches over the SAME groups can
866-
* still mask each other, but that is the state `markActiveDispatchesCancelled`
867-
* already prevents: starting a run cancels prior work on its scope.
861+
* Narrowed to the dispatch's own scope — its groups, and its rows when it
862+
* names any — because `table_row_executions` has no dispatch column. Left
863+
* table-scoped, a live dispatch's cells read as evidence that an abandoned
864+
* dispatch beside it was still working, and the abandoned one is never
865+
* reclaimed: the stuck overlay this sweep exists to clear, made permanent.
866+
*
867+
* The row filter is what covers the auto-fired and row-scoped runs, which do
868+
* NOT cancel overlapping dispatches — `cancelPriorRuns` in `workflow-columns`
869+
* requires `isManualRun`, and the per-row path is a no-op for dispatch
870+
* cancellation — so same-group coexistence is ordinary, not exceptional.
871+
*
872+
* What remains is two table-wide dispatches over the same groups, where
873+
* nothing in the row execution distinguishes whose work it is. Closing that
874+
* needs a `dispatch_id` on `table_row_executions`, threaded through six write
875+
* sites including the shared cell-write path. Until then the residue is a
876+
* delay rather than a permanent mask: the live dispatch's cells stop updating
877+
* when it finishes, and the next sweep after a quiet window reclaims the
878+
* abandoned row.
868879
*/
869880
const isStale = () =>
870881
and(
@@ -876,6 +887,12 @@ export async function cancelStaleDispatches(
876887
AND ${tableRowExecutions.groupId} IN (
877888
SELECT jsonb_array_elements_text(${tableRunDispatches.scope} -> 'groupIds')
878889
)
890+
AND (
891+
jsonb_typeof(${tableRunDispatches.scope} -> 'rowIds') <> 'array'
892+
OR ${tableRowExecutions.rowId} IN (
893+
SELECT jsonb_array_elements_text(${tableRunDispatches.scope} -> 'rowIds')
894+
)
895+
)
879896
AND ${tableRowExecutions.status} IN ('queued', 'running', 'pending')
880897
AND ${tableRowExecutions.updatedAt} >= ${sql.param(staleBefore, tableRowExecutions.updatedAt)}
881898
)`

apps/sim/lib/table/stale-dispatch-recovery.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,15 @@ describe('cancelStaleDispatches', () => {
121121
*/
122122
expect(chunks).toContain('tableRowExecutions.groupId')
123123
expect(chunks.some((chunk) => chunk.includes('jsonb_array_elements_text'))).toBe(true)
124+
/**
125+
* Rows too, when the dispatch names any. Auto-fired and row-scoped runs do
126+
* NOT cancel overlapping dispatches — `cancelPriorRuns` requires
127+
* `isManualRun`, and the per-row path is a no-op for dispatch cancellation —
128+
* so a live dispatch sharing a group is ordinary, and only the row filter
129+
* keeps its cells from vouching for an abandoned neighbour.
130+
*/
131+
expect(chunks).toContain('tableRowExecutions.rowId')
132+
expect(chunks.some((chunk) => chunk.includes("'rowIds'"))).toBe(true)
124133
})
125134

126135
it('emits the terminal event so a stuck client overlay clears', async () => {

0 commit comments

Comments
 (0)