@pikku/queue-pg-boss 0.12.7 → 0.12.9

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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,61 @@
1
+ ## 0.12.9
2
+
3
+ ### Patch Changes
4
+
5
+ - 6794681: Publish the ecosystem surface as per-area sub-barrels under `@pikku/core/ecosystem/*`, and point generated code, the CLI, the inspector and the runtime adapters at them.
6
+
7
+ 348 names that only generated code, the toolchain or a runtime adapter ever imports now have a second home on `@pikku/core/ecosystem/<area>` — one sub-barrel per area, matching how core already publishes its entrypoints, so no single barrel grows without bound and a consumer only pulls in the area it uses.
8
+
9
+ This step is additive: every name is still exported from the entrypoint it was published from before, so nothing downstream breaks. Removing them from the app-facing barrels is a later change, and needs a release carrying `./ecosystem/*` first.
10
+
11
+ - Updated dependencies [7406bfe]
12
+ - Updated dependencies [6794681]
13
+ - @pikku/core@0.12.84
14
+
15
+ ## 0.12.8
16
+
17
+ ### Patch Changes
18
+
19
+ - 6847338: Stop one slow job from holding an entire queue (head-of-line blocking).
20
+
21
+ The worker mapped pikku's parallelism to pg-boss `batchSize`, whose handler
22
+ receives the whole fetched batch and only fetches again once that handler
23
+ resolves. Because the handler `Promise.all`s the batch, a single long-running
24
+ job blocked every sibling in its batch _and_ stalled the next fetch — so on a
25
+ shared queue one slow job could freeze the worker and starve everything behind
26
+ it (observed with workflow-orchestrator jobs, where a long step pinned all 10
27
+ slots for tens of minutes).
28
+
29
+ Parallelism now maps to pg-boss `localConcurrency` with `batchSize: 1`: N
30
+ independent workers each fetch and process a single job and refill the instant
31
+ they finish, so a slow job never holds up the others. Same effective
32
+ concurrency, no head-of-line blocking.
33
+
34
+ - e3d4454: Add job groups, so one shared queue can stay fair without splitting into one
35
+ queue per producer.
36
+
37
+ A job may now carry `group: { id, tier }`, and a worker may cap how many jobs
38
+ of any one group run at once via `groupConcurrency`. On pg-boss this maps to
39
+ `localGroupConcurrency`, which excludes at-capacity groups from the fetch query
40
+ itself, so a capped group costs nothing rather than being fetched and restored.
41
+ BullMQ declares it unsupported (groups are a BullMQ Pro feature) — being
42
+ push-based, it can simply use a queue per group at no polling cost.
43
+
44
+ Workflow services accept a `queueStrategy`. The default `'per-workflow'` is
45
+ unchanged: every workflow gets its own `wf-orchestrator-*` / `wf-step-*` queue,
46
+ which is also what lets serverless providers deploy one unit per workflow. The
47
+ new `'shared-groups'` routes every workflow through the shared
48
+ orchestrator/step-worker queues and isolates them by group instead, so a
49
+ monolith runs one set of pollers rather than one per workflow — on a
50
+ pull-based backend with dozens of workflows that is the difference between
51
+ hundreds of poll loops and twenty. It is for single-process runtimes only; a
52
+ per-unit serverless deploy still needs the per-workflow queues to route to its
53
+ units.
54
+
55
+ - Updated dependencies [24252b8]
56
+ - Updated dependencies [e3d4454]
57
+ - @pikku/core@0.12.69
58
+
1
59
  ## 0.12.7
2
60
 
3
61
  ### Patch Changes
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 - present Yasser Fadl and Pikku contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # @pikku/queue-pg-boss
2
+
3
+ pg-boss queue, worker and scheduler services for Pikku. Backed by Postgres, so
4
+ it needs no extra infrastructure if you already run one.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npm install @pikku/queue-pg-boss
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ ```typescript
15
+ import PgBoss from 'pg-boss'
16
+ import { PgBossQueueService } from '@pikku/queue-pg-boss'
17
+
18
+ const pgBoss = new PgBoss(connectionString)
19
+ await pgBoss.start()
20
+
21
+ const queue = new PgBossQueueService(pgBoss)
22
+ ```
23
+
24
+ Use `PgBossQueueWorkers` to process jobs and `PgBossSchedulerService` for cron
25
+ wirings. For a Redis-backed alternative see `@pikku/queue-bullmq`.
26
+
27
+ ## Docs
28
+
29
+ https://pikku.dev/docs
package/dist/index.d.ts CHANGED
@@ -8,4 +8,5 @@ export { PgBossServiceFactory } from './pg-boss-service-factory.js';
8
8
  export { PgBossQueueService } from './pg-boss-queue-service.js';
