@dbos-inc/dbos-sdk 4.26.10 → 4.27.4-preview

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,6 +15,7 @@ const pg_1 = require("pg");
15
15
  const error_1 = require("./error");
16
16
  const workflow_1 = require("./workflow");
17
17
  const utils_1 = require("./utils");
18
+ const wfqueue_1 = require("./wfqueue");
18
19
  const crypto_1 = require("crypto");
19
20
  const utils_2 = require("./utils");
20
21
  const database_utils_1 = require("./database_utils");
@@ -47,9 +48,15 @@ const QUEUE_COLUMN_BY_FIELD = {
47
48
  rateLimitPeriodSec: 'rate_limit_period_sec',
48
49
  priorityEnabled: 'priority_enabled',
49
50
  partitionQueue: 'partition_queue',
51
+ partitionConcurrency: 'partition_concurrency',
52
+ partitionWorkerConcurrency: 'partition_worker_concurrency',
53
+ partitionRateLimitMax: 'partition_rate_limit_max',
54
+ partitionRateLimitPeriodSec: 'partition_rate_limit_period_sec',
50
55
  pollingIntervalSec: 'polling_interval_sec',
51
56
  };
52
- const QUEUE_COLUMNS = 'name, concurrency, worker_concurrency, rate_limit_max, rate_limit_period_sec, priority_enabled, partition_queue, polling_interval_sec, application_name';
57
+ const QUEUE_COLUMNS = 'name, concurrency, worker_concurrency, rate_limit_max, rate_limit_period_sec, priority_enabled, partition_queue, ' +
58
+ 'partition_concurrency, partition_worker_concurrency, partition_rate_limit_max, partition_rate_limit_period_sec, ' +
59
+ 'polling_interval_sec, application_name';
53
60
  function queueRecordFromRow(row) {
54
61
  return {
55
62
  name: row.name,
@@ -59,6 +66,10 @@ function queueRecordFromRow(row) {
59
66
  rateLimitPeriodSec: row.rate_limit_period_sec,
60
67
  priorityEnabled: row.priority_enabled,
61
68
  partitionQueue: row.partition_queue,
69
+ partitionConcurrency: row.partition_concurrency,
70
+ partitionWorkerConcurrency: row.partition_worker_concurrency,
71
+ partitionRateLimitMax: row.partition_rate_limit_max,
72
+ partitionRateLimitPeriodSec: row.partition_rate_limit_period_sec,
62
73
  pollingIntervalSec: row.polling_interval_sec,
63
74
  applicationName: row.application_name ?? undefined,
64
75
  };
@@ -1618,7 +1629,8 @@ class SystemDatabase {
1618
1629
  // Need to await for the workflow and capture errors.
1619
1630
  const awaitWorkflowPromise = workflowPromise
1620
1631
  .catch((error) => {
1621
- this.logger.debug('Captured error in awaitWorkflowPromise: ' + error);
1632
+ const outcome = this.#destroyed ? 'was abandoned by shutdown' : 'failed';
1633
+ this.logger.debug(`Workflow ${workflowID} ${outcome}: ${error}`);
1622
1634
  })
1623
1635
  .finally(() => {
1624
1636
  onSettled();
@@ -1635,7 +1647,17 @@ class SystemDatabase {
1635
1647
  clearRunningWorkflow(workflowID) {
1636
1648
  this.runningWorkflowMap.delete(workflowID);
1637
1649
  }
1638
- countRunningWorkflowsForQueue(queueName, queuePartitionKey) {
1650
+ /** Workflows this worker is running for a queue, across every partition of it. */
1651
+ countRunningWorkflowsForQueue(queueName) {
1652
+ let count = 0;
1653
+ for (const entry of this.runningWorkflowMap.values()) {
1654
+ if (entry.queueName === queueName)
1655
+ count++;
1656
+ }
1657
+ return count;
1658
+ }
1659
+ /** Workflows this worker is running for one partition of a queue. */
1660
+ countRunningWorkflowsForPartition(queueName, queuePartitionKey) {
1639
1661
  let count = 0;
1640
1662
  for (const entry of this.runningWorkflowMap.values()) {
1641
1663
  if (entry.queueName === queueName && entry.queuePartitionKey === queuePartitionKey)
@@ -1643,10 +1665,35 @@ class SystemDatabase {
1643
1665
  }
1644
1666
  return count;
1645
1667
  }
1646
- async awaitRunningWorkflows() {
1668
+ /** Wait up to `timeoutMS` for locally-running workflows to finish. Without a timeout, do not wait at all. */
1669
+ async awaitRunningWorkflows(timeoutMS) {
1670
+ if (timeoutMS !== undefined && timeoutMS > 0) {
1671
+ const deadline = Date.now() + timeoutMS;
1672
+ if (this.runningWorkflowMap.size > 0) {
1673
+ this.logger.info('Waiting for pending workflows to finish.');
1674
+ }
1675
+ // Each pass picks up workflows a draining workflow started, and awaits any given run only once.
1676
+ const awaited = new Set();
1677
+ for (;;) {
1678
+ const pending = Array.from(this.runningWorkflowMap.values(), (entry) => entry.promise).filter((promise) => !awaited.has(promise));
1679
+ if (pending.length === 0)
1680
+ break;
1681
+ for (const promise of pending)
1682
+ awaited.add(promise);
1683
+ let timer;
1684
+ const timedOut = await Promise.race([
1685
+ Promise.allSettled(pending).then(() => false),
1686
+ new Promise((resolve) => {
1687
+ timer = setTimeout(() => resolve(true), Math.max(0, deadline - Date.now()));
1688
+ }),
1689
+ ]);
1690
+ clearTimeout(timer);
1691
+ if (timedOut)
1692
+ break;
1693
+ }
1694
+ }
1647
1695
  if (this.runningWorkflowMap.size > 0) {
1648
- this.logger.info('Waiting for pending workflows to finish.');
1649
- await Promise.allSettled(Array.from(this.runningWorkflowMap.values(), (entry) => entry.promise));
1696
+ this.logger.warn(`Shutting down while ${this.runningWorkflowMap.size} workflows are still running: ${Array.from(this.runningWorkflowMap.keys()).join(', ')}`);
1650
1697
  }
1651
1698
  if (this.workflowEventsMap.map.size > 0) {
1652
1699
  this.logger.warn('Workflow events map is not empty - shutdown is not clean.');
@@ -2334,78 +2381,93 @@ class SystemDatabase {
2334
2381
  SELECT pk FROM partitions WHERE pk IS NOT NULL`, params);
2335
2382
  return rows.map((row) => row.pk);
2336
2383
  }
2337
- async findAndMarkStartableWorkflows(queue, executorID, appVersion, queuePartitionKey) {
2338
- const limiterPeriodMS = queue.rateLimit ? queue.rateLimit.periodSec * 1000 : 0;
2384
+ async findAndMarkStartableWorkflows(queue, executorID, appVersion, queuePartitionKey, localRunningCount = 0, partitionLocalRunningCount = 0) {
2339
2385
  const claimedIDs = [];
2340
- const localRunningForQueue = this.countRunningWorkflowsForQueue(queue.name, queuePartitionKey);
2341
- // Build partition key filter
2342
- let partitionFilter = '';
2343
- const partitionParams = [];
2344
- if (queuePartitionKey !== undefined) {
2345
- partitionFilter = `AND queue_partition_key = $PARTITION`;
2346
- partitionParams.push(queuePartitionKey);
2347
- }
2386
+ const limits = (0, wfqueue_1.resolveQueueLimits)(queue);
2387
+ const partitionParams = queuePartitionKey !== undefined ? [queuePartitionKey] : [];
2388
+ // Shares a concurrency or rate limit budget with other executors.
2389
+ const hasSharedBudget = limits.globalConcurrency !== undefined ||
2390
+ limits.partitionConcurrency !== undefined ||
2391
+ limits.rateLimit !== undefined ||
2392
+ limits.partitionRateLimit !== undefined;
2393
+ // Shares that budget across partitions too, so sweeps of different partitions read disjoint rows and could each spend it.
2394
+ const hasWriteSkew = queuePartitionKey !== undefined && (limits.globalConcurrency !== undefined || limits.rateLimit !== undefined);
2348
2395
  const client = await this.#connect();
2349
2396
  try {
2350
- // Default to READ COMMITTED except with global concurrency limits or rate limits
2351
- if (queue.concurrency !== undefined || queue.rateLimit !== undefined) {
2352
- await client.query('BEGIN ISOLATION LEVEL REPEATABLE READ');
2397
+ // Default to READ COMMITTED except with a budget shared across executors
2398
+ if (hasSharedBudget) {
2399
+ await client.query(`BEGIN ISOLATION LEVEL ${hasWriteSkew ? 'SERIALIZABLE' : 'REPEATABLE READ'}`);
2353
2400
  }
2354
2401
  else {
2355
2402
  await client.query('BEGIN');
2356
2403
  }
2357
- // If there is a rate limit, compute how many functions have started in its period.
2358
- let numRecentQueries = 0;
2359
- if (queue.rateLimit) {
2360
- const params = [
2361
- queue.name,
2362
- workflow_1.StatusString.ENQUEUED,
2363
- workflow_1.StatusString.DELAYED,
2364
- limiterPeriodMS,
2365
- ...partitionParams,
2366
- ];
2404
+ /** Slots left in a rate limit's rolling window, at the scope that limit applies to. */
2405
+ const rateLimitRemaining = async (rateLimit, partitionScoped) => {
2406
+ const params = [queue.name, workflow_1.StatusString.ENQUEUED, workflow_1.StatusString.DELAYED, rateLimit.periodSec * 1000];
2367
2407
  // Count only what this application would dequeue, matching the select below.
2368
2408
  const scope = this.#appNameFilter('application_name', this.appName, params);
2369
- const countResult = await client.query(`SELECT COUNT(*) FROM "${this.schemaName}".workflow_status
2409
+ const partitionFilter = partitionScoped ? `AND queue_partition_key = $${params.push(queuePartitionKey)}` : '';
2410
+ const { rows } = await client.query(`SELECT COUNT(*) FROM "${this.schemaName}".workflow_status
2370
2411
  WHERE queue_name = $1
2371
2412
  AND rate_limited = TRUE
2372
2413
  AND status NOT IN ($2, $3)
2373
2414
  -- Database clock on both sides, as the claim stamps started_at_epoch_ms with it.
2374
2415
  AND started_at_epoch_ms > (EXTRACT(epoch FROM now()) * 1000)::bigint - $4
2375
2416
  AND ${scope}
2376
- ${partitionFilter.replace('$PARTITION', '$5')}`, params);
2377
- numRecentQueries = Number(countResult.rows[0].count);
2378
- if (numRecentQueries >= queue.rateLimit.limitPerPeriod) {
2379
- await client.query('COMMIT');
2380
- return claimedIDs;
2381
- }
2382
- }
2383
- // Dequeue functions eligible for this worker and ordered by the time at which they were enqueued.
2384
- // If there is a global or local concurrency limit N, select only the N oldest enqueued
2385
- // functions, else select all of them.
2417
+ ${partitionFilter}`, params);
2418
+ return rateLimit.limitPerPeriod - Number(rows[0].count);
2419
+ };
2420
+ /**
2421
+ * Workflows already running, which peer workers count against too. Kept as its own query per
2422
+ * scope: the partition-scoped predicate rides idx_workflow_status_partition_dequeue_v2, which
2423
+ * a queue-wide scan loses.
2424
+ */
2425
+ const pendingCount = async (partitionScoped) => {
2426
+ const params = [queue.name, workflow_1.StatusString.PENDING];
2427
+ const scope = this.#appNameFilter('application_name', this.appName, params);
2428
+ const partitionFilter = partitionScoped ? `AND queue_partition_key = $${params.push(queuePartitionKey)}` : '';
2429
+ const { rows } = await client.query(`SELECT COUNT(*) FROM "${this.schemaName}".workflow_status
2430
+ WHERE queue_name = $1 AND status = $2 AND ${scope} ${partitionFilter}`, params);
2431
+ return Number(rows[0]?.count ?? 0);
2432
+ };
2433
+ // Compute maxTasks, the number of workflows startable under every flow control limit on this queue.
2386
2434
  let maxTasks = Infinity;
2387
- if (queue.rateLimit) {
2435
+ if (limits.workerConcurrency !== undefined) {
2436
+ // Use the in-memory registry for this worker's running count — avoids a DB round trip.
2437
+ maxTasks = Math.min(maxTasks, Math.max(0, limits.workerConcurrency - localRunningCount));
2438
+ }
2439
+ if (limits.partitionWorkerConcurrency !== undefined) {
2440
+ maxTasks = Math.min(maxTasks, Math.max(0, limits.partitionWorkerConcurrency - partitionLocalRunningCount));
2441
+ }
2442
+ if (maxTasks <= 0) {
2443
+ await client.query('COMMIT');
2444
+ return claimedIDs;
2445
+ }
2446
+ if (limits.rateLimit !== undefined) {
2388
2447
  // Bound the claim by the limiter's remaining slots so a backlogged queue locks only what it can start.
2389
- maxTasks = Math.max(0, queue.rateLimit.limitPerPeriod - numRecentQueries);
2448
+ maxTasks = Math.min(maxTasks, await rateLimitRemaining(limits.rateLimit, false));
2390
2449
  }
2391
- if (queue.workerConcurrency !== undefined) {
2392
- // Use the in-memory registry for this worker's running count — avoids a DB round trip.
2393
- maxTasks = Math.min(maxTasks, Math.max(0, queue.workerConcurrency - localRunningForQueue));
2450
+ if (limits.partitionRateLimit !== undefined) {
2451
+ maxTasks = Math.min(maxTasks, await rateLimitRemaining(limits.partitionRateLimit, true));
2452
+ }
2453
+ if (maxTasks <= 0) {
2454
+ await client.query('COMMIT');
2455
+ return claimedIDs;
2394
2456
  }
2395
- if (queue.concurrency !== undefined) {
2457
+ if (limits.globalConcurrency !== undefined) {
2396
2458
  // Global concurrency still requires a DB query since other workers may be running workflows too.
2397
- const params = [queue.name, workflow_1.StatusString.PENDING, ...partitionParams];
2398
- const scope = this.#appNameFilter('application_name', this.appName, params);
2399
- const runningTasksResult = await client.query(`SELECT COUNT(*) as task_count
2400
- FROM "${this.schemaName}".workflow_status
2401
- WHERE queue_name = $1 AND status = $2 AND ${scope}
2402
- ${partitionFilter.replace('$PARTITION', '$3')}`, params);
2403
- const totalRunningTasks = Number(runningTasksResult.rows[0]?.task_count ?? 0);
2404
- if (totalRunningTasks > queue.concurrency) {
2405
- this.logger.warn(`Total running tasks (${totalRunningTasks}) exceeds the global concurrency limit (${queue.concurrency})`);
2459
+ const totalRunningTasks = await pendingCount(false);
2460
+ if (totalRunningTasks > limits.globalConcurrency) {
2461
+ this.logger.warn(`Total running tasks (${totalRunningTasks}) exceeds the global concurrency limit (${limits.globalConcurrency})`);
2406
2462
  }
2407
- const availableTasks = Math.max(0, queue.concurrency - totalRunningTasks);
2408
- maxTasks = Math.min(maxTasks, availableTasks);
2463
+ maxTasks = Math.min(maxTasks, Math.max(0, limits.globalConcurrency - totalRunningTasks));
2464
+ }
2465
+ if (limits.partitionConcurrency !== undefined) {
2466
+ const partitionRunningTasks = await pendingCount(true);
2467
+ if (partitionRunningTasks > limits.partitionConcurrency) {
2468
+ this.logger.warn(`Total running tasks (${partitionRunningTasks}) on partition ${queuePartitionKey} of queue ${queue.name} exceeds the partition concurrency limit (${limits.partitionConcurrency})`);
2469
+ }
2470
+ maxTasks = Math.min(maxTasks, Math.max(0, limits.partitionConcurrency - partitionRunningTasks));
2409
2471
  }
2410
2472
  // Return immediately if there are no available tasks due to flow control limits
2411
2473
  if (maxTasks <= 0) {
@@ -2420,8 +2482,7 @@ class SystemDatabase {
2420
2482
  : 'application_version = $3';
2421
2483
  // A limit shared across processes needs a consistent view of the table: NOWAIT makes an
2422
2484
  // overlapping dequeuer abort rather than claim the next rows and spend the same budget twice.
2423
- const sharedBudget = queue.concurrency !== undefined || queue.rateLimit !== undefined;
2424
- const lockMode = sharedBudget ? 'FOR UPDATE NOWAIT' : 'FOR UPDATE SKIP LOCKED';
2485
+ const lockMode = hasSharedBudget ? 'FOR UPDATE NOWAIT' : 'FOR UPDATE SKIP LOCKED';
2425
2486
  const limitClause = maxTasks !== Infinity ? `LIMIT ${maxTasks}` : '';
2426
2487
  const selectParams = [workflow_1.StatusString.ENQUEUED, queue.name, appVersion, ...partitionParams];
2427
2488
  const selectScope = this.#appNameFilter('application_name', this.appName, selectParams);
@@ -2432,7 +2493,7 @@ class SystemDatabase {
2432
2493
  AND queue_name = $2
2433
2494
  AND ${versionClause}
2434
2495
  AND ${selectScope}
2435
- ${partitionFilter.replace('$PARTITION', '$4')}
2496
+ ${queuePartitionKey !== undefined ? 'AND queue_partition_key = $4' : ''}
2436
2497
  ORDER BY priority ASC, created_at ASC
2437
2498
  ${limitClause}
2438
2499
  ${lockMode}
@@ -2449,7 +2510,7 @@ class SystemDatabase {
2449
2510
  workflow_1.StatusString.PENDING,
2450
2511
  executorID,
2451
2512
  appVersion,
2452
- queue.rateLimit !== undefined,
2513
+ limits.rateLimit !== undefined || limits.partitionRateLimit !== undefined,
2453
2514
  workflowIDs,
2454
2515
  workflow_1.StatusString.ENQUEUED,
2455
2516
  // Claim an unclaimed row for this application; a nameless dequeuer leaves ownership untouched.
@@ -2492,14 +2553,18 @@ class SystemDatabase {
2492
2553
  // Return the IDs of all functions we marked started
2493
2554
  return claimedIDs;
2494
2555
  }
2495
- /** Max heads admitted per sweep: bounds dispatch, not the partition walk; lowest keys win, so higher keys can wait under sustained load. */
2556
+ /** Max heads admitted per sweep: bounds dispatch, not the partition walk; an unconstrained sweep takes the lowest keys, so higher keys can wait under sustained load. */
2496
2557
  partitionedDequeueSweepCap = 8192;
2497
- /** Dequeue each partition's head-of-line workflow in one transaction, at most {@link partitionedDequeueSweepCap} per sweep; only valid for concurrency=1, no-limiter queues. */
2498
- async findAndMarkStartablePartitionedWorkflows(queue, executorID, appVersion) {
2499
- if (queue.concurrency !== 1 || queue.rateLimit !== undefined) {
2500
- throw new error_1.DBOSError(`Batched partitioned dequeue requires a queue with concurrency 1 and no rate limit: ${queue.name}`);
2501
- }
2502
- // workerConcurrency needs no handling here: dispatch routes 0 (the pause-dequeue idiom) to the fallback path, and validation caps any other value at concurrency=1, which the PENDING gate already enforces globally.
2558
+ /** Dequeue each partition's head-of-line workflow in one transaction, at most {@link partitionedDequeueSweepCap} per sweep; only valid for partition-concurrency-1 queues with no queue-wide limit. */
2559
+ async findAndMarkStartablePartitionedWorkflows(queue, executorID, appVersion, maxTasks = Infinity) {
2560
+ const limits = (0, wfqueue_1.resolveQueueLimits)(queue);
2561
+ if (limits.partitionConcurrency !== 1 ||
2562
+ limits.globalConcurrency !== undefined ||
2563
+ limits.rateLimit !== undefined ||
2564
+ limits.partitionRateLimit !== undefined) {
2565
+ throw new error_1.DBOSError(`Batched partitioned dequeue requires a queue with partition concurrency 1 and no queue-wide concurrency or rate limit: ${queue.name}`);
2566
+ }
2567
+ // partitionWorkerConcurrency needs no handling here: any value above 0 is capped at partition concurrency 1, which the PENDING gate already enforces globally, and 0 makes the caller's maxTasks 0.
2503
2568
  const client = await this.#connect();
2504
2569
  try {
2505
2570
  await client.query('BEGIN');
@@ -2508,12 +2573,16 @@ class SystemDatabase {
2508
2573
  const versionClause = (n) => isLatestVersion
2509
2574
  ? `(application_version = $${n} OR application_version IS NULL)`
2510
2575
  : `application_version = $${n}`;
2576
+ // This worker's own budget bounds the sweep alongside the cap.
2577
+ const sweepLimit = Math.min(this.partitionedDequeueSweepCap, maxTasks);
2578
+ // When the worker's budget is the binding constraint, probe partitions in random order to prevent starvation.
2579
+ const sweepOrder = sweepLimit < this.partitionedDequeueSweepCap ? 'random()' : 'partitions.pk ASC';
2511
2580
  const candidateParams = [
2512
2581
  queue.name,
2513
2582
  workflow_1.StatusString.ENQUEUED,
2514
2583
  appVersion,
2515
2584
  workflow_1.StatusString.PENDING,
2516
- this.partitionedDequeueSweepCap,
2585
+ sweepLimit,
2517
2586
  ];
2518
2587
  const candidateScope = this.#appNameFilter('application_name', this.appName, candidateParams);
2519
2588
  // Walk distinct partition keys with a recursive-CTE loose index scan (one seek per key, mirroring getQueuePartitions) so sweep cost scales with partition count, not backlog depth.
@@ -2528,30 +2597,36 @@ class SystemDatabase {
2528
2597
  FROM partitions
2529
2598
  WHERE partitions.pk IS NOT NULL)
2530
2599
  )
2600
+ , chosen AS (
2601
+ SELECT partitions.pk
2602
+ FROM partitions
2603
+ WHERE partitions.pk IS NOT NULL
2604
+ -- Unscoped by design: a mutual-exclusion probe must block on any owner's row.
2605
+ AND NOT EXISTS (
2606
+ SELECT 1
2607
+ FROM "${this.schemaName}".workflow_status
2608
+ WHERE queue_name = $1 AND status = $4
2609
+ AND queue_partition_key IS NOT NULL AND queue_partition_key = partitions.pk
2610
+ )
2611
+ ORDER BY ${sweepOrder}
2612
+ LIMIT $5
2613
+ )
2531
2614
  SELECT head.workflow_uuid
2532
- FROM partitions
2615
+ FROM chosen
2533
2616
  -- LATERAL plans as a tight nested loop; a correlated scalar subquery runs as a slower per-row SubPlan.
2534
2617
  JOIN LATERAL (
2535
2618
  SELECT workflow_uuid
2536
2619
  FROM "${this.schemaName}".workflow_status
2537
2620
  WHERE queue_name = $1 AND status = $2
2538
- AND queue_partition_key = partitions.pk
2621
+ AND queue_partition_key = chosen.pk
2539
2622
  AND ${versionClause(3)}
2540
2623
  AND ${candidateScope}
2541
2624
  -- workflow_uuid totalizes the head order (same head for every worker under created_at ties) and the index's trailing workflow_uuid keeps this a pure top-1 probe.
2542
2625
  ORDER BY priority ASC, created_at ASC, workflow_uuid ASC
2543
2626
  LIMIT 1
2544
2627
  ) head ON TRUE
2545
- WHERE partitions.pk IS NOT NULL
2546
- -- Unscoped by design: a mutual-exclusion probe must block on any owner's row.
2547
- AND NOT EXISTS (
2548
- SELECT 1
2549
- FROM "${this.schemaName}".workflow_status
2550
- WHERE queue_name = $1 AND status = $4
2551
- AND queue_partition_key IS NOT NULL AND queue_partition_key = partitions.pk
2552
- )
2553
- ORDER BY partitions.pk ASC
2554
- LIMIT $5`, candidateParams);
2628
+ -- Which partitions were chosen is settled above; order the result so the claim, and the dispatch it feeds, are deterministic.
2629
+ ORDER BY chosen.pk ASC`, candidateParams);
2555
2630
  const candidateIDs = candidateResult.rows.map((row) => row.workflow_uuid);
2556
2631
  if (candidateIDs.length === 0) {
2557
2632
  await client.query('COMMIT');
@@ -3483,6 +3558,10 @@ class SystemDatabase {
3483
3558
  rate_limit_period_sec = EXCLUDED.rate_limit_period_sec,
3484
3559
  priority_enabled = EXCLUDED.priority_enabled,
3485
3560
  partition_queue = EXCLUDED.partition_queue,
3561
+ partition_concurrency = EXCLUDED.partition_concurrency,
3562
+ partition_worker_concurrency = EXCLUDED.partition_worker_concurrency,
3563
+ partition_rate_limit_max = EXCLUDED.partition_rate_limit_max,
3564
+ partition_rate_limit_period_sec = EXCLUDED.partition_rate_limit_period_sec,
3486
3565
  polling_interval_sec = EXCLUDED.polling_interval_sec,
3487
3566
  updated_at = EXCLUDED.updated_at,
3488
3567
  -- Claim only an unclaimed row, so a registration landing between the check above and this write keeps the name it just took.
@@ -3497,8 +3576,10 @@ class SystemDatabase {
3497
3576
  const resolvedOwner = await this.#resolveRowOwner(client, 'queues', 'name', record.name, owner, 'Queue');
3498
3577
  await client.query(`INSERT INTO "${this.schemaName}".queues
3499
3578
  (name, concurrency, worker_concurrency, rate_limit_max, rate_limit_period_sec,
3500
- priority_enabled, partition_queue, polling_interval_sec, updated_at, application_name)
3501
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
3579
+ priority_enabled, partition_queue, partition_concurrency, partition_worker_concurrency,
3580
+ partition_rate_limit_max, partition_rate_limit_period_sec,
3581
+ polling_interval_sec, updated_at, application_name)
3582
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
3502
3583
  ${onConflict}`, [
3503
3584
  record.name,
3504
3585
  record.concurrency,
@@ -3507,6 +3588,10 @@ class SystemDatabase {
3507
3588
  record.rateLimitPeriodSec,
3508
3589
  record.priorityEnabled,
3509
3590
  record.partitionQueue,
3591
+ record.partitionConcurrency,
3592
+ record.partitionWorkerConcurrency,
3593
+ record.partitionRateLimitMax,
3594
+ record.partitionRateLimitPeriodSec,
3510
3595
  record.pollingIntervalSec,
3511
3596
  now,
3512
3597
  resolvedOwner ?? null,