You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/DEVELOPMENT_STACK.md
+36-3Lines changed: 36 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,6 +1,6 @@
1
1
# Local Development Stack (Docker Compose)
2
2
3
-
A reproducible local stack for the Learnault API: **API**, **wallet worker**, **PostgreSQL**, and **Redis** — started with one command.
3
+
A reproducible local stack for the Learnault API: **API**, **wallet worker**, **scheduler**, **PostgreSQL**, and **Redis** — started with one command.
4
4
5
5
## Prerequisites
6
6
@@ -23,6 +23,7 @@ docker compose ps
23
23
# learnault-dev-db Up ... (healthy)
24
24
# learnault-dev-redis Up ... (healthy)
25
25
# learnault-dev-worker Up ... (healthy)
26
+
# learnault-dev-scheduler Up ...
26
27
```
27
28
28
29
The API is available at `http://localhost:5000` (Swagger UI at `http://localhost:5000/api-docs`).
@@ -37,6 +38,37 @@ The `api` service entrypoint (`docker/entrypoint-dev-api.sh`) waits for PostgreS
37
38
38
39
The `worker` service runs `src/workers/wallet-provisioning.worker.ts`, which polls the idempotent wallet-provisioning outbox and generates Stellar keys through the dev in-memory KMS adapter. In production, swap the KMS adapter for a real one (e.g. AWS KMS) behind the same `KmsSecretStore` interface.
39
40
41
+
The `scheduler` service runs `src/workers/scheduler.worker.ts`. See below.
42
+
43
+
## Scheduled job runner
44
+
45
+
Every recurring queue drain is owned by the `scheduler` service, not by the request that enqueued the work — so a delivery whose `nextAttemptAt` falls due is retried on time even when the API is receiving no traffic, and request latency never includes queue-drain work.
Each tick takes a row lease on `queue_leases` via `JobLeaseService.acquireQueueLease()` before draining, so extra replicas are safe:
50
+
51
+
```bash
52
+
docker compose up -d --scale scheduler=2
53
+
```
54
+
55
+
A replica that loses the race logs a skipped tick and moves on; a replica that crashes mid-drain has its lease expire, and the next tick reclaims the queue.
56
+
57
+
| Variable | Default | Purpose |
58
+
| --- | --- | --- |
59
+
|`SCHEDULER_INTERVAL_MS`|`15000`| Base tick interval for every queue |
60
+
|`SCHEDULER_<QUEUE>_INTERVAL_MS`| — | Per-queue override, e.g. `SCHEDULER_WEBHOOK_INTERVAL_MS`|
61
+
|`SCHEDULER_LEASE_MS`|`60000`| Lease held per tick (floored at 2× the interval) |
62
+
|`SCHEDULER_QUEUES`| all | Comma list restricting which queues this replica runs |
63
+
|`SCHEDULER_DISABLED_QUEUES`| — | Comma list of queues to skip |
64
+
|`SCHEDULER_SHUTDOWN_TIMEOUT_MS`|`30000`| How long `SIGTERM` waits for in-flight ticks |
65
+
|`SCHEDULER_IN_PROCESS`|`false`| Opt-in: run the runner inside the API process for single-process deployments |
66
+
|`LIFECYCLE_SWEEP_INTERVAL_MS`|`0`| When `> 0`, overrides the `account-lifecycle` queue interval |
67
+
68
+
Every tick emits a structured log line carrying per-queue `depth`, `due`, `lagMs` (age of the oldest due row), `durationMs`, and cumulative `attempts` / `failures` / `skipped`.
69
+
70
+
`pnpm scheduler:verify` runs both evidence scenarios against the stack: a due-but-failed delivery drained with no inbound HTTP traffic, then a batch drained by two replicas with no row processed twice.
71
+
40
72
## Health checks & readiness
41
73
42
74
| Endpoint | Meaning |
@@ -54,7 +86,7 @@ The API container only reports **healthy** after `/health/live` responds; `depen
54
86
pnpm stack:up # docker compose up -d --build
55
87
pnpm stack:down # stop the stack (keeps data volumes)
56
88
pnpm stack:reset # stop + delete data volumes (project-scoped reset)
57
-
pnpm stack:logs # follow API + worker logs
89
+
pnpm stack:logs # follow API + worker + scheduler logs
docker compose logs -f scheduler # scheduled job runner only
68
101
```
69
102
70
-
Both services have `stop_grace_period: 30s`, matching the app's graceful-shutdown handler (`SHUTDOWN_TIMEOUT_MS`): `docker compose down` sends `SIGTERM`, the server drains HTTP connections and closes the Prisma pool before exiting.
103
+
`api` and `worker`have `stop_grace_period: 30s` and `scheduler` has `40s`, matching each process's graceful-shutdown handler (`SHUTDOWN_TIMEOUT_MS` / `SCHEDULER_SHUTDOWN_TIMEOUT_MS`): `docker compose down` sends `SIGTERM`, the server drains HTTP connections and closes the Prisma pool before exiting, and the scheduler stops scheduling, waits for in-flight ticks, and releases their queue leases so no queue is left parked.
0 commit comments