9
9
  export { PgBossQueueWorkers } from './pg-boss-queue-worker.js';
10
10
  export { PgBossSchedulerService } from './pg-boss-scheduler-service.js';
11
- export type { QueueService, PikkuWorkerConfig, PikkuJobConfig, QueueJob, JobOptions, } from '@pikku/core/queue';
11
+ export type { QueueService, QueueJob } from '@pikku/core/queue';
12
+ export type { PikkuWorkerConfig, PikkuJobConfig, JobOptions, } from '@pikku/core/ecosystem/queue';
@@ -1,5 +1,6 @@
1
1
  import type { PgBoss, Request as PgBossRequest } from 'pg-boss';
2
- import type { QueueService, QueueJob, JobOptions } from '@pikku/core/queue';
2
+ import type { QueueService, QueueJob } from '@pikku/core/queue';
3
+ import type { JobOptions } from '@pikku/core/ecosystem/queue';
3
4
  export declare const mapPikkuJobToPgBoss: (options?: JobOptions) => PgBossRequest["options"];
4
5
  /**
5
6
  * pg-boss/PostgreSQL queue service implementation
@@ -10,12 +10,20 @@ export const mapPikkuJobToPgBoss = (options) => {
10
10
  if (options?.jobId !== undefined) {
11
11
  pgBossOptions.singletonKey = options.jobId;
12
12
  }
13
+ if (options?.group !== undefined) {
14
+ pgBossOptions.group = {
15
+ id: options.group.id,
16
+ ...(options.group.tier !== undefined ? { tier: options.group.tier } : {}),
17
+ };
18
+ }
13
19
  // Map retry options
14
20
  if (options?.attempts !== undefined) {
15
21
  pgBossOptions.retryLimit = options.attempts - 1;
16
22
  }
17
23
  if (options?.backoff !== undefined) {
18
- const type = typeof options.backoff === 'string' ? options.backoff : options.backoff.type;
24
+ const type = typeof options.backoff === 'string'
25
+ ? options.backoff
26
+ : options.backoff.type;
19
27
  const delay = typeof options.backoff === 'object' ? options.backoff.delay : undefined;
20
28
  pgBossOptions.retryBackoff = type === 'exponential';
21
29
  if (delay !== undefined) {
@@ -1,5 +1,5 @@
1
1
  import type { PgBoss, WorkOptions } from 'pg-boss';
2
- import type { QueueWorkers, PikkuWorkerConfig, QueueConfigMapping, ConfigValidationResult } from '@pikku/core/queue';
2
+ import type { QueueWorkers, PikkuWorkerConfig, QueueConfigMapping, ConfigValidationResult } from '@pikku/core/ecosystem/queue';
3
3
  import type { Logger } from '@pikku/core/services';
4
4
  export declare const mapPikkuWorkerToPgBoss: (workerConfig?: PikkuWorkerConfig) => WorkOptions;
5
5
  /**
@@ -1,16 +1,43 @@
1
- import { registerQueueWorkers, runQueueJob, QueueJobFailedError, QueueJobDiscardedError, } from '@pikku/core/queue';
2
- import { pikkuState } from '@pikku/core/internal';
1
+ import { runQueueJob } from '@pikku/core/queue';
2
+ import { registerQueueWorkers, QueueJobFailedError, QueueJobDiscardedError, } from '@pikku/core/ecosystem/queue';
3
+ import { pikkuState } from '@pikku/core/ecosystem';
3
4
  import { mapPgBossJobToQueueJob } from './utils.js';
4
5
  export const mapPikkuWorkerToPgBoss = (workerConfig) => {
6
+ // Map the requested parallelism to pg-boss `localConcurrency` (independent
7
+ // per-job workers), NOT `batchSize`. A batch worker hands the whole fetched
8
+ // batch to one handler and only fetches again once that handler resolves — so
9
+ // a single slow job blocks every sibling in its batch AND stalls the next
10
+ // fetch. On a shared queue that is head-of-line blocking: one long job can
11
+ // freeze the entire worker and starve everything queued behind it.
12
+ // `localConcurrency` spawns N pollers that each fetch+process a single job and
13
+ // refill the instant they finish, so a slow job never holds up the others.
14
+ const localConcurrency = workerConfig?.batchSize ?? 10;
5
15
  const workerOptions = {
6
- batchSize: 10,
16
+ localConcurrency,
17
+ batchSize: 1,
7
18
  };
8
- if (workerConfig?.batchSize !== undefined) {
9
- workerOptions.batchSize = workerConfig.batchSize;
10
- }
11
19
  if (workerConfig?.pollInterval !== undefined) {
12
20
  workerOptions.pollingIntervalSeconds = Math.round(workerConfig.pollInterval / 1000);
13
21
  }
22
+ // Per-group fairness. `localGroupConcurrency` is in-memory and free — groups
23
+ // already at capacity are excluded from the fetch itself, so an at-capacity
24
+ // group costs nothing rather than being fetched and restored. pg-boss asserts
25
+ // every limit is <= localConcurrency, so clamp instead of letting a config
26
+ // that reads as "let this tier use more" throw at worker start.
27
+ if (workerConfig?.groupConcurrency !== undefined) {
28
+ const clamp = (limit) => Math.min(limit, localConcurrency);
29
+ workerOptions.localGroupConcurrency =
30
+ typeof workerConfig.groupConcurrency === 'number'
31
+ ? clamp(workerConfig.groupConcurrency)
32
+ : {
33
+ default: clamp(workerConfig.groupConcurrency.default),
34
+ ...(workerConfig.groupConcurrency.tiers
35
+ ? {
36
+ tiers: Object.fromEntries(Object.entries(workerConfig.groupConcurrency.tiers).map(([tier, limit]) => [tier, clamp(limit)])),
37
+ }
38
+ : {}),
39
+ };
40
+ }
14
41
  return workerOptions;
15
42
  };
16
43
  /**
@@ -28,8 +55,12 @@ export class PgBossQueueWorkers {
28
55
  // Configurations that are directly supported
29
56
  supported: {
30
57
  batchSize: {
31
- queueProperty: 'batchSize',
32
- description: 'Number of jobs to fetch and process concurrently in parallel',
58
+ queueProperty: 'localConcurrency',
59
+ description: 'Number of jobs to process concurrently',
60
+ },
61
+ groupConcurrency: {
62
+ queueProperty: 'localGroupConcurrency',
63
+ description: 'Maximum concurrent jobs per group, keeping one shared queue fair across groups',
33
64
  },
34
65
  pollInterval: {
35
66
  queueProperty: 'pollingIntervalSeconds',
@@ -1,5 +1,6 @@
1
1
  import type { PgBoss } from 'pg-boss';
2
- import type { ScheduledTaskInfo, ScheduledTaskSummary, CoreUserSession } from '@pikku/core';
2
+ import type { CoreUserSession } from '@pikku/core';
3
+ import type { ScheduledTaskInfo, ScheduledTaskSummary } from '@pikku/core/ecosystem/services';
3
4
  import { SchedulerService } from '@pikku/core';
4
5
  /**
5
6
  * pg-boss scheduler service implementation
@@ -1,6 +1,8 @@
1
- import { SchedulerService, parseDurationString } from '@pikku/core';
2
- import { pikkuState } from '@pikku/core/internal';
3
- import { runScheduledTask, getScheduledTasks } from '@pikku/core/scheduler';
1
+ import { SchedulerService } from '@pikku/core';
2
+ import { parseDurationString } from '@pikku/core/ecosystem/types';
3
+ import { pikkuState } from '@pikku/core/ecosystem';
4
+ import { runScheduledTask } from '@pikku/core/scheduler';
5
+ import { getScheduledTasks } from '@pikku/core/ecosystem/scheduler';
4
6
  /** Prefix for pg-boss queues/schedules backing recurring scheduled tasks. */
