@@ -16,7 +16,13 @@ import {
1616 stringifyDuration ,
1717} from "@trigger.dev/core/v3/isomorphic" ;
1818import { randomBytes } from "node:crypto" ;
19- import type { BackgroundWorker , TaskQueue , TaskQueueType } from "@trigger.dev/database" ;
19+ import type {
20+ BackgroundWorker ,
21+ TaskQueue ,
22+ TaskQueueConcurrencyVersion ,
23+ TaskQueueRole ,
24+ TaskQueueType ,
25+ } from "@trigger.dev/database" ;
2026import cronstrue from "cronstrue" ;
2127import type { PrismaClientOrTransaction , WebhookDatabase } from "~/db.server" ;
2228import { $transaction , Prisma , boundedIn , webhookPrisma } from "~/db.server" ;
@@ -337,6 +343,7 @@ export async function createWorkerResources(
337343
338344 // Create the queues
339345 const queues = await createWorkerQueues ( metadata , worker , environment , prisma ) ;
346+ await createWorkerConcurrencyLimits ( metadata , worker , environment , prisma ) ;
340347
341348 // Create the tasks
342349 const taskEntries = await createWorkerTasks (
@@ -391,25 +398,72 @@ async function createWorkerTask(
391398) : Promise < TaskMetadataEntry | null > {
392399 // Hoisted so the P2002 catch branch can return the same entry shape.
393400 let queue : TaskQueue | undefined ;
401+ let compiledGates : Array < { queue : string ; concurrencyKey ?: string } > = [ ] ;
394402 let resolvedTriggerSource : "SCHEDULED" | "AGENT" | "WEBHOOK" | "STANDARD" | undefined ;
395403 let resolvedTtl : string | null | undefined ;
396404
397405 try {
406+ const concurrency = task . concurrency ;
407+
408+ if ( concurrency && typeof task . queue ?. concurrencyLimit === "number" ) {
409+ throw new ServiceValidationError (
410+ `Task "${ task . id } " declares both a queue concurrencyLimit and the concurrency option; use concurrency.`
411+ ) ;
412+ }
413+
414+ compiledGates = ( concurrency ?. limits ?? [ ] ) . map ( ( name ) => ( {
415+ queue : concurrencyLimitQueueName ( name ) ,
416+ } ) ) ;
417+
418+ let queueConcurrencyLimit = task . queue ?. concurrencyLimit ;
419+ let queueTotalConcurrencyLimit = task . queue ?. combinedConcurrencyLimit ;
420+
421+ if ( concurrency ?. inline ) {
422+ if ( ! task . queue ?. name ) {
423+ queueConcurrencyLimit = concurrency . inline . perKey ?? concurrency . inline . total ;
424+ queueTotalConcurrencyLimit = concurrency . inline . total ;
425+ } else {
426+ if ( compiledGates . length > 1 ) {
427+ throw new ServiceValidationError (
428+ `Task "${ task . id } ": an inline limit on a shared queue uses a gate slot, so at most one named limit can be combined with it.`
429+ ) ;
430+ }
431+ const anonymousName = `task/${ task . id } ` ;
432+ await createWorkerQueue (
433+ {
434+ name : concurrencyLimitQueueName ( anonymousName ) ,
435+ concurrencyLimit : concurrency . inline . perKey ?? concurrency . inline . total ?? null ,
436+ combinedConcurrencyLimit : concurrency . inline . total ?? null ,
437+ } ,
438+ anonymousName ,
439+ "NAMED" ,
440+ worker ,
441+ environment ,
442+ prisma ,
443+ "LIMIT" ,
444+ "V2"
445+ ) ;
446+ compiledGates = [ { queue : concurrencyLimitQueueName ( anonymousName ) } , ...compiledGates ] ;
447+ }
448+ }
449+
398450 queue = queues . find ( ( queue ) => queue . name === task . queue ?. name ) ;
399451
400452 if ( ! queue ) {
401453 // Create a TaskQueue
402454 queue = await createWorkerQueue (
403455 {
404456 name : task . queue ?. name ?? `task/${ task . id } ` ,
405- concurrencyLimit : task . queue ?. concurrencyLimit ,
406- combinedConcurrencyLimit : task . queue ?. combinedConcurrencyLimit ,
457+ concurrencyLimit : queueConcurrencyLimit ,
458+ combinedConcurrencyLimit : queueTotalConcurrencyLimit ,
407459 } ,
408460 task . queue ?. name ?? task . id ,
409461 task . queue ?. name ? "NAMED" : "VIRTUAL" ,
410462 worker ,
411463 environment ,
412- prisma
464+ prisma ,
465+ "QUEUE" ,
466+ concurrency ? "V2" : "V1"
413467 ) ;
414468 }
415469
@@ -437,7 +491,7 @@ async function createWorkerTask(
437491 exportName : task . exportName ,
438492 retryConfig : task . retry ,
439493 queueConfig : task . queue ,
440- gates : task . gates ,
494+ gates : compiledGates . length > 0 ? compiledGates : task . gates ,
441495 machineConfig : task . machine ,
442496 triggerSource : resolvedTriggerSource ,
443497 config : task . agentConfig ? ( task . agentConfig as any ) : undefined ,
@@ -455,7 +509,7 @@ async function createWorkerTask(
455509 triggerSource : resolvedTriggerSource ,
456510 queueId : queue . id ,
457511 queueName : queue . name ,
458- gates : task . gates ?? null ,
512+ gates : compiledGates . length > 0 ? compiledGates : ( task . gates ?? null ) ,
459513 } ;
460514 } catch ( error ) {
461515 if ( error instanceof Prisma . PrismaClientKnownRequestError ) {
@@ -479,7 +533,7 @@ async function createWorkerTask(
479533 triggerSource : resolvedTriggerSource ,
480534 queueId : queue . id ,
481535 queueName : queue . name ,
482- gates : task . gates ?? null ,
536+ gates : compiledGates . length > 0 ? compiledGates : ( task . gates ?? null ) ,
483537 } ;
484538 }
485539 } else {
@@ -540,13 +594,63 @@ async function createWorkerQueues(
540594 return allQueues ;
541595}
542596
597+ /** Queue rows that back named concurrency limits live under this reserved prefix so
598+ * they can never collide with a user's queue names. */
599+ export const CONCURRENCY_LIMIT_QUEUE_PREFIX = "limit/" ;
600+
601+ export function concurrencyLimitQueueName ( limitName : string ) : string {
602+ return `${ CONCURRENCY_LIMIT_QUEUE_PREFIX } ${ sanitizeQueueName ( limitName ) } ` ;
603+ }
604+
605+ /**
606+ * Materializes the worker's declared named concurrency limits (plus any names tasks
607+ * reference without declaring, created uncapped) as LIMIT-role TaskQueue rows. A
608+ * total-only limit stores the total as its per-key limit too, so it truly caps
609+ * keyless runs as well as the keyed group.
610+ */
611+ async function createWorkerConcurrencyLimits (
612+ metadata : BackgroundWorkerMetadata ,
613+ worker : BackgroundWorker ,
614+ environment : AuthenticatedEnvironment ,
615+ prisma : PrismaClientOrTransaction
616+ ) {
617+ const declared = new Map ( ( metadata . concurrencyLimits ?? [ ] ) . map ( ( l ) => [ l . name , l ] ) ) ;
618+
619+ for ( const task of metadata . tasks ) {
620+ for ( const name of task . concurrency ?. limits ?? [ ] ) {
621+ if ( ! declared . has ( name ) ) {
622+ declared . set ( name , { name } ) ;
623+ }
624+ }
625+ }
626+
627+ for ( const limit of declared . values ( ) ) {
628+ await createWorkerQueue (
629+ {
630+ name : concurrencyLimitQueueName ( limit . name ) ,
631+ concurrencyLimit : limit . perKey ?? limit . total ?? null ,
632+ combinedConcurrencyLimit : limit . total ?? null ,
633+ } ,
634+ limit . name ,
635+ "NAMED" ,
636+ worker ,
637+ environment ,
638+ prisma ,
639+ "LIMIT" ,
640+ "V2"
641+ ) ;
642+ }
643+ }
644+
543645async function createWorkerQueue (
544646 queue : QueueManifest ,
545647 orderableName : string ,
546648 queueType : TaskQueueType ,
547649 worker : BackgroundWorker ,
548650 environment : AuthenticatedEnvironment ,
549- prisma : PrismaClientOrTransaction
651+ prisma : PrismaClientOrTransaction ,
652+ role : TaskQueueRole = "QUEUE" ,
653+ concurrencyVersion : TaskQueueConcurrencyVersion = "V1"
550654) {
551655 let queueName = sanitizeQueueName ( queue . name ) ;
552656
@@ -562,7 +666,10 @@ async function createWorkerQueue(
562666 orderableName ,
563667 queueType ,
564668 worker ,
565- prisma
669+ prisma ,
670+ 0 ,
671+ role ,
672+ concurrencyVersion
566673 ) ;
567674
568675 const newConcurrencyLimit = taskQueue . concurrencyLimit ;
@@ -625,7 +732,9 @@ async function upsertWorkerQueueRecord(
625732 queueType : TaskQueueType ,
626733 worker : BackgroundWorker ,
627734 prisma : PrismaClientOrTransaction ,
628- attempt : number = 0
735+ attempt : number = 0 ,
736+ role : TaskQueueRole = "QUEUE" ,
737+ concurrencyVersion : TaskQueueConcurrencyVersion = "V1"
629738) : Promise < TaskQueue > {
630739 if ( attempt > 3 ) {
631740 throw new Error ( "Failed to insert queue record" ) ;
@@ -644,6 +753,8 @@ async function upsertWorkerQueueRecord(
644753 data : {
645754 friendlyId : generateFriendlyId ( "queue" ) ,
646755 version : "V2" ,
756+ role,
757+ concurrencyVersion,
647758 name : queueName ,
648759 orderableName,
649760 concurrencyLimit,
@@ -669,6 +780,7 @@ async function upsertWorkerQueueRecord(
669780 data : {
670781 workers : { connect : { id : worker . id } } ,
671782 version : "V2" ,
783+ concurrencyVersion,
672784 orderableName,
673785 // If overridden, keep current limit and update base; otherwise update limit normally
674786 concurrencyLimit : hasOverride ? undefined : concurrencyLimit ,
@@ -691,7 +803,9 @@ async function upsertWorkerQueueRecord(
691803 queueType ,
692804 worker ,
693805 prisma ,
694- attempt + 1
806+ attempt + 1 ,
807+ role ,
808+ concurrencyVersion
695809 ) ;
696810 }
697811 throw error ;
0 commit comments