@boopkit/cap-background-jobs 0.2.6 → 0.3.1
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/README.md +186 -0
- package/capability.json +1 -1
- package/dist/message-queue.service.d.ts +1 -1
- package/dist/message-queue.service.d.ts.map +1 -1
- package/dist/message-queue.service.js +50 -12
- package/dist/message-queue.service.js.map +1 -1
- package/package.json +4 -2
- package/scaffold/src/controllers/example-job.controller.ts +7 -3
- package/scaffold/src/scheduler.ts +19 -14
- package/scaffold/src/services/index.ts +4 -0
- package/scaffold/src/worker.ts +12 -8
package/README.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# @boopkit/cap-background-jobs
|
|
2
|
+
|
|
3
|
+
**Part of [Boop](https://www.npmjs.com/package/@boopkit/cli)** — a bootstrap platform for AI-first builders. `boop init` scaffolds a running Node/TypeScript web app from composable **capabilities**, each one bundling the code, dependencies, middleware wiring, and agent skills needed for a feature. You pick the capabilities; `boop add` handles the rest.
|
|
4
|
+
|
|
5
|
+
`cap-background-jobs` adds async job processing via RabbitMQ (CloudAMQP). You get a publish/listen API, a dedicated worker process, a scheduler CLI for cron-triggered jobs, and automatic retry/dead-letter topology per queue. Use it for anything that shouldn't block an HTTP response: image processing, outbound email, nightly reconciles, third-party webhooks.
|
|
6
|
+
|
|
7
|
+
## What You Get
|
|
8
|
+
|
|
9
|
+
### Scaffold files
|
|
10
|
+
|
|
11
|
+
Copied into your project on install:
|
|
12
|
+
|
|
13
|
+
- `src/worker.ts` — worker process entry point. Boots services, registers consumers, handles graceful shutdown
|
|
14
|
+
- `src/scheduler.ts` — CLI for publishing scheduled triggers, filtered by `--interval` and `--job`
|
|
15
|
+
- `src/controllers/example-job.controller.ts` — copyable template for message consumers (auto-ACK pattern)
|
|
16
|
+
- `tests/controllers/example-job.controller.test.ts` — example consumer tests
|
|
17
|
+
- `reference/background-jobs.ref.md` — infrastructure topology, ACK modes, Heroku setup
|
|
18
|
+
|
|
19
|
+
### Scripts added to `package.json`
|
|
20
|
+
|
|
21
|
+
| Script | Command | Purpose |
|
|
22
|
+
|--------|---------|---------|
|
|
23
|
+
| `worker` | `tsx src/worker.ts` | Start the background job worker process |
|
|
24
|
+
| `scheduler` | `tsx src/scheduler.ts` | Publish trigger messages to job queues (`run-jobs`, `publish`, `list` subcommands) |
|
|
25
|
+
|
|
26
|
+
### Skills installed
|
|
27
|
+
|
|
28
|
+
- `design.background-jobs.skill.md` — teaches agents how to add a consumer, choose between auto- and manual-ACK, add scheduled jobs, and handle retries.
|
|
29
|
+
|
|
30
|
+
### Post-install
|
|
31
|
+
|
|
32
|
+
1. Provision CloudAMQP (or any RabbitMQ instance) and add `CLOUDAMQP_URL` to `.env`
|
|
33
|
+
2. Set `QUEUE_PREFIX=dev/<yourname>` to isolate your queues from other developers sharing the instance
|
|
34
|
+
3. Add `worker: npm run worker` to your `Procfile`
|
|
35
|
+
4. Configure your external scheduler (e.g. Heroku Scheduler) to run `npm run scheduler -- run-jobs --interval <tag>` on the desired cadence
|
|
36
|
+
|
|
37
|
+
`boop status` reminds you of these steps after install.
|
|
38
|
+
|
|
39
|
+
## Requirements
|
|
40
|
+
|
|
41
|
+
**Other capabilities:**
|
|
42
|
+
|
|
43
|
+
- `cap-services` — the message queue is registered as `services.message-queue` via the service map
|
|
44
|
+
|
|
45
|
+
**npm dependencies added to your project:**
|
|
46
|
+
|
|
47
|
+
- `amqplib` — RabbitMQ client
|
|
48
|
+
- `minimist` — scheduler CLI argument parsing
|
|
49
|
+
|
|
50
|
+
**External services:**
|
|
51
|
+
|
|
52
|
+
- **RabbitMQ** — CloudAMQP is the easiest path on Heroku (`heroku addons:create cloudamqp`, which sets `CLOUDAMQP_URL` automatically). Any RabbitMQ broker works — local Docker, self-hosted, or AWS MQ — point `CLOUDAMQP_URL` at it.
|
|
53
|
+
|
|
54
|
+
**Environment variables:**
|
|
55
|
+
|
|
56
|
+
| Variable | Required | Description |
|
|
57
|
+
|----------|----------|-------------|
|
|
58
|
+
| `CLOUDAMQP_URL` | Yes (at boot) | RabbitMQ connection URL (`amqp://...` or `amqps://...`) |
|
|
59
|
+
| `QUEUE_PREFIX` | No | Queue name prefix for environment isolation (e.g. `dev/mike`). Leave empty in production |
|
|
60
|
+
| `MAX_MESSAGE_RETRIES` | No | Max retry attempts before dead-lettering (default: `5`) |
|
|
61
|
+
|
|
62
|
+
## Usage
|
|
63
|
+
|
|
64
|
+
Three moving parts — web publishes, scheduler publishes on a cron, worker consumes. All share one `MessageQueueService` instance registered in the service map.
|
|
65
|
+
|
|
66
|
+
### Publish a job
|
|
67
|
+
|
|
68
|
+
From a route handler, service, or anywhere in the web process:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { services } from '@services';
|
|
72
|
+
|
|
73
|
+
app.post('/upload', async (req, res) => {
|
|
74
|
+
const upload = await services.uploads.create(req.body);
|
|
75
|
+
|
|
76
|
+
// Enqueue async processing — returns immediately
|
|
77
|
+
await services.messageQueue.publish('process-upload', {
|
|
78
|
+
task: 'process-upload',
|
|
79
|
+
data: { uploadId: upload.id, userId: req.user.id },
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
res.status(202).json({ id: upload.id });
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The publish connection is lazy — it only opens when you actually publish. Your web dynos stay cheap.
|
|
87
|
+
|
|
88
|
+
### Consume a job
|
|
89
|
+
|
|
90
|
+
**1.** Copy `src/controllers/example-job.controller.ts` to `src/controllers/process-upload.controller.ts` and replace the handler body:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
export const QUEUE_NAME = 'process-upload';
|
|
94
|
+
|
|
95
|
+
export async function onMessage(msg: unknown): Promise<boolean> {
|
|
96
|
+
const payload = validatePayload(msg);
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
await services.uploads.process(payload.data.uploadId);
|
|
100
|
+
return true; // ACK — success
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.error('[process-upload] failed:', err);
|
|
103
|
+
return false; // retry with exponential backoff
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**2.** Register the consumer in `src/worker.ts`:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import * as processUploadController from './controllers/process-upload.controller';
|
|
112
|
+
|
|
113
|
+
const consumers = [
|
|
114
|
+
{ queue: exampleJobController.QUEUE_NAME, handler: exampleJobController.onMessage },
|
|
115
|
+
{ queue: processUploadController.QUEUE_NAME, handler: processUploadController.onMessage },
|
|
116
|
+
];
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**3.** Run the worker locally:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
npm run worker
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
On first listen, the service asserts the queue topology: main queue, `.retry` queue (TTL-backed redelivery), and `.failed` dead-letter queue. Failed handlers retry with exponential backoff (4s → 60s, capped) up to `MAX_MESSAGE_RETRIES`, then land in the DLQ for inspection.
|
|
126
|
+
|
|
127
|
+
### Error classification
|
|
128
|
+
|
|
129
|
+
Return value from the handler decides routing:
|
|
130
|
+
|
|
131
|
+
| Return | Result |
|
|
132
|
+
|--------|--------|
|
|
133
|
+
| `return true` | ACK — success, or permanent failure that retrying won't fix |
|
|
134
|
+
| `return false` | Retry with backoff |
|
|
135
|
+
| `throw` | Retry, then DLQ after max retries |
|
|
136
|
+
|
|
137
|
+
### Scheduled jobs
|
|
138
|
+
|
|
139
|
+
Add an entry to the `jobs` array in `src/scheduler.ts`:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
const jobs: JobConfig[] = [
|
|
143
|
+
{ name: 'nightly-sync', queue: 'sync-accounts', interval: '60-minutes',
|
|
144
|
+
payload: { task: 'nightly-sync' } },
|
|
145
|
+
];
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Then configure your external scheduler:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# Run all jobs tagged 60-minutes
|
|
152
|
+
npm run scheduler -- run-jobs --interval 60-minutes
|
|
153
|
+
|
|
154
|
+
# Run a specific job on demand
|
|
155
|
+
npm run scheduler -- run-jobs --job nightly-sync
|
|
156
|
+
|
|
157
|
+
# Publish an ad-hoc message
|
|
158
|
+
npm run scheduler -- publish --queue sync-accounts --task manual-run
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Manual ACK
|
|
162
|
+
|
|
163
|
+
For batch commits or transaction coordination, opt into manual ACK:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
await services.messageQueue.listen('batch-job', async (msg, ack, nack) => {
|
|
167
|
+
try {
|
|
168
|
+
await processBatch(msg);
|
|
169
|
+
ack();
|
|
170
|
+
} catch {
|
|
171
|
+
nack();
|
|
172
|
+
}
|
|
173
|
+
}, { manualAck: true });
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
First call to `ack()` or `nack()` wins — subsequent calls are silent no-ops.
|
|
177
|
+
|
|
178
|
+
See `reference/background-jobs.ref.md` in your scaffolded project for the full topology diagram, connection lifecycle, and Heroku deployment guide.
|
|
179
|
+
|
|
180
|
+
## Added by
|
|
181
|
+
|
|
182
|
+
Not included by default. Add to an existing project with:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
boop add background-jobs
|
|
186
|
+
```
|
package/capability.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"type": "application",
|
|
5
5
|
"package": "@boopkit/cap-background-jobs",
|
|
6
6
|
"requires": ["services"],
|
|
7
|
-
"dependencies": ["amqplib@^0.10", "minimist@^1.2"],
|
|
7
|
+
"dependencies": ["amqplib@^0.10", "minimist@^1.2", "@boopkit/logger", "winston@^3"],
|
|
8
8
|
"devDependencies": ["@types/amqplib@^0.10", "@types/minimist@^1.2"],
|
|
9
9
|
"env": [
|
|
10
10
|
{ "name": "CLOUDAMQP_URL", "description": "CloudAMQP connection URL (amqp://...)", "required": "boot" },
|
|
@@ -42,6 +42,6 @@ interface ListenOpts {
|
|
|
42
42
|
* or throws without settling → retry/DLQ after max retries.
|
|
43
43
|
*/
|
|
44
44
|
declare function listen(queue: string, handler: (...args: any[]) => Promise<any>, opts?: ListenOpts): Promise<void>;
|
|
45
|
-
/** Close all tracked connections (publish +
|
|
45
|
+
/** Close all tracked connections (publish + listener). */
|
|
46
46
|
declare function close(): Promise<void>;
|
|
47
47
|
//# sourceMappingURL=message-queue.service.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message-queue.service.d.ts","sourceRoot":"","sources":["../src/message-queue.service.ts"],"names":[],"mappings":"AAAA,qEAAqE;
|
|
1
|
+
{"version":3,"file":"message-queue.service.d.ts","sourceRoot":"","sources":["../src/message-queue.service.ts"],"names":[],"mappings":"AAAA,qEAAqE;AAyBrE,QAAA,MAAM,mBAAmB;;;;;;CAMxB,CAAC;AAEF,eAAe,mBAAmB,CAAC;AAEnC;;;;GAIG;AACH,iBAAS,IAAI,IAAI,IAAI,CAQpB;AAED;;;;GAIG;AACH,iBAAe,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CA0BxE;AAED,4BAA4B;AAC5B,UAAU,UAAU;IAClB,2FAA2F;IAC3F,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,oFAAoF;IACpF,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;;GAcG;AAEH,iBAAe,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA2EhH;AAED,0DAA0D;AAC1D,iBAAe,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAqCpC"}
|
|
@@ -5,6 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
};
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
const amqplib_1 = __importDefault(require("amqplib"));
|
|
8
|
+
const logger_1 = require("@boopkit/logger");
|
|
9
|
+
const logger = (0, logger_1.createLogger)('background-jobs');
|
|
8
10
|
const BACKOFF_BASE_MS = 4000;
|
|
9
11
|
const BACKOFF_MAX_MS = 60000;
|
|
10
12
|
/** Read env lazily — package imports run before dotenv/app boot. */
|
|
@@ -15,6 +17,8 @@ const connections = [];
|
|
|
15
17
|
/** Lazy-initialized publish connection and confirm channel. */
|
|
16
18
|
let publishConnection = null;
|
|
17
19
|
let publishChannel = null;
|
|
20
|
+
/** Lazy-initialized shared listener connection. */
|
|
21
|
+
let listenerConnection = null;
|
|
18
22
|
const MessageQueueService = {
|
|
19
23
|
name: 'message-queue',
|
|
20
24
|
init,
|
|
@@ -58,7 +62,7 @@ async function publish(queue, payload) {
|
|
|
58
62
|
return true;
|
|
59
63
|
}
|
|
60
64
|
catch (err) {
|
|
61
|
-
|
|
65
|
+
logger.error(`publish error on "${queue}"`, { err });
|
|
62
66
|
resetPublishConnection();
|
|
63
67
|
return false;
|
|
64
68
|
}
|
|
@@ -80,8 +84,7 @@ async function publish(queue, payload) {
|
|
|
80
84
|
*/
|
|
81
85
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
82
86
|
async function listen(queue, handler, opts) {
|
|
83
|
-
const connection = await
|
|
84
|
-
connections.push(connection);
|
|
87
|
+
const connection = await getListenerConnection();
|
|
85
88
|
const channel = await connection.createChannel();
|
|
86
89
|
const { mainQueue, retryQueue, failedQueue } = await assertTopology(channel, queue);
|
|
87
90
|
if (opts?.prefetch != null) {
|
|
@@ -97,7 +100,7 @@ async function listen(queue, handler, opts) {
|
|
|
97
100
|
}
|
|
98
101
|
catch {
|
|
99
102
|
// Poison message — ACK to prevent infinite redelivery
|
|
100
|
-
|
|
103
|
+
logger.error(`unparseable message on "${queue}", sending to DLQ`);
|
|
101
104
|
channel.nack(raw, false, false);
|
|
102
105
|
return;
|
|
103
106
|
}
|
|
@@ -106,7 +109,7 @@ async function listen(queue, handler, opts) {
|
|
|
106
109
|
let settled = false;
|
|
107
110
|
const guardedAck = () => {
|
|
108
111
|
if (settled) {
|
|
109
|
-
|
|
112
|
+
logger.warn(`duplicate ack on "${queue}" — ignored`);
|
|
110
113
|
return;
|
|
111
114
|
}
|
|
112
115
|
settled = true;
|
|
@@ -114,7 +117,7 @@ async function listen(queue, handler, opts) {
|
|
|
114
117
|
};
|
|
115
118
|
const guardedNack = async () => {
|
|
116
119
|
if (settled) {
|
|
117
|
-
|
|
120
|
+
logger.warn(`duplicate nack on "${queue}" — ignored`);
|
|
118
121
|
return;
|
|
119
122
|
}
|
|
120
123
|
settled = true;
|
|
@@ -123,12 +126,12 @@ async function listen(queue, handler, opts) {
|
|
|
123
126
|
try {
|
|
124
127
|
await handler(parsed, guardedAck, guardedNack);
|
|
125
128
|
if (!settled) {
|
|
126
|
-
|
|
129
|
+
logger.warn(`handler resolved without settling on "${queue}" — treating as failure`);
|
|
127
130
|
await retryOrDLQ(channel, raw, retryCount, retryQueue, failedQueue);
|
|
128
131
|
}
|
|
129
132
|
}
|
|
130
133
|
catch (err) {
|
|
131
|
-
|
|
134
|
+
logger.error(`handler threw on "${queue}"`, { err });
|
|
132
135
|
if (!settled) {
|
|
133
136
|
await retryOrDLQ(channel, raw, retryCount, retryQueue, failedQueue);
|
|
134
137
|
}
|
|
@@ -146,15 +149,15 @@ async function listen(queue, handler, opts) {
|
|
|
146
149
|
}
|
|
147
150
|
}
|
|
148
151
|
catch (err) {
|
|
149
|
-
|
|
152
|
+
logger.error(`handler threw on "${queue}"`, { err });
|
|
150
153
|
await retryOrDLQ(channel, raw, retryCount, retryQueue, failedQueue);
|
|
151
154
|
}
|
|
152
155
|
}
|
|
153
156
|
}, { noAck: false });
|
|
154
157
|
}
|
|
155
|
-
/** Close all tracked connections (publish +
|
|
158
|
+
/** Close all tracked connections (publish + listener). */
|
|
156
159
|
async function close() {
|
|
157
|
-
// Close the publish connection explicitly
|
|
160
|
+
// Close the publish connection explicitly
|
|
158
161
|
if (publishConnection) {
|
|
159
162
|
const conn = publishConnection;
|
|
160
163
|
publishConnection = null;
|
|
@@ -169,6 +172,20 @@ async function close() {
|
|
|
169
172
|
// Already closed — ignore
|
|
170
173
|
}
|
|
171
174
|
}
|
|
175
|
+
// Close the shared listener connection explicitly
|
|
176
|
+
if (listenerConnection) {
|
|
177
|
+
const conn = listenerConnection;
|
|
178
|
+
listenerConnection = null;
|
|
179
|
+
const idx = connections.indexOf(conn);
|
|
180
|
+
if (idx !== -1)
|
|
181
|
+
connections.splice(idx, 1);
|
|
182
|
+
try {
|
|
183
|
+
await conn.close();
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// Already closed — ignore
|
|
187
|
+
}
|
|
188
|
+
}
|
|
172
189
|
const closing = connections.map(async (conn) => {
|
|
173
190
|
try {
|
|
174
191
|
await conn.close();
|
|
@@ -187,6 +204,27 @@ function prefixedName(name) {
|
|
|
187
204
|
const prefix = getQueuePrefix();
|
|
188
205
|
return prefix ? `${prefix}/${name}` : name;
|
|
189
206
|
}
|
|
207
|
+
/** Lazy-create or return the shared listener connection. */
|
|
208
|
+
async function getListenerConnection() {
|
|
209
|
+
if (listenerConnection)
|
|
210
|
+
return listenerConnection;
|
|
211
|
+
const conn = await amqplib_1.default.connect(process.env.CLOUDAMQP_URL);
|
|
212
|
+
connections.push(conn);
|
|
213
|
+
listenerConnection = conn;
|
|
214
|
+
conn.on('error', () => resetListenerConnection());
|
|
215
|
+
conn.on('close', () => resetListenerConnection());
|
|
216
|
+
return conn;
|
|
217
|
+
}
|
|
218
|
+
/** Clear cached listener connection so the next listen() call reconnects. */
|
|
219
|
+
function resetListenerConnection() {
|
|
220
|
+
if (listenerConnection) {
|
|
221
|
+
listenerConnection.close().catch(() => { });
|
|
222
|
+
const idx = connections.indexOf(listenerConnection);
|
|
223
|
+
if (idx !== -1)
|
|
224
|
+
connections.splice(idx, 1);
|
|
225
|
+
}
|
|
226
|
+
listenerConnection = null;
|
|
227
|
+
}
|
|
190
228
|
/** Lazy-create or return the existing publish confirm channel. */
|
|
191
229
|
async function getPublishChannel() {
|
|
192
230
|
if (publishChannel)
|
|
@@ -262,7 +300,7 @@ function computeBackoff(retryCount) {
|
|
|
262
300
|
async function retryOrDLQ(channel, raw, retryCount, retryQueue, failedQueue) {
|
|
263
301
|
const maxRetries = getMaxRetries();
|
|
264
302
|
if (retryCount >= maxRetries) {
|
|
265
|
-
|
|
303
|
+
logger.error(`[message-queue] max retries (${maxRetries}) exceeded, moving to DLQ: ${failedQueue}`);
|
|
266
304
|
channel.publish('', failedQueue, raw.content, {
|
|
267
305
|
persistent: true,
|
|
268
306
|
headers: { ...raw.properties.headers, 'x-retry-count': retryCount },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message-queue.service.js","sourceRoot":"","sources":["../src/message-queue.service.ts"],"names":[],"mappings":";AAAA,qEAAqE;;;;;AAErE,sDAA8B;
|
|
1
|
+
{"version":3,"file":"message-queue.service.js","sourceRoot":"","sources":["../src/message-queue.service.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boopkit/cap-background-jobs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Background job processing with RabbitMQ for boopkit-scaffolded projects",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -20,8 +20,10 @@
|
|
|
20
20
|
"build": "tsc -p tsconfig.json"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
+
"@boopkit/logger": "^0.3.0",
|
|
23
24
|
"amqplib": "^0.10.0",
|
|
24
|
-
"minimist": "^1.2.8"
|
|
25
|
+
"minimist": "^1.2.8",
|
|
26
|
+
"winston": "^3.17.0"
|
|
25
27
|
},
|
|
26
28
|
"devDependencies": {
|
|
27
29
|
"@types/amqplib": "^0.10.0",
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
* uncaught throw → retry, then DLQ after max retries
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { createLogger } from '@boopkit/logger';
|
|
15
|
+
|
|
16
|
+
const logger = createLogger('background-jobs');
|
|
17
|
+
|
|
14
18
|
/** Queue name constant — used by worker.ts to register this consumer. */
|
|
15
19
|
export const QUEUE_NAME = 'example-job';
|
|
16
20
|
|
|
@@ -27,7 +31,7 @@ export async function onMessage(msg: unknown): Promise<boolean> {
|
|
|
27
31
|
payload = validatePayload(msg);
|
|
28
32
|
} catch (err) {
|
|
29
33
|
// Poison message — bad shape, retrying won't help
|
|
30
|
-
|
|
34
|
+
logger.error('invalid payload — ACKing to prevent retry loop', {
|
|
31
35
|
error: (err as Error).message,
|
|
32
36
|
});
|
|
33
37
|
return true;
|
|
@@ -37,11 +41,11 @@ export async function onMessage(msg: unknown): Promise<boolean> {
|
|
|
37
41
|
try {
|
|
38
42
|
// Replace this with your actual service call, e.g.:
|
|
39
43
|
// await services.myService.process(payload.task, payload.data);
|
|
40
|
-
|
|
44
|
+
logger.info(`processed: ${payload.task}`);
|
|
41
45
|
return true;
|
|
42
46
|
} catch (err) {
|
|
43
47
|
// Transient error — retry with backoff
|
|
44
|
-
|
|
48
|
+
logger.error('processing failed — retrying', {
|
|
45
49
|
error: (err as Error).message,
|
|
46
50
|
});
|
|
47
51
|
return false;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/** @boopkit/cap-background-jobs — Scheduler entry point for triggering background jobs. */
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { createLogger } from '@boopkit/logger';
|
|
4
|
+
// @ts-ignore - services scaffolded by cap-services
|
|
5
|
+
import { loadServices } from './services';
|
|
4
6
|
import minimist from 'minimist';
|
|
5
|
-
import MessageQueueService from '
|
|
7
|
+
import { MessageQueueService } from '@boopkit/cap-background-jobs';
|
|
6
8
|
|
|
7
9
|
interface JobConfig {
|
|
8
10
|
/** Human-readable job name (used for --job filtering). */
|
|
@@ -33,6 +35,8 @@ const jobs: JobConfig[] = [
|
|
|
33
35
|
},
|
|
34
36
|
];
|
|
35
37
|
|
|
38
|
+
const logger = createLogger('background-jobs');
|
|
39
|
+
|
|
36
40
|
/**
|
|
37
41
|
* Publish trigger messages for jobs matching the given filters.
|
|
38
42
|
*/
|
|
@@ -48,8 +52,8 @@ async function runJobs(
|
|
|
48
52
|
selected = selected.filter((j) => j.name === opts.job);
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
logger.info(
|
|
56
|
+
`${selected.length} job(s) selected` +
|
|
53
57
|
(opts.interval ? ` for interval "${opts.interval}"` : '') +
|
|
54
58
|
(opts.job ? ` matching job "${opts.job}"` : ''),
|
|
55
59
|
);
|
|
@@ -57,14 +61,15 @@ async function runJobs(
|
|
|
57
61
|
for (const job of selected) {
|
|
58
62
|
const ok = await MessageQueueService.publish(job.queue, job.payload);
|
|
59
63
|
if (ok) {
|
|
60
|
-
|
|
64
|
+
logger.info(`published to "${job.queue}": ${job.payload.task}`);
|
|
61
65
|
} else {
|
|
62
|
-
|
|
66
|
+
logger.error(`failed to publish to "${job.queue}"`);
|
|
63
67
|
}
|
|
64
68
|
}
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
function printUsage(): void {
|
|
72
|
+
// printUsage stays as console.log — it's CLI help text, not operational logging
|
|
68
73
|
console.log(`Usage: npm run scheduler -- <command> [options]
|
|
69
74
|
|
|
70
75
|
Commands:
|
|
@@ -101,9 +106,9 @@ async function main(args: string[], opts: Record<string, unknown>): Promise<void
|
|
|
101
106
|
}
|
|
102
107
|
|
|
103
108
|
case 'list': {
|
|
104
|
-
|
|
109
|
+
logger.info('registered jobs:');
|
|
105
110
|
for (const job of jobs) {
|
|
106
|
-
|
|
111
|
+
logger.info(` ${job.name} queue=${job.queue} interval=${job.interval}`);
|
|
107
112
|
}
|
|
108
113
|
break;
|
|
109
114
|
}
|
|
@@ -111,7 +116,7 @@ async function main(args: string[], opts: Record<string, unknown>): Promise<void
|
|
|
111
116
|
case 'publish': {
|
|
112
117
|
const queue = opts.queue as string | undefined;
|
|
113
118
|
if (!queue) {
|
|
114
|
-
|
|
119
|
+
logger.error('--queue is required for publish command');
|
|
115
120
|
process.exit(1);
|
|
116
121
|
}
|
|
117
122
|
const task = (opts.task as string) ?? 'cli';
|
|
@@ -120,22 +125,22 @@ async function main(args: string[], opts: Record<string, unknown>): Promise<void
|
|
|
120
125
|
try {
|
|
121
126
|
data = JSON.parse(opts.data as string);
|
|
122
127
|
} catch {
|
|
123
|
-
|
|
128
|
+
logger.error('--data must be valid JSON');
|
|
124
129
|
process.exit(1);
|
|
125
130
|
}
|
|
126
131
|
}
|
|
127
132
|
const ok = await MessageQueueService.publish(queue, { task, data });
|
|
128
133
|
if (ok) {
|
|
129
|
-
|
|
134
|
+
logger.info(`published to "${queue}": ${task}`);
|
|
130
135
|
} else {
|
|
131
|
-
|
|
136
|
+
logger.error(`failed to publish to "${queue}"`);
|
|
132
137
|
process.exit(1);
|
|
133
138
|
}
|
|
134
139
|
break;
|
|
135
140
|
}
|
|
136
141
|
|
|
137
142
|
default:
|
|
138
|
-
|
|
143
|
+
logger.error(`unknown command: ${command}`);
|
|
139
144
|
printUsage();
|
|
140
145
|
process.exit(1);
|
|
141
146
|
}
|
|
@@ -146,6 +151,6 @@ async function main(args: string[], opts: Record<string, unknown>): Promise<void
|
|
|
146
151
|
|
|
147
152
|
const argv = minimist(process.argv.slice(2));
|
|
148
153
|
main(argv._, argv).catch((err) => {
|
|
149
|
-
|
|
154
|
+
logger.error('failed', { err });
|
|
150
155
|
process.exit(1);
|
|
151
156
|
});
|
package/scaffold/src/worker.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
/** @boopkit/cap-background-jobs — Worker entry point for background job processing. */
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import { createLogger } from '@boopkit/logger';
|
|
4
|
+
// @ts-ignore - services scaffolded by cap-services
|
|
5
|
+
import { loadServices } from './services';
|
|
6
|
+
import { MessageQueueService } from '@boopkit/cap-background-jobs';
|
|
5
7
|
import * as exampleJobController from './controllers/example-job.controller';
|
|
6
8
|
|
|
9
|
+
const logger = createLogger('background-jobs');
|
|
10
|
+
|
|
7
11
|
// Static consumer registration — add new {queue, handler} entries here
|
|
8
12
|
const consumers = [
|
|
9
13
|
{ queue: exampleJobController.QUEUE_NAME, handler: exampleJobController.onMessage },
|
|
@@ -19,29 +23,29 @@ async function boot() {
|
|
|
19
23
|
await MessageQueueService.listen(consumer.queue, consumer.handler);
|
|
20
24
|
}
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
logger.info(`listening on ${consumers.length} queue(s)`);
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
async function shutdown(signal: string) {
|
|
26
30
|
if (shuttingDown) return;
|
|
27
31
|
shuttingDown = true;
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
logger.info(`${signal} received — starting graceful shutdown`);
|
|
30
34
|
|
|
31
35
|
// Drain in-flight messages with timeout
|
|
32
36
|
const drainTimer = setTimeout(() => {
|
|
33
|
-
|
|
37
|
+
logger.error('drain timeout exceeded — forcing exit');
|
|
34
38
|
process.exit(1);
|
|
35
39
|
}, DRAIN_TIMEOUT_MS);
|
|
36
40
|
|
|
37
41
|
try {
|
|
38
42
|
await MessageQueueService.close();
|
|
39
43
|
clearTimeout(drainTimer);
|
|
40
|
-
|
|
44
|
+
logger.info('shutdown complete');
|
|
41
45
|
process.exit(0);
|
|
42
46
|
} catch (err) {
|
|
43
47
|
clearTimeout(drainTimer);
|
|
44
|
-
|
|
48
|
+
logger.error('error during shutdown', { err });
|
|
45
49
|
process.exit(1);
|
|
46
50
|
}
|
|
47
51
|
}
|
|
@@ -50,7 +54,7 @@ process.on('SIGINT', () => shutdown('SIGINT'));
|
|
|
50
54
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
51
55
|
|
|
52
56
|
boot().catch((err) => {
|
|
53
|
-
|
|
57
|
+
logger.error('boot failed', { err });
|
|
54
58
|
process.exit(1);
|
|
55
59
|
});
|
|
56
60
|
|