@chidchanun/bcp 0.2.9 → 0.2.11
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 +196 -129
- package/docs/README.md +49 -43
- package/docs/api-manifest.json +15 -2
- package/docs/api-reference.md +113 -8
- package/docs/docs-web-manifest.json +7 -3
- package/docs/durable-jobs.md +359 -0
- package/docs/platform-manifest.json +26 -4
- package/docs/releases/0.2.10.md +148 -0
- package/docs/releases/0.2.11.md +180 -0
- package/docs/workflow-orchestration.md +374 -0
- package/package.json +6 -1
- package/packages/bundler/src/client-boundary.ts +1 -0
- package/packages/client/src/jobs.mjs +1310 -61
- package/packages/client/src/jobs.ts +17 -0
- package/packages/client/src/workflow.mjs +601 -0
- package/packages/client/src/workflow.ts +23 -0
- package/packages/server/src/jobs-redis.ts +1226 -0
- package/packages/server/src/jobs.ts +768 -128
- package/packages/server/src/workflow.ts +887 -0
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,112 @@ 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 immediate/delayed enqueue, retry/backoff, cancellation, manual `processNext()`, worker concurrency, visibility timeout, heartbeat lease renewal, stale-running recovery, DLQ inspection/requeue, terminal retention cleanup and queue statistics.
|
|
137
|
+
|
|
138
|
+
Durable adapter methods such as `heartbeat()`, `recoverStale()`, `listDeadLetters()`, `requeueDeadLetter()`, `cleanup()` and `stats()` are optional so earlier `JobQueueAdapter` implementations remain compatible.
|
|
139
|
+
|
|
140
|
+
`reserve()` receives an optional second `ReserveJobOptions` argument containing `ownerId` and `visibilityTimeoutMs`. Production durable adapters should use it to create an atomic visibility lease.
|
|
141
|
+
|
|
142
|
+
### Scheduling
|
|
143
|
+
|
|
144
|
+
`createJobScheduler()` supports recurring interval and UTC five-field cron schedules.
|
|
145
|
+
|
|
146
|
+
The scheduler leases due records from `JobScheduleStore`, then enqueues normal jobs using deterministic occurrence IDs. `nextCronTime()` and `nextScheduleTime()` are available for tooling and tests.
|
|
147
|
+
|
|
148
|
+
### Redis-compatible durable adapters
|
|
149
|
+
|
|
150
|
+
`createRedisJobQueueAdapter()` implements the queue contract on a shared Redis-compatible command client.
|
|
151
|
+
|
|
152
|
+
`createRedisJobScheduleStore()` implements the schedule-store contract using the same minimal client shape:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
interface RedisCommandClient {
|
|
156
|
+
sendCommand(
|
|
157
|
+
command: string[]
|
|
158
|
+
): Promise<unknown>;
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
BCP does not install a Redis library and does not create the connection. Applications own authentication, TLS, Cluster/Sentinel configuration and connection shutdown.
|
|
163
|
+
|
|
164
|
+
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.
|
|
165
|
+
|
|
166
|
+
The processing model is at-least-once. Handlers that perform non-idempotent external side effects should use application-level idempotency protection.
|
|
167
|
+
|
|
168
|
+
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).
|
|
169
|
+
|
|
170
|
+
## `bcp/workflow`
|
|
171
|
+
|
|
172
|
+
Server-only Workflow Orchestration APIs added in `0.2.11`.
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
import {
|
|
176
|
+
createMemoryWorkflowStore,
|
|
177
|
+
createWorkflow,
|
|
178
|
+
type CancelWorkflowOptions,
|
|
179
|
+
type MemoryWorkflowStore,
|
|
180
|
+
type ResumeWorkflowOptions,
|
|
181
|
+
type StartWorkflowOptions,
|
|
182
|
+
type Workflow,
|
|
183
|
+
type WorkflowBuilder,
|
|
184
|
+
type WorkflowCompensationHandler,
|
|
185
|
+
type WorkflowOptions,
|
|
186
|
+
type WorkflowParallelBuilder,
|
|
187
|
+
type WorkflowRetryDelay,
|
|
188
|
+
type WorkflowRunRecord,
|
|
189
|
+
type WorkflowRunState,
|
|
190
|
+
type WorkflowStepContext,
|
|
191
|
+
type WorkflowStepHandler,
|
|
192
|
+
type WorkflowStepKind,
|
|
193
|
+
type WorkflowStepOptions,
|
|
194
|
+
type WorkflowStepRecord,
|
|
195
|
+
type WorkflowStepState,
|
|
196
|
+
type WorkflowStore,
|
|
197
|
+
} from "bcp/workflow";
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`createWorkflow()` defines a persistent server-side workflow with sequential steps, parallel groups, per-step retry policies, persisted delays and compensation handlers.
|
|
201
|
+
|
|
202
|
+
Workflow controls include:
|
|
203
|
+
|
|
204
|
+
```text
|
|
205
|
+
start()
|
|
206
|
+
run()
|
|
207
|
+
get()
|
|
208
|
+
list()
|
|
209
|
+
resume()
|
|
210
|
+
retry()
|
|
211
|
+
cancel()
|
|
212
|
+
compensate()
|
|
213
|
+
close()
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Without a queue, workflow execution runs in the current process until it succeeds, fails, is cancelled or reaches a persisted delay.
|
|
217
|
+
|
|
218
|
+
When an existing `BackgroundJobQueue` is passed through `WorkflowOptions.queue`, workflow execution is submitted through `bcp/jobs`. Delay steps enqueue delayed continuation jobs instead of holding a long-running timer.
|
|
219
|
+
|
|
220
|
+
`WorkflowStore` is the workflow persistence boundary. Its `claim()` / `release()` methods form the run-level lease contract for shared multi-instance stores.
|
|
116
221
|
|
|
117
|
-
|
|
222
|
+
`createMemoryWorkflowStore()` is intended for development and deterministic tests. Production multi-instance applications should implement a shared durable store with atomic `claim()` behavior.
|
|
118
223
|
|
|
119
|
-
|
|
224
|
+
Compensation is saga-style and executes successful compensatable steps in reverse completion order. It does not turn external services into one distributed database transaction.
|
|
120
225
|
|
|
121
|
-
Related guides: [
|
|
226
|
+
Related guides: [Workflow Orchestration](workflow-orchestration.md), [Durable Jobs Platform](durable-jobs.md), [Observability Platform v2](observability.md).
|
|
122
227
|
|
|
123
228
|
## `bcp/observability`
|
|
124
229
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"framework": "bcp",
|
|
4
|
-
"versionTarget": "0.2.
|
|
4
|
+
"versionTarget": "0.2.11",
|
|
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, workflow orchestration, 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,8 @@
|
|
|
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" },
|
|
69
|
+
{ "route": "/docs/workflow-orchestration", "source": "workflow-orchestration.md", "title": "Workflow Orchestration" },
|
|
68
70
|
{ "route": "/docs/caching", "source": "caching.md", "title": "Caching" },
|
|
69
71
|
{ "route": "/docs/security", "source": "security.md", "title": "Security" },
|
|
70
72
|
{ "route": "/docs/production-hardening", "source": "production-hardening.md", "title": "Production Hardening" }
|
|
@@ -110,7 +112,9 @@
|
|
|
110
112
|
}
|
|
111
113
|
],
|
|
112
114
|
"releases": [
|
|
113
|
-
{ "route": "/releases/0.2.
|
|
115
|
+
{ "route": "/releases/0.2.11", "source": "releases/0.2.11.md", "version": "0.2.11", "state": "unreleased" },
|
|
116
|
+
{ "route": "/releases/0.2.10", "source": "releases/0.2.10.md", "version": "0.2.10" },
|
|
117
|
+
{ "route": "/releases/0.2.9", "source": "releases/0.2.9.md", "version": "0.2.9" },
|
|
114
118
|
{ "route": "/releases/0.2.8", "source": "releases/0.2.8.md", "version": "0.2.8" },
|
|
115
119
|
{ "route": "/releases/0.2.7", "source": "releases/0.2.7.md", "version": "0.2.7" },
|
|
116
120
|
{ "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.11",
|
|
5
5
|
"releaseState": "unreleased",
|
|
6
|
-
"baseline": "
|
|
6
|
+
"baseline": "workflow-orchestration",
|
|
7
7
|
"runtime": {
|
|
8
8
|
"node": ">=24.11.0",
|
|
9
9
|
"react": "19",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"bcp/database",
|
|
21
21
|
"bcp/auth",
|
|
22
22
|
"bcp/jobs",
|
|
23
|
+
"bcp/workflow",
|
|
23
24
|
"bcp/observability",
|
|
24
25
|
"bcp/server",
|
|
25
26
|
"bcp/server-only",
|
|
@@ -80,6 +81,25 @@
|
|
|
80
81
|
"jobScheduleStoreContract": true,
|
|
81
82
|
"schedulerLeases": true,
|
|
82
83
|
"scheduledRunDeduplication": true,
|
|
84
|
+
"durableJobsPlatform": true,
|
|
85
|
+
"redisJobQueueAdapter": true,
|
|
86
|
+
"redisJobScheduleStore": true,
|
|
87
|
+
"jobVisibilityTimeout": true,
|
|
88
|
+
"jobHeartbeats": true,
|
|
89
|
+
"staleJobRecovery": true,
|
|
90
|
+
"deadLetterQueue": true,
|
|
91
|
+
"deadLetterRequeue": true,
|
|
92
|
+
"jobRetentionCleanup": true,
|
|
93
|
+
"jobQueueStatistics": true,
|
|
94
|
+
"workflowOrchestration": true,
|
|
95
|
+
"workflowStoreContract": true,
|
|
96
|
+
"workflowRunLeases": true,
|
|
97
|
+
"workflowSequentialSteps": true,
|
|
98
|
+
"workflowParallelSteps": true,
|
|
99
|
+
"workflowStepRetries": true,
|
|
100
|
+
"workflowDelays": true,
|
|
101
|
+
"workflowCompensation": true,
|
|
102
|
+
"workflowQueueExecution": true,
|
|
83
103
|
"databaseMigrations": true,
|
|
84
104
|
"databaseAdapterContract": true,
|
|
85
105
|
"databasePostgresql": true,
|
|
@@ -119,7 +139,7 @@
|
|
|
119
139
|
"s3-compatible"
|
|
120
140
|
],
|
|
121
141
|
"compatibility": {
|
|
122
|
-
"previousBaseline": "0.2.
|
|
142
|
+
"previousBaseline": "0.2.10",
|
|
123
143
|
"intentionalBreakingChangesFromPreviousBaseline": false,
|
|
124
144
|
"migrationGuide": "migration-0.2.md"
|
|
125
145
|
},
|
|
@@ -138,7 +158,9 @@
|
|
|
138
158
|
"observability": "observability.md",
|
|
139
159
|
"backgroundJobs": "background-jobs.md",
|
|
140
160
|
"jobScheduling": "job-scheduling.md",
|
|
161
|
+
"durableJobs": "durable-jobs.md",
|
|
162
|
+
"workflowOrchestration": "workflow-orchestration.md",
|
|
141
163
|
"migrationGuide": "migration-0.2.md",
|
|
142
|
-
"releaseNotes": "releases/0.2.
|
|
164
|
+
"releaseNotes": "releases/0.2.11.md"
|
|
143
165
|
}
|
|
144
166
|
}
|