@pikku/queue-pg-boss 0.8.0 → 0.9.0
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 +7 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +8 -0
- package/dist/pg-boss-queue-service.d.ts +29 -0
- package/dist/pg-boss-queue-service.js +63 -0
- package/dist/pg-boss-queue-worker.d.ts +34 -0
- package/dist/pg-boss-queue-worker.js +134 -0
- package/dist/utils.d.ts +6 -0
- package/dist/utils.js +71 -0
- package/package.json +1 -1
- package/src/pg-boss-queue-worker.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# @pikku/services-queue-bullmq
|
|
2
2
|
|
|
3
|
+
## 0.9.0
|
|
4
|
+
|
|
5
|
+
### Breaking Changes
|
|
6
|
+
|
|
7
|
+
- Normalized all transports to use "wirings" instead of events/routes/transports for consistency across the framework
|
|
8
|
+
|
|
9
|
+
|
|
3
10
|
## 0.8.0
|
|
4
11
|
|
|
5
12
|
- **PG Boss integration**: Postgres queue service implementation
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @pikku/queue-pg-boss
|
|
3
|
+
*
|
|
4
|
+
* PostgreSQL queue service for Pikku using pg-boss
|
|
5
|
+
* Provides job results, retries, priorities, and PostgreSQL-based queuing
|
|
6
|
+
*/
|
|
7
|
+
export { PgBossQueueService } from './pg-boss-queue-service.js';
|
|
8
|
+
export { PgBossQueueWorkers } from './pg-boss-queue-worker.js';
|
|
9
|
+
export type { QueueService, PikkuWorkerConfig, PikkuJobConfig, QueueJob, JobOptions, } from '@pikku/core/queue';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @pikku/queue-pg-boss
|
|
3
|
+
*
|
|
4
|
+
* PostgreSQL queue service for Pikku using pg-boss
|
|
5
|
+
* Provides job results, retries, priorities, and PostgreSQL-based queuing
|
|
6
|
+
*/
|
|
7
|
+
export { PgBossQueueService } from './pg-boss-queue-service.js';
|
|
8
|
+
export { PgBossQueueWorkers } from './pg-boss-queue-worker.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import PgBoss from 'pg-boss';
|
|
2
|
+
import type { QueueService, QueueJob, JobOptions } from '@pikku/core/queue';
|
|
3
|
+
export declare const mapPikkuJobToPgBoss: (options?: JobOptions) => PgBoss.JobOptions;
|
|
4
|
+
/**
|
|
5
|
+
* pg-boss/PostgreSQL queue service implementation
|
|
6
|
+
* Supports job results, retries, and PostgreSQL-based queuing
|
|
7
|
+
*/
|
|
8
|
+
export declare class PgBossQueueService implements QueueService {
|
|
9
|
+
readonly name = "pg-boss";
|
|
10
|
+
readonly supportsResults = true;
|
|
11
|
+
protected pgBoss: PgBoss;
|
|
12
|
+
constructor(optionsOrUrl: PgBoss.ConstructorOptions | string);
|
|
13
|
+
/**
|
|
14
|
+
* Initialize the pg-boss instance
|
|
15
|
+
*/
|
|
16
|
+
init(): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Add a job to the queue
|
|
19
|
+
*/
|
|
20
|
+
add<In>(queueName: string, data: In, options?: JobOptions): Promise<string>;
|
|
21
|
+
/**
|
|
22
|
+
* Get job by ID
|
|
23
|
+
*/
|
|
24
|
+
getJob<T, R>(queueName: string, jobId: string): Promise<QueueJob<T, R> | null>;
|
|
25
|
+
/**
|
|
26
|
+
* Close the pg-boss connection
|
|
27
|
+
*/
|
|
28
|
+
close(): Promise<void>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import PgBoss from 'pg-boss';
|
|
2
|
+
import { mapPgBossJobToQueueJob } from './utils.js';
|
|
3
|
+
export const mapPikkuJobToPgBoss = (options) => {
|
|
4
|
+
const pgBossOptions = {};
|
|
5
|
+
if (options?.priority !== undefined) {
|
|
6
|
+
pgBossOptions.priority = options.priority;
|
|
7
|
+
}
|
|
8
|
+
if (options?.delay !== undefined) {
|
|
9
|
+
pgBossOptions.startAfter = new Date(Date.now() + options.delay);
|
|
10
|
+
}
|
|
11
|
+
if (options?.jobId !== undefined) {
|
|
12
|
+
pgBossOptions.singletonKey = options.jobId;
|
|
13
|
+
}
|
|
14
|
+
return pgBossOptions;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* pg-boss/PostgreSQL queue service implementation
|
|
18
|
+
* Supports job results, retries, and PostgreSQL-based queuing
|
|
19
|
+
*/
|
|
20
|
+
export class PgBossQueueService {
|
|
21
|
+
name = 'pg-boss';
|
|
22
|
+
supportsResults = true;
|
|
23
|
+
pgBoss;
|
|
24
|
+
constructor(optionsOrUrl) {
|
|
25
|
+
if (typeof optionsOrUrl === 'string') {
|
|
26
|
+
// If a string is provided, treat it as the connection URL
|
|
27
|
+
optionsOrUrl = { connectionString: optionsOrUrl };
|
|
28
|
+
}
|
|
29
|
+
this.pgBoss = new PgBoss(optionsOrUrl);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Initialize the pg-boss instance
|
|
33
|
+
*/
|
|
34
|
+
async init() {
|
|
35
|
+
await this.pgBoss.start();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Add a job to the queue
|
|
39
|
+
*/
|
|
40
|
+
async add(queueName, data, options) {
|
|
41
|
+
const jobId = await this.pgBoss.send(queueName, data, mapPikkuJobToPgBoss(options));
|
|
42
|
+
if (!jobId) {
|
|
43
|
+
throw new Error('Failed to create job');
|
|
44
|
+
}
|
|
45
|
+
return jobId;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get job by ID
|
|
49
|
+
*/
|
|
50
|
+
async getJob(queueName, jobId) {
|
|
51
|
+
const job = await this.pgBoss.getJobById(queueName, jobId);
|
|
52
|
+
if (!job) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return mapPgBossJobToQueueJob(job, this.pgBoss);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Close the pg-boss connection
|
|
59
|
+
*/
|
|
60
|
+
async close() {
|
|
61
|
+
await this.pgBoss.stop();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import PgBoss from 'pg-boss';
|
|
2
|
+
import type { QueueWorkers, PikkuWorkerConfig, QueueConfigMapping, ConfigValidationResult } from '@pikku/core/queue';
|
|
3
|
+
import { CoreServices, CoreSingletonServices, CreateSessionServices } from '@pikku/core';
|
|
4
|
+
export declare const mapPikkuWorkerToPgBoss: (workerConfig?: PikkuWorkerConfig) => PgBoss.WorkOptions;
|
|
5
|
+
/**
|
|
6
|
+
* pg-boss/PostgreSQL queue worker implementation
|
|
7
|
+
* Supports job results, retries, and PostgreSQL-based queuing
|
|
8
|
+
*/
|
|
9
|
+
export declare class PgBossQueueWorkers implements QueueWorkers {
|
|
10
|
+
private singletonServices;
|
|
11
|
+
private createSessionServices?;
|
|
12
|
+
readonly name = "pg-boss";
|
|
13
|
+
readonly supportsResults = true;
|
|
14
|
+
/**
|
|
15
|
+
* Configuration mapping rules for pg-boss
|
|
16
|
+
* This defines how Pikku worker configs map to pg-boss configurations
|
|
17
|
+
*/
|
|
18
|
+
readonly configMappings: QueueConfigMapping;
|
|
19
|
+
private pgBoss;
|
|
20
|
+
private activeWorkers;
|
|
21
|
+
constructor(options: PgBoss.ConstructorOptions | string, singletonServices: CoreSingletonServices, createSessionServices?: CreateSessionServices<CoreSingletonServices, CoreServices, any>);
|
|
22
|
+
/**
|
|
23
|
+
* Initialize pg-boss
|
|
24
|
+
*/
|
|
25
|
+
init(): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Scan state and register all compatible processors
|
|
28
|
+
*/
|
|
29
|
+
registerQueues(): Promise<Record<string, ConfigValidationResult[]>>;
|
|
30
|
+
/**
|
|
31
|
+
* Close all workers and connections
|
|
32
|
+
*/
|
|
33
|
+
close(): Promise<void>;
|
|
34
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import PgBoss from 'pg-boss';
|
|
2
|
+
import { runQueueJob, registerQueueWorkers } from '@pikku/core/queue';
|
|
3
|
+
import { mapPgBossJobToQueueJob } from './utils.js';
|
|
4
|
+
export const mapPikkuWorkerToPgBoss = (workerConfig) => {
|
|
5
|
+
const workerOptions = {};
|
|
6
|
+
if (workerConfig?.batchSize !== undefined) {
|
|
7
|
+
workerOptions.batchSize = workerConfig.batchSize;
|
|
8
|
+
}
|
|
9
|
+
if (workerConfig?.pollInterval !== undefined) {
|
|
10
|
+
workerOptions.pollingIntervalSeconds = Math.round(workerConfig.pollInterval / 1000);
|
|
11
|
+
}
|
|
12
|
+
return workerOptions;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* pg-boss/PostgreSQL queue worker implementation
|
|
16
|
+
* Supports job results, retries, and PostgreSQL-based queuing
|
|
17
|
+
*/
|
|
18
|
+
export class PgBossQueueWorkers {
|
|
19
|
+
singletonServices;
|
|
20
|
+
createSessionServices;
|
|
21
|
+
name = 'pg-boss';
|
|
22
|
+
supportsResults = true;
|
|
23
|
+
/**
|
|
24
|
+
* Configuration mapping rules for pg-boss
|
|
25
|
+
* This defines how Pikku worker configs map to pg-boss configurations
|
|
26
|
+
*/
|
|
27
|
+
configMappings = {
|
|
28
|
+
// Configurations that are directly supported
|
|
29
|
+
supported: {
|
|
30
|
+
batchSize: {
|
|
31
|
+
queueProperty: 'batchSize',
|
|
32
|
+
description: 'Number of jobs to process in a single batch',
|
|
33
|
+
},
|
|
34
|
+
pollInterval: {
|
|
35
|
+
queueProperty: 'pollingIntervalSeconds',
|
|
36
|
+
transform: (value) => Math.round(value / 1000),
|
|
37
|
+
description: 'How often to poll for new jobs (converted from ms to seconds)',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
// Configurations that are not supported but have reasonable explanations
|
|
41
|
+
unsupported: {
|
|
42
|
+
name: {
|
|
43
|
+
reason: 'Worker names are not supported in pg-boss',
|
|
44
|
+
explanation: 'pg-boss identifies workers by their queue name and process, not custom names',
|
|
45
|
+
},
|
|
46
|
+
autorun: {
|
|
47
|
+
reason: 'Autorun is not configurable in pg-boss (always enabled)',
|
|
48
|
+
explanation: 'pg-boss workers automatically start processing when created',
|
|
49
|
+
},
|
|
50
|
+
lockDuration: {
|
|
51
|
+
reason: 'Lock duration is managed differently in pg-boss',
|
|
52
|
+
explanation: 'pg-boss uses job-level expiration instead of worker-level locks',
|
|
53
|
+
},
|
|
54
|
+
drainDelay: {
|
|
55
|
+
reason: 'Drain delay is not configurable in pg-boss',
|
|
56
|
+
explanation: 'pg-boss handles graceful shutdown internally',
|
|
57
|
+
},
|
|
58
|
+
maxStalledCount: {
|
|
59
|
+
reason: 'Max stalled count is not configurable in pg-boss',
|
|
60
|
+
explanation: 'pg-boss handles stalled jobs through its built-in retry mechanism',
|
|
61
|
+
},
|
|
62
|
+
prefetch: {
|
|
63
|
+
reason: 'Prefetch is managed internally by pg-boss',
|
|
64
|
+
explanation: 'pg-boss optimizes job fetching automatically based on batch size',
|
|
65
|
+
},
|
|
66
|
+
visibilityTimeout: {
|
|
67
|
+
reason: "Visibility timeout concept doesn't apply to pg-boss",
|
|
68
|
+
explanation: 'pg-boss uses PostgreSQL locks and job expiration instead of visibility timeout',
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
// Configurations that have partial support or workarounds
|
|
72
|
+
fallbacks: {
|
|
73
|
+
removeOnComplete: {
|
|
74
|
+
reason: 'Job retention is managed through pg-boss archival system',
|
|
75
|
+
explanation: 'pg-boss automatically archives completed jobs based on database settings',
|
|
76
|
+
fallbackValue: 'Managed by pg-boss database archival',
|
|
77
|
+
},
|
|
78
|
+
removeOnFail: {
|
|
79
|
+
reason: 'Failed job retention is managed through pg-boss archival system',
|
|
80
|
+
explanation: 'pg-boss automatically archives failed jobs based on database settings',
|
|
81
|
+
fallbackValue: 'Managed by pg-boss database archival',
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
pgBoss;
|
|
86
|
+
activeWorkers = new Map();
|
|
87
|
+
constructor(options, singletonServices, createSessionServices) {
|
|
88
|
+
this.singletonServices = singletonServices;
|
|
89
|
+
this.createSessionServices = createSessionServices;
|
|
90
|
+
if (typeof options === 'string') {
|
|
91
|
+
// If a string is provided, treat it as the connection string
|
|
92
|
+
options = { connectionString: options };
|
|
93
|
+
}
|
|
94
|
+
this.pgBoss = new PgBoss(options);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Initialize pg-boss
|
|
98
|
+
*/
|
|
99
|
+
async init() {
|
|
100
|
+
await this.pgBoss.start();
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Scan state and register all compatible processors
|
|
104
|
+
*/
|
|
105
|
+
async registerQueues() {
|
|
106
|
+
return await registerQueueWorkers(this.configMappings, this.singletonServices.logger, async (queueName, processor) => {
|
|
107
|
+
await this.pgBoss.createQueue(queueName);
|
|
108
|
+
const workerId = await this.pgBoss.work(processor.queueName, {
|
|
109
|
+
...mapPikkuWorkerToPgBoss(processor.config),
|
|
110
|
+
includeMetadata: true,
|
|
111
|
+
}, async ([job]) => {
|
|
112
|
+
if (!this.singletonServices) {
|
|
113
|
+
throw new Error('Singleton services not available');
|
|
114
|
+
}
|
|
115
|
+
if (!job) {
|
|
116
|
+
this.singletonServices.logger.warn(`No job received for queue ${queueName}`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
await runQueueJob({
|
|
120
|
+
singletonServices: this.singletonServices,
|
|
121
|
+
createSessionServices: this.createSessionServices,
|
|
122
|
+
job: mapPgBossJobToQueueJob(job, this.pgBoss),
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
this.activeWorkers.set(queueName, workerId);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Close all workers and connections
|
|
130
|
+
*/
|
|
131
|
+
async close() {
|
|
132
|
+
await this.pgBoss.stop();
|
|
133
|
+
}
|
|
134
|
+
}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { QueueJob } from '@pikku/core/queue';
|
|
2
|
+
import PgBoss from 'pg-boss';
|
|
3
|
+
/**
|
|
4
|
+
* Map pg-boss job to QueueJob interface
|
|
5
|
+
*/
|
|
6
|
+
export declare const mapPgBossJobToQueueJob: <In, Out>(pgBossJob: PgBoss.JobWithMetadata<In>, pgBossInstance: PgBoss) => QueueJob<In, Out>;
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map pg-boss job state to our queue job status
|
|
3
|
+
*/
|
|
4
|
+
const mapPgBossStateToStatus = (state) => {
|
|
5
|
+
switch (state) {
|
|
6
|
+
case 'created':
|
|
7
|
+
case 'retry':
|
|
8
|
+
return 'waiting';
|
|
9
|
+
case 'active':
|
|
10
|
+
return 'active';
|
|
11
|
+
case 'completed':
|
|
12
|
+
return 'completed';
|
|
13
|
+
case 'failed':
|
|
14
|
+
return 'failed';
|
|
15
|
+
case 'cancelled':
|
|
16
|
+
return 'failed';
|
|
17
|
+
default:
|
|
18
|
+
return 'waiting';
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Map pg-boss job to QueueJob interface
|
|
23
|
+
*/
|
|
24
|
+
export const mapPgBossJobToQueueJob = (pgBossJob, pgBossInstance) => {
|
|
25
|
+
return {
|
|
26
|
+
queueName: pgBossJob.name,
|
|
27
|
+
id: pgBossJob.id || '',
|
|
28
|
+
data: pgBossJob.data,
|
|
29
|
+
status: async () => {
|
|
30
|
+
const jobWithState = await pgBossInstance.getJobById(pgBossJob.name, pgBossJob.id);
|
|
31
|
+
return mapPgBossStateToStatus(jobWithState?.state || 'created');
|
|
32
|
+
},
|
|
33
|
+
waitForCompletion: async (ttl) => {
|
|
34
|
+
// pg-boss doesn't have a built-in waitForCompletion, so we'll poll
|
|
35
|
+
const startTime = Date.now();
|
|
36
|
+
const timeout = ttl || 30000; // 30 seconds default
|
|
37
|
+
while (Date.now() - startTime < timeout) {
|
|
38
|
+
const job = await pgBossInstance.getJobById(pgBossJob.name, pgBossJob.id);
|
|
39
|
+
if (job?.state === 'completed') {
|
|
40
|
+
return job.output;
|
|
41
|
+
}
|
|
42
|
+
if (job?.state === 'failed' || job?.state === 'cancelled') {
|
|
43
|
+
throw new Error(`Job failed: ${JSON.stringify(job.output)}`);
|
|
44
|
+
}
|
|
45
|
+
// Wait 100ms before polling again
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
47
|
+
}
|
|
48
|
+
throw new Error('Job did not complete within timeout');
|
|
49
|
+
},
|
|
50
|
+
metadata: () => {
|
|
51
|
+
return {
|
|
52
|
+
progress: 0, // pg-boss doesn't have built-in progress tracking
|
|
53
|
+
attemptsMade: pgBossJob.retryCount || 0,
|
|
54
|
+
maxAttempts: pgBossJob.retryLimit || 1,
|
|
55
|
+
createdAt: pgBossJob.createdOn
|
|
56
|
+
? new Date(pgBossJob.createdOn)
|
|
57
|
+
: new Date(),
|
|
58
|
+
processedAt: pgBossJob.startedOn
|
|
59
|
+
? new Date(pgBossJob.startedOn)
|
|
60
|
+
: undefined,
|
|
61
|
+
completedAt: pgBossJob.completedOn
|
|
62
|
+
? new Date(pgBossJob.completedOn)
|
|
63
|
+
: undefined,
|
|
64
|
+
failedAt: pgBossJob.state === 'failed' && pgBossJob.completedOn
|
|
65
|
+
? new Date(pgBossJob.completedOn)
|
|
66
|
+
: undefined,
|
|
67
|
+
error: pgBossJob.data.error,
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
};
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
QueueConfigMapping,
|
|
6
6
|
ConfigValidationResult,
|
|
7
7
|
} from '@pikku/core/queue'
|
|
8
|
-
import { runQueueJob,
|
|
8
|
+
import { runQueueJob, registerQueueWorkers } from '@pikku/core/queue'
|
|
9
9
|
import {
|
|
10
10
|
CoreServices,
|
|
11
11
|
CoreSingletonServices,
|
|
@@ -144,7 +144,7 @@ export class PgBossQueueWorkers implements QueueWorkers {
|
|
|
144
144
|
* Scan state and register all compatible processors
|
|
145
145
|
*/
|
|
146
146
|
async registerQueues(): Promise<Record<string, ConfigValidationResult[]>> {
|
|
147
|
-
return await
|
|
147
|
+
return await registerQueueWorkers(
|
|
148
148
|
this.configMappings,
|
|
149
149
|
this.singletonServices.logger,
|
|
150
150
|
async (queueName, processor) => {
|