@ddd-ts-kit/scheduling 0.0.0-stage → 0.1.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 +424 -2
- package/dist/errors/error-code-of.d.ts +4 -0
- package/dist/errors/error-code-of.d.ts.map +1 -0
- package/dist/errors/error-code-of.js +11 -0
- package/dist/errors/error-code-of.js.map +1 -0
- package/dist/errors/job-aborted-error.d.ts +6 -0
- package/dist/errors/job-aborted-error.d.ts.map +1 -0
- package/dist/errors/job-aborted-error.js +9 -0
- package/dist/errors/job-aborted-error.js.map +1 -0
- package/dist/errors/job-lock-unavailable-error.d.ts +6 -0
- package/dist/errors/job-lock-unavailable-error.d.ts.map +1 -0
- package/dist/errors/job-lock-unavailable-error.js +9 -0
- package/dist/errors/job-lock-unavailable-error.js.map +1 -0
- package/dist/errors/job-timeout-error.d.ts +6 -0
- package/dist/errors/job-timeout-error.d.ts.map +1 -0
- package/dist/errors/job-timeout-error.js +9 -0
- package/dist/errors/job-timeout-error.js.map +1 -0
- package/dist/in-process-job-scheduler.d.ts +66 -0
- package/dist/in-process-job-scheduler.d.ts.map +1 -0
- package/dist/in-process-job-scheduler.js +189 -0
- package/dist/in-process-job-scheduler.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/job-checkpoints.d.ts +11 -0
- package/dist/job-checkpoints.d.ts.map +1 -0
- package/dist/job-checkpoints.js +9 -0
- package/dist/job-checkpoints.js.map +1 -0
- package/dist/job-recorder.d.ts +28 -0
- package/dist/job-recorder.d.ts.map +1 -0
- package/dist/job-recorder.js +114 -0
- package/dist/job-recorder.js.map +1 -0
- package/dist/job-run-observer.d.ts +9 -0
- package/dist/job-run-observer.d.ts.map +1 -0
- package/dist/job-run-observer.js +2 -0
- package/dist/job-run-observer.js.map +1 -0
- package/dist/job-runner.d.ts +29 -0
- package/dist/job-runner.d.ts.map +1 -0
- package/dist/job-runner.js +96 -0
- package/dist/job-runner.js.map +1 -0
- package/dist/job-timers.d.ts +7 -0
- package/dist/job-timers.d.ts.map +1 -0
- package/dist/job-timers.js +5 -0
- package/dist/job-timers.js.map +1 -0
- package/dist/metric-names.d.ts +3 -0
- package/dist/metric-names.d.ts.map +1 -0
- package/dist/metric-names.js +3 -0
- package/dist/metric-names.js.map +1 -0
- package/dist/occurrence.d.ts +6 -0
- package/dist/occurrence.d.ts.map +1 -0
- package/dist/occurrence.js +2 -0
- package/dist/occurrence.js.map +1 -0
- package/dist/occurrences/instants-of-wall-time.d.ts +7 -0
- package/dist/occurrences/instants-of-wall-time.d.ts.map +1 -0
- package/dist/occurrences/instants-of-wall-time.js +15 -0
- package/dist/occurrences/instants-of-wall-time.js.map +1 -0
- package/dist/occurrences/latest-occurrence.d.ts +10 -0
- package/dist/occurrences/latest-occurrence.d.ts.map +1 -0
- package/dist/occurrences/latest-occurrence.js +22 -0
- package/dist/occurrences/latest-occurrence.js.map +1 -0
- package/dist/occurrences/local-day-of.d.ts +4 -0
- package/dist/occurrences/local-day-of.d.ts.map +1 -0
- package/dist/occurrences/local-day-of.js +7 -0
- package/dist/occurrences/local-day-of.js.map +1 -0
- package/dist/occurrences/next-occurrence.d.ts +4 -0
- package/dist/occurrences/next-occurrence.d.ts.map +1 -0
- package/dist/occurrences/next-occurrence.js +20 -0
- package/dist/occurrences/next-occurrence.js.map +1 -0
- package/dist/occurrences/occurrences-of-day.d.ts +4 -0
- package/dist/occurrences/occurrences-of-day.d.ts.map +1 -0
- package/dist/occurrences/occurrences-of-day.js +35 -0
- package/dist/occurrences/occurrences-of-day.js.map +1 -0
- package/dist/occurrences/offset-at.d.ts +3 -0
- package/dist/occurrences/offset-at.d.ts.map +1 -0
- package/dist/occurrences/offset-at.js +32 -0
- package/dist/occurrences/offset-at.js.map +1 -0
- package/dist/registered-job.d.ts +21 -0
- package/dist/registered-job.d.ts.map +1 -0
- package/dist/registered-job.js +27 -0
- package/dist/registered-job.js.map +1 -0
- package/dist/report-failed-runs.d.ts +11 -0
- package/dist/report-failed-runs.d.ts.map +1 -0
- package/dist/report-failed-runs.js +23 -0
- package/dist/report-failed-runs.js.map +1 -0
- package/dist/started-run.d.ts +7 -0
- package/dist/started-run.d.ts.map +1 -0
- package/dist/started-run.js +2 -0
- package/dist/started-run.js.map +1 -0
- package/dist/system-job-timers.d.ts +3 -0
- package/dist/system-job-timers.d.ts.map +1 -0
- package/dist/system-job-timers.js +9 -0
- package/dist/system-job-timers.js.map +1 -0
- package/dist/testing/manual-job-scheduler.d.ts +24 -0
- package/dist/testing/manual-job-scheduler.d.ts.map +1 -0
- package/dist/testing/manual-job-scheduler.js +42 -0
- package/dist/testing/manual-job-scheduler.js.map +1 -0
- package/dist/testing/manual-job-timers.d.ts +23 -0
- package/dist/testing/manual-job-timers.d.ts.map +1 -0
- package/dist/testing/manual-job-timers.js +54 -0
- package/dist/testing/manual-job-timers.js.map +1 -0
- package/dist/testing.d.ts +3 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +3 -0
- package/dist/testing.js.map +1 -0
- package/package.json +36 -4
package/README.md
CHANGED
|
@@ -1,3 +1,425 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ddd-ts-kit/scheduling
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Scheduled jobs for a project built with the kit. A job is a class of the application layer that
|
|
4
|
+
says when it runs and triggers use cases: it decides nothing, the business rules stay in the
|
|
5
|
+
domain. The scheduler opens an execution scope around each run, makes sure one instance of the
|
|
6
|
+
application runs a given occurrence, and records what became of it.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
npm i @ddd-ts-kit/scheduling
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
| Entry | Holds | Imports |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| `@ddd-ts-kit/scheduling` | `InProcessJobScheduler`, `nextOccurrence`, `latestOccurrence`, `systemJobTimers`, the `JobTimers` and `JobCheckpoints` ports, `JobTimeoutError`, `JobAbortedError`, `JobLockUnavailableError`, the metric names | the kernel |
|
|
15
|
+
| `@ddd-ts-kit/scheduling/testing` | `ManualJobScheduler`, `ManualJobTimers` | the kernel |
|
|
16
|
+
|
|
17
|
+
The declaration (`@Scheduled`), the port (`JobScheduler`) and the schedule parser live in
|
|
18
|
+
`@ddd-ts-kit/kernel`, so `application/` imports the kernel and nothing else.
|
|
19
|
+
`@ddd-ts-kit/di` binds `JobScheduler` to an `InProcessJobScheduler` in its kernel module and
|
|
20
|
+
registers the declared jobs: an application built with it needs no line of this README but
|
|
21
|
+
the job itself.
|
|
22
|
+
|
|
23
|
+
`JobScheduler` is not the `Scheduler` of `@ddd-ts-kit/aspects`. That one is a timer for retry
|
|
24
|
+
and timeout (`schedule(work, delayMs)`); this one knows jobs, schedules and instances.
|
|
25
|
+
|
|
26
|
+
## A job
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
// src/contexts/billing/application/scheduling/send-payment-reminders.job.ts
|
|
30
|
+
import { CommandBus, systemActor } from '@ddd-ts-kit/kernel';
|
|
31
|
+
import { Scheduled, type JobExecution, type ScheduledJob } from '@ddd-ts-kit/kernel/declarations';
|
|
32
|
+
import { SendPaymentReminders } from '@/contexts/billing/application/commands/send-payment-reminders/send-payment-reminders.command.js';
|
|
33
|
+
|
|
34
|
+
@Scheduled('billing.send-payment-reminders', { cron: '0 7 * * 1-5', timeZone: 'Europe/Paris' }, [CommandBus])
|
|
35
|
+
export class SendPaymentRemindersJob implements ScheduledJob {
|
|
36
|
+
constructor(private readonly commands: CommandBus) {}
|
|
37
|
+
|
|
38
|
+
async run(execution: JobExecution): Promise<void> {
|
|
39
|
+
await this.commands.execute(
|
|
40
|
+
new SendPaymentReminders(systemActor('billing.send-payment-reminders'), execution.scheduledAt),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- **Where**: `application/scheduling/`, in a file named `*.job.ts`, one job per file.
|
|
47
|
+
- **The name** is `<context>.<job-name>`, in lower case, written as a string literal and unique
|
|
48
|
+
in the application: it is the system actor the job runs as, the key of its lock, its label in
|
|
49
|
+
the metrics and the key of its settings. It survives the renaming of the class.
|
|
50
|
+
- **The dependency list** names the ports of the constructor parameters in order, and the
|
|
51
|
+
compiler checks it, as for every declaration of the kit. A port is an abstract class, and the
|
|
52
|
+
class is what is listed: `[CommandBus]`. A job receives buses and ports. It does not receive
|
|
53
|
+
a repository: what it would do with one is a use case.
|
|
54
|
+
- **`run(execution)`** receives:
|
|
55
|
+
|
|
56
|
+
| Field | |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `scheduledAt` | the instant the run was due. Pass it to the command: "the reminders of that morning" is the same work whenever it really runs |
|
|
59
|
+
| `startedAt` | the instant it started: later by the jitter, a queue or a catch-up |
|
|
60
|
+
| `attempt` | 1 with this scheduler, which never retries |
|
|
61
|
+
| `signal` | aborted when the run exceeds its timeout or the scheduler stops; a long job checks it between two steps |
|
|
62
|
+
|
|
63
|
+
- **The command is sent as the system actor of the job**, as an integration event handler does
|
|
64
|
+
for its consumer. With `@ddd-ts-kit/auth`, grant that actor the permission of the command,
|
|
65
|
+
or declare the command `@Public()` when nothing else can send it.
|
|
66
|
+
- **Checked when the file loads**: a name that is not `<context>.<job-name>`, a cron that
|
|
67
|
+
cannot be read or never fires, a zero interval, an unknown time zone and an option out of
|
|
68
|
+
its values throw a `DeclarationError` that names the job.
|
|
69
|
+
|
|
70
|
+
### The execution scope
|
|
71
|
+
|
|
72
|
+
The scheduler opens an `ExecutionScope` around each run, so the job writes none of it:
|
|
73
|
+
|
|
74
|
+
| Field of the context | Value |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `origin` | `schedule` |
|
|
77
|
+
| `actor` | `systemActor('<job name>')` |
|
|
78
|
+
| `correlationId` | a fresh one for each run, from the `IdGenerator` |
|
|
79
|
+
| `scheduledAt` | the instant the run was due |
|
|
80
|
+
| `deadline` | the start plus `timeoutMs`, when the job has one |
|
|
81
|
+
|
|
82
|
+
Audit columns, the audit trail, log fields, tracing and the correlation stamped on published
|
|
83
|
+
events therefore work as for a request. A job has no tenant: persistence helpers that need
|
|
84
|
+
one (`tenantScopeOf`) are for the use case, which receives the tenant in its command, one
|
|
85
|
+
command per tenant when the work is per tenant.
|
|
86
|
+
|
|
87
|
+
## Schedules
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
{ every: '15m' } // or a number of milliseconds
|
|
91
|
+
{ cron: '0 7 * * 1-5', timeZone: 'Europe/Paris' } // timeZone defaults to UTC
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Intervals** are a whole number of milliseconds or a number and a unit: `'500ms'`, `'30s'`,
|
|
95
|
+
`'15m'`, `'2h'`, `'1d'`. They are **aligned on the Unix epoch**, not on the start of the
|
|
96
|
+
process: `'15m'` falls at :00, :15, :30 and :45, `'1d'` at midnight UTC. This is what makes every
|
|
97
|
+
instance name the same instants, and a restart keep them. An interval that does not divide a
|
|
98
|
+
day (`'7h'`) drifts against the wall clock, and none follows a time zone: for "every morning",
|
|
99
|
+
write a cron.
|
|
100
|
+
|
|
101
|
+
**Cron** has the five standard fields, no seconds:
|
|
102
|
+
|
|
103
|
+
| Field | Values | Names |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| minute | 0-59 | |
|
|
106
|
+
| hour | 0-23 | |
|
|
107
|
+
| day of month | 1-31 | |
|
|
108
|
+
| month | 1-12 | `jan` to `dec` |
|
|
109
|
+
| day of week | 0-7, 0 and 7 are Sunday | `sun` to `sat` |
|
|
110
|
+
|
|
111
|
+
Each field takes `*`, a value, a range `a-b`, a step on either (`*/15`, `8-18/2`) and lists of
|
|
112
|
+
those (`0,30`, `mon-fri`). Names ignore case. When the day of month and the day of week are
|
|
113
|
+
both restricted (neither starts with `*`), a day that fits either one fits, as in every cron:
|
|
114
|
+
`0 0 13 * fri` is the 13th and every Friday. Not supported: `@daily` and the like, `?`, `L`,
|
|
115
|
+
`W`, `#`, a step on a single value (`5/15`), a range that wraps (`fri-mon`).
|
|
116
|
+
|
|
117
|
+
**Time zones** are IANA names, evaluated with `Intl.DateTimeFormat`, without a dependency: the
|
|
118
|
+
rules are those of the runtime, updated with it. The fields are read on the clocks of the zone,
|
|
119
|
+
day fields included.
|
|
120
|
+
|
|
121
|
+
| When clocks are | A job pinned to an hour (`30 2 * * *`) | A job of every hour (`0 * * * *`, `*/20 * * * *`) |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| set forward (02:00 to 03:00) | a local time that does not exist is **skipped**: no run that day, nothing is moved to 03:00 | keeps its rhythm: 01:00, then 03:00, one hour later |
|
|
124
|
+
| set back (03:00 to 02:00) | a local time that occurs twice **runs once**, the first time | keeps its rhythm: the repeated hour is run through twice |
|
|
125
|
+
|
|
126
|
+
A job is "of every hour" when its hour field starts with `*`: it follows elapsed time. Any
|
|
127
|
+
other job follows the clocks. Do not schedule in the changed hour what must run every day:
|
|
128
|
+
`30 2 * * *` in `Europe/Paris` has no run on the last Sunday of March.
|
|
129
|
+
|
|
130
|
+
`nextOccurrence(parseJobSchedule(schedule), after)` and `latestOccurrence(parsed, { after,
|
|
131
|
+
notAfter })` answer the instants, for a screen that shows when a job runs next.
|
|
132
|
+
|
|
133
|
+
## Options
|
|
134
|
+
|
|
135
|
+
Written next to the schedule: `{ every: '15m', overlap: 'queue', timeoutMs: 60_000 }`.
|
|
136
|
+
|
|
137
|
+
| Option | Default | |
|
|
138
|
+
|---|---|---|
|
|
139
|
+
| `overlap` | `'skip'` | an occurrence that comes while the previous run is in progress is not started (`skipped-overlap`). `'queue'` starts it when the run ends; one occurrence waits at most, the next ones are skipped |
|
|
140
|
+
| `missed` | `'skip'` | occurrences the scheduler did not reach in time are dropped (`missed`) and the job waits for its next one. `'run-once'` runs the latest of them, once, however many passed |
|
|
141
|
+
| `timeoutMs` | none | the signal of a longer run is aborted with a `JobTimeoutError` and the run is recorded `failed`, `JOB_TIMEOUT` |
|
|
142
|
+
| `jitterMs` | `0` | a random delay up to this before each run, so that jobs do not all start on the minute |
|
|
143
|
+
| `enabled` | `true` | `false` never starts the job by time; `runNow` still runs it |
|
|
144
|
+
| `singleInstance` | `true` | one instance runs an occurrence: see below |
|
|
145
|
+
|
|
146
|
+
**Missed** means reached later than `lateAfterMs` (60 s by default, an option of the
|
|
147
|
+
scheduler) after the instant, jitter aside: the process was down, asleep, or its event loop
|
|
148
|
+
blocked. A timer a few seconds late still runs its occurrence. What passed **while the process
|
|
149
|
+
was down** is only known with `checkpoints`:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
// the port, exported by this package
|
|
153
|
+
export abstract class JobCheckpoints {
|
|
154
|
+
abstract read(job: string): Promise<Date | undefined>;
|
|
155
|
+
|
|
156
|
+
abstract write(job: string, handled: Date): Promise<void>;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/platform/scheduling/knex-job-checkpoints.ts, your adapter
|
|
160
|
+
export class KnexJobCheckpoints implements JobCheckpoints {
|
|
161
|
+
constructor(private readonly database: Database) {}
|
|
162
|
+
|
|
163
|
+
async read(job: string): Promise<Date | undefined> { /* one row per job */ }
|
|
164
|
+
|
|
165
|
+
async write(job: string, handled: Date): Promise<void> { /* upsert */ }
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The scheduler writes there the last occurrence it handled for each job and reads it when it
|
|
170
|
+
starts. Without it a restart forgets: every job starts from the instant of the start, and
|
|
171
|
+
`run-once` only covers what was missed while the process was alive. The store is yours, a
|
|
172
|
+
table or a key shared by the instances. It is given to the scheduler where the scheduler is
|
|
173
|
+
built: see "Replacing the scheduler".
|
|
174
|
+
|
|
175
|
+
**A timeout does not kill.** A run is a promise, and nothing can stop a promise: the scheduler
|
|
176
|
+
aborts the signal, stops waiting and frees the job for its next occurrence. A job that ignores
|
|
177
|
+
its signal goes on behind, and then overlaps the next run. Pass the signal on, or check it.
|
|
178
|
+
|
|
179
|
+
## Switching a job off, per environment
|
|
180
|
+
|
|
181
|
+
A job is switched off, rescheduled or tuned by configuration, never by a code change. With
|
|
182
|
+
`@ddd-ts-kit/di`:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
createApplication({
|
|
186
|
+
modules,
|
|
187
|
+
jobs: {
|
|
188
|
+
settings: {
|
|
189
|
+
'billing.send-payment-reminders': { enabled: config.jobs.reminders.enabled },
|
|
190
|
+
'billing.export-ledger': { schedule: { cron: config.jobs.ledgerCron, timeZone: 'Europe/Paris' } },
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Settings for a job no module declares are refused (`UNKNOWN_JOB`), so a renamed job cannot stay
|
|
197
|
+
on by a stale key; settings that cannot be read are refused too (`INVALID_JOB_SETTINGS`).
|
|
198
|
+
|
|
199
|
+
## Several instances
|
|
200
|
+
|
|
201
|
+
Every instance of a horizontally scaled application runs a scheduler and computes the same
|
|
202
|
+
instants. Before running an occurrence of a `singleInstance` job, the scheduler takes the
|
|
203
|
+
kernel `Lock` under the key `job:<name>:<scheduled instant>`. The instance that gets it runs;
|
|
204
|
+
the others record `skipped-locked`.
|
|
205
|
+
|
|
206
|
+
- **The lock is not released when the run ends.** An instance whose timer fires a little later
|
|
207
|
+
would take it and run the occurrence again. It is kept until this instance handles the next
|
|
208
|
+
occurrence of the job, or until its time to live has passed: `timeoutMs` (15 minutes for a
|
|
209
|
+
job without timeout, `lockTtlMs`) plus the jitter and `lateAfterMs`.
|
|
210
|
+
- **With `InMemoryLock`**, the default of `kernelModule()`, the lock excludes inside one
|
|
211
|
+
process only: two instances both run every job. It is correct for a single instance and for
|
|
212
|
+
tests, and nothing more.
|
|
213
|
+
- **With a lock between processes** (`RedisLock` of `@ddd-ts-kit/redis`, bound to `Lock`), the
|
|
214
|
+
guarantee is real, and it is this one: **at most one run per occurrence, best effort**.
|
|
215
|
+
|
|
216
|
+
What is not guaranteed, so that you design for it:
|
|
217
|
+
|
|
218
|
+
- An occurrence may run **twice**: the lock expires while its holder is paused or the run
|
|
219
|
+
outlives the time to live and a late instance arrives; the lock store fails over and forgets
|
|
220
|
+
a key; an instance restarts with `missed: 'run-once'` after the lock of the occurrence it
|
|
221
|
+
catches up has expired; clocks disagree by more than the time to live.
|
|
222
|
+
- An occurrence may run **zero times**: the instance that took the lock crashes before or
|
|
223
|
+
during the run (nobody retries, the lock stays), every instance is down at the instant
|
|
224
|
+
(`missed`), or the lock store cannot be reached, in which case the occurrence is not run and
|
|
225
|
+
is recorded `failed`, `JOB_LOCK_UNAVAILABLE`, rather than run everywhere.
|
|
226
|
+
- `runNow` takes no lock: it is one deliberate run.
|
|
227
|
+
|
|
228
|
+
**Therefore a job is idempotent, or the command it sends is.** A run that happens twice must
|
|
229
|
+
leave the same state as once: derive what to do from the state ("invoices due and not yet
|
|
230
|
+
reminded"), not from the fact of being called, or key the work on `scheduledAt` behind a unique
|
|
231
|
+
constraint. The kernel says it of `Lock` itself: an efficiency tool, never the guard of an
|
|
232
|
+
invariant. Work that must happen exactly once and survive a crash is not a scheduled job but
|
|
233
|
+
a row in a table, processed by a job that is safe to repeat.
|
|
234
|
+
|
|
235
|
+
Instances should keep their clocks within a few seconds of each other (NTP): the instants are
|
|
236
|
+
computed from the `Clock`, and the lock key is the instant.
|
|
237
|
+
|
|
238
|
+
## A dedicated worker process
|
|
239
|
+
|
|
240
|
+
By default every process that starts the application runs the jobs. To run them in one
|
|
241
|
+
deployment and serve requests in another, tell the others not to:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
// src/main.ts, the process that serves requests
|
|
245
|
+
const app = createApplication({ modules, jobs: { start: false } });
|
|
246
|
+
|
|
247
|
+
// src/apps/worker/main.ts, the process that runs the jobs: the same modules, no HTTP server
|
|
248
|
+
const app = createApplication({ modules, jobs: { settings: jobSettings(config) } });
|
|
249
|
+
await app.start();
|
|
250
|
+
process.on('SIGTERM', () => void app.stop());
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
With `start: false` the jobs are still registered and described, and `runNow` still runs one,
|
|
254
|
+
which gives an operations endpoint for free. One composition root reading a flag
|
|
255
|
+
(`jobs: { start: config.runJobs }`) is the same thing with one entry point. Keep
|
|
256
|
+
`singleInstance` on even with one worker: a rolling deployment runs two for a while.
|
|
257
|
+
|
|
258
|
+
A context extracted as its own service takes its jobs with it: they are declarations of the
|
|
259
|
+
context, registered by the application that lists its module. Nothing is global.
|
|
260
|
+
|
|
261
|
+
In a project generated by the CLI: `ddd-ts-kit generate job` writes the job and its line in
|
|
262
|
+
`src/platform/job-settings.ts` (switched off with `JOB_<CONTEXT>_<JOB>_ENABLED=false`),
|
|
263
|
+
`main.ts` passes `jobs: { start: config.runJobs, settings: jobSettings(process.env) }`
|
|
264
|
+
(`RUN_JOBS`), and `ddd-ts-kit generate worker` writes `src/apps/worker/main.ts` and turns the
|
|
265
|
+
default of `RUN_JOBS` off for the API.
|
|
266
|
+
|
|
267
|
+
## Lifecycle
|
|
268
|
+
|
|
269
|
+
`application.start()` starts the scheduler last, once the consumers are subscribed, and
|
|
270
|
+
`application.stop()` stops it first. `stop()` starts no new run, waits for the runs in
|
|
271
|
+
progress up to `graceMs` (30 s by default), then aborts their signals with a `JobAbortedError`
|
|
272
|
+
and returns; a run that waited in a queue is dropped and recorded `missed`. Set the grace
|
|
273
|
+
below the termination grace of the platform.
|
|
274
|
+
|
|
275
|
+
### Replacing the scheduler
|
|
276
|
+
|
|
277
|
+
`kernelModule()` builds the scheduler on the kernel ports it binds: `Clock`, `Lock`,
|
|
278
|
+
`ExecutionScope`, `IdGenerator`, `Logger` and `Metrics`, with `reportFailedRuns` on the bound
|
|
279
|
+
`ErrorReporter` as `onRun`. To build it otherwise (with checkpoints, another grace period,
|
|
280
|
+
another observer), give the kernel module yours:
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
import { Clock, ExecutionScope, IdGenerator, Lock, Logger, Metrics } from '@ddd-ts-kit/kernel';
|
|
284
|
+
import { createApplication, kernelModule } from '@ddd-ts-kit/di';
|
|
285
|
+
import { InProcessJobScheduler, JobCheckpoints } from '@ddd-ts-kit/scheduling';
|
|
286
|
+
|
|
287
|
+
const kernel = kernelModule({
|
|
288
|
+
jobScheduler: (resolve) =>
|
|
289
|
+
new InProcessJobScheduler({
|
|
290
|
+
clock: resolve(Clock),
|
|
291
|
+
// asked at each occurrence: building the scheduler must not open a connection
|
|
292
|
+
lock: { acquire: (key, options) => resolve(Lock).acquire(key, options) },
|
|
293
|
+
scope: resolve(ExecutionScope),
|
|
294
|
+
ids: resolve(IdGenerator),
|
|
295
|
+
logger: resolve(Logger),
|
|
296
|
+
metrics: resolve(Metrics),
|
|
297
|
+
checkpoints: resolve(JobCheckpoints),
|
|
298
|
+
graceMs: 10_000,
|
|
299
|
+
}),
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const app = createApplication({ modules: [kernel, platformModule, billingModule] });
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
with, in the module of the platform, `builder.bind(JobCheckpoints).toClass(KnexJobCheckpoints, [Database])`.
|
|
306
|
+
A later module replaces the scheduler the same way, with
|
|
307
|
+
`builder.rebind(JobScheduler).toFactory((resolve) => new InProcessJobScheduler({ ... }))`, and
|
|
308
|
+
so does another implementation of the `JobScheduler` port (on a database, on a queue).
|
|
309
|
+
|
|
310
|
+
Without `@ddd-ts-kit/di`:
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
const scheduler = new InProcessJobScheduler({ clock, lock, scope, ids, logger, metrics });
|
|
314
|
+
scheduler.register({ name: 'billing.send-payment-reminders', schedule: { every: '15m' }, run: (execution) => job.run(execution) });
|
|
315
|
+
await scheduler.start();
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
| Option | Default | |
|
|
319
|
+
|---|---|---|
|
|
320
|
+
| `clock`, `lock`, `scope`, `ids` | required | every instant comes from the `Clock`; the system clock is never read |
|
|
321
|
+
| `logger`, `metrics` | write and measure nothing | |
|
|
322
|
+
| `timers` | `systemJobTimers` | how it sleeps; `ManualJobTimers` in a test |
|
|
323
|
+
| `checkpoints` | none | see "Options" |
|
|
324
|
+
| `lateAfterMs` | `60000` | later than this, an occurrence is missed |
|
|
325
|
+
| `graceMs` | `30000` | how long `stop()` waits |
|
|
326
|
+
| `lockTtlMs` | `900000` | time to live of the lock of a job without `timeoutMs` |
|
|
327
|
+
| `random` | `Math.random` | draws the jitter |
|
|
328
|
+
| `onRun` | none | `(run, error?, detail?) => void` for every occurrence handled, with what the job threw and the correlation id of the run: `reportFailedRuns(errorReporter)` is the observer that reports to the kernel `ErrorReporter` every run that failed with anything but a `DomainError` |
|
|
329
|
+
|
|
330
|
+
It sleeps at most one hour at a time and computes again when it wakes, so a clock that is
|
|
331
|
+
corrected, or a suspended machine, is noticed within the hour.
|
|
332
|
+
|
|
333
|
+
## Observability
|
|
334
|
+
|
|
335
|
+
`scheduler.describe()` answers, for each job, `name`, `schedule` (`every 15m`,
|
|
336
|
+
`cron 0 7 * * 1-5 (Europe/Paris)`), `enabled`, `running`, `nextRunAt` and `lastRun`, as data.
|
|
337
|
+
`application.describe().jobs` lists the declared jobs with the settings they run with.
|
|
338
|
+
|
|
339
|
+
Every occurrence handled has one outcome, recorded three times:
|
|
340
|
+
|
|
341
|
+
| Outcome | Means | Log level |
|
|
342
|
+
|---|---|---|
|
|
343
|
+
| `succeeded` | the job ran | info |
|
|
344
|
+
| `failed` | the job threw, timed out, was aborted, or its lock could not be asked | error |
|
|
345
|
+
| `skipped-overlap` | the previous run was still in progress | warn |
|
|
346
|
+
| `skipped-locked` | another instance runs this occurrence | debug |
|
|
347
|
+
| `missed` | the occurrence passed and was not run | warn |
|
|
348
|
+
|
|
349
|
+
- **Log**: one entry, `Scheduled job <outcome>`, with `job`, `context`, `outcome`,
|
|
350
|
+
`scheduledAt`, and for a run `durationMs`, `correlationId`, `errorCode`, `errorType`.
|
|
351
|
+
- **Metrics**: the counter `scheduled_job_runs_total` and, for a run, the histogram
|
|
352
|
+
`scheduled_job_duration_ms`, labelled `job`, `context`, `status` (the outcome) and
|
|
353
|
+
`error_code`. An alert on "no `succeeded` for this job since N intervals" catches every way a
|
|
354
|
+
job stops running, which an alert on failures does not.
|
|
355
|
+
- **`onRun`**: the same record, plus the error and the correlation id of the run.
|
|
356
|
+
|
|
357
|
+
`missed` is recorded once for a gap, with the first instant that was missed, not once per
|
|
358
|
+
instant. The `errorCode` is the `code` of what was thrown (a `DomainError`, `JOB_TIMEOUT`,
|
|
359
|
+
`JOB_ABORTED`, `JOB_LOCK_UNAVAILABLE`) or `UNEXPECTED_ERROR`. **The message of an error is
|
|
360
|
+
never recorded**: it quotes data. A job that fails stops neither the scheduler nor the other
|
|
361
|
+
jobs, and its next occurrence runs as usual: there is no retry, the next occurrence is the
|
|
362
|
+
retry.
|
|
363
|
+
|
|
364
|
+
## Testing
|
|
365
|
+
|
|
366
|
+
A spec of a job is a spec of the use case it triggers: run the job, assert the fakes. Time is
|
|
367
|
+
not involved, and the test application of `@ddd-ts-kit/di` never starts jobs by time.
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
const app = createTestApplication({ modules: [contextModule('billing', billing)], fakes: [fake(Mailer, mailer)] });
|
|
371
|
+
|
|
372
|
+
await app.jobs.runNow('billing.send-payment-reminders');
|
|
373
|
+
|
|
374
|
+
expect(mailer.sent).toHaveLength(2);
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
`app.jobs.runNow(name)` runs the job as the scheduler would (scope of origin `schedule`,
|
|
378
|
+
system actor, lock aside) and throws what the job threw. For a spec about time itself,
|
|
379
|
+
`app.jobs.scheduler` is a `ManualJobScheduler` on the clock of the application:
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
import { FixedClock, FixedExecutionScope, InMemoryLock, SequentialIdGenerator } from '@ddd-ts-kit/kernel';
|
|
383
|
+
import { ManualJobScheduler } from '@ddd-ts-kit/scheduling/testing';
|
|
384
|
+
|
|
385
|
+
const clock = new FixedClock(new Date('2026-03-02T09:58:30Z'));
|
|
386
|
+
const scheduler = new ManualJobScheduler({ clock, lock: new InMemoryLock(clock), scope: new FixedExecutionScope(), ids: new SequentialIdGenerator() });
|
|
387
|
+
scheduler.register({ name: 'billing.send-payment-reminders', schedule: { every: '15m' }, run });
|
|
388
|
+
await scheduler.start();
|
|
389
|
+
|
|
390
|
+
await scheduler.advance(32 * 60_000);
|
|
391
|
+
|
|
392
|
+
expect(scheduler.runs.map((run) => run.scheduledAt.toISOString())).toEqual(['2026-03-02T10:00:00.000Z', '2026-03-02T10:15:00.000Z', '2026-03-02T10:30:00.000Z']);
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
- `advance(ms)` moves the clock and fires each timer at its instant, in order. When it
|
|
396
|
+
resolves, every job that waits on promises alone has ended; a job the test keeps blocked is
|
|
397
|
+
still running, which is how overlap, timeout and stop are tested.
|
|
398
|
+
- `runs` lists every occurrence handled with its outcome, `failures` what the failed runs
|
|
399
|
+
threw, `runsOf(job)` the runs of one job. There is no jitter.
|
|
400
|
+
- `ManualJobTimers` alone drives an `InProcessJobScheduler` built by hand, for a spec that sets
|
|
401
|
+
`random` or `checkpoints`.
|
|
402
|
+
- Two schedulers sharing a clock and a lock are two instances: this is how the single-instance
|
|
403
|
+
behaviour is specified in this package.
|
|
404
|
+
|
|
405
|
+
Another scheduler (on a database, on a queue) implements the `JobScheduler` port of the kernel
|
|
406
|
+
and passes `jobSchedulerContract` of `@ddd-ts-kit/testing/contracts`.
|
|
407
|
+
|
|
408
|
+
## Limits
|
|
409
|
+
|
|
410
|
+
- **No persistence of its own.** Runs, outcomes and `lastRun` live in the memory of the
|
|
411
|
+
instance; `describe()` is the view of one instance, and only `checkpoints` crosses a restart.
|
|
412
|
+
- **No retry and no backoff.** A failed occurrence is over. Put retry where the failure is: an
|
|
413
|
+
aspect on the adapter (`retry` of `@ddd-ts-kit/aspects`), or a command that is safe to send
|
|
414
|
+
again at the next occurrence.
|
|
415
|
+
- **Minute resolution for cron**, millisecond for intervals; an interval under a second is
|
|
416
|
+
possible and rarely wise, as each occurrence costs a lock.
|
|
417
|
+
- **At most once per occurrence is best effort**, as said above. Exactly-once does not exist
|
|
418
|
+
here.
|
|
419
|
+
- **Not a queue.** A job does a bounded amount of work on a schedule. Work that arrives, must
|
|
420
|
+
not be lost and must be retried belongs to the outbox and to integration events.
|
|
421
|
+
- **One process, one event loop.** A job that blocks the loop delays every other job; past
|
|
422
|
+
`lateAfterMs` their occurrences are missed.
|
|
423
|
+
- **Time zone rules are those of the runtime.** A zone whose rules changed needs an updated
|
|
424
|
+
Node. Two changes of offset within two days of each other, which no zone has, are not
|
|
425
|
+
handled.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-code-of.d.ts","sourceRoot":"","sources":["../../src/errors/error-code-of.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB,qBAAqB,CAAC;AAInD,6FAA6F;AAC7F,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAQlD"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const UNEXPECTED_ERROR = 'UNEXPECTED_ERROR';
|
|
2
|
+
const SHORT_CODE = /^[A-Za-z0-9_.:-]{1,64}$/;
|
|
3
|
+
/** The `code` of a `DomainError` or of a fault that carries a short one. Never a message. */
|
|
4
|
+
export function errorCodeOf(error) {
|
|
5
|
+
if (typeof error !== 'object' || error === null || !('code' in error)) {
|
|
6
|
+
return UNEXPECTED_ERROR;
|
|
7
|
+
}
|
|
8
|
+
const { code } = error;
|
|
9
|
+
return typeof code === 'string' && SHORT_CODE.test(code) ? code : UNEXPECTED_ERROR;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=error-code-of.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-code-of.js","sourceRoot":"","sources":["../../src/errors/error-code-of.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,gBAAgB,GAAG,kBAAkB,CAAC;AAEnD,MAAM,UAAU,GAAG,yBAAyB,CAAC;AAE7C,6FAA6F;AAC7F,MAAM,UAAU,WAAW,CAAC,KAAc;IACxC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,CAAC,MAAM,IAAI,KAAK,CAAC,EAAE,CAAC;QACtE,OAAO,gBAAgB,CAAC;IAC1B,CAAC;IAED,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC;IAEvB,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,gBAAgB,CAAC;AACrF,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** The scheduler stopped and the run outlived the grace period. It is the reason of the aborted signal. */
|
|
2
|
+
export declare class JobAbortedError extends Error {
|
|
3
|
+
readonly code = "JOB_ABORTED";
|
|
4
|
+
constructor(job: string);
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=job-aborted-error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"job-aborted-error.d.ts","sourceRoot":"","sources":["../../src/errors/job-aborted-error.ts"],"names":[],"mappings":"AAAA,2GAA2G;AAC3G,qBAAa,eAAgB,SAAQ,KAAK;IACxC,QAAQ,CAAC,IAAI,iBAAiB;IAE9B,YAAY,GAAG,EAAE,MAAM,EAGtB;CACF"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** The scheduler stopped and the run outlived the grace period. It is the reason of the aborted signal. */
|
|
2
|
+
export class JobAbortedError extends Error {
|
|
3
|
+
code = 'JOB_ABORTED';
|
|
4
|
+
constructor(job) {
|
|
5
|
+
super(`Job "${job}" was still running when the scheduler stopped.`);
|
|
6
|
+
this.name = 'JobAbortedError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=job-aborted-error.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"job-aborted-error.js","sourceRoot":"","sources":["../../src/errors/job-aborted-error.ts"],"names":[],"mappings":"AAAA,2GAA2G;AAC3G,MAAM,OAAO,eAAgB,SAAQ,KAAK;IAC/B,IAAI,GAAG,aAAa,CAAC;IAE9B,YAAY,GAAW;QACrB,KAAK,CAAC,QAAQ,GAAG,iDAAiD,CAAC,CAAC;QACpE,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAChC,CAAC;CACF"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** The lock could not be asked, so nothing says that no other instance runs the occurrence. */
|
|
2
|
+
export declare class JobLockUnavailableError extends Error {
|
|
3
|
+
readonly code = "JOB_LOCK_UNAVAILABLE";
|
|
4
|
+
constructor(job: string, options: ErrorOptions);
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=job-lock-unavailable-error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"job-lock-unavailable-error.d.ts","sourceRoot":"","sources":["../../src/errors/job-lock-unavailable-error.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAC/F,qBAAa,uBAAwB,SAAQ,KAAK;IAChD,QAAQ,CAAC,IAAI,0BAA0B;IAEvC,YAAY,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAG7C;CACF"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** The lock could not be asked, so nothing says that no other instance runs the occurrence. */
|
|
2
|
+
export class JobLockUnavailableError extends Error {
|
|
3
|
+
code = 'JOB_LOCK_UNAVAILABLE';
|
|
4
|
+
constructor(job, options) {
|
|
5
|
+
super(`The lock of job "${job}" could not be asked: the occurrence is not run.`, options);
|
|
6
|
+
this.name = 'JobLockUnavailableError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=job-lock-unavailable-error.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"job-lock-unavailable-error.js","sourceRoot":"","sources":["../../src/errors/job-lock-unavailable-error.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAC/F,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IACvC,IAAI,GAAG,sBAAsB,CAAC;IAEvC,YAAY,GAAW,EAAE,OAAqB;QAC5C,KAAK,CAAC,oBAAoB,GAAG,kDAAkD,EAAE,OAAO,CAAC,CAAC;QAC1F,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IACxC,CAAC;CACF"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** A run lasted longer than the `timeoutMs` of its job. It is the reason of the aborted signal. */
|
|
2
|
+
export declare class JobTimeoutError extends Error {
|
|
3
|
+
readonly code = "JOB_TIMEOUT";
|
|
4
|
+
constructor(job: string, timeoutMs: number);
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=job-timeout-error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"job-timeout-error.d.ts","sourceRoot":"","sources":["../../src/errors/job-timeout-error.ts"],"names":[],"mappings":"AAAA,mGAAmG;AACnG,qBAAa,eAAgB,SAAQ,KAAK;IACxC,QAAQ,CAAC,IAAI,iBAAiB;IAE9B,YAAY,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAGzC;CACF"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** A run lasted longer than the `timeoutMs` of its job. It is the reason of the aborted signal. */
|
|
2
|
+
export class JobTimeoutError extends Error {
|
|
3
|
+
code = 'JOB_TIMEOUT';
|
|
4
|
+
constructor(job, timeoutMs) {
|
|
5
|
+
super(`Job "${job}" ran longer than ${timeoutMs} ms.`);
|
|
6
|
+
this.name = 'JobTimeoutError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=job-timeout-error.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"job-timeout-error.js","sourceRoot":"","sources":["../../src/errors/job-timeout-error.ts"],"names":[],"mappings":"AAAA,mGAAmG;AACnG,MAAM,OAAO,eAAgB,SAAQ,KAAK;IAC/B,IAAI,GAAG,aAAa,CAAC;IAE9B,YAAY,GAAW,EAAE,SAAiB;QACxC,KAAK,CAAC,QAAQ,GAAG,qBAAqB,SAAS,MAAM,CAAC,CAAC;QACvD,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAChC,CAAC;CACF"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type Clock, type ExecutionScope, type IdGenerator, type JobDefinition, type JobDescription, type JobRun, type JobScheduler, type Lock, type Logger, type Metrics } from '@ddd-ts-kit/kernel';
|
|
2
|
+
import type { JobCheckpoints } from './job-checkpoints.js';
|
|
3
|
+
import type { JobRunObserver } from './job-run-observer.js';
|
|
4
|
+
import type { JobTimers } from './job-timers.js';
|
|
5
|
+
export interface InProcessJobSchedulerOptions {
|
|
6
|
+
/** Every instant comes from it: the system clock is never read. */
|
|
7
|
+
readonly clock: Clock;
|
|
8
|
+
/** Taken for each occurrence of a `singleInstance` job. Between processes only with an adapter that is. */
|
|
9
|
+
readonly lock: Lock;
|
|
10
|
+
/** Each run happens inside it, in a context of origin `schedule`. */
|
|
11
|
+
readonly scope: ExecutionScope;
|
|
12
|
+
/** Gives each run its correlation id. */
|
|
13
|
+
readonly ids: IdGenerator;
|
|
14
|
+
/** Default: writes nothing. */
|
|
15
|
+
readonly logger?: Logger;
|
|
16
|
+
/** Default: measures nothing. */
|
|
17
|
+
readonly metrics?: Metrics;
|
|
18
|
+
/** Default: the timers of the runtime. */
|
|
19
|
+
readonly timers?: JobTimers;
|
|
20
|
+
/** Default: none, and a restart forgets what passed while the process was down. */
|
|
21
|
+
readonly checkpoints?: JobCheckpoints;
|
|
22
|
+
/** Draws the jitter, in [0, 1). Default `Math.random`. */
|
|
23
|
+
readonly random?: () => number;
|
|
24
|
+
/** An occurrence reached later than this, jitter aside, is missed. Default 60 000. */
|
|
25
|
+
readonly lateAfterMs?: number;
|
|
26
|
+
/** How long `stop()` waits for the runs in progress before aborting them. Default 30 000. */
|
|
27
|
+
readonly graceMs?: number;
|
|
28
|
+
/** How long the lock of an occurrence is kept for a job without `timeoutMs`. Default 900 000. */
|
|
29
|
+
readonly lockTtlMs?: number;
|
|
30
|
+
readonly onRun?: JobRunObserver;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The scheduler inside one process. Instants come from the `Clock`, sleeps go through the
|
|
34
|
+
* timers, and nothing is remembered across a restart unless `checkpoints` is given. Several
|
|
35
|
+
* instances of an application each run one: the `Lock` decides which of them runs an occurrence.
|
|
36
|
+
*/
|
|
37
|
+
export declare class InProcessJobScheduler implements JobScheduler {
|
|
38
|
+
private readonly jobs;
|
|
39
|
+
private readonly ticks;
|
|
40
|
+
private readonly runsInProgress;
|
|
41
|
+
private readonly settings;
|
|
42
|
+
private readonly recorder;
|
|
43
|
+
private readonly runner;
|
|
44
|
+
private started;
|
|
45
|
+
private stopping;
|
|
46
|
+
constructor(options: InProcessJobSchedulerOptions);
|
|
47
|
+
register(definition: JobDefinition): void;
|
|
48
|
+
start(): Promise<void>;
|
|
49
|
+
stop(): Promise<void>;
|
|
50
|
+
runNow(name: string): Promise<JobRun>;
|
|
51
|
+
describe(): readonly JobDescription[];
|
|
52
|
+
/** A tick never rejects: what goes wrong while following a job is logged, and the others go on. */
|
|
53
|
+
private follow;
|
|
54
|
+
private begin;
|
|
55
|
+
/** A checkpoint that cannot be read is no reason to leave the job asleep: it starts from now. */
|
|
56
|
+
private remembered;
|
|
57
|
+
private tick;
|
|
58
|
+
/** Of the occurrences that became due, the latest is run when it is on time or owed; the others are missed. */
|
|
59
|
+
private handleDue;
|
|
60
|
+
private sleep;
|
|
61
|
+
private trigger;
|
|
62
|
+
private occupy;
|
|
63
|
+
/** A run that waited behind another is dropped when the scheduler is stopping. */
|
|
64
|
+
private afterPrevious;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=in-process-job-scheduler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"in-process-job-scheduler.d.ts","sourceRoot":"","sources":["../src/in-process-job-scheduler.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,KAAK,EACV,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,MAAM,EACX,KAAK,YAAY,EACjB,KAAK,IAAI,EACT,KAAK,MAAM,EACX,KAAK,OAAO,EACb,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAOjD,MAAM,WAAW,4BAA4B;IAC3C,mEAAmE;IACnE,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,2GAA2G;IAC3G,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,qEAAqE;IACrE,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAC/B,yCAAyC;IACzC,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAC;IAC1B,+BAA+B;IAC/B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,iCAAiC;IACjC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,0CAA0C;IAC1C,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC;IAC5B,mFAAmF;IACnF,QAAQ,CAAC,WAAW,CAAC,EAAE,cAAc,CAAC;IACtC,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,MAAM,CAAC;IAC/B,sFAAsF;IACtF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,6FAA6F;IAC7F,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,iGAAiG;IACjG,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,cAAc,CAAC;CACjC;AAYD;;;;GAIG;AACH,qBAAa,qBAAsB,YAAW,YAAY;IACxD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAoC;IAEzD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA4B;IAElD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA8B;IAE7D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAW;IAEpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAc;IAEvC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAY;IAEnC,OAAO,CAAC,OAAO,CAAS;IAExB,OAAO,CAAC,QAAQ,CAAS;IAEzB,YAAY,OAAO,EAAE,4BAA4B,EA4BhD;IAED,QAAQ,CAAC,UAAU,EAAE,aAAa,GAAG,IAAI,CAYxC;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAO3B;IAEK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAmB1B;IAEK,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAQ1C;IAED,QAAQ,IAAI,SAAS,cAAc,EAAE,CASpC;IAED,mGAAmG;IACnG,OAAO,CAAC,MAAM;YAUA,KAAK;IASnB,iGAAiG;YACnF,UAAU;YAUV,IAAI;IAQlB,+GAA+G;YACjG,SAAS;IA0BvB,OAAO,CAAC,KAAK;IAcb,OAAO,CAAC,OAAO;IAmBf,OAAO,CAAC,MAAM;IAed,kFAAkF;IAClF,OAAO,CAAC,aAAa;CAKtB"}
|