@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 +58 -0
- package/LICENSE +21 -0
- package/README.md +29 -0
- package/dist/index.d.ts +2 -1
- package/dist/pg-boss-queue-service.d.ts +2 -1
- package/dist/pg-boss-queue-service.js +9 -1
- package/dist/pg-boss-queue-worker.d.ts +1 -1
- package/dist/pg-boss-queue-worker.js +39 -8
- package/dist/pg-boss-scheduler-service.d.ts +2 -1
- package/dist/pg-boss-scheduler-service.js +5 -3
- package/package.json +3 -2
- package/src/index.ts +2 -3
- package/src/pg-boss-queue-service.test.ts +19 -0
- package/src/pg-boss-queue-service.ts +12 -2
- package/src/pg-boss-queue-worker.test.ts +79 -1
- package/src/pg-boss-queue-worker.ts +45 -11
- package/src/pg-boss-scheduler-service.ts +7 -5
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,
|
|
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
|
|
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'
|
|
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 {
|
|
2
|
-
import {
|
|
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
|
-
|
|
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: '
|
|
32
|
-
description: 'Number of jobs to
|
|
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 {
|
|
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
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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.
|
|
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.
|
|
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
|
|
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'
|
|
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 {
|
|
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/
|
|
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
|
-
|
|
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: '
|
|
84
|
+
queueProperty: 'localConcurrency',
|
|
85
|
+
description: 'Number of jobs to process concurrently',
|
|
86
|
+
},
|
|
87
|
+
groupConcurrency: {
|
|
88
|
+
queueProperty: 'localGroupConcurrency',
|
|
55
89
|
description:
|
|
56
|
-
'
|
|
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
|
-
|
|
6
|
-
} from '@pikku/core'
|
|
7
|
-
import {
|
|
8
|
-
import { pikkuState } from '@pikku/core/
|
|
9
|
-
import { runScheduledTask
|
|
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
|