Skip to content

Commit b6a6735

Browse files
fix(tables): address PR review on dispatch cap + cascade drain
- Don't consume the row cap when batchEnqueueAndWait fails; a transient failure no longer completes a capped dispatch with zero rows started. - Outer cascade-drain loop only re-drives a genuine queued marker, not any eligible group, so an empty-output group can't re-run forever. - completeDispatch forwards limit on the terminal SSE event. - Extract shared LIMITED_RUN_PRESETS for the Run-N-rows menu items.
1 parent a7bc6ea commit b6a6735

3 files changed

Lines changed: 32 additions & 15 deletions

File tree

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/workflow-group-meta-cell.tsx

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ import type { DisplayColumn } from '../types'
3131

3232
const WORKFLOW_META_BG_ALPHA = 12 // 0–255
3333

34+
/** Fixed row-cap presets for the "Run N empty rows" shortcuts. Shared by the
35+
* group-header options menu and the inline quick-run dropdown so the two
36+
* surfaces stay in sync. */
37+
const LIMITED_RUN_PRESETS = [10, 1000] as const
38+
3439
interface ColumnOptionsMenuProps {
3540
open: boolean
3641
onOpenChange: (open: boolean) => void
@@ -133,16 +138,12 @@ export function ColumnOptionsMenu({
133138
<DropdownMenuItem onSelect={() => onRunColumnIncomplete?.()}>
134139
Run empty rows
135140
</DropdownMenuItem>
136-
{onRunColumnLimited && (
137-
<>
138-
<DropdownMenuItem onSelect={() => onRunColumnLimited(10)}>
139-
Run 10 empty rows
140-
</DropdownMenuItem>
141-
<DropdownMenuItem onSelect={() => onRunColumnLimited(1000)}>
142-
Run 1,000 empty rows
141+
{onRunColumnLimited &&
142+
LIMITED_RUN_PRESETS.map((max) => (
143+
<DropdownMenuItem key={max} onSelect={() => onRunColumnLimited(max)}>
144+
{`Run ${max.toLocaleString()} empty rows`}
143145
</DropdownMenuItem>
144-
</>
145-
)}
146+
))}
146147
</DropdownMenuSubContent>
147148
</DropdownMenuSub>
148149
<DropdownMenuSeparator />
@@ -448,12 +449,11 @@ export function WorkflowGroupMetaCell({
448449
)}
449450
<DropdownMenuItem onSelect={handleRunAll}>Run all rows</DropdownMenuItem>
450451
<DropdownMenuItem onSelect={handleRunIncomplete}>Run empty rows</DropdownMenuItem>
451-
<DropdownMenuItem onSelect={() => handleRunLimited(10)}>
452-
Run 10 empty rows
453-
</DropdownMenuItem>
454-
<DropdownMenuItem onSelect={() => handleRunLimited(1000)}>
455-
Run 1,000 empty rows
456-
</DropdownMenuItem>
452+
{LIMITED_RUN_PRESETS.map((max) => (
453+
<DropdownMenuItem key={max} onSelect={() => handleRunLimited(max)}>
454+
{`Run ${max.toLocaleString()} empty rows`}
455+
</DropdownMenuItem>
456+
))}
457457
</DropdownMenuContent>
458458
</DropdownMenu>
459459
)}

apps/sim/background/workflow-column-execution.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,15 @@ export async function executeWorkflowGroupCellJob(
5757
if (!freshRow) break
5858
const next = pickNextEligibleGroupForRow(freshTable, freshRow)
5959
if (!next) break
60+
// Only re-drive a genuine queued marker (an explicit run request whose
61+
// cell-task bailed during our release window). The inner cascade loop has
62+
// already drained every auto-eligible group, so re-driving a non-marker
63+
// group here would re-run forever — e.g. a group that completed with empty
64+
// outputs stays auto-eligible (the inner loop excludes it via
65+
// `excludeGroupId`, but this outer pass has no such anchor).
66+
const nextExec = freshRow.executions?.[next.id]
67+
const hasQueuedMarker = nextExec?.status === 'pending' && nextExec.executionId == null
68+
if (!hasQueuedMarker) break
6069
currentPayload = {
6170
...currentPayload,
6271
groupId: next.id,

apps/sim/lib/table/dispatcher.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,13 @@ export async function dispatcherStep(dispatchId: string): Promise<DispatcherStep
492492
logger.error(`[${dispatchId}] batch dispatch failed`, {
493493
error: toError(err).message,
494494
})
495+
// These rows never actually ran, so they must not consume the row cap —
496+
// otherwise a transient failure on the only window of a `max: N` run would
497+
// exhaust the budget and complete the dispatch with zero rows started.
498+
// The cursor still advances past the window (cells are flipped to a
499+
// re-runnable `error` below), so later windows fulfill the remaining cap.
500+
dispatchedRows = 0
501+
budgetExhausted = false
495502
// Cursor advances past this window, so flip the un-claimed pre-stamps to
496503
// terminal `error` (+ SSE) — visible, not stuck pending, re-runnable.
497504
const failedAt = new Date()
@@ -573,6 +580,7 @@ async function completeDispatch(dispatch: DispatchRow, cursor: number): Promise<
573580
cursor,
574581
mode: dispatch.mode,
575582
isManualRun: dispatch.isManualRun,
583+
...(dispatch.limit ? { limit: dispatch.limit } : {}),
576584
})
577585
}
578586

0 commit comments

Comments
 (0)