@chidchanun/bcp 0.2.9 → 0.2.10
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 +122 -128
- package/docs/README.md +45 -41
- package/docs/api-manifest.json +3 -2
- package/docs/api-reference.md +69 -8
- package/docs/docs-web-manifest.json +5 -3
- package/docs/durable-jobs.md +359 -0
- package/docs/platform-manifest.json +15 -4
- package/docs/releases/0.2.10.md +148 -0
- package/package.json +1 -1
- package/packages/client/src/jobs.mjs +1310 -61
- package/packages/client/src/jobs.ts +17 -0
- package/packages/server/src/jobs-redis.ts +1226 -0
- package/packages/server/src/jobs.ts +768 -128
package/docs/api-reference.md
CHANGED
|
@@ -66,30 +66,38 @@ Related guides: [Authentication](authentication.md), [Auth Session Stores](auth-
|
|
|
66
66
|
|
|
67
67
|
## `bcp/jobs`
|
|
68
68
|
|
|
69
|
-
Server-only Background Jobs
|
|
69
|
+
Server-only Background Jobs, Job Scheduling and Durable Jobs APIs.
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
Core queue APIs:
|
|
72
72
|
|
|
73
73
|
```ts
|
|
74
74
|
import {
|
|
75
75
|
createJobQueue,
|
|
76
76
|
createMemoryJobQueueAdapter,
|
|
77
77
|
type BackgroundJobQueue,
|
|
78
|
+
type CleanupJobsOptions,
|
|
79
|
+
type DeadLetterJobRecord,
|
|
78
80
|
type EnqueueJobOptions,
|
|
81
|
+
type HeartbeatJobOptions,
|
|
79
82
|
type JobHandler,
|
|
80
83
|
type JobHandlerContext,
|
|
81
84
|
type JobQueueAdapter,
|
|
82
85
|
type JobQueueOptions,
|
|
86
|
+
type JobQueueStats,
|
|
83
87
|
type JobRecord,
|
|
84
88
|
type JobRetryDelay,
|
|
85
89
|
type JobState,
|
|
86
90
|
type JobWorker,
|
|
87
91
|
type MemoryJobQueueAdapter,
|
|
92
|
+
type ProcessNextJobOptions,
|
|
93
|
+
type RecoverStaleJobsOptions,
|
|
94
|
+
type RequeueDeadLetterOptions,
|
|
95
|
+
type ReserveJobOptions,
|
|
88
96
|
type StartJobWorkerOptions,
|
|
89
97
|
} from "bcp/jobs";
|
|
90
98
|
```
|
|
91
99
|
|
|
92
|
-
Scheduling APIs
|
|
100
|
+
Scheduling APIs:
|
|
93
101
|
|
|
94
102
|
```ts
|
|
95
103
|
import {
|
|
@@ -110,15 +118,68 @@ import {
|
|
|
110
118
|
} from "bcp/jobs";
|
|
111
119
|
```
|
|
112
120
|
|
|
113
|
-
|
|
121
|
+
Durable Redis-compatible APIs added in `0.2.10`:
|
|
114
122
|
|
|
115
|
-
|
|
123
|
+
```ts
|
|
124
|
+
import {
|
|
125
|
+
createRedisJobQueueAdapter,
|
|
126
|
+
createRedisJobScheduleStore,
|
|
127
|
+
type RedisCommandClient,
|
|
128
|
+
type RedisJobQueueAdapter,
|
|
129
|
+
type RedisJobsAdapterOptions,
|
|
130
|
+
type RedisJobScheduleStore,
|
|
131
|
+
} from "bcp/jobs";
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Queue lifecycle
|
|
135
|
+
|
|
136
|
+
`createJobQueue()` supports:
|
|
137
|
+
|
|
138
|
+
```text
|
|
139
|
+
immediate/delayed enqueue
|
|
140
|
+
retry/backoff
|
|
141
|
+
cancellation
|
|
142
|
+
manual processNext()
|
|
143
|
+
worker concurrency
|
|
144
|
+
visibility timeout
|
|
145
|
+
heartbeat lease renewal
|
|
146
|
+
stale-running recovery
|
|
147
|
+
DLQ inspection/requeue
|
|
148
|
+
terminal retention cleanup
|
|
149
|
+
queue statistics
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Durable adapter methods such as `heartbeat()`, `recoverStale()`, `listDeadLetters()`, `requeueDeadLetter()`, `cleanup()` and `stats()` are optional so earlier `JobQueueAdapter` implementations remain compatible.
|
|
153
|
+
|
|
154
|
+
`reserve()` receives an optional second `ReserveJobOptions` argument containing `ownerId` and `visibilityTimeoutMs`. Production durable adapters should use it to create an atomic visibility lease.
|
|
155
|
+
|
|
156
|
+
### Scheduling
|
|
157
|
+
|
|
158
|
+
`createJobScheduler()` supports recurring interval and UTC five-field cron schedules.
|
|
159
|
+
|
|
160
|
+
The scheduler leases due records from `JobScheduleStore`, then enqueues normal jobs using deterministic occurrence IDs. `nextCronTime()` and `nextScheduleTime()` are available for tooling and tests.
|
|
161
|
+
|
|
162
|
+
### Redis-compatible durable adapters
|
|
163
|
+
|
|
164
|
+
`createRedisJobQueueAdapter()` implements the queue contract on a shared Redis-compatible command client.
|
|
165
|
+
|
|
166
|
+
`createRedisJobScheduleStore()` implements the schedule-store contract using the same minimal client shape:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
interface RedisCommandClient {
|
|
170
|
+
sendCommand(
|
|
171
|
+
command: string[]
|
|
172
|
+
): Promise<unknown>;
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
BCP does not install a Redis library and does not create the connection. Applications own authentication, TLS, Cluster/Sentinel configuration and connection shutdown.
|
|
116
177
|
|
|
117
|
-
The
|
|
178
|
+
The reference adapters use atomic Lua state transitions. The default namespace `bcp:{jobs}` uses a Redis Cluster hash tag so all job/schedule keys share one slot.
|
|
118
179
|
|
|
119
|
-
|
|
180
|
+
The processing model is at-least-once. Handlers that perform non-idempotent external side effects should use application-level idempotency protection.
|
|
120
181
|
|
|
121
|
-
Related guides: [Background Jobs Platform](background-jobs.md), [Job Scheduling Platform](job-scheduling.md), [Observability Platform v2](observability.md).
|
|
182
|
+
Related guides: [Background Jobs Platform](background-jobs.md), [Job Scheduling Platform](job-scheduling.md), [Durable Jobs Platform](durable-jobs.md), [Observability Platform v2](observability.md).
|
|
122
183
|
|
|
123
184
|
## `bcp/observability`
|
|
124
185
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"framework": "bcp",
|
|
4
|
-
"versionTarget": "0.2.
|
|
4
|
+
"versionTarget": "0.2.10",
|
|
5
5
|
"releaseState": "unreleased",
|
|
6
6
|
"sections": [
|
|
7
7
|
{
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
{
|
|
58
58
|
"id": "runtime",
|
|
59
59
|
"title": "Runtime & Infrastructure",
|
|
60
|
-
"description": "Middleware, background jobs, scheduling, observability, logging, caching, security and production hardening.",
|
|
60
|
+
"description": "Middleware, durable background jobs, scheduling, observability, logging, caching, security and production hardening.",
|
|
61
61
|
"pages": [
|
|
62
62
|
{ "route": "/docs/middleware", "source": "middleware.md", "title": "Middleware" },
|
|
63
63
|
{ "route": "/docs/hydration", "source": "hydration.md", "title": "Hydration" },
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
{ "route": "/docs/observability", "source": "observability.md", "title": "Observability Platform v2" },
|
|
66
66
|
{ "route": "/docs/background-jobs", "source": "background-jobs.md", "title": "Background Jobs Platform" },
|
|
67
67
|
{ "route": "/docs/job-scheduling", "source": "job-scheduling.md", "title": "Job Scheduling Platform" },
|
|
68
|
+
{ "route": "/docs/durable-jobs", "source": "durable-jobs.md", "title": "Durable Jobs Platform" },
|
|
68
69
|
{ "route": "/docs/caching", "source": "caching.md", "title": "Caching" },
|
|
69
70
|
{ "route": "/docs/security", "source": "security.md", "title": "Security" },
|
|
70
71
|
{ "route": "/docs/production-hardening", "source": "production-hardening.md", "title": "Production Hardening" }
|
|
@@ -110,7 +111,8 @@
|
|
|
110
111
|
}
|
|
111
112
|
],
|
|
112
113
|
"releases": [
|
|
113
|
-
{ "route": "/releases/0.2.
|
|
114
|
+
{ "route": "/releases/0.2.10", "source": "releases/0.2.10.md", "version": "0.2.10", "state": "unreleased" },
|
|
115
|
+
{ "route": "/releases/0.2.9", "source": "releases/0.2.9.md", "version": "0.2.9" },
|
|
114
116
|
{ "route": "/releases/0.2.8", "source": "releases/0.2.8.md", "version": "0.2.8" },
|
|
115
117
|
{ "route": "/releases/0.2.7", "source": "releases/0.2.7.md", "version": "0.2.7" },
|
|
116
118
|
{ "route": "/releases/0.2.6", "source": "releases/0.2.6.md", "version": "0.2.6" },
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
# Durable Jobs Platform
|
|
2
|
+
|
|
3
|
+
BCP Framework `0.2.10` extends `bcp/jobs` with production-oriented worker lifecycle primitives, dead-letter handling, retention utilities and Redis-compatible queue/scheduler adapters.
|
|
4
|
+
|
|
5
|
+
The public entrypoint remains:
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import {
|
|
9
|
+
createJobQueue,
|
|
10
|
+
createJobScheduler,
|
|
11
|
+
} from "bcp/jobs";
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`0.2.10` is additive to the queue contract introduced in `0.2.8` and the scheduling contract introduced in `0.2.9`.
|
|
15
|
+
|
|
16
|
+
## What 0.2.10 adds
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
visibility timeout
|
|
20
|
+
worker lease owner
|
|
21
|
+
heartbeat renewal
|
|
22
|
+
stale-running recovery
|
|
23
|
+
dead-letter queue (DLQ)
|
|
24
|
+
dead-letter requeue
|
|
25
|
+
terminal-job cleanup
|
|
26
|
+
queue statistics
|
|
27
|
+
Redis-compatible JobQueueAdapter
|
|
28
|
+
Redis-compatible JobScheduleStore
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The built-in memory adapter implements the same lifecycle for development and deterministic tests. Durable multi-process deployments should use a shared adapter such as the Redis-compatible adapter or another implementation of the public contracts.
|
|
32
|
+
|
|
33
|
+
## Worker visibility timeout
|
|
34
|
+
|
|
35
|
+
Workers now reserve jobs with a visibility lease:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
const worker =
|
|
39
|
+
jobs.startWorker({
|
|
40
|
+
workerId: "email-worker",
|
|
41
|
+
concurrency: 4,
|
|
42
|
+
visibilityTimeoutMs: 30_000,
|
|
43
|
+
heartbeatIntervalMs: 10_000,
|
|
44
|
+
pollIntervalMs: 250,
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
A durable adapter can persist:
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
leaseOwner
|
|
52
|
+
leaseUntil
|
|
53
|
+
heartbeatAt
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
While a handler is running, BCP renews the lease when the adapter implements `heartbeat()`.
|
|
57
|
+
|
|
58
|
+
If the process disappears and the lease expires, `recoverStale()` can move the job back to `queued` so another worker can process it.
|
|
59
|
+
|
|
60
|
+
This is an **at-least-once** model. Handlers that perform non-idempotent side effects should use application-level idempotency keys or transactions.
|
|
61
|
+
|
|
62
|
+
## Manual stale recovery
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
const recovered =
|
|
66
|
+
await jobs.recoverStale({
|
|
67
|
+
limit: 100,
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Workers also request stale-job recovery before reserving work when the adapter implements the capability.
|
|
72
|
+
|
|
73
|
+
A recovered job keeps its attempt count. It may therefore reach `maxAttempts` after repeated crashes or visibility-timeout recoveries.
|
|
74
|
+
|
|
75
|
+
## Dead-letter queue
|
|
76
|
+
|
|
77
|
+
When a handler exhausts `maxAttempts`, the memory and Redis adapters keep the job in terminal `failed` state and also add it to the DLQ index.
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
const failedJobs =
|
|
81
|
+
await jobs.deadLetters();
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Each result includes:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
{
|
|
88
|
+
state: "failed",
|
|
89
|
+
deadLetteredAt: number,
|
|
90
|
+
// normal JobRecord fields
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Requeue a DLQ job:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
await jobs.requeueDeadLetter(
|
|
98
|
+
jobId,
|
|
99
|
+
{
|
|
100
|
+
delayMs: 5_000,
|
|
101
|
+
resetAttempts: true,
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`resetAttempts` defaults to `true`.
|
|
107
|
+
|
|
108
|
+
## Queue statistics
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
const stats =
|
|
112
|
+
await jobs.stats();
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Shape:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
{
|
|
119
|
+
total: number;
|
|
120
|
+
queued: number;
|
|
121
|
+
running: number;
|
|
122
|
+
succeeded: number;
|
|
123
|
+
failed: number;
|
|
124
|
+
cancelled: number;
|
|
125
|
+
deadLetters: number;
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Adapters may provide an optimized `stats()` implementation. Otherwise BCP derives statistics from `list()` plus the optional DLQ contract.
|
|
130
|
+
|
|
131
|
+
## Retention cleanup
|
|
132
|
+
|
|
133
|
+
Terminal jobs can be removed after an application-defined retention window:
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
const sevenDaysAgo =
|
|
137
|
+
Date.now() -
|
|
138
|
+
7 * 24 * 60 * 60 * 1000;
|
|
139
|
+
|
|
140
|
+
await jobs.cleanup({
|
|
141
|
+
before: sevenDaysAgo,
|
|
142
|
+
states: [
|
|
143
|
+
"succeeded",
|
|
144
|
+
"cancelled",
|
|
145
|
+
],
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
If `states` is omitted, cleanup considers:
|
|
150
|
+
|
|
151
|
+
```text
|
|
152
|
+
succeeded
|
|
153
|
+
failed
|
|
154
|
+
cancelled
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
For failed jobs, cleanup also removes the DLQ index entry.
|
|
158
|
+
|
|
159
|
+
The Redis reference adapter removes at most 1,000 matching terminal records per cleanup call. Large installations can invoke cleanup repeatedly from a scheduled maintenance job.
|
|
160
|
+
|
|
161
|
+
## Redis-compatible queue adapter
|
|
162
|
+
|
|
163
|
+
BCP does not install or own a Redis client library. The adapter accepts a minimal command client:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
interface RedisCommandClient {
|
|
167
|
+
sendCommand(
|
|
168
|
+
command: string[]
|
|
169
|
+
): Promise<unknown>;
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Create the queue adapter:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import {
|
|
177
|
+
createJobQueue,
|
|
178
|
+
createRedisJobQueueAdapter,
|
|
179
|
+
} from "bcp/jobs";
|
|
180
|
+
|
|
181
|
+
const adapter =
|
|
182
|
+
createRedisJobQueueAdapter({
|
|
183
|
+
client: redisCommandClient,
|
|
184
|
+
namespace: "my-app:{jobs}",
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
export const jobs =
|
|
188
|
+
createJobQueue({
|
|
189
|
+
adapter,
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The default namespace is:
|
|
194
|
+
|
|
195
|
+
```text
|
|
196
|
+
bcp:{jobs}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The `{jobs}` Redis hash tag keeps all adapter keys in the same Redis Cluster slot so Lua operations can remain atomic.
|
|
200
|
+
|
|
201
|
+
### Connection lifecycle
|
|
202
|
+
|
|
203
|
+
Applications own the Redis connection. When desired, provide a close hook:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
const adapter =
|
|
207
|
+
createRedisJobQueueAdapter({
|
|
208
|
+
client: redisCommandClient,
|
|
209
|
+
close: async () => {
|
|
210
|
+
await redisClient.quit();
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Then:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
await jobs.close();
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
will stop BCP workers and invoke the adapter close hook.
|
|
222
|
+
|
|
223
|
+
If queue and scheduler share one Redis connection, normally provide the close hook to only one owner or coordinate connection shutdown at the application layer.
|
|
224
|
+
|
|
225
|
+
## Redis environment configuration
|
|
226
|
+
|
|
227
|
+
A typical application can use:
|
|
228
|
+
|
|
229
|
+
```dotenv
|
|
230
|
+
REDIS_URL=redis://localhost:6379
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
`bcp/jobs` does **not** read `REDIS_URL` automatically. Connection creation remains application-owned so credentials, TLS, Sentinel/Cluster configuration and client-library choice stay explicit.
|
|
234
|
+
|
|
235
|
+
## Atomic Redis behavior
|
|
236
|
+
|
|
237
|
+
The reference Redis adapter uses Lua `EVAL` for state-changing operations that require atomicity, including:
|
|
238
|
+
|
|
239
|
+
```text
|
|
240
|
+
enqueue duplicate protection
|
|
241
|
+
job reservation
|
|
242
|
+
complete/fail/cancel
|
|
243
|
+
heartbeat lease renewal
|
|
244
|
+
stale-running recovery
|
|
245
|
+
DLQ requeue
|
|
246
|
+
retention cleanup
|
|
247
|
+
schedule upsert/remove
|
|
248
|
+
schedule lease acquisition
|
|
249
|
+
schedule completion/release
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Queue reservation atomically removes one eligible job from the available index, marks it running and writes the visibility lease before another worker can reserve it.
|
|
253
|
+
|
|
254
|
+
## Redis-backed scheduler
|
|
255
|
+
|
|
256
|
+
The same Redis command client can back the scheduling contract:
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
import {
|
|
260
|
+
createJobScheduler,
|
|
261
|
+
createRedisJobScheduleStore,
|
|
262
|
+
} from "bcp/jobs";
|
|
263
|
+
|
|
264
|
+
const scheduleStore =
|
|
265
|
+
createRedisJobScheduleStore({
|
|
266
|
+
client: redisCommandClient,
|
|
267
|
+
namespace: "my-app:{jobs}",
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
export const scheduler =
|
|
271
|
+
createJobScheduler({
|
|
272
|
+
queue: jobs,
|
|
273
|
+
store: scheduleStore,
|
|
274
|
+
ownerId: "scheduler-a",
|
|
275
|
+
});
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
`acquireDue()` is implemented with an atomic Redis lease, allowing multiple scheduler processes to share the same schedule store.
|
|
279
|
+
|
|
280
|
+
## Recommended production topology
|
|
281
|
+
|
|
282
|
+
```text
|
|
283
|
+
Web/API instances
|
|
284
|
+
|
|
|
285
|
+
| enqueue / schedule
|
|
286
|
+
v
|
|
287
|
+
Redis-compatible shared job state
|
|
288
|
+
|
|
|
289
|
+
+-------------------+
|
|
290
|
+
| |
|
|
291
|
+
v v
|
|
292
|
+
Worker A Worker B
|
|
293
|
+
visibility lease visibility lease
|
|
294
|
+
heartbeat heartbeat
|
|
295
|
+
|
|
|
296
|
+
+---- failure ----> retry / DLQ
|
|
297
|
+
|
|
298
|
+
Scheduler A -----+
|
|
299
|
+
+---- shared schedule lease
|
|
300
|
+
Scheduler B -----+
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Queue workers and schedulers can run in the same Node process for small deployments, but separate worker processes/containers are recommended when job load can affect web latency.
|
|
304
|
+
|
|
305
|
+
## Adapter compatibility
|
|
306
|
+
|
|
307
|
+
Existing `0.2.8` `JobQueueAdapter` implementations remain valid. Durable methods are optional:
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
heartbeat?()
|
|
311
|
+
recoverStale?()
|
|
312
|
+
listDeadLetters?()
|
|
313
|
+
requeueDeadLetter?()
|
|
314
|
+
cleanup?()
|
|
315
|
+
stats?()
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
The `reserve()` method now receives an optional second argument with the worker owner ID and visibility timeout. Existing JavaScript adapters that ignore the optional argument continue to work.
|
|
319
|
+
|
|
320
|
+
For production durable adapters, implementations should support the lease contract rather than ignoring it.
|
|
321
|
+
|
|
322
|
+
## Failure model
|
|
323
|
+
|
|
324
|
+
BCP jobs intentionally use practical at-least-once delivery semantics.
|
|
325
|
+
|
|
326
|
+
Applications should assume that a job can execute more than once when:
|
|
327
|
+
|
|
328
|
+
- a worker completes external side effects but crashes before recording success,
|
|
329
|
+
- a visibility lease expires during a long or blocked handler,
|
|
330
|
+
- a DLQ job is manually requeued,
|
|
331
|
+
- infrastructure retries a command after an ambiguous network result.
|
|
332
|
+
|
|
333
|
+
Use idempotency keys, unique database constraints or transactional outbox/inbox patterns for side effects where duplicates are unsafe.
|
|
334
|
+
|
|
335
|
+
## Observability
|
|
336
|
+
|
|
337
|
+
`jobs.stats()` can feed application metrics from `bcp/observability`:
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
const stats =
|
|
341
|
+
await jobs.stats();
|
|
342
|
+
|
|
343
|
+
queueDepth.set(
|
|
344
|
+
stats.queued
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
dlqDepth.set(
|
|
348
|
+
stats.deadLetters
|
|
349
|
+
);
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
For Redis deployments, infrastructure-level Redis monitoring should complement application-level BCP metrics.
|
|
353
|
+
|
|
354
|
+
## Related guides
|
|
355
|
+
|
|
356
|
+
- [Background Jobs Platform](background-jobs.md)
|
|
357
|
+
- [Job Scheduling Platform](job-scheduling.md)
|
|
358
|
+
- [Observability Platform v2](observability.md)
|
|
359
|
+
- [Production Hardening](production-hardening.md)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"framework": "bcp",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.10",
|
|
5
5
|
"releaseState": "unreleased",
|
|
6
|
-
"baseline": "
|
|
6
|
+
"baseline": "durable-jobs-platform",
|
|
7
7
|
"runtime": {
|
|
8
8
|
"node": ">=24.11.0",
|
|
9
9
|
"react": "19",
|
|
@@ -80,6 +80,16 @@
|
|
|
80
80
|
"jobScheduleStoreContract": true,
|
|
81
81
|
"schedulerLeases": true,
|
|
82
82
|
"scheduledRunDeduplication": true,
|
|
83
|
+
"durableJobsPlatform": true,
|
|
84
|
+
"redisJobQueueAdapter": true,
|
|
85
|
+
"redisJobScheduleStore": true,
|
|
86
|
+
"jobVisibilityTimeout": true,
|
|
87
|
+
"jobHeartbeats": true,
|
|
88
|
+
"staleJobRecovery": true,
|
|
89
|
+
"deadLetterQueue": true,
|
|
90
|
+
"deadLetterRequeue": true,
|
|
91
|
+
"jobRetentionCleanup": true,
|
|
92
|
+
"jobQueueStatistics": true,
|
|
83
93
|
"databaseMigrations": true,
|
|
84
94
|
"databaseAdapterContract": true,
|
|
85
95
|
"databasePostgresql": true,
|
|
@@ -119,7 +129,7 @@
|
|
|
119
129
|
"s3-compatible"
|
|
120
130
|
],
|
|
121
131
|
"compatibility": {
|
|
122
|
-
"previousBaseline": "0.2.
|
|
132
|
+
"previousBaseline": "0.2.9",
|
|
123
133
|
"intentionalBreakingChangesFromPreviousBaseline": false,
|
|
124
134
|
"migrationGuide": "migration-0.2.md"
|
|
125
135
|
},
|
|
@@ -138,7 +148,8 @@
|
|
|
138
148
|
"observability": "observability.md",
|
|
139
149
|
"backgroundJobs": "background-jobs.md",
|
|
140
150
|
"jobScheduling": "job-scheduling.md",
|
|
151
|
+
"durableJobs": "durable-jobs.md",
|
|
141
152
|
"migrationGuide": "migration-0.2.md",
|
|
142
|
-
"releaseNotes": "releases/0.2.
|
|
153
|
+
"releaseNotes": "releases/0.2.10.md"
|
|
143
154
|
}
|
|
144
155
|
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# 0.2.10 — Durable Jobs Platform
|
|
2
|
+
|
|
3
|
+
**State:** unreleased development target
|
|
4
|
+
|
|
5
|
+
BCP Framework `0.2.10` extends the Background Jobs and Job Scheduling platforms with durable worker lifecycle contracts, dead-letter handling, retention/statistics APIs and Redis-compatible reference adapters.
|
|
6
|
+
|
|
7
|
+
## Highlights
|
|
8
|
+
|
|
9
|
+
### Durable worker leases
|
|
10
|
+
|
|
11
|
+
Workers can reserve jobs with a visibility timeout and stable worker identity.
|
|
12
|
+
|
|
13
|
+
New job record metadata:
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
leaseOwner
|
|
17
|
+
leaseUntil
|
|
18
|
+
heartbeatAt
|
|
19
|
+
recoveredAt
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
When an adapter supports `heartbeat()`, BCP workers renew the lease while a handler is running.
|
|
23
|
+
|
|
24
|
+
When an adapter supports `recoverStale()`, expired running jobs can be returned to the queue for at-least-once recovery.
|
|
25
|
+
|
|
26
|
+
### Dead-letter queue
|
|
27
|
+
|
|
28
|
+
Retry-exhausted jobs remain terminal `failed` jobs and can also be indexed in the adapter DLQ.
|
|
29
|
+
|
|
30
|
+
New queue APIs:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
jobs.deadLetters()
|
|
34
|
+
jobs.requeueDeadLetter()
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The built-in memory adapter and Redis-compatible adapter implement both APIs.
|
|
38
|
+
|
|
39
|
+
### Queue maintenance
|
|
40
|
+
|
|
41
|
+
New operational APIs:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
jobs.stats()
|
|
45
|
+
jobs.cleanup()
|
|
46
|
+
jobs.recoverStale()
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Statistics include queue state totals and DLQ depth.
|
|
50
|
+
|
|
51
|
+
Cleanup can remove old succeeded, failed and cancelled records after an application-defined retention period.
|
|
52
|
+
|
|
53
|
+
### Redis-compatible queue adapter
|
|
54
|
+
|
|
55
|
+
New public API:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
createRedisJobQueueAdapter()
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The adapter consumes a minimal application-owned Redis command client exposing:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
sendCommand(command: string[]): Promise<unknown>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
BCP does not add a Redis client runtime dependency.
|
|
68
|
+
|
|
69
|
+
The Redis adapter uses atomic Lua operations for:
|
|
70
|
+
|
|
71
|
+
- duplicate-safe enqueue,
|
|
72
|
+
- job reservation,
|
|
73
|
+
- complete/fail/cancel transitions,
|
|
74
|
+
- heartbeat lease renewal,
|
|
75
|
+
- stale-job recovery,
|
|
76
|
+
- DLQ requeue,
|
|
77
|
+
- retention cleanup.
|
|
78
|
+
|
|
79
|
+
The default Redis namespace is `bcp:{jobs}` so all adapter keys share one Redis Cluster hash slot.
|
|
80
|
+
|
|
81
|
+
### Redis-compatible scheduler store
|
|
82
|
+
|
|
83
|
+
New public API:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
createRedisJobScheduleStore()
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The scheduler store implements the `0.2.9` `JobScheduleStore` contract on shared Redis state and uses atomic schedule leasing for multi-instance schedulers.
|
|
90
|
+
|
|
91
|
+
### Compiled npm runtime
|
|
92
|
+
|
|
93
|
+
The published `bcp/jobs` default runtime continues to use generated `jobs.mjs` rather than requiring consumers to execute framework TypeScript directly.
|
|
94
|
+
|
|
95
|
+
The Durable Jobs package smoke imports that compiled runtime and verifies the new public APIs.
|
|
96
|
+
|
|
97
|
+
## Public API additions
|
|
98
|
+
|
|
99
|
+
`bcp/jobs` adds:
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
createRedisJobQueueAdapter
|
|
103
|
+
createRedisJobScheduleStore
|
|
104
|
+
RedisCommandClient
|
|
105
|
+
RedisJobQueueAdapter
|
|
106
|
+
RedisJobScheduleStore
|
|
107
|
+
RedisJobsAdapterOptions
|
|
108
|
+
|
|
109
|
+
CleanupJobsOptions
|
|
110
|
+
DeadLetterJobRecord
|
|
111
|
+
HeartbeatJobOptions
|
|
112
|
+
JobQueueStats
|
|
113
|
+
ProcessNextJobOptions
|
|
114
|
+
RecoverStaleJobsOptions
|
|
115
|
+
RequeueDeadLetterOptions
|
|
116
|
+
ReserveJobOptions
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Existing queue and scheduling APIs remain available.
|
|
120
|
+
|
|
121
|
+
## Compatibility
|
|
122
|
+
|
|
123
|
+
The release is intended to be backward compatible with `0.2.9`.
|
|
124
|
+
|
|
125
|
+
Existing `JobQueueAdapter` implementations remain valid because durable capabilities are optional and the new reservation metadata is provided as an optional second parameter to `reserve()`.
|
|
126
|
+
|
|
127
|
+
Adapters that need production visibility-timeout semantics should implement the new durable methods.
|
|
128
|
+
|
|
129
|
+
## Delivery semantics
|
|
130
|
+
|
|
131
|
+
Durable jobs use at-least-once processing semantics.
|
|
132
|
+
|
|
133
|
+
Applications should make side-effecting handlers idempotent when duplicate execution would be unsafe.
|
|
134
|
+
|
|
135
|
+
## Validation target
|
|
136
|
+
|
|
137
|
+
Before tagging/publishing `0.2.10`, run:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
npm run typecheck
|
|
141
|
+
npm run test:unit
|
|
142
|
+
npm run test:integration
|
|
143
|
+
npm run test:e2e
|
|
144
|
+
npm run test:package
|
|
145
|
+
npm run rc:check
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The final release tag must point to the exact commit that passed the complete RC sequence.
|