@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.js
CHANGED
|
@@ -172,6 +172,13 @@ function countFrom(from) {
|
|
|
172
172
|
return from >= FIRST_DATE_MS ? from : FIRST_DATE_MS - 1;
|
|
173
173
|
}
|
|
174
174
|
function fireAfter(parsed, from) {
|
|
175
|
+
const found = croneFireAfter(parsed, from);
|
|
176
|
+
const start = countFrom(from);
|
|
177
|
+
if (start === null) return found;
|
|
178
|
+
const moved = gapFireAfter(parsed, start);
|
|
179
|
+
return moved !== null && (found === null || moved < found) ? moved : found;
|
|
180
|
+
}
|
|
181
|
+
function croneFireAfter(parsed, from) {
|
|
175
182
|
const cron = crons.get(parsed);
|
|
176
183
|
if (!cron) throw new Error(`schedule "${parsed.source}" was not made by parseSchedule`);
|
|
177
184
|
if (never.has(parsed)) return null;
|
|
@@ -187,6 +194,43 @@ function fireAfter(parsed, from) {
|
|
|
187
194
|
}
|
|
188
195
|
return null;
|
|
189
196
|
}
|
|
197
|
+
function springGap(timezone, at) {
|
|
198
|
+
const jump = springJump(timezone, at);
|
|
199
|
+
return jump && at < jump.jumpAt + jump.gapMs ? jump : null;
|
|
200
|
+
}
|
|
201
|
+
var JUMP_LOOKBACK_MS = 3 * 36e5;
|
|
202
|
+
function springJump(timezone, at) {
|
|
203
|
+
if (at - JUMP_LOOKBACK_MS < FIRST_DATE_MS || at >= LAST_DATE_MS) return null;
|
|
204
|
+
const after2 = utcOffset(at, timezone);
|
|
205
|
+
const before = utcOffset(at - JUMP_LOOKBACK_MS, timezone);
|
|
206
|
+
if (after2 <= before) return null;
|
|
207
|
+
let lo = at - JUMP_LOOKBACK_MS;
|
|
208
|
+
let hi = at;
|
|
209
|
+
while (hi - lo > 6e4) {
|
|
210
|
+
const mid = lo + Math.floor((hi - lo) / 2);
|
|
211
|
+
if (utcOffset(mid, timezone) === after2) hi = mid;
|
|
212
|
+
else lo = mid;
|
|
213
|
+
}
|
|
214
|
+
return { jumpAt: Math.floor(hi / 6e4) * 6e4, gapMs: after2 - before, beforeMs: before };
|
|
215
|
+
}
|
|
216
|
+
var wallCrons = /* @__PURE__ */ new WeakMap();
|
|
217
|
+
function wallCron(parsed) {
|
|
218
|
+
let wall = wallCrons.get(parsed);
|
|
219
|
+
if (!wall) {
|
|
220
|
+
wall = new Cron(parsed.source, { timezone: "UTC" });
|
|
221
|
+
wallCrons.set(parsed, wall);
|
|
222
|
+
}
|
|
223
|
+
return wall;
|
|
224
|
+
}
|
|
225
|
+
function gapFireAfter(parsed, start) {
|
|
226
|
+
if (never.has(parsed)) return null;
|
|
227
|
+
const gap = springGap(parsed.timezone, start);
|
|
228
|
+
if (!gap) return null;
|
|
229
|
+
const wall = wallCron(parsed);
|
|
230
|
+
const [slot] = runsAfter(wall, 1, start + gap.beforeMs);
|
|
231
|
+
if (slot === void 0 || slot >= gap.jumpAt + gap.beforeMs + gap.gapMs) return null;
|
|
232
|
+
return slot - gap.beforeMs;
|
|
233
|
+
}
|
|
190
234
|
function firesBetween(parsed, from, to, limit) {
|
|
191
235
|
const cron = crons.get(parsed);
|
|
192
236
|
if (!cron) throw new Error(`schedule "${parsed.source}" was not made by parseSchedule`);
|
|
@@ -197,15 +241,34 @@ function firesBetween(parsed, from, to, limit) {
|
|
|
197
241
|
let probe = start;
|
|
198
242
|
let last = start;
|
|
199
243
|
for (let guard = 0; guard < 1e3; guard++) {
|
|
244
|
+
if (springGap(parsed.timezone, last)) {
|
|
245
|
+
const t = fireAfter(parsed, last);
|
|
246
|
+
if (t === null || t > to) return out;
|
|
247
|
+
out.push(t);
|
|
248
|
+
last = probe = t;
|
|
249
|
+
if (out.length > limit) return null;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
200
252
|
const batch = runsAfter(cron, Math.min(limit + 1 - out.length, 24), probe);
|
|
201
253
|
if (batch.length === 0) return out;
|
|
254
|
+
let stepped = false;
|
|
202
255
|
for (const t of batch) {
|
|
203
256
|
if (t <= last) continue;
|
|
257
|
+
if (springGap(parsed.timezone, t)) {
|
|
258
|
+
const next = fireAfter(parsed, last);
|
|
259
|
+
if (next === null || next > to) return out;
|
|
260
|
+
out.push(next);
|
|
261
|
+
last = probe = next;
|
|
262
|
+
if (out.length > limit) return null;
|
|
263
|
+
stepped = true;
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
204
266
|
if (t > to) return out;
|
|
205
267
|
out.push(t);
|
|
206
268
|
last = t;
|
|
207
269
|
if (out.length > limit) return null;
|
|
208
270
|
}
|
|
271
|
+
if (stepped) continue;
|
|
209
272
|
const end = batch[batch.length - 1];
|
|
210
273
|
probe = end > probe ? end : probe + 36e5;
|
|
211
274
|
}
|
|
@@ -218,10 +281,14 @@ function nextFire(parsed, from, lastRunAt) {
|
|
|
218
281
|
}
|
|
219
282
|
return fireAfter(parsed, from);
|
|
220
283
|
}
|
|
221
|
-
function
|
|
284
|
+
function intervalDue(everyMs, startedAt, finishedAt) {
|
|
285
|
+
const due = startedAt + everyMs;
|
|
286
|
+
return finishedAt !== null && finishedAt > due ? finishedAt + everyMs : due;
|
|
287
|
+
}
|
|
288
|
+
function expectation(parsed, lastRunAt, registeredAt, graceMs2, lastFinishedAt = null) {
|
|
222
289
|
let dueAt;
|
|
223
290
|
if (parsed.kind === "interval") {
|
|
224
|
-
dueAt =
|
|
291
|
+
dueAt = lastRunAt === null ? registeredAt + parsed.everyMs : intervalDue(parsed.everyMs, lastRunAt, lastFinishedAt);
|
|
225
292
|
} else if (lastRunAt === null) {
|
|
226
293
|
dueAt = fireAfter(parsed, registeredAt - 1);
|
|
227
294
|
} else {
|
|
@@ -230,33 +297,25 @@ function expectation(parsed, lastRunAt, registeredAt, graceMs2) {
|
|
|
230
297
|
return dueAt === null ? null : { dueAt, deadline: dueAt + graceMs2 };
|
|
231
298
|
}
|
|
232
299
|
function dueAfterRun(parsed, startedAt) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
300
|
+
let next = fireAfter(parsed, startedAt);
|
|
301
|
+
for (let guard = 0; next !== null && guard < 1e5; guard++) {
|
|
302
|
+
const following = fireAfter(parsed, next);
|
|
303
|
+
const covers = runCovers(startedAt, next, following) || inSpringForwardGap(parsed, startedAt, next);
|
|
304
|
+
if (!covers) return next;
|
|
305
|
+
next = following;
|
|
306
|
+
}
|
|
307
|
+
return next;
|
|
238
308
|
}
|
|
239
309
|
function runCovers(startedAt, dueAt, followingAt = null) {
|
|
240
310
|
const slack = followingAt === null ? EARLY_SLACK_MS : Math.min(EARLY_SLACK_MS, Math.floor((followingAt - dueAt) / 2));
|
|
241
311
|
return startedAt >= dueAt - slack;
|
|
242
312
|
}
|
|
243
313
|
function inSpringForwardGap(parsed, startedAt, fireAt) {
|
|
244
|
-
const
|
|
245
|
-
if (
|
|
246
|
-
const
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
if (gap <= 0) return false;
|
|
250
|
-
let lo = fireAt - LOOKBACK;
|
|
251
|
-
let hi = fireAt;
|
|
252
|
-
while (hi - lo > 6e4) {
|
|
253
|
-
const mid = lo + Math.floor((hi - lo) / 2);
|
|
254
|
-
if (utcOffset(mid, parsed.timezone) === after2) hi = mid;
|
|
255
|
-
else lo = mid;
|
|
256
|
-
}
|
|
257
|
-
const jumpAt = Math.floor(hi / 6e4) * 6e4;
|
|
258
|
-
if (fireAt - jumpAt >= gap || startedAt < jumpAt - EARLY_SLACK_MS || startedAt >= fireAt) return false;
|
|
259
|
-
return fireAfter(parsed, jumpAt - 1) === fireAt;
|
|
314
|
+
const gap = springGap(parsed.timezone, fireAt);
|
|
315
|
+
if (!gap || startedAt < gap.jumpAt - EARLY_SLACK_MS || startedAt >= fireAt) return false;
|
|
316
|
+
const wall = wallCron(parsed);
|
|
317
|
+
const fires = (at) => runsAfter(wall, 1, at - 1)[0] === at;
|
|
318
|
+
return fires(fireAt + gap.beforeMs) && !fires(fireAt + gap.beforeMs + gap.gapMs);
|
|
260
319
|
}
|
|
261
320
|
var formatters = /* @__PURE__ */ new Map();
|
|
262
321
|
function utcOffset(at, timezone) {
|
|
@@ -353,15 +412,75 @@ var SEND_LEASE_MS = 5 * 6e4;
|
|
|
353
412
|
function alertKey(alert) {
|
|
354
413
|
return `${alert.type}|${alert.at}|${alert.run?.id ?? ""}`;
|
|
355
414
|
}
|
|
415
|
+
var HELD_TEXT_CAP = 2048;
|
|
416
|
+
var HELD_TEXT_SLACK = 64;
|
|
417
|
+
function heldRun(run) {
|
|
418
|
+
const error = typeof run.error === "string" && run.error.length > HELD_TEXT_CAP + HELD_TEXT_SLACK ? keepHead(run.error) : run.error;
|
|
419
|
+
const output = typeof run.output === "string" && run.output.length > HELD_TEXT_CAP + HELD_TEXT_SLACK ? keepTail(run.output) : run.output;
|
|
420
|
+
return error === run.error && output === run.output ? run : { ...run, error, output };
|
|
421
|
+
}
|
|
422
|
+
var HELD_MESSAGE_CAP = 4096;
|
|
423
|
+
function keepHead(text, cap = HELD_TEXT_CAP) {
|
|
424
|
+
let end = cap;
|
|
425
|
+
if (isHighSurrogate(text.charCodeAt(end - 1))) end -= 1;
|
|
426
|
+
return `${text.slice(0, end)}
|
|
427
|
+
[${text.length - end} more characters trimmed]`;
|
|
428
|
+
}
|
|
429
|
+
function keepTail(text) {
|
|
430
|
+
let start = text.length - HELD_TEXT_CAP;
|
|
431
|
+
if (isLowSurrogate(text.charCodeAt(start)) && isHighSurrogate(text.charCodeAt(start - 1))) start += 1;
|
|
432
|
+
return `[${start} earlier characters trimmed]
|
|
433
|
+
${text.slice(start)}`;
|
|
434
|
+
}
|
|
435
|
+
function isHighSurrogate(code) {
|
|
436
|
+
return code >= 55296 && code <= 56319;
|
|
437
|
+
}
|
|
438
|
+
function isLowSurrogate(code) {
|
|
439
|
+
return code >= 56320 && code <= 57343;
|
|
440
|
+
}
|
|
441
|
+
function heldAlert(alert) {
|
|
442
|
+
const run = alert.run;
|
|
443
|
+
const trimmedRun = isObject(run) ? heldRun(run) : run;
|
|
444
|
+
const message = alert.message;
|
|
445
|
+
const trimmedMessage = typeof message === "string" && message.length > HELD_MESSAGE_CAP + HELD_TEXT_SLACK ? keepHead(message, HELD_MESSAGE_CAP) : message;
|
|
446
|
+
if (trimmedRun === run && trimmedMessage === message) return alert;
|
|
447
|
+
return { ...alert, run: trimmedRun, message: trimmedMessage };
|
|
448
|
+
}
|
|
449
|
+
function channelKeys(names) {
|
|
450
|
+
const seen = /* @__PURE__ */ new Map();
|
|
451
|
+
const taken = /* @__PURE__ */ new Set();
|
|
452
|
+
return names.map((name) => {
|
|
453
|
+
let n = (seen.get(name) ?? 0) + 1;
|
|
454
|
+
seen.set(name, n);
|
|
455
|
+
let key = n === 1 ? name : `${name}#${n}`;
|
|
456
|
+
while (taken.has(key)) key = `${name}#${++n}`;
|
|
457
|
+
taken.add(key);
|
|
458
|
+
return key;
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
function owedChannels(alert, configured) {
|
|
462
|
+
const owed = alert.owed;
|
|
463
|
+
if (!Array.isArray(owed)) return [...configured];
|
|
464
|
+
return configured.filter((key) => owed.includes(key));
|
|
465
|
+
}
|
|
466
|
+
function afterSend(alert, tried, failed) {
|
|
467
|
+
const owed = tried.filter((key) => failed.includes(key));
|
|
468
|
+
return owed.length === 0 ? null : { ...alert, owed };
|
|
469
|
+
}
|
|
470
|
+
function withoutOwed(alert) {
|
|
471
|
+
if (!("owed" in alert)) return alert;
|
|
472
|
+
const { owed: _owed, ...rest } = alert;
|
|
473
|
+
return rest;
|
|
474
|
+
}
|
|
356
475
|
function newest(list, max) {
|
|
357
476
|
return { kept: list.slice(-max), dropped: Math.max(0, list.length - max) };
|
|
358
477
|
}
|
|
359
478
|
function queueUndelivered(state, alerts) {
|
|
360
479
|
const next = cloneState(state);
|
|
361
|
-
const byKey = new Map(alerts.map((alert) => [alertKey(alert), alert]));
|
|
480
|
+
const byKey = new Map(alerts.map((alert) => [alertKey(alert), heldAlert(alert)]));
|
|
362
481
|
const queue = next.undelivered.map((alert) => byKey.get(alertKey(alert)) ?? alert);
|
|
363
482
|
const known = new Set(queue.map(alertKey));
|
|
364
|
-
queue.push(...
|
|
483
|
+
queue.push(...[...byKey.values()].filter((alert) => !known.has(alertKey(alert))));
|
|
365
484
|
const { kept, dropped } = newest(queue, MAX_UNDELIVERED);
|
|
366
485
|
next.undelivered = kept;
|
|
367
486
|
return { state: next, dropped };
|
|
@@ -370,7 +489,7 @@ function holdAlerts(state, alerts, until, deferred) {
|
|
|
370
489
|
if (alerts.length === 0) return { state, dropped: 0 };
|
|
371
490
|
if (deferred) return queueUndelivered(state, alerts);
|
|
372
491
|
const next = cloneState(state);
|
|
373
|
-
const { kept, dropped } = newest([...next.sending ?? [], ...alerts.map((alert) => ({ until, alert }))], MAX_UNDELIVERED);
|
|
492
|
+
const { kept, dropped } = newest([...next.sending ?? [], ...alerts.map((alert) => ({ until, alert: heldAlert(alert) }))], MAX_UNDELIVERED);
|
|
374
493
|
next.sending = kept;
|
|
375
494
|
return { state: next, dropped };
|
|
376
495
|
}
|
|
@@ -384,7 +503,7 @@ function releaseSending(state, now) {
|
|
|
384
503
|
lapsed.filter((entry) => typeof entry?.alert === "object" && entry.alert !== null).map((entry) => entry.alert)
|
|
385
504
|
);
|
|
386
505
|
}
|
|
387
|
-
function recordSent(state, delivered, failed, stale, now) {
|
|
506
|
+
function recordSent(state, delivered, failed, stale, now, reached = delivered.length > 0) {
|
|
388
507
|
const next = cloneState(state);
|
|
389
508
|
const done = new Set([...delivered, ...stale].map(alertKey));
|
|
390
509
|
next.undelivered = next.undelivered.filter((alert) => !done.has(alertKey(alert)));
|
|
@@ -392,7 +511,7 @@ function recordSent(state, delivered, failed, stale, now) {
|
|
|
392
511
|
const held = (next.sending ?? []).filter((entry) => !(typeof entry?.alert === "object" && entry.alert !== null && sent.has(alertKey(entry.alert))));
|
|
393
512
|
if (held.length > 0) next.sending = held;
|
|
394
513
|
else delete next.sending;
|
|
395
|
-
if (
|
|
514
|
+
if (reached) next.lastAlertAt = now;
|
|
396
515
|
return queueUndelivered(next, failed);
|
|
397
516
|
}
|
|
398
517
|
function claimUndelivered(state, now, until) {
|
|
@@ -500,9 +619,21 @@ function formatNumber(n) {
|
|
|
500
619
|
if (Number.isInteger(n)) return n.toLocaleString("en-US");
|
|
501
620
|
return n.toLocaleString("en-US", { maximumFractionDigits: 4 });
|
|
502
621
|
}
|
|
503
|
-
function
|
|
622
|
+
function finishedAtOf(run) {
|
|
623
|
+
return run.status !== "running" && typeof run.finishedAt === "number" ? run.finishedAt : null;
|
|
624
|
+
}
|
|
625
|
+
function catchesUp(def, run, now) {
|
|
626
|
+
if (!def.schedule) return true;
|
|
627
|
+
try {
|
|
628
|
+
const exp = expectation(parseSchedule(def.schedule, def.timezone), run.startedAt, run.startedAt, graceMs(def), finishedAtOf(run));
|
|
629
|
+
return exp === null || now <= exp.deadline;
|
|
630
|
+
} catch {
|
|
631
|
+
return true;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
function onRunStart(def, run, state, now) {
|
|
504
635
|
const next = cloneState(state);
|
|
505
|
-
closeCondition(next, "missed");
|
|
636
|
+
if (catchesUp(def, run, now)) closeCondition(next, "missed");
|
|
506
637
|
closeCondition(next, "stuck");
|
|
507
638
|
return next;
|
|
508
639
|
}
|
|
@@ -511,7 +642,7 @@ function onRunFinish(def, run, state, history, now) {
|
|
|
511
642
|
const alerts = [];
|
|
512
643
|
if (run.status === "ok") {
|
|
513
644
|
next.consecutiveFailures = 0;
|
|
514
|
-
closeCondition(next, "missed");
|
|
645
|
+
if (catchesUp(def, run, now)) closeCondition(next, "missed");
|
|
515
646
|
closeCondition(next, "stuck");
|
|
516
647
|
closeCondition(next, "failed");
|
|
517
648
|
const slow = slowThreshold(def, history);
|
|
@@ -548,7 +679,7 @@ function onRunFinish(def, run, state, history, now) {
|
|
|
548
679
|
return { state: next, alerts };
|
|
549
680
|
}
|
|
550
681
|
next.consecutiveFailures = Math.min(next.consecutiveFailures + 1, MAX_DURATION_MS);
|
|
551
|
-
closeCondition(next, "missed");
|
|
682
|
+
if (catchesUp(def, run, now)) closeCondition(next, "missed");
|
|
552
683
|
const threshold = Math.max(1, def.failuresBeforeAlert ?? 1);
|
|
553
684
|
const condition = run.status === "timeout" ? "stuck" : "failed";
|
|
554
685
|
if (next.consecutiveFailures >= threshold) {
|
|
@@ -573,8 +704,8 @@ function onCheck(def, stored, lastRun, state, now) {
|
|
|
573
704
|
const parsed = parseSchedule(def.schedule, def.timezone);
|
|
574
705
|
const grace = graceMs(def);
|
|
575
706
|
const lastRunAt = lastRun?.startedAt ?? null;
|
|
576
|
-
const exp = expectation(parsed, lastRunAt, stored.createdAt, grace);
|
|
577
|
-
const nextExpectedAt = parsed.kind === "interval" ?
|
|
707
|
+
const exp = expectation(parsed, lastRunAt, stored.createdAt, grace, lastRun ? finishedAtOf(lastRun) : null);
|
|
708
|
+
const nextExpectedAt = parsed.kind === "interval" ? exp.dueAt : nextFire(parsed, now, null);
|
|
578
709
|
if (!exp) return { state: next, alerts, nextExpectedAt, dueAt: null };
|
|
579
710
|
if (parsed.kind === "interval" && lastRun?.status === "running") {
|
|
580
711
|
return { state: next, alerts, nextExpectedAt, dueAt: exp.dueAt };
|
|
@@ -860,11 +991,15 @@ function stringify(part) {
|
|
|
860
991
|
}
|
|
861
992
|
}
|
|
862
993
|
function createRecorder(run) {
|
|
994
|
+
const window = OUTPUT_CAP + REDACT_EDGE + 2;
|
|
995
|
+
const copy = (text) => JSON.parse(JSON.stringify(text));
|
|
863
996
|
const lines = [];
|
|
864
997
|
let size = 0;
|
|
865
998
|
const head = [];
|
|
866
999
|
let headSize = 0;
|
|
867
|
-
let
|
|
1000
|
+
let whole = "";
|
|
1001
|
+
let tail2 = "";
|
|
1002
|
+
let logged = false;
|
|
868
1003
|
const metrics = {};
|
|
869
1004
|
const controller = new AbortController();
|
|
870
1005
|
const context = {
|
|
@@ -873,16 +1008,23 @@ function createRecorder(run) {
|
|
|
873
1008
|
startedAt: run.startedAt,
|
|
874
1009
|
signal: controller.signal,
|
|
875
1010
|
log(...parts) {
|
|
876
|
-
|
|
1011
|
+
let line = parts.map(stringify).join(" ");
|
|
1012
|
+
const separator = logged ? "\n" : "";
|
|
1013
|
+
logged = true;
|
|
1014
|
+
if (whole !== null) whole = whole.length + separator.length + line.length <= 2 * OUTPUT_CAP ? whole + separator + line : null;
|
|
1015
|
+
tail2 = copy((tail2 + separator + copy(line.slice(-OUTPUT_CAP))).slice(-OUTPUT_CAP));
|
|
877
1016
|
if (headSize < OUTPUT_CAP) {
|
|
878
|
-
head.push(line);
|
|
1017
|
+
head.push(copy(line.slice(0, OUTPUT_CAP - headSize)));
|
|
879
1018
|
headSize += line.length + 1;
|
|
880
1019
|
}
|
|
1020
|
+
line = stripNul(line);
|
|
1021
|
+
if (line.length >= window) {
|
|
1022
|
+
line = copy(line.slice(-68609));
|
|
1023
|
+
}
|
|
881
1024
|
lines.push(line);
|
|
882
1025
|
size += line.length + 1;
|
|
883
|
-
while (size >
|
|
1026
|
+
while (size > window && lines.length > 1) {
|
|
884
1027
|
size -= lines.shift().length + 1;
|
|
885
|
-
dropped = true;
|
|
886
1028
|
}
|
|
887
1029
|
},
|
|
888
1030
|
metric(name, value) {
|
|
@@ -899,10 +1041,8 @@ function createRecorder(run) {
|
|
|
899
1041
|
context,
|
|
900
1042
|
output: () => lines.length === 0 ? null : lines.join("\n"),
|
|
901
1043
|
expectText: () => {
|
|
902
|
-
if (
|
|
903
|
-
|
|
904
|
-
if (!dropped && all.length <= 2 * OUTPUT_CAP) return all;
|
|
905
|
-
return head.join("\n").slice(0, OUTPUT_CAP) + "\n" + all.slice(-OUTPUT_CAP);
|
|
1044
|
+
if (!logged) return null;
|
|
1045
|
+
return whole ?? head.join("\n").slice(0, OUTPUT_CAP) + "\n" + tail2;
|
|
906
1046
|
},
|
|
907
1047
|
metrics: () => ({ ...metrics }),
|
|
908
1048
|
abort: () => controller.abort()
|
|
@@ -1076,7 +1216,7 @@ function staticAsset(path, base) {
|
|
|
1076
1216
|
}
|
|
1077
1217
|
|
|
1078
1218
|
// src/version.ts
|
|
1079
|
-
var VERSION = "0.
|
|
1219
|
+
var VERSION = "0.14.1";
|
|
1080
1220
|
var API_VERSION = 1;
|
|
1081
1221
|
|
|
1082
1222
|
// src/routes/timeline.ts
|
|
@@ -1129,13 +1269,13 @@ function dueTimes(job, parsed, runs, from, to) {
|
|
|
1129
1269
|
if (parsed.kind === "interval") {
|
|
1130
1270
|
const every = parsed.everyMs;
|
|
1131
1271
|
if ((to - from) / every > MAX_TICKS) return { times: [], dense: true };
|
|
1132
|
-
const
|
|
1272
|
+
const ordered = [...runs].sort((a, b) => a.startedAt - b.startedAt);
|
|
1273
|
+
const dues = ordered.map((r) => intervalDue(every, r.startedAt, finishedAtOf(r)));
|
|
1133
1274
|
const times2 = /* @__PURE__ */ new Set();
|
|
1134
|
-
for (const
|
|
1135
|
-
const t2 = start + every;
|
|
1275
|
+
for (const t2 of dues) {
|
|
1136
1276
|
if (t2 >= from && t2 <= to) times2.add(t2);
|
|
1137
1277
|
}
|
|
1138
|
-
let t =
|
|
1278
|
+
let t = dues.length ? dues[dues.length - 1] : job.nextExpectedAt;
|
|
1139
1279
|
if (t !== null) {
|
|
1140
1280
|
if (t < from) t += Math.ceil((from - t) / every) * every;
|
|
1141
1281
|
for (; t <= to; t += every) times2.add(t);
|
|
@@ -1148,8 +1288,8 @@ function dueTimes(job, parsed, runs, from, to) {
|
|
|
1148
1288
|
function missedAt(job, parsed, times, now) {
|
|
1149
1289
|
if (!parsed || !job.open.includes("missed")) return null;
|
|
1150
1290
|
const grace = safely(() => graceMs(job.definition), 0);
|
|
1151
|
-
const
|
|
1152
|
-
if (
|
|
1291
|
+
const lastRun = job.lastRun;
|
|
1292
|
+
if (lastRun) return safely(() => expectation(parsed, lastRun.startedAt, lastRun.startedAt, grace, finishedAtOf(lastRun))?.dueAt ?? null, null);
|
|
1153
1293
|
const past = times.filter((t) => t + grace < now);
|
|
1154
1294
|
return past.length ? past[past.length - 1] : null;
|
|
1155
1295
|
}
|
|
@@ -1858,13 +1998,15 @@ function signInReturn(referer, publicOrigin, base) {
|
|
|
1858
1998
|
return url.searchParams.has("token") ? `${base}/` : referer;
|
|
1859
1999
|
}
|
|
1860
2000
|
function stripBase(pathname, base) {
|
|
1861
|
-
|
|
2001
|
+
if (base !== "" && pathname !== base && !pathname.startsWith(`${base}/`)) return null;
|
|
2002
|
+
let path = pathname.slice(base.length);
|
|
1862
2003
|
if (path === "") path = "/";
|
|
1863
2004
|
if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1);
|
|
1864
2005
|
return path;
|
|
1865
2006
|
}
|
|
1866
2007
|
var MAX_REQUEST_BODY = 1 << 20;
|
|
1867
2008
|
var TOO_LARGE = /* @__PURE__ */ Symbol("too large");
|
|
2009
|
+
var BAD_BODY = /* @__PURE__ */ Symbol("malformed body");
|
|
1868
2010
|
async function readLimited(request) {
|
|
1869
2011
|
const length = Number(request.headers.get("content-length"));
|
|
1870
2012
|
if (Number.isFinite(length) && length > MAX_REQUEST_BODY) return TOO_LARGE;
|
|
@@ -1911,14 +2053,14 @@ async function readBody(request) {
|
|
|
1911
2053
|
try {
|
|
1912
2054
|
if (json2) {
|
|
1913
2055
|
const data = await copy.json();
|
|
1914
|
-
return typeof data === "object" && data !== null ? Object.fromEntries(Object.entries(data).map(([k, v]) => [k, String(v)])) :
|
|
2056
|
+
return typeof data === "object" && data !== null && !Array.isArray(data) ? Object.fromEntries(Object.entries(data).map(([k, v]) => [k, String(v)])) : BAD_BODY;
|
|
1915
2057
|
}
|
|
1916
2058
|
const fields = await copy.formData();
|
|
1917
2059
|
const out = {};
|
|
1918
2060
|
for (const [k, v] of fields.entries()) out[k] = String(v);
|
|
1919
2061
|
return out;
|
|
1920
2062
|
} catch {
|
|
1921
|
-
return
|
|
2063
|
+
return BAD_BODY;
|
|
1922
2064
|
}
|
|
1923
2065
|
}
|
|
1924
2066
|
function crossSite(request, publicOrigin) {
|
|
@@ -1984,6 +2126,7 @@ function buildRoutes(cw, options = {}) {
|
|
|
1984
2126
|
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);
|
|
1985
2127
|
if (token && method === "POST" && path === "/signin") {
|
|
1986
2128
|
const body = await readBody(request);
|
|
2129
|
+
if (body === BAD_BODY) return html(messagePage("Bad request", "Malformed request body", base), 400);
|
|
1987
2130
|
const sent = body === TOO_LARGE ? void 0 : body.token;
|
|
1988
2131
|
if (sent === void 0 || !constantTimeEqual(sent, token)) return signInPage();
|
|
1989
2132
|
return redirect(signInReturn(request.headers.get("referer"), publicOrigin, base), { "set-cookie": await signInCookie() });
|
|
@@ -2041,6 +2184,7 @@ function buildRoutes(cw, options = {}) {
|
|
|
2041
2184
|
if (parts[2] === "silence") {
|
|
2042
2185
|
const body = await readBody(request);
|
|
2043
2186
|
if (body === TOO_LARGE) return html(messagePage("Not silenced", "The request was too large.", base), 413);
|
|
2187
|
+
if (body === BAD_BODY) return html(messagePage("Not silenced", "Malformed request body", base), 400);
|
|
2044
2188
|
let duration;
|
|
2045
2189
|
try {
|
|
2046
2190
|
duration = silenceDuration(body.for);
|
|
@@ -2078,6 +2222,7 @@ function buildRoutes(cw, options = {}) {
|
|
|
2078
2222
|
if (rest[2] === "silence") {
|
|
2079
2223
|
const body = await readBody(request);
|
|
2080
2224
|
if (body === TOO_LARGE) return api({ ok: false, error: "Request body too large" }, 413);
|
|
2225
|
+
if (body === BAD_BODY) return api({ ok: false, error: "Malformed request body" }, 400);
|
|
2081
2226
|
let duration;
|
|
2082
2227
|
try {
|
|
2083
2228
|
duration = silenceDuration(body.for ?? url.searchParams.get("for"));
|
|
@@ -2114,6 +2259,7 @@ function buildRoutes(cw, options = {}) {
|
|
|
2114
2259
|
try {
|
|
2115
2260
|
const url = new URL(request.url);
|
|
2116
2261
|
const path = stripBase(url.pathname, base);
|
|
2262
|
+
if (path === null) return html(messagePage("Not found", url.pathname, base), 404);
|
|
2117
2263
|
wantsHtml = !path.startsWith("/api");
|
|
2118
2264
|
return await serve(request, url, path, wantsHtml);
|
|
2119
2265
|
} catch (e) {
|
|
@@ -2468,7 +2614,7 @@ var Cronwatch = class {
|
|
|
2468
2614
|
warnNoSecret() {
|
|
2469
2615
|
if (this.warnedNoSecret) return;
|
|
2470
2616
|
this.warnedNoSecret = true;
|
|
2471
|
-
this.
|
|
2617
|
+
this.report(new Error("handler() refused a request because no CRON_SECRET is set; pass secret: null to allow unauthenticated requests"), "handler");
|
|
2472
2618
|
}
|
|
2473
2619
|
/** onError, for places that must carry on even when onError itself throws. */
|
|
2474
2620
|
report(error, where) {
|
|
@@ -2481,6 +2627,13 @@ var Cronwatch = class {
|
|
|
2481
2627
|
if (!this.ready) {
|
|
2482
2628
|
this.ready = (async () => {
|
|
2483
2629
|
if (this.store.init) await this.store.init();
|
|
2630
|
+
const missing = ["compareAndSetState", "updateRunIf"].filter((method) => typeof this.store[method] !== "function");
|
|
2631
|
+
if (missing.length > 0) {
|
|
2632
|
+
this.report(
|
|
2633
|
+
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`),
|
|
2634
|
+
"store"
|
|
2635
|
+
);
|
|
2636
|
+
}
|
|
2484
2637
|
if (this.usingDefaultStore && isProduction()) {
|
|
2485
2638
|
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.");
|
|
2486
2639
|
}
|
|
@@ -2628,10 +2781,11 @@ var Cronwatch = class {
|
|
|
2628
2781
|
await this.store.insertRun({ ...run });
|
|
2629
2782
|
recorded = true;
|
|
2630
2783
|
} catch (e) {
|
|
2631
|
-
this.
|
|
2784
|
+
this.report(e, `recording ${name}`);
|
|
2632
2785
|
}
|
|
2633
|
-
const
|
|
2634
|
-
|
|
2786
|
+
const begun = { ...run };
|
|
2787
|
+
const started = recorded ? this.updateState(name, (before) => ({ state: holdRun(onRunStart(definition, begun, before, this.now()), run.id), result: void 0 })).then(() => {
|
|
2788
|
+
}, (e) => this.report(e, `starting ${name}`)) : Promise.resolve();
|
|
2635
2789
|
const recorder = createRecorder(run);
|
|
2636
2790
|
const clearTimer = after(timeoutMs(definition), () => recorder.abort());
|
|
2637
2791
|
let result;
|
|
@@ -2656,7 +2810,7 @@ var Cronwatch = class {
|
|
|
2656
2810
|
const ignored = await this.recordFinish(definition, run, recorded, finishedAt);
|
|
2657
2811
|
if (ignored) this.report(new Error(`run ${run.id} of ${name} ${ignored}; ignored`), `finishing ${name}`);
|
|
2658
2812
|
} catch (e) {
|
|
2659
|
-
this.
|
|
2813
|
+
this.report(e, `recording ${name}`);
|
|
2660
2814
|
}
|
|
2661
2815
|
return { run, result, error, threw };
|
|
2662
2816
|
}
|
|
@@ -2704,9 +2858,10 @@ var Cronwatch = class {
|
|
|
2704
2858
|
if (stored.job !== run.job) return `belongs to job "${stored.job}"`;
|
|
2705
2859
|
}
|
|
2706
2860
|
}
|
|
2861
|
+
const tracked = (await this.readState(run.job)).pendingRuns?.includes(run.id) ?? false;
|
|
2707
2862
|
const claim = await this.claimFinish(run);
|
|
2708
2863
|
if ("ignored" in claim) return claim.ignored;
|
|
2709
|
-
if (!claim.lateAfterTimeout || run.status === "ok") await this.finishRun(toStored(definition), run, finishedAt);
|
|
2864
|
+
if (!claim.lateAfterTimeout || run.status === "ok") await this.finishRun(toStored(definition), run, finishedAt, tracked && !claim.lateAfterTimeout);
|
|
2710
2865
|
return null;
|
|
2711
2866
|
}
|
|
2712
2867
|
/** A conditional write (Store.updateRunIf), or for a store without one, a read then a plain write. */
|
|
@@ -2759,7 +2914,7 @@ ${id}`;
|
|
|
2759
2914
|
await this.ensureReady();
|
|
2760
2915
|
stored = await this.store.getRun(id);
|
|
2761
2916
|
} catch (e) {
|
|
2762
|
-
this.
|
|
2917
|
+
this.report(e, `recording ${name}`);
|
|
2763
2918
|
}
|
|
2764
2919
|
if (stored) return this.existingHandle(definition, stored);
|
|
2765
2920
|
}
|
|
@@ -2783,10 +2938,10 @@ ${id}`;
|
|
|
2783
2938
|
} catch (e) {
|
|
2784
2939
|
const stored = id === void 0 ? null : await this.store.getRun(id).catch(() => null);
|
|
2785
2940
|
if (stored) return this.existingHandle(definition, stored);
|
|
2786
|
-
this.
|
|
2941
|
+
this.report(e, `recording ${name}`);
|
|
2787
2942
|
}
|
|
2788
2943
|
if (recorded) {
|
|
2789
|
-
await this.updateState(name, (before) => ({ state: holdRun(onRunStart(before), run.id), result: void 0 })).catch((e) => this.
|
|
2944
|
+
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}`));
|
|
2790
2945
|
}
|
|
2791
2946
|
return this.runHandle(definition, run.id, run, recorded, null);
|
|
2792
2947
|
}
|
|
@@ -2798,7 +2953,7 @@ ${id}`;
|
|
|
2798
2953
|
await this.ensureReady();
|
|
2799
2954
|
stored = await this.store.getRun(runId);
|
|
2800
2955
|
} catch (e) {
|
|
2801
|
-
this.
|
|
2956
|
+
this.report(e, `resuming ${definition.name}`);
|
|
2802
2957
|
return this.runHandle(definition, runId, null, true, null);
|
|
2803
2958
|
}
|
|
2804
2959
|
if (!stored) return this.runHandle(definition, runId, null, true, "was not found");
|
|
@@ -2999,9 +3154,9 @@ ${id}`;
|
|
|
2999
3154
|
throw e;
|
|
3000
3155
|
}
|
|
3001
3156
|
if (!evaluate) return [];
|
|
3002
|
-
await this.updateState(run.job, (before) => ({ state: holdRun(onRunStart(before), run.id), result: void 0 }));
|
|
3157
|
+
await this.updateState(run.job, (before) => ({ state: holdRun(onRunStart(definition, run, before, this.now()), run.id), result: void 0 }));
|
|
3003
3158
|
if (run.status === "running") return [];
|
|
3004
|
-
return this.finishRun(definition, run, this.now());
|
|
3159
|
+
return this.finishRun(definition, run, this.now(), true);
|
|
3005
3160
|
}
|
|
3006
3161
|
/** recordRun() for a run already stored. */
|
|
3007
3162
|
async recordOver(definition, stored, run, evaluate) {
|
|
@@ -3010,6 +3165,7 @@ ${id}`;
|
|
|
3010
3165
|
return [];
|
|
3011
3166
|
}
|
|
3012
3167
|
if (stored.status !== "running" && stored.status !== "timeout" || run.status === "running") return [];
|
|
3168
|
+
const tracked = (await this.readState(run.job)).pendingRuns?.includes(run.id) ?? false;
|
|
3013
3169
|
const claim = await this.claimFinish(run);
|
|
3014
3170
|
if ("ignored" in claim) {
|
|
3015
3171
|
this.report(new Error(`run ${run.id} of ${run.job} ${claim.ignored}; ignored`), `recording ${run.job}`);
|
|
@@ -3020,24 +3176,25 @@ ${id}`;
|
|
|
3020
3176
|
return [];
|
|
3021
3177
|
}
|
|
3022
3178
|
if (claim.lateAfterTimeout && run.status !== "ok") return [];
|
|
3023
|
-
return this.finishRun(definition, run, this.now());
|
|
3179
|
+
return this.finishRun(definition, run, this.now(), tracked && !claim.lateAfterTimeout);
|
|
3024
3180
|
}
|
|
3025
3181
|
/**
|
|
3026
3182
|
* Evaluate a finished run (ok, failed, or timed out by a check), already
|
|
3027
3183
|
* written, against the job's state and send what that produces. The
|
|
3028
3184
|
* alerts are written with that state (see outbox()). Never throws.
|
|
3029
3185
|
*/
|
|
3030
|
-
async finishRun(definition, run, now) {
|
|
3186
|
+
async finishRun(definition, run, now, pendingOnly = false) {
|
|
3031
3187
|
let held;
|
|
3032
3188
|
try {
|
|
3033
3189
|
let history = null;
|
|
3034
3190
|
({ result: held } = await this.updateState(run.job, async (previous) => {
|
|
3191
|
+
if (pendingOnly && !(previous.pendingRuns ?? []).includes(run.id)) return { state: previous, result: { alerts: [], dropped: 0 } };
|
|
3035
3192
|
history ??= await this.history(run);
|
|
3036
3193
|
const before = releaseRun(previous, run.id);
|
|
3037
3194
|
return this.outbox(applySilence(before, onRunFinish(definition, run, before, history, now), now), definition, now);
|
|
3038
3195
|
}));
|
|
3039
3196
|
} catch (e) {
|
|
3040
|
-
this.
|
|
3197
|
+
this.report(e, `evaluating ${run.job}`);
|
|
3041
3198
|
return [];
|
|
3042
3199
|
}
|
|
3043
3200
|
this.reportDropped(run.job, held.dropped);
|
|
@@ -3076,7 +3233,7 @@ ${id}`;
|
|
|
3076
3233
|
}
|
|
3077
3234
|
/**
|
|
3078
3235
|
* Look for missed and stuck runs across every job, send alerts, retry
|
|
3079
|
-
* alerts
|
|
3236
|
+
* alerts to the channels that refused them, and prune old runs. Call it from an interval
|
|
3080
3237
|
* (start()), a cron hitting the mounted routes, or by hand. Concurrent
|
|
3081
3238
|
* calls share one check.
|
|
3082
3239
|
*/
|
|
@@ -3176,7 +3333,7 @@ ${id}`;
|
|
|
3176
3333
|
continue;
|
|
3177
3334
|
}
|
|
3178
3335
|
if (run.status === "running" || typeof run.finishedAt !== "number" || run.finishedAt > now - SEND_LEASE_MS) continue;
|
|
3179
|
-
alerts.push(...await this.finishRun(stored.definition, run, now));
|
|
3336
|
+
alerts.push(...await this.finishRun(stored.definition, run, now, true));
|
|
3180
3337
|
} catch (e) {
|
|
3181
3338
|
this.onError(e, `evaluating run ${id} of ${stored.name}`);
|
|
3182
3339
|
}
|
|
@@ -3296,7 +3453,7 @@ ${id}`;
|
|
|
3296
3453
|
this.warnedDeferredStart = true;
|
|
3297
3454
|
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.');
|
|
3298
3455
|
}
|
|
3299
|
-
const tick = () => this.check().catch((e) => this.
|
|
3456
|
+
const tick = () => this.check().catch((e) => this.report(e, "check"));
|
|
3300
3457
|
this.timer = setInterval(tick, ms);
|
|
3301
3458
|
if (typeof this.timer === "object" && "unref" in this.timer) this.timer.unref();
|
|
3302
3459
|
this.firstTick = setTimeout(() => {
|
|
@@ -3345,21 +3502,28 @@ ${id}`;
|
|
|
3345
3502
|
if (alerts.length === 0 || this.deferDelivery) return alerts;
|
|
3346
3503
|
const delivered = [];
|
|
3347
3504
|
const failed = [];
|
|
3505
|
+
let reached = false;
|
|
3348
3506
|
for (const alert of alerts) {
|
|
3349
3507
|
if (this.triage && alert.type !== "recovered") await this.addTriage(alert, TRIAGE_TIMEOUT_MS);
|
|
3350
|
-
|
|
3508
|
+
const sent = await this.deliver(alert);
|
|
3509
|
+
reached ||= sent.reached;
|
|
3510
|
+
if (sent.left) failed.push(sent.left);
|
|
3511
|
+
else delivered.push(alert);
|
|
3351
3512
|
}
|
|
3352
|
-
await this.recordDelivery(name, delivered, failed, [], now);
|
|
3513
|
+
await this.recordDelivery(name, delivered, failed, [], now, reached);
|
|
3353
3514
|
return alerts;
|
|
3354
3515
|
}
|
|
3355
3516
|
/**
|
|
3356
|
-
* Send the alerts
|
|
3357
|
-
* first
|
|
3358
|
-
*
|
|
3359
|
-
*
|
|
3360
|
-
*
|
|
3361
|
-
*
|
|
3362
|
-
*
|
|
3517
|
+
* Send the alerts some channel refused last time, once each, oldest
|
|
3518
|
+
* first, to the channels that still owe them (HeldAlert.owed) and no
|
|
3519
|
+
* others, from the copy held in state, its run trimmed (heldRun). They
|
|
3520
|
+
* are claimed first (claimUndelivered): moved into `sending` with a lease,
|
|
3521
|
+
* in one conditional write, so when several processes check at once only
|
|
3522
|
+
* the one whose write lands sends them. An alert that no longer describes
|
|
3523
|
+
* the job (staleAlert) is dropped instead. Retries across a check share
|
|
3524
|
+
* RETRY_BUDGET_MS of wall-clock time; claimed alerts it has no time left
|
|
3525
|
+
* for go back to the queue for the next check. Returns the alerts some
|
|
3526
|
+
* channel took on this attempt.
|
|
3363
3527
|
*/
|
|
3364
3528
|
async retryUndelivered(name, state, now, budget) {
|
|
3365
3529
|
if ((state.undelivered ?? []).length === 0 || this.deferDelivery) return [];
|
|
@@ -3370,12 +3534,13 @@ ${id}`;
|
|
|
3370
3534
|
return { state: claim.state, result: claim.claimed };
|
|
3371
3535
|
}));
|
|
3372
3536
|
} catch (e) {
|
|
3373
|
-
this.
|
|
3537
|
+
this.report(e, `claiming undelivered alerts for ${name}`);
|
|
3374
3538
|
return [];
|
|
3375
3539
|
}
|
|
3376
3540
|
if (claimed.length === 0) return [];
|
|
3377
3541
|
const delivered = [];
|
|
3378
3542
|
const failed = [];
|
|
3543
|
+
const sent = [];
|
|
3379
3544
|
for (const alert of claimed) {
|
|
3380
3545
|
const left = RETRY_BUDGET_MS - budget.spentMs;
|
|
3381
3546
|
if (left <= 0) {
|
|
@@ -3384,45 +3549,61 @@ ${id}`;
|
|
|
3384
3549
|
}
|
|
3385
3550
|
const started = Date.now();
|
|
3386
3551
|
if (this.triage && alert.type !== "recovered" && alert.triage === void 0) await this.addTriage(alert, Math.min(TRIAGE_TIMEOUT_MS, left));
|
|
3387
|
-
|
|
3552
|
+
const outcome = await this.deliver(alert);
|
|
3553
|
+
if (outcome.reached) sent.push(withoutOwed(alert));
|
|
3554
|
+
if (outcome.left) failed.push(outcome.left);
|
|
3555
|
+
else delivered.push(alert);
|
|
3388
3556
|
budget.spentMs += Math.max(0, Date.now() - started);
|
|
3389
3557
|
}
|
|
3390
|
-
await this.recordDelivery(name, delivered, failed, [], now);
|
|
3391
|
-
return
|
|
3558
|
+
await this.recordDelivery(name, delivered, failed, [], now, sent.length > 0);
|
|
3559
|
+
return sent;
|
|
3392
3560
|
}
|
|
3393
3561
|
/**
|
|
3394
3562
|
* Mark delivered alerts done, drop stale ones, and keep failed ones for the
|
|
3395
3563
|
* next check, taking them all out of `sending` (recordSent). A failed alert
|
|
3396
|
-
* replaces its stored copy, so a triage made on this attempt
|
|
3397
|
-
*
|
|
3398
|
-
*
|
|
3564
|
+
* replaces its stored copy, so a triage made on this attempt and the
|
|
3565
|
+
* channels that still owe it are kept. lastAlertAt moves when `reached`,
|
|
3566
|
+
* some channel accepted an alert on this attempt. When this write fails,
|
|
3567
|
+
* alerts still in `sending` are retried once their lease runs out.
|
|
3399
3568
|
*/
|
|
3400
|
-
async recordDelivery(name, delivered, failed, dropped, now) {
|
|
3569
|
+
async recordDelivery(name, delivered, failed, dropped, now, reached) {
|
|
3401
3570
|
try {
|
|
3402
3571
|
const { result: trimmed } = await this.updateState(name, (previous) => {
|
|
3403
|
-
const { state, dropped: letGo } = recordSent(normalizeState(previous, name), delivered, failed, dropped, now);
|
|
3572
|
+
const { state, dropped: letGo } = recordSent(normalizeState(previous, name), delivered, failed, dropped, now, reached);
|
|
3404
3573
|
return { state, result: letGo };
|
|
3405
3574
|
});
|
|
3406
3575
|
this.reportDropped(name, trimmed);
|
|
3407
3576
|
} catch (e) {
|
|
3408
|
-
this.
|
|
3577
|
+
this.report(e, `recording alert delivery for ${name}`);
|
|
3409
3578
|
}
|
|
3410
3579
|
}
|
|
3411
|
-
/**
|
|
3580
|
+
/**
|
|
3581
|
+
* Send to every channel that still owes the alert (owedChannels: all of
|
|
3582
|
+
* them for an alert not sent before), at once, each failure reported under
|
|
3583
|
+
* its channel's name. `left` is the alert with `owed` cut to the channels
|
|
3584
|
+
* that refused it, or null when none did (or none is left to owe it);
|
|
3585
|
+
* `reached` is whether any channel accepted it. With no channels at all it
|
|
3586
|
+
* counts as delivered, as it always has.
|
|
3587
|
+
*/
|
|
3412
3588
|
async deliver(alert) {
|
|
3413
|
-
if (this.alerts.length === 0) return true;
|
|
3414
|
-
const
|
|
3415
|
-
|
|
3589
|
+
if (this.alerts.length === 0) return { left: null, reached: true };
|
|
3590
|
+
const keys = channelKeys(this.alerts.map((channel) => channel.name));
|
|
3591
|
+
const owed = owedChannels(alert, keys);
|
|
3592
|
+
if (owed.length === 0) return { left: null, reached: false };
|
|
3593
|
+
const plain = withoutOwed(alert);
|
|
3594
|
+
const failed = [];
|
|
3595
|
+
await Promise.all(
|
|
3596
|
+
this.alerts.map(async (channel, i) => {
|
|
3597
|
+
if (!owed.includes(keys[i])) return;
|
|
3416
3598
|
try {
|
|
3417
|
-
await withTimeout(channel.send(
|
|
3418
|
-
return true;
|
|
3599
|
+
await withTimeout(channel.send(plain, { onError: (e) => this.report(e, `alert channel ${channel.name}`) }), CHANNEL_TIMEOUT_MS);
|
|
3419
3600
|
} catch (e) {
|
|
3420
|
-
|
|
3421
|
-
|
|
3601
|
+
failed.push(keys[i]);
|
|
3602
|
+
this.report(e, `alert channel ${channel.name}`);
|
|
3422
3603
|
}
|
|
3423
3604
|
})
|
|
3424
3605
|
);
|
|
3425
|
-
return
|
|
3606
|
+
return { left: afterSend(alert, owed, failed), reached: failed.length < owed.length };
|
|
3426
3607
|
}
|
|
3427
3608
|
/** Sets alert.triage to the diagnosis, or to null when there is none, so it is tried once per alert. */
|
|
3428
3609
|
async addTriage(alert, timeout) {
|
|
@@ -3434,7 +3615,7 @@ ${id}`;
|
|
|
3434
3615
|
} catch (e) {
|
|
3435
3616
|
controller.abort();
|
|
3436
3617
|
alert.triage = null;
|
|
3437
|
-
this.
|
|
3618
|
+
this.report(e, `triage for ${alert.job}`);
|
|
3438
3619
|
}
|
|
3439
3620
|
}
|
|
3440
3621
|
};
|