@pikku/queue-bullmq 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 +48 -0
- package/dist/bull-queue-service.d.ts +8 -3
- package/dist/bull-queue-service.js +28 -0
- package/dist/bull-queue-worker.d.ts +1 -1
- package/knowledge/decisions/a-delayed-rpc-job-carries-the-callers-session-and-a-recurring-job-carries-none.md +31 -0
- package/knowledge/decisions/index.md +12 -0
- package/knowledge/index.md +13 -0
- package/package.json +9 -7
- package/run-tests.sh +21 -9
- package/src/bull-queue-service.test.ts +0 -39
- package/src/bull-queue-service.ts +0 -131
- package/src/bull-queue-worker.test.ts +0 -15
- package/src/bull-queue-worker.ts +0 -209
- package/src/bull-scheduler-service.ts +0 -198
- package/src/bull-service-factory.ts +0 -83
- package/src/index.ts +0 -19
- package/src/utils.ts +0 -66
- package/tsconfig.json +0 -17
- package/tsconfig.tsbuildinfo +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,51 @@
|
|
|
1
|
+
## 0.12.9
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- de9af6a: RPCs for Pikku Studio's migrated screens. Runtime state that must be visible on a deployed app lives in `@pikku/addon-admin`; local-copy tools stay in `@pikku/addon-console`.
|
|
6
|
+
|
|
7
|
+
`@pikku/addon-admin`:
|
|
8
|
+
|
|
9
|
+
- `admin:getSchedulerHistory` returns each scheduled task's last run, next run and recent runs, from a new `SchedulerService.getTaskRuns()` (empty by default). `InMemorySchedulerService` records the last 50 runs per cron task with status, duration and error. Scope `admin:schedules:read`.
|
|
10
|
+
- `admin:getQueueHistory({ queueNames?, limit? })` returns live counts and recent jobs per queue, from a new optional `QueueService.inspect(queueName, { limit })`, implemented by `InMemoryQueueService` and `BullQueueService` and forwarded by `SignedQueueService`. Scope `admin:queues:read`.
|
|
11
|
+
- `admin:listIncomingWebhookReceipts` / `admin:getIncomingWebhookReceipt` expose received webhooks, backed by a new `IncomingWebhookService.getReceipt(receiptId)` (null by default; implemented by `KyselyIncomingWebhookService`). Scope `admin:webhooks:read`.
|
|
12
|
+
|
|
13
|
+
`@pikku/addon-console`:
|
|
14
|
+
|
|
15
|
+
- `console:variableGet` / `console:variableSet` read and write a project variable, gated on `pikku:console:variables:read` / `:write`.
|
|
16
|
+
- `console:runScenarios({ names?, environment? })` starts `pikku scenario run` as a detached child process and returns `{ runId }` to follow through `listScenarioRuns` / `getScenarioRun`, gated on the new `pikku:console:scenarios:run` scope. `pikku scenario run` takes `--run-id <id>` to record under a caller-chosen id.
|
|
17
|
+
|
|
18
|
+
- Updated dependencies [de9af6a]
|
|
19
|
+
- @pikku/core@0.12.143
|
|
20
|
+
|
|
21
|
+
## 0.12.8
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- 4c7a1b5: Run the monorepo's own scripts through bun instead of yarn. What moves is the
|
|
26
|
+
package manager each package's `prepublishOnly` and build scripts invoke, plus
|
|
27
|
+
the two manifest fixes bun needs to resolve the tree: `uWebSockets.js` is
|
|
28
|
+
declared with an explicit `github:` specifier, and `@pikku/uws-handler` marks
|
|
29
|
+
its `uWebSockets.js` peer optional so a bun install of a consumer that brings
|
|
30
|
+
its own uWS app does not try to fetch it from the registry.
|
|
31
|
+
|
|
32
|
+
Three published behaviours change, all of them cases where an isolated
|
|
33
|
+
`node_modules` or bun as the runtime had been papered over by yarn's hoisting:
|
|
34
|
+
|
|
35
|
+
- `@pikku/migrator-sql` turns foreign keys on when it opens a sqlite database
|
|
36
|
+
through bun. `node:sqlite` enforces them by default and `bun:sqlite` does not,
|
|
37
|
+
which silently turned every `ON DELETE CASCADE` into a no-op under
|
|
38
|
+
`bunx --bun pikku`.
|
|
39
|
+
- `@pikku/cli` resolves a deploy provider against the project being deployed
|
|
40
|
+
rather than against wherever the CLI itself is installed, which is what its
|
|
41
|
+
own "is not installed" error asks the user to arrange.
|
|
42
|
+
- `@pikku/cli` treats a specifier a runtime hands straight back — bun does this
|
|
43
|
+
for the modules it implements itself — as not resolved from the project, so
|
|
44
|
+
it falls back rather than loading the runtime's own copy.
|
|
45
|
+
|
|
46
|
+
- Updated dependencies [4c7a1b5]
|
|
47
|
+
- @pikku/core@0.12.108
|
|
48
|
+
|
|
1
49
|
## 0.12.7
|
|
2
50
|
|
|
3
51
|
### Patch Changes
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { ConnectionOptions, JobsOptions } from 'bullmq';
|
|
2
2
|
import type Bull from 'bullmq';
|
|
3
|
-
import
|
|
3
|
+
import { Queue, QueueEvents } from 'bullmq';
|
|
4
|
+
import type { QueueInspection, QueueJob, QueueJobRecord, QueueJobStatus, QueueService } from '@pikku/core/queue';
|
|
4
5
|
import type { JobOptions } from '@pikku/core/queue';
|
|
5
6
|
export declare const mapPikkuJobToBull: (options?: JobOptions) => JobsOptions;
|
|
7
|
+
export declare const mapBullJobToRecord: (job: Bull.Job, status: QueueJobStatus) => QueueJobRecord;
|
|
6
8
|
/**
|
|
7
9
|
* Bull/Redis queue service implementation
|
|
8
10
|
* Supports job results, retries, and most queue features
|
|
@@ -11,8 +13,8 @@ export declare class BullQueueService implements QueueService {
|
|
|
11
13
|
private redisConnectionOptions;
|
|
12
14
|
readonly name = "bull";
|
|
13
15
|
readonly supportsResults = true;
|
|
14
|
-
protected queues: Map<string,
|
|
15
|
-
protected queueEvents: Map<string,
|
|
16
|
+
protected queues: Map<string, Queue<any, any, string, any, any, string, Bull.RedisQueueBackend, ConnectionOptions>>;
|
|
17
|
+
protected queueEvents: Map<string, QueueEvents<any, Bull.RedisQueueBackend, ConnectionOptions>>;
|
|
16
18
|
constructor(redisConnectionOptions?: ConnectionOptions);
|
|
17
19
|
/**
|
|
18
20
|
* Create a Bull queue with translated configuration
|
|
@@ -22,6 +24,9 @@ export declare class BullQueueService implements QueueService {
|
|
|
22
24
|
* Add a job to the queue
|
|
23
25
|
*/
|
|
24
26
|
add<In>(queueName: string, data: In, options?: JobOptions): Promise<string>;
|
|
27
|
+
inspect(queueName: string, opts?: {
|
|
28
|
+
limit?: number;
|
|
29
|
+
}): Promise<QueueInspection>;
|
|
25
30
|
/**
|
|
26
31
|
* Get job by ID
|
|
27
32
|
*/
|
|
@@ -30,6 +30,24 @@ export const mapPikkuJobToBull = (options) => {
|
|
|
30
30
|
}
|
|
31
31
|
return bullOptions;
|
|
32
32
|
};
|
|
33
|
+
const INSPECTED_STATES = [
|
|
34
|
+
'waiting',
|
|
35
|
+
'active',
|
|
36
|
+
'delayed',
|
|
37
|
+
'completed',
|
|
38
|
+
'failed',
|
|
39
|
+
];
|
|
40
|
+
export const mapBullJobToRecord = (job, status) => ({
|
|
41
|
+
jobId: job.id,
|
|
42
|
+
status,
|
|
43
|
+
attemptsMade: job.attemptsMade,
|
|
44
|
+
maxAttempts: job.opts.attempts ?? 1,
|
|
45
|
+
createdAt: new Date(job.timestamp),
|
|
46
|
+
processedAt: job.processedOn ? new Date(job.processedOn) : null,
|
|
47
|
+
finishedAt: job.finishedOn ? new Date(job.finishedOn) : null,
|
|
48
|
+
durationMs: job.finishedOn && job.processedOn ? job.finishedOn - job.processedOn : null,
|
|
49
|
+
error: job.failedReason || null,
|
|
50
|
+
});
|
|
33
51
|
/**
|
|
34
52
|
* Bull/Redis queue service implementation
|
|
35
53
|
* Supports job results, retries, and most queue features
|
|
@@ -76,6 +94,16 @@ export class BullQueueService {
|
|
|
76
94
|
}
|
|
77
95
|
return bullJob.id;
|
|
78
96
|
}
|
|
97
|
+
async inspect(queueName, opts) {
|
|
98
|
+
const queue = await this.createQueue(queueName);
|
|
99
|
+
const limit = opts?.limit ?? 50;
|
|
100
|
+
const counts = (await queue.getJobCounts(...INSPECTED_STATES));
|
|
101
|
+
const recent = (await Promise.all(INSPECTED_STATES.map(async (status) => (await queue.getJobs([status], 0, limit - 1)).map((job) => mapBullJobToRecord(job, status)))))
|
|
102
|
+
.flat()
|
|
103
|
+
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
|
|
104
|
+
.slice(0, limit);
|
|
105
|
+
return { counts, recent };
|
|
106
|
+
}
|
|
79
107
|
/**
|
|
80
108
|
* Get job by ID
|
|
81
109
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ConnectionOptions, WorkerOptions } from 'bullmq';
|
|
2
2
|
import type { QueueWorkers, PikkuWorkerConfig, QueueConfigMapping, ConfigValidationResult } from '@pikku/core/queue';
|
|
3
|
-
export declare const mapPikkuWorkerToBull: (workerConfig?: PikkuWorkerConfig) => Omit<WorkerOptions,
|
|
3
|
+
export declare const mapPikkuWorkerToBull: (workerConfig?: PikkuWorkerConfig) => Omit<WorkerOptions, 'connection'>;
|
|
4
4
|
/**
|
|
5
5
|
* Bull/Redis queue service implementation
|
|
6
6
|
* Supports job results, retries, and most queue features
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A delayed RPC job carries the caller's session in its data, and a recurring job carries none
|
|
4
|
+
description: BullSchedulerService stores the session in the job data and returns it from getTask, while the recurring worker runs the task by name alone
|
|
5
|
+
tags: bullmq, scheduler, sessions, rpc
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A delayed RPC job carries the caller's session in its data, and a recurring job carries none
|
|
9
|
+
|
|
10
|
+
`BullSchedulerService` in `packages/services/queue-bullmq/src/bull-scheduler-service.ts` defines the job data
|
|
11
|
+
as `{ rpcName, data?, session? }` (lines 17-21).
|
|
12
|
+
|
|
13
|
+
**Delayed.** `scheduleRPC` takes an optional `session` (line 56) and puts it in the job data (lines 62-66). The
|
|
14
|
+
job is added to the `pikku-remote-internal-rpc` queue with the delay (lines 69-72). `getTask` returns the stored
|
|
15
|
+
`session` (line 116). `getAllTasks` returns only the id, rpc name and time (lines 133-137). The session is
|
|
16
|
+
therefore stored in Redis with the job.
|
|
17
|
+
|
|
18
|
+
**What runs it.** This file has no worker for that queue. The CLI generates one, `remoteRPCHandler`, whose
|
|
19
|
+
function destructures only `{ rpcName, data }` and calls `rpc.invoke(rpcName, data)`
|
|
20
|
+
(`packages/cli/src/functions/wirings/rpc/serialize-remote-rpc.ts:37-39`). It is wired to the queue at lines
|
|
21
|
+
43-47 of the same file. Nothing in that handler reads the job's `session`.
|
|
22
|
+
|
|
23
|
+
**Recurring.** `start` creates a worker on `pikku-recurring-scheduled-task` (lines 161-169) that calls
|
|
24
|
+
`runScheduledTask({ name: rpcName })` (line 166). The job data written for each task is `{ rpcName: name }`
|
|
25
|
+
(line 179), so there is no session to carry.
|
|
26
|
+
|
|
27
|
+
The same question for the in-memory scheduler is open in
|
|
28
|
+
[does-a-delayed-rpc-run-as-the-session-it-was-scheduled-with](../../../../core/knowledge/questions/does-a-delayed-rpc-run-as-the-session-it-was-scheduled-with.md).
|
|
29
|
+
|
|
30
|
+
**What this rules out:** reading `getTask().session` as evidence that the delayed run executes as that user,
|
|
31
|
+
and putting anything secret in a session passed to `scheduleRPC`, since it is stored in the job data.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: overview
|
|
3
|
+
title: Decisions
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Decisions
|
|
7
|
+
|
|
8
|
+
A rule that was chosen, and what it rules out.
|
|
9
|
+
|
|
10
|
+
<!-- pikku:knowledge-index -->
|
|
11
|
+
- [A delayed RPC job carries the caller's session in its data, and a recurring job carries none](a-delayed-rpc-job-carries-the-callers-session-and-a-recurring-job-carries-none.md) — BullSchedulerService stores the session in the job data and returns it from getTask, while the recurring worker runs the task by name alone
|
|
12
|
+
<!-- /pikku:knowledge-index -->
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: overview
|
|
3
|
+
title: Knowledge
|
|
4
|
+
description: What identity a BullMQ job carries when it runs
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Knowledge
|
|
8
|
+
|
|
9
|
+
The BullMQ scheduler runs delayed and recurring jobs. These notes record which of them carry a caller's session.
|
|
10
|
+
|
|
11
|
+
<!-- pikku:knowledge-index -->
|
|
12
|
+
- [decisions](decisions/index.md) — a rule that was chosen, and what it rules out
|
|
13
|
+
<!-- /pikku:knowledge-index -->
|
package/package.json
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/queue-bullmq",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.9",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/pikkujs/pikku.git",
|
|
7
|
+
"directory": "packages/services/queue-bullmq"
|
|
8
|
+
},
|
|
4
9
|
"description": "BullMQ queue, worker and scheduler services for Pikku",
|
|
5
10
|
"author": "yasser.fadl@gmail.com",
|
|
6
11
|
"license": "MIT",
|
|
@@ -13,19 +18,16 @@
|
|
|
13
18
|
"ncu": "npx npm-check-updates",
|
|
14
19
|
"build": "tsc -b",
|
|
15
20
|
"release": "npm run build && npm test",
|
|
16
|
-
"prepublishOnly": "
|
|
21
|
+
"prepublishOnly": "bun run build"
|
|
17
22
|
},
|
|
18
23
|
"peerDependencies": {
|
|
19
|
-
"@pikku/core": "^0.12.
|
|
24
|
+
"@pikku/core": "^0.12.143"
|
|
20
25
|
},
|
|
21
26
|
"dependencies": {
|
|
22
27
|
"bullmq": "^6.1.1",
|
|
23
28
|
"ioredis": "^6.0.0"
|
|
24
29
|
},
|
|
25
|
-
"devDependencies": {
|
|
26
|
-
"typescript": "^6.0.3"
|
|
27
|
-
},
|
|
28
30
|
"exports": {
|
|
29
31
|
".": "./dist/index.js"
|
|
30
32
|
}
|
|
31
|
-
}
|
|
33
|
+
}
|
package/run-tests.sh
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
|
|
3
|
-
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
4
|
-
cd "$script_dir"
|
|
5
|
-
|
|
6
3
|
# Enable nullglob to handle cases where no files match the pattern
|
|
7
4
|
shopt -s nullglob
|
|
8
5
|
|
|
@@ -37,20 +34,35 @@ files=($(find src -type f -name "*.test.ts"))
|
|
|
37
34
|
# Check if any files matched the pattern
|
|
38
35
|
if [ ${#files[@]} -eq 0 ]; then
|
|
39
36
|
echo "No test files found matching pattern: $pattern"
|
|
37
|
+
if [ "${STRICT_TEST_DISCOVERY}" = "1" ]; then
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
40
|
exit 0
|
|
41
41
|
fi
|
|
42
42
|
|
|
43
|
-
# Construct the
|
|
44
|
-
|
|
43
|
+
# Construct the bun command
|
|
44
|
+
#
|
|
45
|
+
# --parallel gives every test file its own worker process, which is what node's
|
|
46
|
+
# `--test` did. Several suites keep module-level caches (a TypeScript program, a
|
|
47
|
+
# temp fixture directory) that a shared process would leak between files.
|
|
48
|
+
#
|
|
49
|
+
# --timeout because bun caps a test at 5s by default where node had no cap, and
|
|
50
|
+
# the suites that build real TypeScript programs run well past that.
|
|
51
|
+
bun_cmd=(bun test --parallel --timeout 120000)
|
|
45
52
|
|
|
46
53
|
# Append options based on flags
|
|
47
54
|
if [ "$watch_mode" = true ]; then
|
|
48
|
-
|
|
55
|
+
# --watch reruns in one process; parallel workers have nothing to watch.
|
|
56
|
+
bun_cmd=(bun test --watch --timeout 120000)
|
|
49
57
|
fi
|
|
50
58
|
|
|
51
59
|
if [ "$coverage_mode" = true ]; then
|
|
52
|
-
|
|
60
|
+
# --coverage-dir=. keeps lcov.info at the package root: CI merges every
|
|
61
|
+
# package's report by prefixing its SF paths with the file's own directory,
|
|
62
|
+
# so a nested coverage/ directory would re-root them one level too deep.
|
|
63
|
+
bun_cmd+=(--coverage --coverage-reporter=lcov --coverage-reporter=text --coverage-dir=.)
|
|
64
|
+
export PIKKU_TEST_COVERAGE=1
|
|
53
65
|
fi
|
|
54
66
|
|
|
55
|
-
# Execute the
|
|
56
|
-
"${
|
|
67
|
+
# Execute the bun command with the expanded list of files
|
|
68
|
+
"${bun_cmd[@]}" "${files[@]}"
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { describe, test } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
|
|
4
|
-
import { mapPikkuJobToBull } from './bull-queue-service.js'
|
|
5
|
-
|
|
6
|
-
// Same contract as the pg-boss adapter: the workflow's resolved retry policy
|
|
7
|
-
// (attempts + backoff) must reach BullMQ. backoff mapping in particular was
|
|
8
|
-
// previously dropped, so a step's backoff silently never applied on Redis.
|
|
9
|
-
describe('mapPikkuJobToBull — retry mapping', () => {
|
|
10
|
-
test('attempts passes through unchanged (BullMQ uses attempts directly)', () => {
|
|
11
|
-
assert.equal(mapPikkuJobToBull({ attempts: 6 }).attempts, 6)
|
|
12
|
-
assert.equal(mapPikkuJobToBull({ attempts: 1 }).attempts, 1)
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
test("string backoff 'exponential' → { type: 'exponential' }", () => {
|
|
16
|
-
assert.deepEqual(
|
|
17
|
-
mapPikkuJobToBull({ attempts: 6, backoff: 'exponential' }).backoff,
|
|
18
|
-
{
|
|
19
|
-
type: 'exponential',
|
|
20
|
-
}
|
|
21
|
-
)
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
test('object backoff → { type, delay } (delay stays in ms)', () => {
|
|
25
|
-
assert.deepEqual(
|
|
26
|
-
mapPikkuJobToBull({ attempts: 4, backoff: { type: 'fixed', delay: 250 } })
|
|
27
|
-
.backoff,
|
|
28
|
-
{ type: 'fixed', delay: 250 }
|
|
29
|
-
)
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
test('no backoff → backoff left unset', () => {
|
|
33
|
-
assert.equal(mapPikkuJobToBull({ attempts: 6 }).backoff, undefined)
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
test('no options → empty job options', () => {
|
|
37
|
-
assert.deepEqual(mapPikkuJobToBull(undefined), {})
|
|
38
|
-
})
|
|
39
|
-
})
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import type { ConnectionOptions, JobsOptions } from 'bullmq'
|
|
2
|
-
import type Bull from 'bullmq'
|
|
3
|
-
import { Queue, QueueEvents } from 'bullmq'
|
|
4
|
-
import type { QueueJob, QueueService } from '@pikku/core/queue'
|
|
5
|
-
import type { JobOptions } from '@pikku/core/queue'
|
|
6
|
-
import { mapBullJobToQueueJob } from './utils.js'
|
|
7
|
-
|
|
8
|
-
export const mapPikkuJobToBull = (options?: JobOptions): JobsOptions => {
|
|
9
|
-
const bullOptions: JobsOptions = {}
|
|
10
|
-
|
|
11
|
-
if (options?.priority !== undefined) {
|
|
12
|
-
bullOptions.priority = options.priority
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
if (options?.delay !== undefined) {
|
|
16
|
-
bullOptions.delay = options.delay
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (options?.attempts !== undefined) {
|
|
20
|
-
bullOptions.attempts = options.attempts
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Map retry backoff. BullMQ accepts `{ type, delay }`; our `delay` is in ms,
|
|
24
|
-
// which is what BullMQ expects too (unlike pg-boss, which uses seconds).
|
|
25
|
-
if (options?.backoff !== undefined) {
|
|
26
|
-
bullOptions.backoff =
|
|
27
|
-
typeof options.backoff === 'string'
|
|
28
|
-
? { type: options.backoff }
|
|
29
|
-
: { type: options.backoff.type, delay: options.backoff.delay }
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (options?.removeOnComplete !== undefined) {
|
|
33
|
-
bullOptions.removeOnComplete = options.removeOnComplete
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (options?.removeOnFail !== undefined) {
|
|
37
|
-
bullOptions.removeOnFail = options.removeOnFail
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (options?.jobId !== undefined) {
|
|
41
|
-
bullOptions.jobId = options.jobId
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return bullOptions
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Bull/Redis queue service implementation
|
|
49
|
-
* Supports job results, retries, and most queue features
|
|
50
|
-
*/
|
|
51
|
-
export class BullQueueService implements QueueService {
|
|
52
|
-
readonly name = 'bull'
|
|
53
|
-
readonly supportsResults = true
|
|
54
|
-
protected queues = new Map<string, Bull.Queue>()
|
|
55
|
-
protected queueEvents = new Map<string, Bull.QueueEvents>()
|
|
56
|
-
|
|
57
|
-
constructor(private redisConnectionOptions: ConnectionOptions = {}) {}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Create a Bull queue with translated configuration
|
|
61
|
-
*/
|
|
62
|
-
protected async createQueue(
|
|
63
|
-
queueName: string,
|
|
64
|
-
config?: Bull.QueueOptions
|
|
65
|
-
): Promise<Bull.Queue> {
|
|
66
|
-
if (this.queues.has(queueName)) {
|
|
67
|
-
return this.queues.get(queueName)!
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const queue = new Queue(queueName, config)
|
|
71
|
-
queue.on('error', (error) => {
|
|
72
|
-
console.error('Redis connection error:', error)
|
|
73
|
-
})
|
|
74
|
-
await queue.waitUntilReady()
|
|
75
|
-
|
|
76
|
-
this.queues.set(queueName, queue)
|
|
77
|
-
|
|
78
|
-
if (!this.queueEvents.has(queueName)) {
|
|
79
|
-
const queueEvents = new QueueEvents(
|
|
80
|
-
queueName,
|
|
81
|
-
this.redisConnectionOptions
|
|
82
|
-
? { connection: this.redisConnectionOptions }
|
|
83
|
-
: undefined
|
|
84
|
-
)
|
|
85
|
-
await queueEvents.waitUntilReady()
|
|
86
|
-
this.queueEvents.set(queueName, queueEvents)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return queue
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Add a job to the queue
|
|
94
|
-
*/
|
|
95
|
-
public async add<In>(
|
|
96
|
-
queueName: string,
|
|
97
|
-
data: In,
|
|
98
|
-
options?: JobOptions
|
|
99
|
-
): Promise<string> {
|
|
100
|
-
const queue = await this.createQueue(queueName)
|
|
101
|
-
const bullJob = await queue.add(queueName, data, mapPikkuJobToBull(options))
|
|
102
|
-
if (!bullJob.id) {
|
|
103
|
-
throw new Error(`Failed to add job to queue '${queueName}'.`)
|
|
104
|
-
}
|
|
105
|
-
return bullJob.id
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Get job by ID
|
|
110
|
-
*/
|
|
111
|
-
async getJob<T, R>(
|
|
112
|
-
queueName: string,
|
|
113
|
-
jobId: string
|
|
114
|
-
): Promise<QueueJob<T, R> | null> {
|
|
115
|
-
const queue = this.queues.get(queueName)
|
|
116
|
-
if (!queue) {
|
|
117
|
-
throw new Error(`Queue '${queueName}' not found.`)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const bullJob = await queue.getJob(jobId)
|
|
121
|
-
if (!bullJob) {
|
|
122
|
-
return null
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return mapBullJobToQueueJob<T, R>(
|
|
126
|
-
bullJob,
|
|
127
|
-
this.redisConnectionOptions,
|
|
128
|
-
this.queueEvents
|
|
129
|
-
)
|
|
130
|
-
}
|
|
131
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { describe, test } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
|
|
4
|
-
import { BullQueueWorkers } from './bull-queue-worker.js'
|
|
5
|
-
|
|
6
|
-
describe('BullQueueWorkers', () => {
|
|
7
|
-
test('requires a logger (explicit or from singleton services)', async () => {
|
|
8
|
-
const workers = new BullQueueWorkers({})
|
|
9
|
-
|
|
10
|
-
await assert.rejects(() => workers.registerQueues(), {
|
|
11
|
-
message:
|
|
12
|
-
'Logger is required for registerQueues — pass it explicitly or ensure singleton services are initialized first',
|
|
13
|
-
})
|
|
14
|
-
})
|
|
15
|
-
})
|
package/src/bull-queue-worker.ts
DELETED
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
import type { ConnectionOptions, WorkerOptions, QueueEvents } from 'bullmq'
|
|
2
|
-
import type Bull from 'bullmq'
|
|
3
|
-
import { Worker } from 'bullmq'
|
|
4
|
-
import type {
|
|
5
|
-
QueueWorkers,
|
|
6
|
-
PikkuWorkerConfig,
|
|
7
|
-
QueueConfigMapping,
|
|
8
|
-
ConfigValidationResult,
|
|
9
|
-
} from '@pikku/core/queue'
|
|
10
|
-
import { runQueueJob } from '@pikku/core/queue'
|
|
11
|
-
import {
|
|
12
|
-
registerQueueWorkers,
|
|
13
|
-
QueueJobFailedError,
|
|
14
|
-
QueueJobDiscardedError,
|
|
15
|
-
} from '@pikku/core/queue'
|
|
16
|
-
import { pikkuState } from '@pikku/core/state'
|
|
17
|
-
import { mapBullJobToQueueJob } from './utils.js'
|
|
18
|
-
|
|
19
|
-
export const mapPikkuWorkerToBull = (
|
|
20
|
-
workerConfig?: PikkuWorkerConfig
|
|
21
|
-
): Omit<WorkerOptions, 'connection'> => {
|
|
22
|
-
const workerOptions: Omit<WorkerOptions, 'connection'> = {}
|
|
23
|
-
|
|
24
|
-
// Direct mappings
|
|
25
|
-
if (workerConfig?.name !== undefined) {
|
|
26
|
-
workerOptions.name = workerConfig.name
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (workerConfig?.batchSize !== undefined) {
|
|
30
|
-
workerOptions.concurrency = workerConfig.batchSize
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (workerConfig?.autorun !== undefined) {
|
|
34
|
-
workerOptions.autorun = workerConfig.autorun
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (workerConfig?.lockDuration !== undefined) {
|
|
38
|
-
workerOptions.lockDuration = workerConfig.lockDuration
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (workerConfig?.drainDelay !== undefined) {
|
|
42
|
-
workerOptions.drainDelay = workerConfig.drainDelay
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (workerConfig?.maxStalledCount !== undefined) {
|
|
46
|
-
workerOptions.maxStalledCount = workerConfig.maxStalledCount
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Job retention
|
|
50
|
-
if (workerConfig?.removeOnComplete !== undefined) {
|
|
51
|
-
workerOptions.removeOnComplete = { count: workerConfig.removeOnComplete }
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (workerConfig?.removeOnFail !== undefined) {
|
|
55
|
-
workerOptions.removeOnFail = { count: workerConfig.removeOnFail }
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return workerOptions
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Bull/Redis queue service implementation
|
|
63
|
-
* Supports job results, retries, and most queue features
|
|
64
|
-
*/
|
|
65
|
-
export class BullQueueWorkers implements QueueWorkers {
|
|
66
|
-
readonly name = 'bull'
|
|
67
|
-
readonly supportsResults = true
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Configuration mapping rules for BullMQ
|
|
71
|
-
* This defines how Pikku worker configs map to BullMQ configurations
|
|
72
|
-
*/
|
|
73
|
-
readonly configMappings: QueueConfigMapping = {
|
|
74
|
-
// Configurations that are directly supported
|
|
75
|
-
supported: {
|
|
76
|
-
name: {
|
|
77
|
-
queueProperty: 'name',
|
|
78
|
-
description: 'Worker name for identification and monitoring',
|
|
79
|
-
},
|
|
80
|
-
batchSize: {
|
|
81
|
-
queueProperty: 'concurrency',
|
|
82
|
-
description:
|
|
83
|
-
"Number of jobs to process concurrently, this is Bull's concurrency setting",
|
|
84
|
-
},
|
|
85
|
-
autorun: {
|
|
86
|
-
queueProperty: 'autorun',
|
|
87
|
-
description: 'Whether to start processing jobs automatically',
|
|
88
|
-
},
|
|
89
|
-
lockDuration: {
|
|
90
|
-
queueProperty: 'lockDuration',
|
|
91
|
-
description: 'Duration of job lock in milliseconds',
|
|
92
|
-
},
|
|
93
|
-
drainDelay: {
|
|
94
|
-
queueProperty: 'drainDelay',
|
|
95
|
-
description: 'Delay when queue is empty before polling again',
|
|
96
|
-
},
|
|
97
|
-
maxStalledCount: {
|
|
98
|
-
queueProperty: 'maxStalledCount',
|
|
99
|
-
description:
|
|
100
|
-
'Maximum number of times a job can be recovered from stalled state',
|
|
101
|
-
},
|
|
102
|
-
removeOnComplete: {
|
|
103
|
-
queueProperty: 'removeOnComplete',
|
|
104
|
-
description: 'Number of completed jobs to keep for inspection',
|
|
105
|
-
},
|
|
106
|
-
removeOnFail: {
|
|
107
|
-
queueProperty: 'removeOnFail',
|
|
108
|
-
description: 'Number of failed jobs to keep for inspection',
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
|
|
112
|
-
// Configurations that are not supported
|
|
113
|
-
unsupported: {
|
|
114
|
-
groupConcurrency: {
|
|
115
|
-
reason: 'Job groups are a BullMQ Pro feature, not available in BullMQ',
|
|
116
|
-
explanation:
|
|
117
|
-
'Give each group its own queue instead — BullMQ is push-based, so extra queues cost no polling',
|
|
118
|
-
},
|
|
119
|
-
visibilityTimeout: {
|
|
120
|
-
reason: 'Bull does not use visibility timeout',
|
|
121
|
-
explanation:
|
|
122
|
-
'BullMQ locks jobs during processing instead of using visibility timeouts',
|
|
123
|
-
},
|
|
124
|
-
pollInterval: {
|
|
125
|
-
reason: 'Bull is push-based and does not use polling intervals',
|
|
126
|
-
explanation:
|
|
127
|
-
'BullMQ uses Redis pub/sub for real-time job notifications',
|
|
128
|
-
},
|
|
129
|
-
prefetch: {
|
|
130
|
-
reason: 'Bull manages job prefetching internally',
|
|
131
|
-
explanation:
|
|
132
|
-
'BullMQ optimizes job fetching automatically based on concurrency settings',
|
|
133
|
-
},
|
|
134
|
-
},
|
|
135
|
-
|
|
136
|
-
// No fallback configurations for BullMQ currently
|
|
137
|
-
fallbacks: {},
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
private workers = new Map<string, Bull.Worker>()
|
|
141
|
-
private queueEvents = new Map<string, QueueEvents>()
|
|
142
|
-
|
|
143
|
-
constructor(private redisConnectionOptions: ConnectionOptions) {}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Scan state and register all compatible processors
|
|
147
|
-
*/
|
|
148
|
-
async registerQueues(): Promise<Record<string, ConfigValidationResult[]>> {
|
|
149
|
-
const logger = pikkuState(null, 'package', 'singletonServices')?.logger
|
|
150
|
-
if (!logger) {
|
|
151
|
-
throw new Error(
|
|
152
|
-
'Logger is required for registerQueues — pass it explicitly or ensure singleton services are initialized first'
|
|
153
|
-
)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return await registerQueueWorkers(
|
|
157
|
-
this.configMappings,
|
|
158
|
-
logger,
|
|
159
|
-
async (queueName, processor) => {
|
|
160
|
-
const worker = new Worker(
|
|
161
|
-
processor.name,
|
|
162
|
-
async (job: Bull.Job) => {
|
|
163
|
-
try {
|
|
164
|
-
return await runQueueJob({
|
|
165
|
-
job: await mapBullJobToQueueJob(
|
|
166
|
-
job,
|
|
167
|
-
this.redisConnectionOptions,
|
|
168
|
-
this.queueEvents
|
|
169
|
-
),
|
|
170
|
-
updateProgress: async (progress) => {
|
|
171
|
-
await job.updateProgress(progress)
|
|
172
|
-
},
|
|
173
|
-
})
|
|
174
|
-
} catch (error) {
|
|
175
|
-
if (error instanceof QueueJobFailedError) {
|
|
176
|
-
// Let BullMQ handle this as a failed job
|
|
177
|
-
throw new Error(error.message)
|
|
178
|
-
} else if (error instanceof QueueJobDiscardedError) {
|
|
179
|
-
return // Don't throw, job is discarded, which I guess is considered
|
|
180
|
-
// a successful job removal
|
|
181
|
-
}
|
|
182
|
-
throw error
|
|
183
|
-
}
|
|
184
|
-
},
|
|
185
|
-
{
|
|
186
|
-
connection: this.redisConnectionOptions,
|
|
187
|
-
...mapPikkuWorkerToBull(processor.config),
|
|
188
|
-
}
|
|
189
|
-
)
|
|
190
|
-
worker.on('error', console.error)
|
|
191
|
-
this.workers.set(queueName, worker)
|
|
192
|
-
}
|
|
193
|
-
)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Close all queues and connections
|
|
198
|
-
*/
|
|
199
|
-
async close(): Promise<void> {
|
|
200
|
-
await Promise.all([
|
|
201
|
-
...Array.from(this.workers.values()).map((worker) => worker.close()),
|
|
202
|
-
...Array.from(this.queueEvents.values()).map((queueEvents) =>
|
|
203
|
-
queueEvents.close()
|
|
204
|
-
),
|
|
205
|
-
])
|
|
206
|
-
this.workers.clear()
|
|
207
|
-
this.queueEvents.clear()
|
|
208
|
-
}
|
|
209
|
-
}
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
import type { ConnectionOptions } from 'bullmq'
|
|
2
|
-
import { Queue, Worker } from 'bullmq'
|
|
3
|
-
import { runScheduledTask } from '@pikku/core/scheduler'
|
|
4
|
-
import { SchedulerService } from '@pikku/core/services'
|
|
5
|
-
import type { CoreUserSession } from '@pikku/core/types'
|
|
6
|
-
import type {
|
|
7
|
-
ScheduledTaskInfo,
|
|
8
|
-
ScheduledTaskSummary,
|
|
9
|
-
} from '@pikku/core/services'
|
|
10
|
-
import { parseDurationString } from '@pikku/core/utils'
|
|
11
|
-
import { pikkuState } from '@pikku/core/state'
|
|
12
|
-
import { getScheduledTasks } from '@pikku/core/scheduler'
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Data stored in scheduled job
|
|
16
|
-
*/
|
|
17
|
-
interface ScheduledJobData {
|
|
18
|
-
rpcName: string
|
|
19
|
-
data?: any
|
|
20
|
-
session?: CoreUserSession
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const RECURRING_QUEUE_NAME = 'pikku-recurring-scheduled-task'
|
|
24
|
-
|
|
25
|
-
export class BullSchedulerService extends SchedulerService {
|
|
26
|
-
private queue: Queue
|
|
27
|
-
private recurringQueue: Queue
|
|
28
|
-
private recurringWorker?: Worker
|
|
29
|
-
private jobSchedulerIds: string[] = []
|
|
30
|
-
|
|
31
|
-
constructor(private redisConnectionOptions: ConnectionOptions) {
|
|
32
|
-
super()
|
|
33
|
-
this.queue = new Queue('pikku-remote-internal-rpc', {
|
|
34
|
-
connection: redisConnectionOptions,
|
|
35
|
-
})
|
|
36
|
-
this.recurringQueue = new Queue(RECURRING_QUEUE_NAME, {
|
|
37
|
-
connection: redisConnectionOptions,
|
|
38
|
-
})
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Initialize - wait for queue to be ready
|
|
43
|
-
*/
|
|
44
|
-
async init(): Promise<void> {
|
|
45
|
-
await this.queue.waitUntilReady()
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Schedule a one-off delayed RPC call
|
|
50
|
-
* Uses BullMQ add() with delay option
|
|
51
|
-
*/
|
|
52
|
-
async scheduleRPC(
|
|
53
|
-
delay: number | string,
|
|
54
|
-
rpcName: string,
|
|
55
|
-
data?: any,
|
|
56
|
-
session?: CoreUserSession
|
|
57
|
-
): Promise<string> {
|
|
58
|
-
// Parse delay if it's a string
|
|
59
|
-
const delayMs =
|
|
60
|
-
typeof delay === 'string' ? parseDurationString(delay) : delay
|
|
61
|
-
|
|
62
|
-
const jobData: ScheduledJobData = {
|
|
63
|
-
rpcName,
|
|
64
|
-
data,
|
|
65
|
-
session,
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Use add() with delay for one-off delayed execution
|
|
69
|
-
const job = await this.queue.add('pikku-remote-internal-rpc', jobData, {
|
|
70
|
-
delay: delayMs, // delay in milliseconds
|
|
71
|
-
jobId: `${rpcName}-${Date.now()}`, // Ensure uniqueness
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
if (!job.id) {
|
|
75
|
-
throw new Error('Failed to schedule RPC')
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return job.id
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Unschedule (remove) a task by ID
|
|
83
|
-
*/
|
|
84
|
-
async unschedule(taskId: string): Promise<boolean> {
|
|
85
|
-
const job = await this.queue.getJob(taskId)
|
|
86
|
-
if (job) {
|
|
87
|
-
await job.remove()
|
|
88
|
-
return true
|
|
89
|
-
}
|
|
90
|
-
return false
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Get a scheduled task by ID with full details
|
|
95
|
-
*/
|
|
96
|
-
async getTask(taskId: string): Promise<ScheduledTaskInfo | null> {
|
|
97
|
-
const job = await this.queue.getJob(taskId)
|
|
98
|
-
|
|
99
|
-
if (!job) {
|
|
100
|
-
return null
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const jobData = job.data as ScheduledJobData
|
|
104
|
-
const state = await job.getState()
|
|
105
|
-
|
|
106
|
-
// Calculate scheduled time from delay or processedOn
|
|
107
|
-
const scheduledFor = job.delay
|
|
108
|
-
? new Date(job.timestamp + job.delay)
|
|
109
|
-
: new Date(job.timestamp)
|
|
110
|
-
|
|
111
|
-
return {
|
|
112
|
-
taskId: job.id!,
|
|
113
|
-
rpcName: jobData.rpcName,
|
|
114
|
-
scheduledFor,
|
|
115
|
-
data: jobData.data,
|
|
116
|
-
session: jobData.session,
|
|
117
|
-
status: state as any,
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Get all scheduled tasks with minimal info
|
|
123
|
-
*/
|
|
124
|
-
async getAllTasks(): Promise<ScheduledTaskSummary[]> {
|
|
125
|
-
const jobs = await this.queue.getJobs(['delayed', 'waiting'])
|
|
126
|
-
|
|
127
|
-
return jobs.map((job) => {
|
|
128
|
-
const jobData = job.data as ScheduledJobData
|
|
129
|
-
const scheduledFor = job.delay
|
|
130
|
-
? new Date(job.timestamp + job.delay)
|
|
131
|
-
: new Date(job.timestamp)
|
|
132
|
-
|
|
133
|
-
return {
|
|
134
|
-
taskId: job.id!,
|
|
135
|
-
rpcName: jobData.rpcName,
|
|
136
|
-
scheduledFor,
|
|
137
|
-
}
|
|
138
|
-
})
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Close the queue connection
|
|
143
|
-
*/
|
|
144
|
-
async close(): Promise<void> {
|
|
145
|
-
if (this.recurringWorker) {
|
|
146
|
-
await this.recurringWorker.close()
|
|
147
|
-
}
|
|
148
|
-
await this.recurringQueue.close()
|
|
149
|
-
await this.queue.close()
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Start recurring scheduled tasks.
|
|
154
|
-
* Creates a BullMQ Worker to process repeat jobs via runScheduledTask.
|
|
155
|
-
*/
|
|
156
|
-
async start(): Promise<void> {
|
|
157
|
-
const logger = pikkuState(null, 'package', 'singletonServices')!.logger
|
|
158
|
-
const scheduledTasks = getScheduledTasks()
|
|
159
|
-
|
|
160
|
-
// Create a worker to process recurring scheduled task jobs
|
|
161
|
-
this.recurringWorker = new Worker(
|
|
162
|
-
RECURRING_QUEUE_NAME,
|
|
163
|
-
async (job) => {
|
|
164
|
-
const { rpcName } = job.data as ScheduledJobData
|
|
165
|
-
logger.info(`Running scheduled task: ${rpcName}`)
|
|
166
|
-
await runScheduledTask({ name: rpcName })
|
|
167
|
-
},
|
|
168
|
-
{ connection: this.redisConnectionOptions }
|
|
169
|
-
)
|
|
170
|
-
this.recurringWorker.on('error', (err) => {
|
|
171
|
-
logger.error(`Recurring task worker error: ${err}`)
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
for (const [name, task] of scheduledTasks) {
|
|
175
|
-
const jobSchedulerId = `recurring:${name}`
|
|
176
|
-
await this.recurringQueue.upsertJobScheduler(
|
|
177
|
-
jobSchedulerId,
|
|
178
|
-
{ pattern: task.schedule },
|
|
179
|
-
{ name, data: { rpcName: name } as ScheduledJobData }
|
|
180
|
-
)
|
|
181
|
-
this.jobSchedulerIds.push(jobSchedulerId)
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Stop recurring scheduled tasks by removing their job schedulers.
|
|
187
|
-
*/
|
|
188
|
-
async stop(): Promise<void> {
|
|
189
|
-
for (const id of this.jobSchedulerIds) {
|
|
190
|
-
await this.recurringQueue.removeJobScheduler(id)
|
|
191
|
-
}
|
|
192
|
-
this.jobSchedulerIds = []
|
|
193
|
-
if (this.recurringWorker) {
|
|
194
|
-
await this.recurringWorker.close()
|
|
195
|
-
this.recurringWorker = undefined
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import type { ConnectionOptions } from 'bullmq'
|
|
2
|
-
import { BullQueueService } from './bull-queue-service.js'
|
|
3
|
-
import { BullQueueWorkers } from './bull-queue-worker.js'
|
|
4
|
-
import { BullSchedulerService } from './bull-scheduler-service.js'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Factory class for BullMQ services
|
|
8
|
-
* Manages Redis connection options shared across queue, worker, and scheduler services
|
|
9
|
-
*/
|
|
10
|
-
export class BullServiceFactory {
|
|
11
|
-
private queueService?: BullQueueService
|
|
12
|
-
private queueWorkers?: BullQueueWorkers
|
|
13
|
-
private schedulerService?: BullSchedulerService
|
|
14
|
-
|
|
15
|
-
constructor(private redisConnectionOptions: ConnectionOptions = {}) {}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Initialize - no-op for BullMQ as connections are lazy
|
|
19
|
-
*/
|
|
20
|
-
async init(): Promise<void> {
|
|
21
|
-
// No-op - BullMQ creates connections lazily
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Get the queue service for publishing jobs
|
|
26
|
-
*/
|
|
27
|
-
getQueueService(): BullQueueService {
|
|
28
|
-
if (!this.queueService) {
|
|
29
|
-
this.queueService = new BullQueueService(this.redisConnectionOptions)
|
|
30
|
-
}
|
|
31
|
-
return this.queueService
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Get the queue workers for processing jobs
|
|
36
|
-
*/
|
|
37
|
-
getQueueWorkers(): BullQueueWorkers {
|
|
38
|
-
if (!this.queueWorkers) {
|
|
39
|
-
this.queueWorkers = new BullQueueWorkers(this.redisConnectionOptions)
|
|
40
|
-
}
|
|
41
|
-
return this.queueWorkers
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Get the scheduler service for managing delayed tasks
|
|
46
|
-
*/
|
|
47
|
-
getSchedulerService(): BullSchedulerService {
|
|
48
|
-
if (!this.schedulerService) {
|
|
49
|
-
this.schedulerService = new BullSchedulerService(
|
|
50
|
-
this.redisConnectionOptions
|
|
51
|
-
)
|
|
52
|
-
}
|
|
53
|
-
return this.schedulerService
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Close all services
|
|
58
|
-
*/
|
|
59
|
-
async close(): Promise<void> {
|
|
60
|
-
const closePromises: Promise<void>[] = []
|
|
61
|
-
|
|
62
|
-
if (this.queueService) {
|
|
63
|
-
// BullQueueService doesn't have close method currently
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (this.queueWorkers) {
|
|
67
|
-
closePromises.push(this.queueWorkers.close())
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (this.schedulerService) {
|
|
71
|
-
closePromises.push(this.schedulerService.close())
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
await Promise.all(closePromises)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Alias for close() - used by stopSingletonServices
|
|
79
|
-
*/
|
|
80
|
-
async stop(): Promise<void> {
|
|
81
|
-
await this.close()
|
|
82
|
-
}
|
|
83
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module @pikku/queue-bullmq
|
|
3
|
-
*
|
|
4
|
-
* Bull/Redis queue service for Pikku
|
|
5
|
-
* Provides job results, retries, priorities, and most queue features
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export { BullServiceFactory } from './bull-service-factory.js'
|
|
9
|
-
export { BullQueueService } from './bull-queue-service.js'
|
|
10
|
-
export { BullQueueWorkers } from './bull-queue-worker.js'
|
|
11
|
-
export { BullSchedulerService } from './bull-scheduler-service.js'
|
|
12
|
-
|
|
13
|
-
// Re-export core queue types for convenience
|
|
14
|
-
export type { QueueJob, QueueService } from '@pikku/core/queue'
|
|
15
|
-
export type {
|
|
16
|
-
PikkuWorkerConfig,
|
|
17
|
-
PikkuJobConfig,
|
|
18
|
-
JobOptions,
|
|
19
|
-
} from '@pikku/core/queue'
|
package/src/utils.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import type { QueueJob, QueueJobStatus } from '@pikku/core/queue'
|
|
2
|
-
import type { ConnectionOptions, Job } from 'bullmq'
|
|
3
|
-
import { QueueEvents } from 'bullmq'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Map Bull job state to our queue job status
|
|
7
|
-
*/
|
|
8
|
-
const mapBullStateToStatus = (state: string): QueueJobStatus => {
|
|
9
|
-
switch (state) {
|
|
10
|
-
case 'waiting':
|
|
11
|
-
return 'waiting'
|
|
12
|
-
case 'active':
|
|
13
|
-
return 'active'
|
|
14
|
-
case 'completed':
|
|
15
|
-
return 'completed'
|
|
16
|
-
case 'failed':
|
|
17
|
-
return 'failed'
|
|
18
|
-
case 'delayed':
|
|
19
|
-
return 'delayed'
|
|
20
|
-
default:
|
|
21
|
-
return 'waiting'
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Map Bull job to QueueJob interface
|
|
27
|
-
*/
|
|
28
|
-
export const mapBullJobToQueueJob = async <In, Out>(
|
|
29
|
-
bullJob: Job,
|
|
30
|
-
redisConnection: ConnectionOptions | undefined,
|
|
31
|
-
queueEventsMap: Map<string, QueueEvents>
|
|
32
|
-
): Promise<QueueJob<In, Out>> => {
|
|
33
|
-
return {
|
|
34
|
-
queueName: bullJob.name,
|
|
35
|
-
id: bullJob.id || '',
|
|
36
|
-
data: bullJob.data,
|
|
37
|
-
result: bullJob.returnvalue as Out,
|
|
38
|
-
status: async () => mapBullStateToStatus(await bullJob.getState()),
|
|
39
|
-
waitForCompletion: async (ttl?: number): Promise<Out> => {
|
|
40
|
-
let queueEvents = queueEventsMap.get(bullJob.queueName)
|
|
41
|
-
if (!queueEvents) {
|
|
42
|
-
queueEvents = new QueueEvents(
|
|
43
|
-
bullJob.queueName,
|
|
44
|
-
redisConnection ? { connection: redisConnection } : undefined
|
|
45
|
-
)
|
|
46
|
-
queueEventsMap.set(bullJob.queueName, queueEvents)
|
|
47
|
-
await queueEvents.waitUntilReady()
|
|
48
|
-
}
|
|
49
|
-
return await bullJob.waitUntilFinished(queueEvents, ttl)
|
|
50
|
-
},
|
|
51
|
-
metadata: () => ({
|
|
52
|
-
progress: bullJob.progress,
|
|
53
|
-
attemptsMade: bullJob.attemptsMade,
|
|
54
|
-
maxAttempts: bullJob.opts.attempts || 1,
|
|
55
|
-
createdAt: new Date(bullJob.timestamp),
|
|
56
|
-
processedAt: bullJob.processedOn
|
|
57
|
-
? new Date(bullJob.processedOn)
|
|
58
|
-
: undefined,
|
|
59
|
-
completedAt: bullJob.finishedOn
|
|
60
|
-
? new Date(bullJob.finishedOn)
|
|
61
|
-
: undefined,
|
|
62
|
-
failedAt: bullJob.finishedOn ? new Date(bullJob.finishedOn) : undefined,
|
|
63
|
-
error: bullJob.failedReason,
|
|
64
|
-
}),
|
|
65
|
-
}
|
|
66
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "../../tsconfig.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"rootDir": "./src",
|
|
5
|
-
"module": "Node18",
|
|
6
|
-
"outDir": "dist",
|
|
7
|
-
"target": "esnext",
|
|
8
|
-
"declaration": true
|
|
9
|
-
},
|
|
10
|
-
"include": ["src/**/*.ts"],
|
|
11
|
-
"exclude": ["**/*.test.ts", "node_modules"],
|
|
12
|
-
"references": [
|
|
13
|
-
{
|
|
14
|
-
"path": "../../core/tsconfig.json"
|
|
15
|
-
}
|
|
16
|
-
]
|
|
17
|
-
}
|
package/tsconfig.tsbuildinfo
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"root":["./src/bull-queue-service.ts","./src/bull-queue-worker.ts","./src/bull-scheduler-service.ts","./src/bull-service-factory.ts","./src/index.ts","./src/utils.ts"],"version":"6.0.3"}
|