@stonyx/cron 0.2.1-alpha.12 → 0.2.1-alpha.14
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 +77 -16
- package/package.json +1 -1
package/dist/service.js
CHANGED
|
@@ -12,6 +12,24 @@ import { locked } from './locked.js';
|
|
|
12
12
|
import { normalizeJobInput, recoverFlatParams } from './normalize.js';
|
|
13
13
|
import RunLog from './run-log.js';
|
|
14
14
|
const MAX_TIMER_DELAY_MS = 60_000;
|
|
15
|
+
/**
|
|
16
|
+
* Describe a thrown value without ever throwing.
|
|
17
|
+
*
|
|
18
|
+
* `String(err)` is not total: a null-prototype object, or any object whose
|
|
19
|
+
* `toString`/`Symbol.toPrimitive` throws, raises "Cannot convert object to
|
|
20
|
+
* primitive value". Consumer callbacks throw arbitrary values, so the error
|
|
21
|
+
* handler itself must not be a second failure source.
|
|
22
|
+
*/
|
|
23
|
+
function describeError(err) {
|
|
24
|
+
if (err instanceof Error)
|
|
25
|
+
return err.message;
|
|
26
|
+
try {
|
|
27
|
+
return String(err);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return 'unknown error';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
15
33
|
export default class CronService {
|
|
16
34
|
jobs;
|
|
17
35
|
heap;
|
|
@@ -191,7 +209,26 @@ export default class CronService {
|
|
|
191
209
|
// awaited here holding no lock at all, so a callback that never settles
|
|
192
210
|
// cannot poison the lock chain.
|
|
193
211
|
for (const job of dueJobs) {
|
|
194
|
-
|
|
212
|
+
try {
|
|
213
|
+
await this.executeJob(job, true);
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
// One job's unexpected throw must not abort the batch. Every job in
|
|
217
|
+
// `dueJobs` is already claimed - marked running and detached from the
|
|
218
|
+
// heap - and only its own settle releases it, so aborting here would
|
|
219
|
+
// strand every sibling permanently un-due.
|
|
220
|
+
//
|
|
221
|
+
// This is the outermost handler on the timer path, so it is the one
|
|
222
|
+
// that must not be able to throw. `log()` is public, overridable and
|
|
223
|
+
// can reach a file transport, so its own failure is swallowed here
|
|
224
|
+
// rather than being allowed to take the batch down.
|
|
225
|
+
try {
|
|
226
|
+
this.log(`Job "${job.name}" (${job.id}) execution failed unexpectedly: ${describeError(err)}`);
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// Nothing left to report to.
|
|
230
|
+
}
|
|
231
|
+
}
|
|
195
232
|
}
|
|
196
233
|
}
|
|
197
234
|
finally {
|
|
@@ -235,29 +272,40 @@ export default class CronService {
|
|
|
235
272
|
if (!claimed)
|
|
236
273
|
return { status: 'skipped', reason: 'already running' };
|
|
237
274
|
}
|
|
238
|
-
// -- Phase 2: invoke (NOT locked) --
|
|
239
275
|
const startMs = Date.now();
|
|
240
276
|
let status = 'ok';
|
|
241
277
|
let error;
|
|
242
278
|
let summary;
|
|
279
|
+
let settled;
|
|
280
|
+
// The claim above marked the job running and detached it from the heap.
|
|
281
|
+
// Phase 3 is the ONLY thing that undoes either, so it must survive every
|
|
282
|
+
// non-local exit from phase 2 - including a throw from the catch handler
|
|
283
|
+
// itself. A claim with no matching settle is not a degraded state, it is a
|
|
284
|
+
// permanently dead job: `runningAtMs` set, no heap entry, `isDue` false
|
|
285
|
+
// forever and `run()` refused forever.
|
|
243
286
|
try {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
287
|
+
// -- Phase 2: invoke (NOT locked) --
|
|
288
|
+
try {
|
|
289
|
+
if (this.onJobDue) {
|
|
290
|
+
const result = await this.onJobDue(job);
|
|
291
|
+
if (result) {
|
|
292
|
+
status = result.status || 'ok';
|
|
293
|
+
error = result.error;
|
|
294
|
+
summary = result.summary;
|
|
295
|
+
}
|
|
250
296
|
}
|
|
251
297
|
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
status = 'error';
|
|
300
|
+
error = describeError(err);
|
|
301
|
+
this.log(`Job "${job.name}" (${job.id}) failed: ${error}`);
|
|
302
|
+
}
|
|
252
303
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
this.log(`Job "${job.name}" (${job.id}) failed: ${error}`);
|
|
304
|
+
finally {
|
|
305
|
+
// -- Phase 3: settle (locked) --
|
|
306
|
+
settled = await locked(() => this.settleJob(job, status, error, summary, startMs, Date.now() - startMs));
|
|
257
307
|
}
|
|
258
|
-
|
|
259
|
-
// -- Phase 3: settle (locked) --
|
|
260
|
-
return locked(() => this.settleJob(job, status, error, summary, startMs, durationMs));
|
|
308
|
+
return settled;
|
|
261
309
|
}
|
|
262
310
|
/**
|
|
263
311
|
* Phase 1 - claim. Must be called while holding the lock.
|
|
@@ -337,6 +385,19 @@ export default class CronService {
|
|
|
337
385
|
log(message) {
|
|
338
386
|
if (!config.cron?.log)
|
|
339
387
|
return;
|
|
340
|
-
log.cron
|
|
388
|
+
// `log.cron` is created by `log.defineType`, which runs in `Cron.init()`
|
|
389
|
+
// (src/main.ts) - a DIFFERENT class. A consumer wiring CronService directly
|
|
390
|
+
// never runs it, while `config/environment.js` defaults `cron.log` to true,
|
|
391
|
+
// so an unguarded call throws `log.cron is not a function`. That throw
|
|
392
|
+
// escapes executeJob's catch, and the error-reporting path must never be
|
|
393
|
+
// the thing that kills the scheduler. `src/types/stonyx.d.ts:19` declares
|
|
394
|
+
// `cron()` unconditionally, so the type system will not catch this.
|
|
395
|
+
const { logColor = '#888', logMethod = 'cron' } = config.cron ?? {};
|
|
396
|
+
if (typeof log[logMethod] !== 'function')
|
|
397
|
+
log.defineType(logMethod, logColor);
|
|
398
|
+
const method = log[logMethod];
|
|
399
|
+
if (typeof method !== 'function')
|
|
400
|
+
return;
|
|
401
|
+
method.call(log, `Cron — ${message}`);
|
|
341
402
|
}
|
|
342
403
|
}
|