5
7
  const RECURRING_TASK_PREFIX = 'pikku-recurring-scheduled-task_';
6
8
  /**
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@pikku/queue-pg-boss",
3
- "version": "0.12.7",
3
+ "version": "0.12.9",
4
+ "description": "pg-boss queue, worker and scheduler services for Pikku, backed by Postgres",
4
5
  "author": "yasser.fadl@gmail.com",
5
6
  "license": "MIT",
6
7
  "module": "dist/index.js",
@@ -15,7 +16,7 @@
15
16
  "prepublishOnly": "yarn build"
16
17
  },
17
18
  "peerDependencies": {
18
- "@pikku/core": "^0.12.56"
19
+ "@pikku/core": "^0.12.84"
19
20
  },
20
21
  "dependencies": {
21
22
  "pg-boss": "^12.14.0"
package/src/index.ts CHANGED
@@ -11,10 +11,9 @@ export { PgBossQueueWorkers } from './pg-boss-queue-worker.js'
11
11
  export { PgBossSchedulerService } from './pg-boss-scheduler-service.js'
12
12
 
13
13
  // Re-export core queue types for convenience
14
+ export type { QueueService, QueueJob } from '@pikku/core/queue'
14
15
  export type {
15
- QueueService,
16
16
  PikkuWorkerConfig,
17
17
  PikkuJobConfig,
18
- QueueJob,
19
18
  JobOptions,
20
- } from '@pikku/core/queue'
19
+ } from '@pikku/core/ecosystem/queue'
@@ -57,3 +57,22 @@ describe('mapPikkuJobToPgBoss — retry mapping', () => {
57
57
  assert.equal(opts.retryBackoff, undefined)
58
58
  })
59
59
  })
60
+
61
+ describe('mapPikkuJobToPgBoss — group mapping', () => {
62
+ test('group id is forwarded so the worker can enforce a per-group limit', () => {
63
+ assert.deepEqual(mapPikkuJobToPgBoss({ group: { id: 'deployWorkflow' } })
64
+ .group, { id: 'deployWorkflow' })
65
+ })
66
+
67
+ test('tier is only set when provided', () => {
68
+ const opts = mapPikkuJobToPgBoss({
69
+ group: { id: 'deployWorkflow', tier: 'slow' },
70
+ })
71
+ assert.deepEqual(opts.group, { id: 'deployWorkflow', tier: 'slow' })
72
+ assert.equal('tier' in mapPikkuJobToPgBoss({ group: { id: 'a' } }).group!, false)
73
+ })
74
+
75
+ test('no group → no group field', () => {
76
+ assert.equal(mapPikkuJobToPgBoss({ attempts: 2 }).group, undefined)
77
+ })
78
+ })
@@ -1,5 +1,6 @@
1
1
  import type { PgBoss, Request as PgBossRequest } from 'pg-boss'
2
- import type { QueueService, QueueJob, JobOptions } from '@pikku/core/queue'
2
+ import type { QueueService, QueueJob } from '@pikku/core/queue'
3
+ import type { JobOptions } from '@pikku/core/ecosystem/queue'
3
4
  import { mapPgBossJobToQueueJob } from './utils.js'
4
5
 
5
6
  export const mapPikkuJobToPgBoss = (
@@ -19,6 +20,13 @@ export const mapPikkuJobToPgBoss = (
19
20
  pgBossOptions.singletonKey = options.jobId
20
21
  }
21
22
 
23
+ if (options?.group !== undefined) {
24
+ pgBossOptions.group = {
25
+ id: options.group.id,
26
+ ...(options.group.tier !== undefined ? { tier: options.group.tier } : {}),
27
+ }
28
+ }
29
+
22
30
  // Map retry options
23
31
  if (options?.attempts !== undefined) {
24
32
  pgBossOptions.retryLimit = options.attempts - 1
@@ -26,7 +34,9 @@ export const mapPikkuJobToPgBoss = (
26
34
 
27
35
  if (options?.backoff !== undefined) {
28
36
  const type =
29
- typeof options.backoff === 'string' ? options.backoff : options.backoff.type
37
+ typeof options.backoff === 'string'
38
+ ? options.backoff
39
+ : options.backoff.type
30
40
  const delay =
31
41
  typeof options.backoff === 'object' ? options.backoff.delay : undefined
32
42
  pgBossOptions.retryBackoff = type === 'exponential'
@@ -1,7 +1,10 @@
1
1
  import { describe, test } from 'node:test'
2
2
  import assert from 'node:assert/strict'
3
3
 
4
- import { PgBossQueueWorkers } from './pg-boss-queue-worker.js'
4
+ import {
5
+ PgBossQueueWorkers,
6
+ mapPikkuWorkerToPgBoss,
7
+ } from './pg-boss-queue-worker.js'
5
8
 
6
9
  describe('PgBossQueueWorkers', () => {
7
10
  test('requires a logger (explicit or from singleton services)', async () => {
@@ -13,3 +16,78 @@ describe('PgBossQueueWorkers', () => {
13
16
  })
14
17
  })
15
18
  })
19
+
20
+ describe('mapPikkuWorkerToPgBoss', () => {
21
+ test('parallelism maps to independent workers (localConcurrency), never batch fetching', () => {
22
+ // batchSize: 1 keeps each worker single-job so a slow job can never hold its
23
+ // batch siblings — head-of-line blocking is impossible.
24
+ assert.deepEqual(mapPikkuWorkerToPgBoss(), {
25
+ localConcurrency: 10,
26
+ batchSize: 1,
27
+ })
28
+
29
+ assert.deepEqual(mapPikkuWorkerToPgBoss({ batchSize: 25 }), {
30
+ localConcurrency: 25,
31
+ batchSize: 1,
32
+ })
33
+ })
34
+
35
+ test('pollInterval is converted from ms to seconds', () => {
36
+ assert.deepEqual(mapPikkuWorkerToPgBoss({ pollInterval: 3000 }), {
37
+ localConcurrency: 10,
38
+ batchSize: 1,
39
+ pollingIntervalSeconds: 3,
40
+ })
41
+ })
42
+
43
+ test('groupConcurrency caps how much of the worker one group can take', () => {
44
+ assert.deepEqual(mapPikkuWorkerToPgBoss({ groupConcurrency: 2 }), {
45
+ localConcurrency: 10,
46
+ batchSize: 1,
47
+ localGroupConcurrency: 2,
48
+ })
49
+ })
50
+
51
+ test('tiered groupConcurrency passes tiers through', () => {
52
+ assert.deepEqual(
53
+ mapPikkuWorkerToPgBoss({
54
+ batchSize: 20,
55
+ groupConcurrency: { default: 2, tiers: { slowWorkflow: 1 } },
56
+ }),
57
+ {
58
+ localConcurrency: 20,
59
+ batchSize: 1,
60
+ localGroupConcurrency: { default: 2, tiers: { slowWorkflow: 1 } },
61
+ }
62
+ )
63
+ })
64
+
65
+ test('group limits are clamped to the worker concurrency', () => {
66
+ // pg-boss asserts every group limit is <= localConcurrency and throws at
67
+ // worker start otherwise — clamp so an over-eager config degrades to "this
68
+ // group may use the whole worker" instead of failing to start.
69
+ assert.deepEqual(
70
+ mapPikkuWorkerToPgBoss({
71
+ batchSize: 4,
72
+ groupConcurrency: { default: 10, tiers: { greedy: 99 } },
73
+ }),
74
+ {
75
+ localConcurrency: 4,
76
+ batchSize: 1,
77
+ localGroupConcurrency: { default: 4, tiers: { greedy: 4 } },
78
+ }
79
+ )
80
+
81
+ assert.deepEqual(
82
+ mapPikkuWorkerToPgBoss({ batchSize: 3, groupConcurrency: 8 }),
83
+ { localConcurrency: 3, batchSize: 1, localGroupConcurrency: 3 }
84
+ )
85
+ })
86
+
87
+ test('no group config means no group limits', () => {
88
+ assert.equal(
89
+ 'localGroupConcurrency' in mapPikkuWorkerToPgBoss({ batchSize: 5 }),
90
+ false
91
+ )
92
+ })
93
+ })
@@ -4,26 +4,32 @@ import type {
4
4
  PikkuWorkerConfig,
5
5
  QueueConfigMapping,
6
6
  ConfigValidationResult,
7
- } from '@pikku/core/queue'
7
+ } from '@pikku/core/ecosystem/queue'
8
+ import { runQueueJob } from '@pikku/core/queue'
8
9
  import {
9
10
  registerQueueWorkers,
10
- runQueueJob,
11
11
  QueueJobFailedError,
12
12
  QueueJobDiscardedError,
13
- } from '@pikku/core/queue'
14
- import { pikkuState } from '@pikku/core/internal'
13
+ } from '@pikku/core/ecosystem/queue'
14
+ import { pikkuState } from '@pikku/core/ecosystem'
15
15
  import type { Logger } from '@pikku/core/services'
16
16
  import { mapPgBossJobToQueueJob } from './utils.js'
17
17
 
18
18
  export const mapPikkuWorkerToPgBoss = (
19
19
  workerConfig?: PikkuWorkerConfig
20
20
  ): WorkOptions => {
21
+ // Map the requested parallelism to pg-boss `localConcurrency` (independent
22
+ // per-job workers), NOT `batchSize`. A batch worker hands the whole fetched
23
+ // batch to one handler and only fetches again once that handler resolves — so
24
+ // a single slow job blocks every sibling in its batch AND stalls the next
25
+ // fetch. On a shared queue that is head-of-line blocking: one long job can
26
+ // freeze the entire worker and starve everything queued behind it.
27
+ // `localConcurrency` spawns N pollers that each fetch+process a single job and
28
+ // refill the instant they finish, so a slow job never holds up the others.
29
+ const localConcurrency = workerConfig?.batchSize ?? 10
21
30
  const workerOptions: WorkOptions = {
22
- batchSize: 10,
23
- }
24
-
25
- if (workerConfig?.batchSize !== undefined) {
26
- workerOptions.batchSize = workerConfig.batchSize
31
+ localConcurrency,
32
+ batchSize: 1,
27
33
  }
28
34
 
29
35
  if (workerConfig?.pollInterval !== undefined) {
@@ -32,6 +38,30 @@ export const mapPikkuWorkerToPgBoss = (
32
38
  )
33
39
  }
34
40
 
41
+ // Per-group fairness. `localGroupConcurrency` is in-memory and free — groups
42
+ // already at capacity are excluded from the fetch itself, so an at-capacity
43
+ // group costs nothing rather than being fetched and restored. pg-boss asserts
44
+ // every limit is <= localConcurrency, so clamp instead of letting a config
45
+ // that reads as "let this tier use more" throw at worker start.
46
+ if (workerConfig?.groupConcurrency !== undefined) {
47
+ const clamp = (limit: number) => Math.min(limit, localConcurrency)
48
+ workerOptions.localGroupConcurrency =
49
+ typeof workerConfig.groupConcurrency === 'number'
50
+ ? clamp(workerConfig.groupConcurrency)
51
+ : {
52
+ default: clamp(workerConfig.groupConcurrency.default),
53
+ ...(workerConfig.groupConcurrency.tiers
54
+ ? {
55
+ tiers: Object.fromEntries(
56
+ Object.entries(workerConfig.groupConcurrency.tiers).map(
57
+ ([tier, limit]) => [tier, clamp(limit)]
58
+ )
59
+ ),
60
+ }
61
+ : {}),
62
+ }
63
+ }
64
+
35
65
  return workerOptions
36
66
  }
37
67
 
@@ -51,9 +81,13 @@ export class PgBossQueueWorkers implements QueueWorkers {
51
81
  // Configurations that are directly supported
52
82
  supported: {
53
83
  batchSize: {
54
- queueProperty: 'batchSize',
84
+ queueProperty: 'localConcurrency',
85
+ description: 'Number of jobs to process concurrently',
86
+ },
87
+ groupConcurrency: {
88
+ queueProperty: 'localGroupConcurrency',
55
89
  description:
56
- 'Number of jobs to fetch and process concurrently in parallel',
90
+ 'Maximum concurrent jobs per group, keeping one shared queue fair across groups',
57
91
  },
58
92
  pollInterval: {
59
93
  queueProperty: 'pollingIntervalSeconds',
@@ -1,12 +1,14 @@
1
1
  import type { PgBoss } from 'pg-boss'
2
+ import type { CoreUserSession } from '@pikku/core'
2
3
  import type {
3
4
  ScheduledTaskInfo,
4
5
  ScheduledTaskSummary,
5
- CoreUserSession,
6
- } from '@pikku/core'
7
- import { SchedulerService, parseDurationString } from '@pikku/core'
8
- import { pikkuState } from '@pikku/core/internal'
9
- import { runScheduledTask, getScheduledTasks } from '@pikku/core/scheduler'
6
+ } from '@pikku/core/ecosystem/services'
7
+ import { SchedulerService } from '@pikku/core'
8
+ import { parseDurationString } from '@pikku/core/ecosystem/types'
9
+ import { pikkuState } from '@pikku/core/ecosystem'
10
+ import { runScheduledTask } from '@pikku/core/scheduler'
11
+ import { getScheduledTasks } from '@pikku/core/ecosystem/scheduler'
10
12
 
11
13
  /**
12
14
  * Data stored in scheduled job