@@ -40,11 +40,6 @@ import {
4040
4141const logger = createLogger ( 'TableRunDispatcher' )
4242
43- /** Window size matches the cell-execution concurrency cap so one window
44- * saturates the pool before the next is loaded — yields a row-major
45- * scan-line crawl (rows 1-20 finish before 21-40 start). */
46- const WINDOW_SIZE = TABLE_CONCURRENCY_LIMIT
47-
4843const ACTIVE_DISPATCH_STATUSES = [ 'pending' , 'dispatching' ] as const
4944
5045export type DispatchStatus = 'pending' | 'dispatching' | 'complete' | 'cancelled'
@@ -323,14 +318,23 @@ export async function readDispatch(dispatchId: string): Promise<DispatchRow | nu
323318
324319/** Drive `dispatcherStep` to completion. Shared between the trigger.dev task
325320 * wrapper (`tableRunDispatcherTask`) and the in-process inline path so both
326- * runtimes use identical loop semantics + error logging. */
327- export async function runDispatcherToCompletion ( dispatchId : string ) : Promise < void > {
328- while ( ( await dispatcherStep ( dispatchId ) ) === 'continue' ) { }
321+ * runtimes use identical loop semantics + error logging. `concurrency` is the
322+ * invoker's plan-resolved window size (see `resolveTableDispatchConcurrency`),
323+ * threaded via the task payload; absent on payloads from before the field
324+ * existed → legacy cap. */
325+ export async function runDispatcherToCompletion (
326+ dispatchId : string ,
327+ concurrency ?: number
328+ ) : Promise < void > {
329+ while ( ( await dispatcherStep ( dispatchId , concurrency ) ) === 'continue' ) { }
329330}
330331
331332/** Run one window of the dispatcher state machine. Caller re-invokes (via the
332333 * trigger.dev task wrapper) until the returned status is `'done'`. */
333- export async function dispatcherStep ( dispatchId : string ) : Promise < DispatcherStepResult > {
334+ export async function dispatcherStep (
335+ dispatchId : string ,
336+ concurrency ?: number
337+ ) : Promise < DispatcherStepResult > {
334338 const dispatch = await readDispatch ( dispatchId )
335339 if ( ! dispatch ) {
336340 logger . warn ( `[${ dispatchId } ] dispatch row missing — aborting` )
@@ -380,6 +384,11 @@ export async function dispatcherStep(dispatchId: string): Promise<DispatcherStep
380384 } )
381385 }
382386
387+ // Window size = the invoker's plan-resolved parallelism, so one window
388+ // saturates the cell pool before the next is loaded — yields a row-major
389+ // scan-line crawl. Payloads without the field fall back to the legacy cap.
390+ const windowSize = concurrency ?? TABLE_CONCURRENCY_LIMIT
391+
383392 const filters = [
384393 eq ( userTableRows . tableId , dispatch . tableId ) ,
385394 gt ( userTableRows . position , dispatch . cursor ) ,
@@ -429,7 +438,7 @@ export async function dispatcherStep(dispatchId: string): Promise<DispatcherStep
429438 . from ( userTableRows )
430439 . where ( and ( ...filters ) )
431440 . orderBy ( asc ( userTableRows . position ) )
432- . limit ( WINDOW_SIZE )
441+ . limit ( windowSize )
433442 // Filtered scopes carry a jsonb predicate the planner can't estimate — left alone it
434443 // seq-scans the whole shared relation per window; keep it on the tenant's position index.
435444 const chunk = hasJsonbFilter
@@ -533,8 +542,8 @@ export async function dispatcherStep(dispatchId: string): Promise<DispatcherStep
533542 // (CRIU-checkpointed wait); database backend calls the cell-task runner
534543 // directly via Promise.all (skips async_jobs since we're awaiting in-
535544 // process anyway). Either way the parent dispatcher blocks until every
536- // cell in the window terminates — bounds queue depth at WINDOW_SIZE .
537- const items = await buildEnqueueItems ( windowRuns )
545+ // cell in the window terminates — bounds queue depth at the window size .
546+ const items = await buildEnqueueItems ( windowRuns , windowSize )
538547 const queue = await getJobQueue ( )
539548 try {
540549 await queue . batchEnqueueAndWait ( 'workflow-group-cell' , items )
0 commit comments