@cronwatch/sdk 0.13.1 → 0.14.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/dist/anthropic.cjs +3 -3
- package/dist/anthropic.cjs.map +1 -1
- package/dist/anthropic.d.cts +1 -1
- package/dist/anthropic.d.ts +1 -1
- package/dist/anthropic.js +3 -3
- package/dist/anthropic.js.map +1 -1
- package/dist/bugsnag.cjs +47 -9
- package/dist/bugsnag.cjs.map +1 -1
- package/dist/bugsnag.d.cts +1 -1
- package/dist/bugsnag.d.ts +1 -1
- package/dist/bugsnag.js +47 -9
- package/dist/bugsnag.js.map +1 -1
- package/dist/{client-DAJH7YiG.d.ts → client-C8j7B2Bo.d.ts} +31 -19
- package/dist/{client-z1LdR_m2.d.cts → client-CHjrNHH-.d.cts} +31 -19
- package/dist/d1.cjs.map +1 -1
- package/dist/d1.d.cts +2 -2
- package/dist/d1.d.ts +2 -2
- package/dist/d1.js.map +1 -1
- package/dist/datadog.cjs +47 -9
- package/dist/datadog.cjs.map +1 -1
- package/dist/datadog.d.cts +1 -1
- package/dist/datadog.d.ts +1 -1
- package/dist/datadog.js +47 -9
- package/dist/datadog.js.map +1 -1
- package/dist/discord.cjs +55 -10
- package/dist/discord.cjs.map +1 -1
- package/dist/discord.d.cts +1 -1
- package/dist/discord.d.ts +1 -1
- package/dist/discord.js +55 -10
- package/dist/discord.js.map +1 -1
- package/dist/{email-CCUzlLe9.d.cts → email-D7SfsU0F.d.ts} +2 -2
- package/dist/{email-CFtDqlvZ.d.ts → email-zSazJClH.d.cts} +2 -2
- package/dist/honeybadger.cjs +47 -9
- package/dist/honeybadger.cjs.map +1 -1
- package/dist/honeybadger.d.cts +1 -1
- package/dist/honeybadger.d.ts +1 -1
- package/dist/honeybadger.js +47 -9
- package/dist/honeybadger.js.map +1 -1
- package/dist/index.cjs +281 -100
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -7
- package/dist/index.d.ts +11 -7
- package/dist/index.js +281 -100
- package/dist/index.js.map +1 -1
- package/dist/mailgun.cjs +47 -9
- package/dist/mailgun.cjs.map +1 -1
- package/dist/mailgun.d.cts +2 -2
- package/dist/mailgun.d.ts +2 -2
- package/dist/mailgun.js +47 -9
- package/dist/mailgun.js.map +1 -1
- package/dist/newrelic.cjs +47 -9
- package/dist/newrelic.cjs.map +1 -1
- package/dist/newrelic.d.cts +1 -1
- package/dist/newrelic.d.ts +1 -1
- package/dist/newrelic.js +47 -9
- package/dist/newrelic.js.map +1 -1
- package/dist/pg-cron.cjs.map +1 -1
- package/dist/pg-cron.d.cts +2 -2
- package/dist/pg-cron.d.ts +2 -2
- package/dist/pg-cron.js.map +1 -1
- package/dist/postgres.cjs.map +1 -1
- package/dist/postgres.d.cts +2 -2
- package/dist/postgres.d.ts +2 -2
- package/dist/postgres.js.map +1 -1
- package/dist/postmark.cjs +47 -9
- package/dist/postmark.cjs.map +1 -1
- package/dist/postmark.d.cts +2 -2
- package/dist/postmark.d.ts +2 -2
- package/dist/postmark.js +47 -9
- package/dist/postmark.js.map +1 -1
- package/dist/resend.cjs +47 -9
- package/dist/resend.cjs.map +1 -1
- package/dist/resend.d.cts +2 -2
- package/dist/resend.d.ts +2 -2
- package/dist/resend.js +47 -9
- package/dist/resend.js.map +1 -1
- package/dist/rollbar.cjs +47 -9
- package/dist/rollbar.cjs.map +1 -1
- package/dist/rollbar.d.cts +1 -1
- package/dist/rollbar.d.ts +1 -1
- package/dist/rollbar.js +47 -9
- package/dist/rollbar.js.map +1 -1
- package/dist/sendgrid.cjs +47 -9
- package/dist/sendgrid.cjs.map +1 -1
- package/dist/sendgrid.d.cts +2 -2
- package/dist/sendgrid.d.ts +2 -2
- package/dist/sendgrid.js +47 -9
- package/dist/sendgrid.js.map +1 -1
- package/dist/sentry.cjs +47 -9
- package/dist/sentry.cjs.map +1 -1
- package/dist/sentry.d.cts +1 -1
- package/dist/sentry.d.ts +1 -1
- package/dist/sentry.js +47 -9
- package/dist/sentry.js.map +1 -1
- package/dist/ses.cjs +47 -9
- package/dist/ses.cjs.map +1 -1
- package/dist/ses.d.cts +2 -2
- package/dist/ses.d.ts +2 -2
- package/dist/ses.js +47 -9
- package/dist/ses.js.map +1 -1
- package/dist/slack.cjs +59 -11
- package/dist/slack.cjs.map +1 -1
- package/dist/slack.d.cts +6 -2
- package/dist/slack.d.ts +6 -2
- package/dist/slack.js +59 -11
- package/dist/slack.js.map +1 -1
- package/dist/sqlite.cjs.map +1 -1
- package/dist/sqlite.d.cts +2 -2
- package/dist/sqlite.d.ts +2 -2
- package/dist/sqlite.js.map +1 -1
- package/dist/twilio.cjs +47 -9
- package/dist/twilio.cjs.map +1 -1
- package/dist/twilio.d.cts +1 -1
- package/dist/twilio.d.ts +1 -1
- package/dist/twilio.js +47 -9
- package/dist/twilio.js.map +1 -1
- package/dist/{types-D6t0Z846.d.cts → types-B8IFVK5k.d.cts} +23 -7
- package/dist/{types-D6t0Z846.d.ts → types-B8IFVK5k.d.ts} +23 -7
- package/dist/webhook.cjs +39 -9
- package/dist/webhook.cjs.map +1 -1
- package/dist/webhook.d.cts +24 -6
- package/dist/webhook.d.ts +24 -6
- package/dist/webhook.js +39 -9
- package/dist/webhook.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -174,6 +174,13 @@ function countFrom(from) {
|
|
|
174
174
|
return from >= FIRST_DATE_MS ? from : FIRST_DATE_MS - 1;
|
|
175
175
|
}
|
|
176
176
|
function fireAfter(parsed, from) {
|
|
177
|
+
const found = croneFireAfter(parsed, from);
|
|
178
|
+
const start = countFrom(from);
|
|
179
|
+
if (start === null) return found;
|
|
180
|
+
const moved = gapFireAfter(parsed, start);
|
|
181
|
+
return moved !== null && (found === null || moved < found) ? moved : found;
|
|
182
|
+
}
|
|
183
|
+
function croneFireAfter(parsed, from) {
|
|
177
184
|
const cron = crons.get(parsed);
|
|
178
185
|
if (!cron) throw new Error(`schedule "${parsed.source}" was not made by parseSchedule`);
|
|
179
186
|
if (never.has(parsed)) return null;
|
|
@@ -189,6 +196,43 @@ function fireAfter(parsed, from) {
|
|
|
189
196
|
}
|
|
190
197
|
return null;
|
|
191
198
|
}
|
|
199
|
+
function springGap(timezone, at) {
|
|
200
|
+
const jump = springJump(timezone, at);
|
|
201
|
+
return jump && at < jump.jumpAt + jump.gapMs ? jump : null;
|
|
202
|
+
}
|
|
203
|
+
var JUMP_LOOKBACK_MS = 3 * 36e5;
|
|
204
|
+
function springJump(timezone, at) {
|
|
205
|
+
if (at - JUMP_LOOKBACK_MS < FIRST_DATE_MS || at >= LAST_DATE_MS) return null;
|
|
206
|
+
const after2 = utcOffset(at, timezone);
|
|
207
|
+
const before = utcOffset(at - JUMP_LOOKBACK_MS, timezone);
|
|
208
|
+
if (after2 <= before) return null;
|
|
209
|
+
let lo = at - JUMP_LOOKBACK_MS;
|
|
210
|
+
let hi = at;
|
|
211
|
+
while (hi - lo > 6e4) {
|
|
212
|
+
const mid = lo + Math.floor((hi - lo) / 2);
|
|
213
|
+
if (utcOffset(mid, timezone) === after2) hi = mid;
|
|
214
|
+
else lo = mid;
|
|
215
|
+
}
|
|
216
|
+
return { jumpAt: Math.floor(hi / 6e4) * 6e4, gapMs: after2 - before, beforeMs: before };
|
|
217
|
+
}
|
|
218
|
+
var wallCrons = /* @__PURE__ */ new WeakMap();
|
|
219
|
+
function wallCron(parsed) {
|
|
220
|
+
let wall = wallCrons.get(parsed);
|
|
221
|
+
if (!wall) {
|
|
222
|
+
wall = new croner.Cron(parsed.source, { timezone: "UTC" });
|
|
223
|
+
wallCrons.set(parsed, wall);
|
|
224
|
+
}
|
|
225
|
+
return wall;
|
|
226
|
+
}
|
|
227
|
+
function gapFireAfter(parsed, start) {
|
|
228
|
+
if (never.has(parsed)) return null;
|
|
229
|
+
const gap = springGap(parsed.timezone, start);
|
|
230
|
+
if (!gap) return null;
|
|
231
|
+
const wall = wallCron(parsed);
|
|
232
|
+
const [slot] = runsAfter(wall, 1, start + gap.beforeMs);
|
|
233
|
+
if (slot === void 0 || slot >= gap.jumpAt + gap.beforeMs + gap.gapMs) return null;
|
|
234
|
+
return slot - gap.beforeMs;
|
|
235
|
+
}
|
|
192
236
|
function firesBetween(parsed, from, to, limit) {
|
|
193
237
|
const cron = crons.get(parsed);
|
|
194
238
|
if (!cron) throw new Error(`schedule "${parsed.source}" was not made by parseSchedule`);
|
|
@@ -199,15 +243,34 @@ function firesBetween(parsed, from, to, limit) {
|
|
|
199
243
|
let probe = start;
|
|
200
244
|
let last = start;
|
|
201
245
|
for (let guard = 0; guard < 1e3; guard++) {
|
|
246
|
+
if (springGap(parsed.timezone, last)) {
|
|
247
|
+
const t = fireAfter(parsed, last);
|
|
248
|
+
if (t === null || t > to) return out;
|
|
249
|
+
out.push(t);
|
|
250
|
+
last = probe = t;
|
|
251
|
+
if (out.length > limit) return null;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
202
254
|
const batch = runsAfter(cron, Math.min(limit + 1 - out.length, 24), probe);
|
|
203
255
|
if (batch.length === 0) return out;
|
|
256
|
+
let stepped = false;
|
|
204
257
|
for (const t of batch) {
|
|
205
258
|
if (t <= last) continue;
|
|
259
|
+
if (springGap(parsed.timezone, t)) {
|
|
260
|
+
const next = fireAfter(parsed, last);
|
|
261
|
+
if (next === null || next > to) return out;
|
|
262
|
+
out.push(next);
|
|
263
|
+
last = probe = next;
|
|
264
|
+
if (out.length > limit) return null;
|
|
265
|
+
stepped = true;
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
206
268
|
if (t > to) return out;
|
|
207
269
|
out.push(t);
|
|
208
270
|
last = t;
|
|
209
271
|
if (out.length > limit) return null;
|
|
210
272
|
}
|
|
273
|
+
if (stepped) continue;
|
|
211
274
|
const end = batch[batch.length - 1];
|
|
212
275
|
probe = end > probe ? end : probe + 36e5;
|
|
213
276
|
}
|
|
@@ -220,10 +283,14 @@ function nextFire(parsed, from, lastRunAt) {
|
|
|
220
283
|
}
|
|
221
284
|
return fireAfter(parsed, from);
|
|
222
285
|
}
|
|
223
|
-
function
|
|
286
|
+
function intervalDue(everyMs, startedAt, finishedAt) {
|
|
287
|
+
const due = startedAt + everyMs;
|
|
288
|
+
return finishedAt !== null && finishedAt > due ? finishedAt + everyMs : due;
|
|
289
|
+
}
|
|
290
|
+
function expectation(parsed, lastRunAt, registeredAt, graceMs2, lastFinishedAt = null) {
|
|
224
291
|
let dueAt;
|
|
225
292
|
if (parsed.kind === "interval") {
|
|
226
|
-
dueAt =
|
|
293
|
+
dueAt = lastRunAt === null ? registeredAt + parsed.everyMs : intervalDue(parsed.everyMs, lastRunAt, lastFinishedAt);
|
|
227
294
|
} else if (lastRunAt === null) {
|
|
228
295
|
dueAt = fireAfter(parsed, registeredAt - 1);
|
|
229
296
|
} else {
|
|
@@ -232,33 +299,25 @@ function expectation(parsed, lastRunAt, registeredAt, graceMs2) {
|
|
|
232
299
|
return dueAt === null ? null : { dueAt, deadline: dueAt + graceMs2 };
|
|
233
300
|
}
|
|
234
301
|
function dueAfterRun(parsed, startedAt) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
302
|
+
let next = fireAfter(parsed, startedAt);
|
|
303
|
+
for (let guard = 0; next !== null && guard < 1e5; guard++) {
|
|
304
|
+
const following = fireAfter(parsed, next);
|
|
305
|
+
const covers = runCovers(startedAt, next, following) || inSpringForwardGap(parsed, startedAt, next);
|
|
306
|
+
if (!covers) return next;
|
|
307
|
+
next = following;
|
|
308
|
+
}
|
|
309
|
+
return next;
|
|
240
310
|
}
|
|
241
311
|
function runCovers(startedAt, dueAt, followingAt = null) {
|
|
242
312
|
const slack = followingAt === null ? EARLY_SLACK_MS : Math.min(EARLY_SLACK_MS, Math.floor((followingAt - dueAt) / 2));
|
|
243
313
|
return startedAt >= dueAt - slack;
|
|
244
314
|
}
|
|
245
315
|
function inSpringForwardGap(parsed, startedAt, fireAt) {
|
|
246
|
-
const
|
|
247
|
-
if (
|
|
248
|
-
const
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
if (gap <= 0) return false;
|
|
252
|
-
let lo = fireAt - LOOKBACK;
|
|
253
|
-
let hi = fireAt;
|
|
254
|
-
while (hi - lo > 6e4) {
|
|
255
|
-
const mid = lo + Math.floor((hi - lo) / 2);
|
|
256
|
-
if (utcOffset(mid, parsed.timezone) === after2) hi = mid;
|
|
257
|
-
else lo = mid;
|
|
258
|
-
}
|
|
259
|
-
const jumpAt = Math.floor(hi / 6e4) * 6e4;
|
|
260
|
-
if (fireAt - jumpAt >= gap || startedAt < jumpAt - EARLY_SLACK_MS || startedAt >= fireAt) return false;
|
|
261
|
-
return fireAfter(parsed, jumpAt - 1) === fireAt;
|
|
316
|
+
const gap = springGap(parsed.timezone, fireAt);
|
|
317
|
+
if (!gap || startedAt < gap.jumpAt - EARLY_SLACK_MS || startedAt >= fireAt) return false;
|
|
318
|
+
const wall = wallCron(parsed);
|
|
319
|
+
const fires = (at) => runsAfter(wall, 1, at - 1)[0] === at;
|
|
320
|
+
return fires(fireAt + gap.beforeMs) && !fires(fireAt + gap.beforeMs + gap.gapMs);
|
|
262
321
|
}
|
|
263
322
|
var formatters = /* @__PURE__ */ new Map();
|
|
264
323
|
function utcOffset(at, timezone) {
|
|
@@ -355,15 +414,75 @@ var SEND_LEASE_MS = 5 * 6e4;
|
|
|
355
414
|
function alertKey(alert) {
|
|
356
415
|
return `${alert.type}|${alert.at}|${alert.run?.id ?? ""}`;
|
|
357
416
|
}
|
|
417
|
+
var HELD_TEXT_CAP = 2048;
|
|
418
|
+
var HELD_TEXT_SLACK = 64;
|
|
419
|
+
function heldRun(run) {
|
|
420
|
+
const error = typeof run.error === "string" && run.error.length > HELD_TEXT_CAP + HELD_TEXT_SLACK ? keepHead(run.error) : run.error;
|
|
421
|
+
const output = typeof run.output === "string" && run.output.length > HELD_TEXT_CAP + HELD_TEXT_SLACK ? keepTail(run.output) : run.output;
|
|
422
|
+
return error === run.error && output === run.output ? run : { ...run, error, output };
|
|
423
|
+
}
|
|
424
|
+
var HELD_MESSAGE_CAP = 4096;
|
|
425
|
+
function keepHead(text, cap = HELD_TEXT_CAP) {
|
|
426
|
+
let end = cap;
|
|
427
|
+
if (isHighSurrogate(text.charCodeAt(end - 1))) end -= 1;
|
|
428
|
+
return `${text.slice(0, end)}
|
|
429
|
+
[${text.length - end} more characters trimmed]`;
|
|
430
|
+
}
|
|
431
|
+
function keepTail(text) {
|
|
432
|
+
let start = text.length - HELD_TEXT_CAP;
|
|
433
|
+
if (isLowSurrogate(text.charCodeAt(start)) && isHighSurrogate(text.charCodeAt(start - 1))) start += 1;
|
|
434
|
+
return `[${start} earlier characters trimmed]
|
|
435
|
+
${text.slice(start)}`;
|
|
436
|
+
}
|
|
437
|
+
function isHighSurrogate(code) {
|
|
438
|
+
return code >= 55296 && code <= 56319;
|
|
439
|
+
}
|
|
440
|
+
function isLowSurrogate(code) {
|
|
441
|
+
return code >= 56320 && code <= 57343;
|
|
442
|
+
}
|
|
443
|
+
function heldAlert(alert) {
|
|
444
|
+
const run = alert.run;
|
|
445
|
+
const trimmedRun = isObject(run) ? heldRun(run) : run;
|
|
446
|
+
const message = alert.message;
|
|
447
|
+
const trimmedMessage = typeof message === "string" && message.length > HELD_MESSAGE_CAP + HELD_TEXT_SLACK ? keepHead(message, HELD_MESSAGE_CAP) : message;
|
|
448
|
+
if (trimmedRun === run && trimmedMessage === message) return alert;
|
|
449
|
+
return { ...alert, run: trimmedRun, message: trimmedMessage };
|
|
450
|
+
}
|
|
451
|
+
function channelKeys(names) {
|
|
452
|
+
const seen = /* @__PURE__ */ new Map();
|
|
453
|
+
const taken = /* @__PURE__ */ new Set();
|
|
454
|
+
return names.map((name) => {
|
|
455
|
+
let n = (seen.get(name) ?? 0) + 1;
|
|
456
|
+
seen.set(name, n);
|
|
457
|
+
let key = n === 1 ? name : `${name}#${n}`;
|
|
458
|
+
while (taken.has(key)) key = `${name}#${++n}`;
|
|
459
|
+
taken.add(key);
|
|
460
|
+
return key;
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
function owedChannels(alert, configured) {
|
|
464
|
+
const owed = alert.owed;
|
|
465
|
+
if (!Array.isArray(owed)) return [...configured];
|
|
466
|
+
return configured.filter((key) => owed.includes(key));
|
|
467
|
+
}
|
|
468
|
+
function afterSend(alert, tried, failed) {
|
|
469
|
+
const owed = tried.filter((key) => failed.includes(key));
|
|
470
|
+
return owed.length === 0 ? null : { ...alert, owed };
|
|
471
|
+
}
|
|
472
|
+
function withoutOwed(alert) {
|
|
473
|
+
if (!("owed" in alert)) return alert;
|
|
474
|
+
const { owed: _owed, ...rest } = alert;
|
|
475
|
+
return rest;
|
|
476
|
+
}
|
|
358
477
|
function newest(list, max) {
|
|
359
478
|
return { kept: list.slice(-max), dropped: Math.max(0, list.length - max) };
|
|
360
479
|
}
|
|
361
480
|
function queueUndelivered(state, alerts) {
|
|
362
481
|
const next = cloneState(state);
|
|
363
|
-
const byKey = new Map(alerts.map((alert) => [alertKey(alert), alert]));
|
|
482
|
+
const byKey = new Map(alerts.map((alert) => [alertKey(alert), heldAlert(alert)]));
|
|
364
483
|
const queue = next.undelivered.map((alert) => byKey.get(alertKey(alert)) ?? alert);
|
|
365
484
|
const known = new Set(queue.map(alertKey));
|
|
366
|
-
queue.push(...
|
|
485
|
+
queue.push(...[...byKey.values()].filter((alert) => !known.has(alertKey(alert))));
|
|
367
486
|
const { kept, dropped } = newest(queue, MAX_UNDELIVERED);
|
|
368
487
|
next.undelivered = kept;
|
|
369
488
|
return { state: next, dropped };
|
|
@@ -372,7 +491,7 @@ function holdAlerts(state, alerts, until, deferred) {
|
|
|
372
491
|
if (alerts.length === 0) return { state, dropped: 0 };
|
|
373
492
|
if (deferred) return queueUndelivered(state, alerts);
|
|
374
493
|
const next = cloneState(state);
|
|
375
|
-
const { kept, dropped } = newest([...next.sending ?? [], ...alerts.map((alert) => ({ until, alert }))], MAX_UNDELIVERED);
|
|
494
|
+
const { kept, dropped } = newest([...next.sending ?? [], ...alerts.map((alert) => ({ until, alert: heldAlert(alert) }))], MAX_UNDELIVERED);
|
|
376
495
|
next.sending = kept;
|
|
377
496
|
return { state: next, dropped };
|
|
378
497
|
}
|
|
@@ -386,7 +505,7 @@ function releaseSending(state, now) {
|
|
|
386
505
|
lapsed.filter((entry) => typeof entry?.alert === "object" && entry.alert !== null).map((entry) => entry.alert)
|
|
387
506
|
);
|
|
388
507
|
}
|
|
389
|
-
function recordSent(state, delivered, failed, stale, now) {
|
|
508
|
+
function recordSent(state, delivered, failed, stale, now, reached = delivered.length > 0) {
|
|
390
509
|
const next = cloneState(state);
|
|
391
510
|
const done = new Set([...delivered, ...stale].map(alertKey));
|
|
392
511
|
next.undelivered = next.undelivered.filter((alert) => !done.has(alertKey(alert)));
|
|
@@ -394,7 +513,7 @@ function recordSent(state, delivered, failed, stale, now) {
|
|
|
394
513
|
const held = (next.sending ?? []).filter((entry) => !(typeof entry?.alert === "object" && entry.alert !== null && sent.has(alertKey(entry.alert))));
|
|
395
514
|
if (held.length > 0) next.sending = held;
|
|
396
515
|
else delete next.sending;
|
|
397
|
-
if (
|
|
516
|
+
if (reached) next.lastAlertAt = now;
|
|
398
517
|
return queueUndelivered(next, failed);
|
|
399
518
|
}
|
|
400
519
|
function claimUndelivered(state, now, until) {
|
|
@@ -502,9 +621,21 @@ function formatNumber(n) {
|
|
|
502
621
|
if (Number.isInteger(n)) return n.toLocaleString("en-US");
|
|
503
622
|
return n.toLocaleString("en-US", { maximumFractionDigits: 4 });
|
|
504
623
|
}
|
|
505
|
-
function
|
|
624
|
+
function finishedAtOf(run) {
|
|
625
|
+
return run.status !== "running" && typeof run.finishedAt === "number" ? run.finishedAt : null;
|
|
626
|
+
}
|
|
627
|
+
function catchesUp(def, run, now) {
|
|
628
|
+
if (!def.schedule) return true;
|
|
629
|
+
try {
|
|
630
|
+
const exp = expectation(parseSchedule(def.schedule, def.timezone), run.startedAt, run.startedAt, graceMs(def), finishedAtOf(run));
|
|
631
|
+
return exp === null || now <= exp.deadline;
|
|
632
|
+
} catch {
|
|
633
|
+
return true;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
function onRunStart(def, run, state, now) {
|
|
506
637
|
const next = cloneState(state);
|
|
507
|
-
closeCondition(next, "missed");
|
|
638
|
+
if (catchesUp(def, run, now)) closeCondition(next, "missed");
|
|
508
639
|
closeCondition(next, "stuck");
|
|
509
640
|
return next;
|
|
510
641
|
}
|
|
@@ -513,7 +644,7 @@ function onRunFinish(def, run, state, history, now) {
|
|
|
513
644
|
const alerts = [];
|
|
514
645
|
if (run.status === "ok") {
|
|
515
646
|
next.consecutiveFailures = 0;
|
|
516
|
-
closeCondition(next, "missed");
|
|
647
|
+
if (catchesUp(def, run, now)) closeCondition(next, "missed");
|
|
517
648
|
closeCondition(next, "stuck");
|
|
518
649
|
closeCondition(next, "failed");
|
|
519
650
|
const slow = slowThreshold(def, history);
|
|
@@ -550,7 +681,7 @@ function onRunFinish(def, run, state, history, now) {
|
|
|
550
681
|
return { state: next, alerts };
|
|
551
682
|
}
|
|
552
683
|
next.consecutiveFailures = Math.min(next.consecutiveFailures + 1, MAX_DURATION_MS);
|
|
553
|
-
closeCondition(next, "missed");
|
|
684
|
+
if (catchesUp(def, run, now)) closeCondition(next, "missed");
|
|
554
685
|
const threshold = Math.max(1, def.failuresBeforeAlert ?? 1);
|
|
555
686
|
const condition = run.status === "timeout" ? "stuck" : "failed";
|
|
556
687
|
if (next.consecutiveFailures >= threshold) {
|
|
@@ -575,8 +706,8 @@ function onCheck(def, stored, lastRun, state, now) {
|
|
|
575
706
|
const parsed = parseSchedule(def.schedule, def.timezone);
|
|
576
707
|
const grace = graceMs(def);
|
|
577
708
|
const lastRunAt = lastRun?.startedAt ?? null;
|
|
578
|
-
const exp = expectation(parsed, lastRunAt, stored.createdAt, grace);
|
|
579
|
-
const nextExpectedAt = parsed.kind === "interval" ?
|
|
709
|
+
const exp = expectation(parsed, lastRunAt, stored.createdAt, grace, lastRun ? finishedAtOf(lastRun) : null);
|
|
710
|
+
const nextExpectedAt = parsed.kind === "interval" ? exp.dueAt : nextFire(parsed, now, null);
|
|
580
711
|
if (!exp) return { state: next, alerts, nextExpectedAt, dueAt: null };
|
|
581
712
|
if (parsed.kind === "interval" && lastRun?.status === "running") {
|
|
582
713
|
return { state: next, alerts, nextExpectedAt, dueAt: exp.dueAt };
|
|
@@ -862,11 +993,15 @@ function stringify(part) {
|
|
|
862
993
|
}
|
|
863
994
|
}
|
|
864
995
|
function createRecorder(run) {
|
|
996
|
+
const window = OUTPUT_CAP + REDACT_EDGE + 2;
|
|
997
|
+
const copy = (text) => JSON.parse(JSON.stringify(text));
|
|
865
998
|
const lines = [];
|
|
866
999
|
let size = 0;
|
|
867
1000
|
const head = [];
|
|
868
1001
|
let headSize = 0;
|
|
869
|
-
let
|
|
1002
|
+
let whole = "";
|
|
1003
|
+
let tail2 = "";
|
|
1004
|
+
let logged = false;
|
|
870
1005
|
const metrics = {};
|
|
871
1006
|
const controller = new AbortController();
|
|
872
1007
|
const context = {
|
|
@@ -875,16 +1010,23 @@ function createRecorder(run) {
|
|
|
875
1010
|
startedAt: run.startedAt,
|
|
876
1011
|
signal: controller.signal,
|
|
877
1012
|
log(...parts) {
|
|
878
|
-
|
|
1013
|
+
let line = parts.map(stringify).join(" ");
|
|
1014
|
+
const separator = logged ? "\n" : "";
|
|
1015
|
+
logged = true;
|
|
1016
|
+
if (whole !== null) whole = whole.length + separator.length + line.length <= 2 * OUTPUT_CAP ? whole + separator + line : null;
|
|
1017
|
+
tail2 = copy((tail2 + separator + copy(line.slice(-OUTPUT_CAP))).slice(-OUTPUT_CAP));
|
|
879
1018
|
if (headSize < OUTPUT_CAP) {
|
|
880
|
-
head.push(line);
|
|
1019
|
+
head.push(copy(line.slice(0, OUTPUT_CAP - headSize)));
|
|
881
1020
|
headSize += line.length + 1;
|
|
882
1021
|
}
|
|
1022
|
+
line = stripNul(line);
|
|
1023
|
+
if (line.length >= window) {
|
|
1024
|
+
line = copy(line.slice(-68609));
|
|
1025
|
+
}
|
|
883
1026
|
lines.push(line);
|
|
884
1027
|
size += line.length + 1;
|
|
885
|
-
while (size >
|
|
1028
|
+
while (size > window && lines.length > 1) {
|
|
886
1029
|
size -= lines.shift().length + 1;
|
|
887
|
-
dropped = true;
|
|
888
1030
|
}
|
|
889
1031
|
},
|
|
890
1032
|
metric(name, value) {
|
|
@@ -901,10 +1043,8 @@ function createRecorder(run) {
|
|
|
901
1043
|
context,
|
|
902
1044
|
output: () => lines.length === 0 ? null : lines.join("\n"),
|
|
903
1045
|
expectText: () => {
|
|
904
|
-
if (
|
|
905
|
-
|
|
906
|
-
if (!dropped && all.length <= 2 * OUTPUT_CAP) return all;
|
|
907
|
-
return head.join("\n").slice(0, OUTPUT_CAP) + "\n" + all.slice(-OUTPUT_CAP);
|
|
1046
|
+
if (!logged) return null;
|
|
1047
|
+
return whole ?? head.join("\n").slice(0, OUTPUT_CAP) + "\n" + tail2;
|
|
908
1048
|
},
|
|
909
1049
|
metrics: () => ({ ...metrics }),
|
|
910
1050
|
abort: () => controller.abort()
|
|
@@ -1078,7 +1218,7 @@ function staticAsset(path, base) {
|
|
|
1078
1218
|
}
|
|
1079
1219
|
|
|
1080
1220
|
// src/version.ts
|
|
1081
|
-
var VERSION = "0.
|
|
1221
|
+
var VERSION = "0.14.1";
|
|
1082
1222
|
var API_VERSION = 1;
|
|
1083
1223
|
|
|
1084
1224
|
// src/routes/timeline.ts
|
|
@@ -1131,13 +1271,13 @@ function dueTimes(job, parsed, runs, from, to) {
|
|
|
1131
1271
|
if (parsed.kind === "interval") {
|
|
1132
1272
|
const every = parsed.everyMs;
|
|
1133
1273
|
if ((to - from) / every > MAX_TICKS) return { times: [], dense: true };
|
|
1134
|
-
const
|
|
1274
|
+
const ordered = [...runs].sort((a, b) => a.startedAt - b.startedAt);
|
|
1275
|
+
const dues = ordered.map((r) => intervalDue(every, r.startedAt, finishedAtOf(r)));
|
|
1135
1276
|
const times2 = /* @__PURE__ */ new Set();
|
|
1136
|
-
for (const
|
|
1137
|
-
const t2 = start + every;
|
|
1277
|
+
for (const t2 of dues) {
|
|
1138
1278
|
if (t2 >= from && t2 <= to) times2.add(t2);
|
|
1139
1279
|
}
|
|
1140
|
-
let t =
|
|
1280
|
+
let t = dues.length ? dues[dues.length - 1] : job.nextExpectedAt;
|
|
1141
1281
|
if (t !== null) {
|
|
1142
1282
|
if (t < from) t += Math.ceil((from - t) / every) * every;
|
|
1143
1283
|
for (; t <= to; t += every) times2.add(t);
|
|
@@ -1150,8 +1290,8 @@ function dueTimes(job, parsed, runs, from, to) {
|
|
|
1150
1290
|
function missedAt(job, parsed, times, now) {
|
|
1151
1291
|
if (!parsed || !job.open.includes("missed")) return null;
|
|
1152
1292
|
const grace = safely(() => graceMs(job.definition), 0);
|
|
1153
|
-
const
|
|
1154
|
-
if (
|
|
1293
|
+
const lastRun = job.lastRun;
|
|
1294
|
+
if (lastRun) return safely(() => expectation(parsed, lastRun.startedAt, lastRun.startedAt, grace, finishedAtOf(lastRun))?.dueAt ?? null, null);
|
|
1155
1295
|
const past = times.filter((t) => t + grace < now);
|
|
1156
1296
|
return past.length ? past[past.length - 1] : null;
|
|
1157
1297
|
}
|
|
@@ -1860,13 +2000,15 @@ function signInReturn(referer, publicOrigin, base) {
|
|
|
1860
2000
|
return url.searchParams.has("token") ? `${base}/` : referer;
|
|
1861
2001
|
}
|
|
1862
2002
|
function stripBase(pathname, base) {
|
|
1863
|
-
|
|
2003
|
+
if (base !== "" && pathname !== base && !pathname.startsWith(`${base}/`)) return null;
|
|
2004
|
+
let path = pathname.slice(base.length);
|
|
1864
2005
|
if (path === "") path = "/";
|
|
1865
2006
|
if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1);
|
|
1866
2007
|
return path;
|
|
1867
2008
|
}
|
|
1868
2009
|
var MAX_REQUEST_BODY = 1 << 20;
|
|
1869
2010
|
var TOO_LARGE = /* @__PURE__ */ Symbol("too large");
|
|
2011
|
+
var BAD_BODY = /* @__PURE__ */ Symbol("malformed body");
|
|
1870
2012
|
async function readLimited(request) {
|
|
1871
2013
|
const length = Number(request.headers.get("content-length"));
|
|
1872
2014
|
if (Number.isFinite(length) && length > MAX_REQUEST_BODY) return TOO_LARGE;
|
|
@@ -1913,14 +2055,14 @@ async function readBody(request) {
|
|
|
1913
2055
|
try {
|
|
1914
2056
|
if (json2) {
|
|
1915
2057
|
const data = await copy.json();
|
|
1916
|
-
return typeof data === "object" && data !== null ? Object.fromEntries(Object.entries(data).map(([k, v]) => [k, String(v)])) :
|
|
2058
|
+
return typeof data === "object" && data !== null && !Array.isArray(data) ? Object.fromEntries(Object.entries(data).map(([k, v]) => [k, String(v)])) : BAD_BODY;
|
|
1917
2059
|
}
|
|
1918
2060
|
const fields = await copy.formData();
|
|
1919
2061
|
const out = {};
|
|
1920
2062
|
for (const [k, v] of fields.entries()) out[k] = String(v);
|
|
1921
2063
|
return out;
|
|
1922
2064
|
} catch {
|
|
1923
|
-
return
|
|
2065
|
+
return BAD_BODY;
|
|
1924
2066
|
}
|
|
1925
2067
|
}
|
|
1926
2068
|
function crossSite(request, publicOrigin) {
|
|
@@ -1986,6 +2128,7 @@ function buildRoutes(cw, options = {}) {
|
|
|
1986
2128
|
const signInPage = () => generated ? html(messagePage("Sign in", "CRONWATCH_TOKEN is not set, so this development server made a token. The sign-in link is in the server log: open it once, or enter the token from it below, and this browser stays signed in.", base, true), 401) : html(messagePage("Sign in", "Enter your CRONWATCH_TOKEN and this browser stays signed in.", base, true), 401);
|
|
1987
2129
|
if (token && method === "POST" && path === "/signin") {
|
|
1988
2130
|
const body = await readBody(request);
|
|
2131
|
+
if (body === BAD_BODY) return html(messagePage("Bad request", "Malformed request body", base), 400);
|
|
1989
2132
|
const sent = body === TOO_LARGE ? void 0 : body.token;
|
|
1990
2133
|
if (sent === void 0 || !constantTimeEqual(sent, token)) return signInPage();
|
|
1991
2134
|
return redirect(signInReturn(request.headers.get("referer"), publicOrigin, base), { "set-cookie": await signInCookie() });
|
|
@@ -2043,6 +2186,7 @@ function buildRoutes(cw, options = {}) {
|
|
|
2043
2186
|
if (parts[2] === "silence") {
|
|
2044
2187
|
const body = await readBody(request);
|
|
2045
2188
|
if (body === TOO_LARGE) return html(messagePage("Not silenced", "The request was too large.", base), 413);
|
|
2189
|
+
if (body === BAD_BODY) return html(messagePage("Not silenced", "Malformed request body", base), 400);
|
|
2046
2190
|
let duration;
|
|
2047
2191
|
try {
|
|
2048
2192
|
duration = silenceDuration(body.for);
|
|
@@ -2080,6 +2224,7 @@ function buildRoutes(cw, options = {}) {
|
|
|
2080
2224
|
if (rest[2] === "silence") {
|
|
2081
2225
|
const body = await readBody(request);
|
|
2082
2226
|
if (body === TOO_LARGE) return api({ ok: false, error: "Request body too large" }, 413);
|
|
2227
|
+
if (body === BAD_BODY) return api({ ok: false, error: "Malformed request body" }, 400);
|
|
2083
2228
|
let duration;
|
|
2084
2229
|
try {
|
|
2085
2230
|
duration = silenceDuration(body.for ?? url.searchParams.get("for"));
|
|
@@ -2116,6 +2261,7 @@ function buildRoutes(cw, options = {}) {
|
|
|
2116
2261
|
try {
|
|
2117
2262
|
const url = new URL(request.url);
|
|
2118
2263
|
const path = stripBase(url.pathname, base);
|
|
2264
|
+
if (path === null) return html(messagePage("Not found", url.pathname, base), 404);
|
|
2119
2265
|
wantsHtml = !path.startsWith("/api");
|
|
2120
2266
|
return await serve(request, url, path, wantsHtml);
|
|
2121
2267
|
} catch (e) {
|
|
@@ -2470,7 +2616,7 @@ var Cronwatch = class {
|
|
|
2470
2616
|
warnNoSecret() {
|
|
2471
2617
|
if (this.warnedNoSecret) return;
|
|
2472
2618
|
this.warnedNoSecret = true;
|
|
2473
|
-
this.
|
|
2619
|
+
this.report(new Error("handler() refused a request because no CRON_SECRET is set; pass secret: null to allow unauthenticated requests"), "handler");
|
|
2474
2620
|
}
|
|
2475
2621
|
/** onError, for places that must carry on even when onError itself throws. */
|
|
2476
2622
|
report(error, where) {
|
|
@@ -2483,6 +2629,13 @@ var Cronwatch = class {
|
|
|
2483
2629
|
if (!this.ready) {
|
|
2484
2630
|
this.ready = (async () => {
|
|
2485
2631
|
if (this.store.init) await this.store.init();
|
|
2632
|
+
const missing = ["compareAndSetState", "updateRunIf"].filter((method) => typeof this.store[method] !== "function");
|
|
2633
|
+
if (missing.length > 0) {
|
|
2634
|
+
this.report(
|
|
2635
|
+
new Error(`the store has no ${missing.join(" or ")}, so two processes sharing it can overwrite each other's state or both evaluate the same run; it is safe only while one process at a time uses it`),
|
|
2636
|
+
"store"
|
|
2637
|
+
);
|
|
2638
|
+
}
|
|
2486
2639
|
if (this.usingDefaultStore && isProduction()) {
|
|
2487
2640
|
console.warn("[cronwatch] using the in-memory store: runs and state are lost on restart. Pass a store from @cronwatch/sdk/sqlite or @cronwatch/sdk/postgres.");
|
|
2488
2641
|
}
|
|
@@ -2630,10 +2783,11 @@ var Cronwatch = class {
|
|
|
2630
2783
|
await this.store.insertRun({ ...run });
|
|
2631
2784
|
recorded = true;
|
|
2632
2785
|
} catch (e) {
|
|
2633
|
-
this.
|
|
2786
|
+
this.report(e, `recording ${name}`);
|
|
2634
2787
|
}
|
|
2635
|
-
const
|
|
2636
|
-
|
|
2788
|
+
const begun = { ...run };
|
|
2789
|
+
const started = recorded ? this.updateState(name, (before) => ({ state: holdRun(onRunStart(definition, begun, before, this.now()), run.id), result: void 0 })).then(() => {
|
|
2790
|
+
}, (e) => this.report(e, `starting ${name}`)) : Promise.resolve();
|
|
2637
2791
|
const recorder = createRecorder(run);
|
|
2638
2792
|
const clearTimer = after(timeoutMs(definition), () => recorder.abort());
|
|
2639
2793
|
let result;
|
|
@@ -2658,7 +2812,7 @@ var Cronwatch = class {
|
|
|
2658
2812
|
const ignored = await this.recordFinish(definition, run, recorded, finishedAt);
|
|
2659
2813
|
if (ignored) this.report(new Error(`run ${run.id} of ${name} ${ignored}; ignored`), `finishing ${name}`);
|
|
2660
2814
|
} catch (e) {
|
|
2661
|
-
this.
|
|
2815
|
+
this.report(e, `recording ${name}`);
|
|
2662
2816
|
}
|
|
2663
2817
|
return { run, result, error, threw };
|
|
2664
2818
|
}
|
|
@@ -2706,9 +2860,10 @@ var Cronwatch = class {
|
|
|
2706
2860
|
if (stored.job !== run.job) return `belongs to job "${stored.job}"`;
|
|
2707
2861
|
}
|
|
2708
2862
|
}
|
|
2863
|
+
const tracked = (await this.readState(run.job)).pendingRuns?.includes(run.id) ?? false;
|
|
2709
2864
|
const claim = await this.claimFinish(run);
|
|
2710
2865
|
if ("ignored" in claim) return claim.ignored;
|
|
2711
|
-
if (!claim.lateAfterTimeout || run.status === "ok") await this.finishRun(toStored(definition), run, finishedAt);
|
|
2866
|
+
if (!claim.lateAfterTimeout || run.status === "ok") await this.finishRun(toStored(definition), run, finishedAt, tracked && !claim.lateAfterTimeout);
|
|
2712
2867
|
return null;
|
|
2713
2868
|
}
|
|
2714
2869
|
/** A conditional write (Store.updateRunIf), or for a store without one, a read then a plain write. */
|
|
@@ -2761,7 +2916,7 @@ ${id}`;
|
|
|
2761
2916
|
await this.ensureReady();
|
|
2762
2917
|
stored = await this.store.getRun(id);
|
|
2763
2918
|
} catch (e) {
|
|
2764
|
-
this.
|
|
2919
|
+
this.report(e, `recording ${name}`);
|
|
2765
2920
|
}
|
|
2766
2921
|
if (stored) return this.existingHandle(definition, stored);
|
|
2767
2922
|
}
|
|
@@ -2785,10 +2940,10 @@ ${id}`;
|
|
|
2785
2940
|
} catch (e) {
|
|
2786
2941
|
const stored = id === void 0 ? null : await this.store.getRun(id).catch(() => null);
|
|
2787
2942
|
if (stored) return this.existingHandle(definition, stored);
|
|
2788
|
-
this.
|
|
2943
|
+
this.report(e, `recording ${name}`);
|
|
2789
2944
|
}
|
|
2790
2945
|
if (recorded) {
|
|
2791
|
-
await this.updateState(name, (before) => ({ state: holdRun(onRunStart(before), run.id), result: void 0 })).catch((e) => this.
|
|
2946
|
+
await this.updateState(name, (before) => ({ state: holdRun(onRunStart(definition, run, before, this.now()), run.id), result: void 0 })).catch((e) => this.report(e, `starting ${name}`));
|
|
2792
2947
|
}
|
|
2793
2948
|
return this.runHandle(definition, run.id, run, recorded, null);
|
|
2794
2949
|
}
|
|
@@ -2800,7 +2955,7 @@ ${id}`;
|
|
|
2800
2955
|
await this.ensureReady();
|
|
2801
2956
|
stored = await this.store.getRun(runId);
|
|
2802
2957
|
} catch (e) {
|
|
2803
|
-
this.
|
|
2958
|
+
this.report(e, `resuming ${definition.name}`);
|
|
2804
2959
|
return this.runHandle(definition, runId, null, true, null);
|
|
2805
2960
|
}
|
|
2806
2961
|
if (!stored) return this.runHandle(definition, runId, null, true, "was not found");
|
|
@@ -3001,9 +3156,9 @@ ${id}`;
|
|
|
3001
3156
|
throw e;
|
|
3002
3157
|
}
|
|
3003
3158
|
if (!evaluate) return [];
|
|
3004
|
-
await this.updateState(run.job, (before) => ({ state: holdRun(onRunStart(before), run.id), result: void 0 }));
|
|
3159
|
+
await this.updateState(run.job, (before) => ({ state: holdRun(onRunStart(definition, run, before, this.now()), run.id), result: void 0 }));
|
|
3005
3160
|
if (run.status === "running") return [];
|
|
3006
|
-
return this.finishRun(definition, run, this.now());
|
|
3161
|
+
return this.finishRun(definition, run, this.now(), true);
|
|
3007
3162
|
}
|
|
3008
3163
|
/** recordRun() for a run already stored. */
|
|
3009
3164
|
async recordOver(definition, stored, run, evaluate) {
|
|
@@ -3012,6 +3167,7 @@ ${id}`;
|
|
|
3012
3167
|
return [];
|
|
3013
3168
|
}
|
|
3014
3169
|
if (stored.status !== "running" && stored.status !== "timeout" || run.status === "running") return [];
|
|
3170
|
+
const tracked = (await this.readState(run.job)).pendingRuns?.includes(run.id) ?? false;
|
|
3015
3171
|
const claim = await this.claimFinish(run);
|
|
3016
3172
|
if ("ignored" in claim) {
|
|
3017
3173
|
this.report(new Error(`run ${run.id} of ${run.job} ${claim.ignored}; ignored`), `recording ${run.job}`);
|
|
@@ -3022,24 +3178,25 @@ ${id}`;
|
|
|
3022
3178
|
return [];
|
|
3023
3179
|
}
|
|
3024
3180
|
if (claim.lateAfterTimeout && run.status !== "ok") return [];
|
|
3025
|
-
return this.finishRun(definition, run, this.now());
|
|
3181
|
+
return this.finishRun(definition, run, this.now(), tracked && !claim.lateAfterTimeout);
|
|
3026
3182
|
}
|
|
3027
3183
|
/**
|
|
3028
3184
|
* Evaluate a finished run (ok, failed, or timed out by a check), already
|
|
3029
3185
|
* written, against the job's state and send what that produces. The
|
|
3030
3186
|
* alerts are written with that state (see outbox()). Never throws.
|
|
3031
3187
|
*/
|
|
3032
|
-
async finishRun(definition, run, now) {
|
|
3188
|
+
async finishRun(definition, run, now, pendingOnly = false) {
|
|
3033
3189
|
let held;
|
|
3034
3190
|
try {
|
|
3035
3191
|
let history = null;
|
|
3036
3192
|
({ result: held } = await this.updateState(run.job, async (previous) => {
|
|
3193
|
+
if (pendingOnly && !(previous.pendingRuns ?? []).includes(run.id)) return { state: previous, result: { alerts: [], dropped: 0 } };
|
|
3037
3194
|
history ??= await this.history(run);
|
|
3038
3195
|
const before = releaseRun(previous, run.id);
|
|
3039
3196
|
return this.outbox(applySilence(before, onRunFinish(definition, run, before, history, now), now), definition, now);
|
|
3040
3197
|
}));
|
|
3041
3198
|
} catch (e) {
|
|
3042
|
-
this.
|
|
3199
|
+
this.report(e, `evaluating ${run.job}`);
|
|
3043
3200
|
return [];
|
|
3044
3201
|
}
|
|
3045
3202
|
this.reportDropped(run.job, held.dropped);
|
|
@@ -3078,7 +3235,7 @@ ${id}`;
|
|
|
3078
3235
|
}
|
|
3079
3236
|
/**
|
|
3080
3237
|
* Look for missed and stuck runs across every job, send alerts, retry
|
|
3081
|
-
* alerts
|
|
3238
|
+
* alerts to the channels that refused them, and prune old runs. Call it from an interval
|
|
3082
3239
|
* (start()), a cron hitting the mounted routes, or by hand. Concurrent
|
|
3083
3240
|
* calls share one check.
|
|
3084
3241
|
*/
|
|
@@ -3178,7 +3335,7 @@ ${id}`;
|
|
|
3178
3335
|
continue;
|
|
3179
3336
|
}
|
|
3180
3337
|
if (run.status === "running" || typeof run.finishedAt !== "number" || run.finishedAt > now - SEND_LEASE_MS) continue;
|
|
3181
|
-
alerts.push(...await this.finishRun(stored.definition, run, now));
|
|
3338
|
+
alerts.push(...await this.finishRun(stored.definition, run, now, true));
|
|
3182
3339
|
} catch (e) {
|
|
3183
3340
|
this.onError(e, `evaluating run ${id} of ${stored.name}`);
|
|
3184
3341
|
}
|
|
@@ -3298,7 +3455,7 @@ ${id}`;
|
|
|
3298
3455
|
this.warnedDeferredStart = true;
|
|
3299
3456
|
console.warn('[cronwatch] startChecking() was called with deliver: "check", so these checks send no alerts. Another process must run checks with deliver: "now" (the default) to send them.');
|
|
3300
3457
|
}
|
|
3301
|
-
const tick = () => this.check().catch((e) => this.
|
|
3458
|
+
const tick = () => this.check().catch((e) => this.report(e, "check"));
|
|
3302
3459
|
this.timer = setInterval(tick, ms);
|
|
3303
3460
|
if (typeof this.timer === "object" && "unref" in this.timer) this.timer.unref();
|
|
3304
3461
|
this.firstTick = setTimeout(() => {
|
|
@@ -3347,21 +3504,28 @@ ${id}`;
|
|
|
3347
3504
|
if (alerts.length === 0 || this.deferDelivery) return alerts;
|
|
3348
3505
|
const delivered = [];
|
|
3349
3506
|
const failed = [];
|
|
3507
|
+
let reached = false;
|
|
3350
3508
|
for (const alert of alerts) {
|
|
3351
3509
|
if (this.triage && alert.type !== "recovered") await this.addTriage(alert, TRIAGE_TIMEOUT_MS);
|
|
3352
|
-
|
|
3510
|
+
const sent = await this.deliver(alert);
|
|
3511
|
+
reached ||= sent.reached;
|
|
3512
|
+
if (sent.left) failed.push(sent.left);
|
|
3513
|
+
else delivered.push(alert);
|
|
3353
3514
|
}
|
|
3354
|
-
await this.recordDelivery(name, delivered, failed, [], now);
|
|
3515
|
+
await this.recordDelivery(name, delivered, failed, [], now, reached);
|
|
3355
3516
|
return alerts;
|
|
3356
3517
|
}
|
|
3357
3518
|
/**
|
|
3358
|
-
* Send the alerts
|
|
3359
|
-
* first
|
|
3360
|
-
*
|
|
3361
|
-
*
|
|
3362
|
-
*
|
|
3363
|
-
*
|
|
3364
|
-
*
|
|
3519
|
+
* Send the alerts some channel refused last time, once each, oldest
|
|
3520
|
+
* first, to the channels that still owe them (HeldAlert.owed) and no
|
|
3521
|
+
* others, from the copy held in state, its run trimmed (heldRun). They
|
|
3522
|
+
* are claimed first (claimUndelivered): moved into `sending` with a lease,
|
|
3523
|
+
* in one conditional write, so when several processes check at once only
|
|
3524
|
+
* the one whose write lands sends them. An alert that no longer describes
|
|
3525
|
+
* the job (staleAlert) is dropped instead. Retries across a check share
|
|
3526
|
+
* RETRY_BUDGET_MS of wall-clock time; claimed alerts it has no time left
|
|
3527
|
+
* for go back to the queue for the next check. Returns the alerts some
|
|
3528
|
+
* channel took on this attempt.
|
|
3365
3529
|
*/
|
|
3366
3530
|
async retryUndelivered(name, state, now, budget) {
|
|
3367
3531
|
if ((state.undelivered ?? []).length === 0 || this.deferDelivery) return [];
|
|
@@ -3372,12 +3536,13 @@ ${id}`;
|
|
|
3372
3536
|
return { state: claim.state, result: claim.claimed };
|
|
3373
3537
|
}));
|
|
3374
3538
|
} catch (e) {
|
|
3375
|
-
this.
|
|
3539
|
+
this.report(e, `claiming undelivered alerts for ${name}`);
|
|
3376
3540
|
return [];
|
|
3377
3541
|
}
|
|
3378
3542
|
if (claimed.length === 0) return [];
|
|
3379
3543
|
const delivered = [];
|
|
3380
3544
|
const failed = [];
|
|
3545
|
+
const sent = [];
|
|
3381
3546
|
for (const alert of claimed) {
|
|
3382
3547
|
const left = RETRY_BUDGET_MS - budget.spentMs;
|
|
3383
3548
|
if (left <= 0) {
|
|
@@ -3386,45 +3551,61 @@ ${id}`;
|
|
|
3386
3551
|
}
|
|
3387
3552
|
const started = Date.now();
|
|
3388
3553
|
if (this.triage && alert.type !== "recovered" && alert.triage === void 0) await this.addTriage(alert, Math.min(TRIAGE_TIMEOUT_MS, left));
|
|
3389
|
-
|
|
3554
|
+
const outcome = await this.deliver(alert);
|
|
3555
|
+
if (outcome.reached) sent.push(withoutOwed(alert));
|
|
3556
|
+
if (outcome.left) failed.push(outcome.left);
|
|
3557
|
+
else delivered.push(alert);
|
|
3390
3558
|
budget.spentMs += Math.max(0, Date.now() - started);
|
|
3391
3559
|
}
|
|
3392
|
-
await this.recordDelivery(name, delivered, failed, [], now);
|
|
3393
|
-
return
|
|
3560
|
+
await this.recordDelivery(name, delivered, failed, [], now, sent.length > 0);
|
|
3561
|
+
return sent;
|
|
3394
3562
|
}
|
|
3395
3563
|
/**
|
|
3396
3564
|
* Mark delivered alerts done, drop stale ones, and keep failed ones for the
|
|
3397
3565
|
* next check, taking them all out of `sending` (recordSent). A failed alert
|
|
3398
|
-
* replaces its stored copy, so a triage made on this attempt
|
|
3399
|
-
*
|
|
3400
|
-
*
|
|
3566
|
+
* replaces its stored copy, so a triage made on this attempt and the
|
|
3567
|
+
* channels that still owe it are kept. lastAlertAt moves when `reached`,
|
|
3568
|
+
* some channel accepted an alert on this attempt. When this write fails,
|
|
3569
|
+
* alerts still in `sending` are retried once their lease runs out.
|
|
3401
3570
|
*/
|
|
3402
|
-
async recordDelivery(name, delivered, failed, dropped, now) {
|
|
3571
|
+
async recordDelivery(name, delivered, failed, dropped, now, reached) {
|
|
3403
3572
|
try {
|
|
3404
3573
|
const { result: trimmed } = await this.updateState(name, (previous) => {
|
|
3405
|
-
const { state, dropped: letGo } = recordSent(normalizeState(previous, name), delivered, failed, dropped, now);
|
|
3574
|
+
const { state, dropped: letGo } = recordSent(normalizeState(previous, name), delivered, failed, dropped, now, reached);
|
|
3406
3575
|
return { state, result: letGo };
|
|
3407
3576
|
});
|
|
3408
3577
|
this.reportDropped(name, trimmed);
|
|
3409
3578
|
} catch (e) {
|
|
3410
|
-
this.
|
|
3579
|
+
this.report(e, `recording alert delivery for ${name}`);
|
|
3411
3580
|
}
|
|
3412
3581
|
}
|
|
3413
|
-
/**
|
|
3582
|
+
/**
|
|
3583
|
+
* Send to every channel that still owes the alert (owedChannels: all of
|
|
3584
|
+
* them for an alert not sent before), at once, each failure reported under
|
|
3585
|
+
* its channel's name. `left` is the alert with `owed` cut to the channels
|
|
3586
|
+
* that refused it, or null when none did (or none is left to owe it);
|
|
3587
|
+
* `reached` is whether any channel accepted it. With no channels at all it
|
|
3588
|
+
* counts as delivered, as it always has.
|
|
3589
|
+
*/
|
|
3414
3590
|
async deliver(alert) {
|
|
3415
|
-
if (this.alerts.length === 0) return true;
|
|
3416
|
-
const
|
|
3417
|
-
|
|
3591
|
+
if (this.alerts.length === 0) return { left: null, reached: true };
|
|
3592
|
+
const keys = channelKeys(this.alerts.map((channel) => channel.name));
|
|
3593
|
+
const owed = owedChannels(alert, keys);
|
|
3594
|
+
if (owed.length === 0) return { left: null, reached: false };
|
|
3595
|
+
const plain = withoutOwed(alert);
|
|
3596
|
+
const failed = [];
|
|
3597
|
+
await Promise.all(
|
|
3598
|
+
this.alerts.map(async (channel, i) => {
|
|
3599
|
+
if (!owed.includes(keys[i])) return;
|
|
3418
3600
|
try {
|
|
3419
|
-
await withTimeout(channel.send(
|
|
3420
|
-
return true;
|
|
3601
|
+
await withTimeout(channel.send(plain, { onError: (e) => this.report(e, `alert channel ${channel.name}`) }), CHANNEL_TIMEOUT_MS);
|
|
3421
3602
|
} catch (e) {
|
|
3422
|
-
|
|
3423
|
-
|
|
3603
|
+
failed.push(keys[i]);
|
|
3604
|
+
this.report(e, `alert channel ${channel.name}`);
|
|
3424
3605
|
}
|
|
3425
3606
|
})
|
|
3426
3607
|
);
|
|
3427
|
-
return
|
|
3608
|
+
return { left: afterSend(alert, owed, failed), reached: failed.length < owed.length };
|
|
3428
3609
|
}
|
|
3429
3610
|
/** Sets alert.triage to the diagnosis, or to null when there is none, so it is tried once per alert. */
|
|
3430
3611
|
async addTriage(alert, timeout) {
|
|
@@ -3436,7 +3617,7 @@ ${id}`;
|
|
|
3436
3617
|
} catch (e) {
|
|
3437
3618
|
controller.abort();
|
|
3438
3619
|
alert.triage = null;
|
|
3439
|
-
this.
|
|
3620
|
+
this.report(e, `triage for ${alert.job}`);
|
|
3440
3621
|
}
|
|
3441
3622
|
}
|
|
3442
3623
|
};
|