@stonyx/cron 0.2.1-alpha.55 → 0.2.1-alpha.57
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/dist/service.js +58 -23
- package/package.json +1 -1
package/dist/service.js
CHANGED
|
@@ -65,6 +65,13 @@ function forLog(value, maxLength) {
|
|
|
65
65
|
* reject instead of returning an `ExecuteResult`: a contract violation in the
|
|
66
66
|
* function written to prevent exactly that. Reading and coercing `message`
|
|
67
67
|
* inside the `try` is what closes it.
|
|
68
|
+
*
|
|
69
|
+
* `Cron` in `main.ts` carries its own `describeError` and the two deliberately
|
|
70
|
+
* differ: it renders for a log line only, so it prefers `err.stack`; this one
|
|
71
|
+
* is also returned to the caller as `ExecuteResult.error` and persisted in a
|
|
72
|
+
* per-job run log, where a stack would be an unbounded blob in every stored
|
|
73
|
+
* failure. Recorded in `docs/architecture.md` under Error Handling — do not
|
|
74
|
+
* merge them into a shared helper without reading that first.
|
|
68
75
|
*/
|
|
69
76
|
function describeError(err) {
|
|
70
77
|
try {
|
|
@@ -100,32 +107,60 @@ export default class CronService {
|
|
|
100
107
|
if (this.started)
|
|
101
108
|
return;
|
|
102
109
|
this.started = true;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
110
|
+
// `finally`, not a trailing statement. Reconciled with the same guard #53
|
|
111
|
+
// puts around `register`'s `runOnInit` invocation: a scheduler that is
|
|
112
|
+
// marked started but never armed is the terminal state both fixes exist to
|
|
113
|
+
// remove, reached here through the other entry point. `initialJobs` crosses
|
|
114
|
+
// a serialization boundary — it is whatever the consumer's store handed
|
|
115
|
+
// back — so `Job[]` is a compile-time claim about runtime data, and a row
|
|
116
|
+
// missing `state` throws mid-loop. Without this, `start()` leaves
|
|
117
|
+
// `started: true` (so it is now a no-op), the rows registered before the
|
|
118
|
+
// throw sitting in the heap, and NO timer: measured, `status()` then
|
|
119
|
+
// reports `{ started: true, jobCount: 1, nextWakeAtMs: <real> }` while
|
|
120
|
+
// nothing will ever fire. Silent and healthy-looking, again.
|
|
121
|
+
try {
|
|
122
|
+
if (initialJobs) {
|
|
123
|
+
for (const job of initialJobs) {
|
|
124
|
+
// A `runningAtMs` on a rehydrated job is always stale. The claim it
|
|
125
|
+
// records was taken by a process that is gone, so nothing will ever
|
|
126
|
+
// settle it, and nothing reaps it — there is no lease on the field
|
|
127
|
+
// (tracked on #35). Left in place it is a permanently dead job that
|
|
128
|
+
// still reports healthy: `isDue` returns false forever because of the
|
|
129
|
+
// flag, `run()` answers `'already running'` forever, `update()` never
|
|
130
|
+
// touches `state.runningAtMs`, and `status()` counts it like any other.
|
|
131
|
+
// The consumer's only recovery would be remove() + add(), losing the
|
|
132
|
+
// job id and its run history.
|
|
133
|
+
//
|
|
134
|
+
// Same hazard, same treatment as the hand-release on the `'removed'`
|
|
135
|
+
// path in `#executeClaimed`: a claim with no reachable settle must be
|
|
136
|
+
// released. Assigned directly rather than via `applyResult` for the same
|
|
137
|
+
// reason — this releases the claim and nothing else. The job did not
|
|
138
|
+
// run, so it gets no run-log row, no `lastStatus`, and no recomputed
|
|
139
|
+
// `nextRunAtMs`; it is rescheduled from the store's own value below.
|
|
140
|
+
//
|
|
141
|
+
// Written unconditionally, and deliberately NOT guarded on a
|
|
142
|
+
// `job.state.runningAtMs` read. Guarding it makes `start()` accept a
|
|
143
|
+
// row whose `state` is frozen — `structuredClone` + `Object.freeze`
|
|
144
|
+
// is an ordinary defensive rehydration — and that row is not usable
|
|
145
|
+
// by this class at all: `markRunning` writes the same field on every
|
|
146
|
+
// execution. Measured, the guard moves the failure from a throw out
|
|
147
|
+
// of `start()`, which the consumer's own `await` can catch, to a
|
|
148
|
+
// TypeError raised inside `onTimer`'s batch claim — a bare timer
|
|
149
|
+
// callback, so it surfaces as an unhandled rejection and is
|
|
150
|
+
// process-fatal under Node's default. Failing loudly at the store
|
|
151
|
+
// boundary is the better of the two, and the `finally` above keeps
|
|
152
|
+
// the rows that loaded before it scheduled.
|
|
153
|
+
job.state.runningAtMs = undefined;
|
|
154
|
+
this.jobs.set(job.id, job);
|
|
155
|
+
if (job.enabled && job.state.nextRunAtMs) {
|
|
156
|
+
this.heap.push({ key: job.id, nextTrigger: job.state.nextRunAtMs });
|
|
157
|
+
}
|
|
125
158
|
}
|
|
126
159
|
}
|
|
127
160
|
}
|
|
128
|
-
|
|
161
|
+
finally {
|
|
162
|
+
this.armTimer();
|
|
163
|
+
}
|
|
129
164
|
}
|
|
130
165
|
/**
|
|
131
166
|
* Stop the service. Clears timer.
|