@cronwatch/sdk 0.8.0 → 0.10.0
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.d.cts +1 -1
- package/dist/anthropic.d.ts +1 -1
- package/dist/bugsnag.d.cts +1 -1
- package/dist/bugsnag.d.ts +1 -1
- package/dist/{client-B5hM4l-D.d.ts → client-BcIjVBiZ.d.ts} +79 -13
- package/dist/{client-Di5HAWQz.d.cts → client-DUoSea7X.d.cts} +79 -13
- package/dist/d1.cjs +29 -19
- package/dist/d1.cjs.map +1 -1
- package/dist/d1.d.cts +1 -1
- package/dist/d1.d.ts +1 -1
- package/dist/d1.js +29 -19
- package/dist/d1.js.map +1 -1
- package/dist/datadog.d.cts +1 -1
- package/dist/datadog.d.ts +1 -1
- package/dist/discord.cjs +17 -2
- package/dist/discord.cjs.map +1 -1
- package/dist/discord.d.cts +11 -2
- package/dist/discord.d.ts +11 -2
- package/dist/discord.js +16 -3
- package/dist/discord.js.map +1 -1
- package/dist/{email-DX6gkd5H.d.cts → email-JZs3vxwq.d.cts} +1 -1
- package/dist/{email-Ciq7GRn4.d.ts → email-VrGsVz43.d.ts} +1 -1
- package/dist/honeybadger.d.cts +1 -1
- package/dist/honeybadger.d.ts +1 -1
- package/dist/index.cjs +253 -99
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +253 -99
- package/dist/index.js.map +1 -1
- package/dist/mailgun.d.cts +2 -2
- package/dist/mailgun.d.ts +2 -2
- package/dist/newrelic.d.cts +1 -1
- package/dist/newrelic.d.ts +1 -1
- package/dist/pg-cron.cjs +47 -5
- package/dist/pg-cron.cjs.map +1 -1
- package/dist/pg-cron.d.cts +5 -2
- package/dist/pg-cron.d.ts +5 -2
- package/dist/pg-cron.js +47 -5
- package/dist/pg-cron.js.map +1 -1
- package/dist/postgres.cjs +22 -14
- package/dist/postgres.cjs.map +1 -1
- package/dist/postgres.d.cts +1 -1
- package/dist/postgres.d.ts +1 -1
- package/dist/postgres.js +22 -14
- package/dist/postgres.js.map +1 -1
- package/dist/postmark.d.cts +2 -2
- package/dist/postmark.d.ts +2 -2
- package/dist/resend.d.cts +2 -2
- package/dist/resend.d.ts +2 -2
- package/dist/rollbar.d.cts +1 -1
- package/dist/rollbar.d.ts +1 -1
- package/dist/sendgrid.d.cts +2 -2
- package/dist/sendgrid.d.ts +2 -2
- package/dist/sentry.d.cts +1 -1
- package/dist/sentry.d.ts +1 -1
- package/dist/ses.d.cts +2 -2
- package/dist/ses.d.ts +2 -2
- package/dist/slack.d.cts +1 -1
- package/dist/slack.d.ts +1 -1
- package/dist/sqlite.cjs +26 -18
- package/dist/sqlite.cjs.map +1 -1
- package/dist/sqlite.d.cts +1 -1
- package/dist/sqlite.d.ts +1 -1
- package/dist/sqlite.js +26 -18
- package/dist/sqlite.js.map +1 -1
- package/dist/twilio.d.cts +1 -1
- package/dist/twilio.d.ts +1 -1
- package/dist/{types-BjBbCGeJ.d.cts → types-DyWCxlSi.d.cts} +20 -1
- package/dist/{types-BjBbCGeJ.d.ts → types-DyWCxlSi.d.ts} +20 -1
- package/dist/webhook.d.cts +1 -1
- package/dist/webhook.d.ts +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -268,23 +268,81 @@ function runDuration(startedAt, finishedAt) {
|
|
|
268
268
|
const ms = finishedAt - startedAt;
|
|
269
269
|
return ms > 0 ? Math.min(ms, MAX_DURATION_MS) : 0;
|
|
270
270
|
}
|
|
271
|
+
function silenceEnd(now, ms) {
|
|
272
|
+
return Math.min(now + Math.floor(Math.min(ms, MAX_DURATION_MS)), MAX_DURATION_MS);
|
|
273
|
+
}
|
|
271
274
|
function stateVersion(state) {
|
|
272
275
|
const version = state?.version;
|
|
273
276
|
return typeof version === "number" && Number.isSafeInteger(version) && version >= 0 ? version + 0 : 0;
|
|
274
277
|
}
|
|
278
|
+
function failureCount(state) {
|
|
279
|
+
const count = state?.consecutiveFailures;
|
|
280
|
+
if (typeof count !== "number" || !Number.isInteger(count) || count <= 0) return 0;
|
|
281
|
+
return Math.min(count, MAX_DURATION_MS);
|
|
282
|
+
}
|
|
275
283
|
function emptyState(job) {
|
|
276
284
|
return { job, open: {}, consecutiveFailures: 0, silencedUntil: null, lastAlertAt: null, pendingRecovery: [], undelivered: [] };
|
|
277
285
|
}
|
|
278
286
|
function normalizeState(state, job) {
|
|
279
287
|
if (!state) return emptyState(job);
|
|
288
|
+
const { sending, ...rest } = state;
|
|
280
289
|
return {
|
|
281
290
|
...emptyState(job),
|
|
282
|
-
...
|
|
291
|
+
...rest,
|
|
283
292
|
open: { ...state.open },
|
|
293
|
+
consecutiveFailures: failureCount(state),
|
|
284
294
|
pendingRecovery: [...state.pendingRecovery ?? []],
|
|
285
|
-
undelivered: [...state.undelivered ?? []]
|
|
295
|
+
undelivered: [...state.undelivered ?? []],
|
|
296
|
+
...Array.isArray(sending) && sending.length > 0 ? { sending: [...sending] } : {}
|
|
286
297
|
};
|
|
287
298
|
}
|
|
299
|
+
var MAX_UNDELIVERED = 20;
|
|
300
|
+
var SEND_LEASE_MS = 5 * 6e4;
|
|
301
|
+
function alertKey(alert) {
|
|
302
|
+
return `${alert.type}|${alert.at}|${alert.run?.id ?? ""}`;
|
|
303
|
+
}
|
|
304
|
+
function newest(list, max) {
|
|
305
|
+
return { kept: list.slice(-max), dropped: Math.max(0, list.length - max) };
|
|
306
|
+
}
|
|
307
|
+
function queueUndelivered(state, alerts) {
|
|
308
|
+
const next = cloneState(state);
|
|
309
|
+
const byKey = new Map(alerts.map((alert) => [alertKey(alert), alert]));
|
|
310
|
+
const queue = next.undelivered.map((alert) => byKey.get(alertKey(alert)) ?? alert);
|
|
311
|
+
const known = new Set(queue.map(alertKey));
|
|
312
|
+
queue.push(...alerts.filter((alert) => !known.has(alertKey(alert))));
|
|
313
|
+
const { kept, dropped } = newest(queue, MAX_UNDELIVERED);
|
|
314
|
+
next.undelivered = kept;
|
|
315
|
+
return { state: next, dropped };
|
|
316
|
+
}
|
|
317
|
+
function holdAlerts(state, alerts, until, deferred) {
|
|
318
|
+
if (alerts.length === 0) return { state, dropped: 0 };
|
|
319
|
+
if (deferred) return queueUndelivered(state, alerts);
|
|
320
|
+
const next = cloneState(state);
|
|
321
|
+
const { kept, dropped } = newest([...next.sending ?? [], ...alerts.map((alert) => ({ until, alert }))], MAX_UNDELIVERED);
|
|
322
|
+
next.sending = kept;
|
|
323
|
+
return { state: next, dropped };
|
|
324
|
+
}
|
|
325
|
+
function releaseSending(state, now) {
|
|
326
|
+
const sending = state.sending ?? [];
|
|
327
|
+
const lapsed = sending.filter((entry) => !(typeof entry?.until === "number" && entry.until > now));
|
|
328
|
+
if (lapsed.length === 0) return { state, dropped: 0 };
|
|
329
|
+
const held = sending.filter((entry) => !lapsed.includes(entry));
|
|
330
|
+
return queueUndelivered(
|
|
331
|
+
{ ...state, sending: held },
|
|
332
|
+
lapsed.filter((entry) => typeof entry?.alert === "object" && entry.alert !== null).map((entry) => entry.alert)
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
function recordSent(state, delivered, failed, stale, now) {
|
|
336
|
+
const next = cloneState(state);
|
|
337
|
+
const done = new Set([...delivered, ...stale].map(alertKey));
|
|
338
|
+
next.undelivered = next.undelivered.filter((alert) => !done.has(alertKey(alert)));
|
|
339
|
+
const sent = new Set([...delivered, ...failed, ...stale].map(alertKey));
|
|
340
|
+
const held = (next.sending ?? []).filter((entry) => !(typeof entry?.alert === "object" && entry.alert !== null && sent.has(alertKey(entry.alert))));
|
|
341
|
+
if (held.length > 0) next.sending = held;
|
|
342
|
+
else delete next.sending;
|
|
343
|
+
if (delivered.length > 0) next.lastAlertAt = now;
|
|
344
|
+
return queueUndelivered(next, failed);
|
|
345
|
+
}
|
|
288
346
|
function cloneState(state) {
|
|
289
347
|
return normalizeState(state, state.job);
|
|
290
348
|
}
|
|
@@ -379,7 +437,7 @@ function onRunFinish(def, run, state, history, now) {
|
|
|
379
437
|
}
|
|
380
438
|
return { state: next, alerts };
|
|
381
439
|
}
|
|
382
|
-
next.consecutiveFailures
|
|
440
|
+
next.consecutiveFailures = Math.min(next.consecutiveFailures + 1, MAX_DURATION_MS);
|
|
383
441
|
closeCondition(next, "missed");
|
|
384
442
|
const threshold = Math.max(1, def.failuresBeforeAlert ?? 1);
|
|
385
443
|
const condition = run.status === "timeout" ? "stuck" : "failed";
|
|
@@ -588,13 +646,22 @@ var OUTPUT_CAP = 16 * 1024;
|
|
|
588
646
|
function capOutput(text) {
|
|
589
647
|
const clean = stripNul(text);
|
|
590
648
|
if (clean.length <= OUTPUT_CAP) return clean;
|
|
591
|
-
return
|
|
649
|
+
return TRIMMED + clean.slice(clean.length - OUTPUT_CAP);
|
|
650
|
+
}
|
|
651
|
+
var TRIMMED = "[earlier output trimmed]\n";
|
|
652
|
+
var REDACT_EDGE = 3 * (OUTPUT_CAP + 1024);
|
|
653
|
+
function redactAndCap(text, redact) {
|
|
654
|
+
const clean = stripNul(text);
|
|
655
|
+
const from = clean.length - (OUTPUT_CAP + REDACT_EDGE);
|
|
656
|
+
if (from <= 0) return capOutput(redact(clean));
|
|
657
|
+
const redacted = stripNul(redact(clean.slice(from)));
|
|
658
|
+
return TRIMMED + redacted.slice(Math.max(redacted.length - OUTPUT_CAP, REDACT_EDGE));
|
|
592
659
|
}
|
|
593
660
|
function stripNul(text) {
|
|
594
661
|
return text.includes("\0") ? text.replace(/\u0000/g, "") : text;
|
|
595
662
|
}
|
|
596
|
-
function
|
|
597
|
-
return
|
|
663
|
+
function stripJsonNul(json2) {
|
|
664
|
+
return json2.includes("\\u0000") ? json2.replace(/\\(u0000|[\s\S])/g, (escape, next) => next === "u0000" ? "" : escape) : json2;
|
|
598
665
|
}
|
|
599
666
|
function describeError(error) {
|
|
600
667
|
if (error instanceof Error) {
|
|
@@ -704,7 +771,7 @@ function createRecorder(run) {
|
|
|
704
771
|
};
|
|
705
772
|
return {
|
|
706
773
|
context,
|
|
707
|
-
output: () => lines.length === 0 ? null :
|
|
774
|
+
output: () => lines.length === 0 ? null : lines.join("\n"),
|
|
708
775
|
expectText: () => {
|
|
709
776
|
if (lines.length === 0) return null;
|
|
710
777
|
const all = lines.join("\n");
|
|
@@ -1445,7 +1512,7 @@ function jobPage(job, runs, now, base, complete = true) {
|
|
|
1445
1512
|
run.error ? `<details class="out error" open><summary>error</summary><pre>${h(run.error)}</pre></details>` : "",
|
|
1446
1513
|
run.output ? `<details class="out"${run.status === "ok" ? "" : " open"}><summary>output</summary><pre>${h(run.output)}</pre></details>` : ""
|
|
1447
1514
|
].join("");
|
|
1448
|
-
const metrics = Object.entries(run.metrics).map(([k, v]) => `<span><span class="k">${h(k)}</span> ${h(Number.isInteger(v) ? v : v.toFixed(4))}</span>`).join("");
|
|
1515
|
+
const metrics = Object.entries(run.metrics ?? {}).filter(([, v]) => typeof v === "number" && Number.isFinite(v)).map(([k, v]) => `<span><span class="k">${h(k)}</span> ${h(Number.isInteger(v) ? v : v.toFixed(4))}</span>`).join("");
|
|
1449
1516
|
return `<tr${detail ? ` class="has-detail"` : ""}>
|
|
1450
1517
|
<td class="nowrap">${runState(run)}</td>
|
|
1451
1518
|
<td class="nowrap">${h(when2(run.startedAt, now))} <span class="muted">UTC</span><span class="sub">${stamp(run.startedAt, now)}</span></td>
|
|
@@ -1845,13 +1912,21 @@ function memory() {
|
|
|
1845
1912
|
const states = /* @__PURE__ */ new Map();
|
|
1846
1913
|
let seq = 0;
|
|
1847
1914
|
const clone = (v) => JSON.parse(JSON.stringify(v));
|
|
1915
|
+
const kept = (v) => JSON.parse(stripJsonNul(JSON.stringify(v)));
|
|
1916
|
+
const keptRun = (run) => ({
|
|
1917
|
+
...clone(run),
|
|
1918
|
+
trigger: stripNul(run.trigger),
|
|
1919
|
+
output: run.output === null ? null : stripNul(run.output),
|
|
1920
|
+
error: run.error === null ? null : stripNul(run.error),
|
|
1921
|
+
metrics: kept(run.metrics)
|
|
1922
|
+
});
|
|
1848
1923
|
const byName = (a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
|
|
1849
1924
|
return {
|
|
1850
1925
|
async upsertJob(definition, now) {
|
|
1851
1926
|
const existing = jobs.get(definition.name);
|
|
1852
1927
|
jobs.set(definition.name, {
|
|
1853
1928
|
name: definition.name,
|
|
1854
|
-
definition:
|
|
1929
|
+
definition: kept(definition),
|
|
1855
1930
|
createdAt: existing?.createdAt ?? now,
|
|
1856
1931
|
updatedAt: now
|
|
1857
1932
|
});
|
|
@@ -1873,19 +1948,19 @@ function memory() {
|
|
|
1873
1948
|
},
|
|
1874
1949
|
async insertRun(run) {
|
|
1875
1950
|
if (runs.has(run.id)) throw new Error(`run ${run.id} already exists`);
|
|
1876
|
-
runs.set(run.id,
|
|
1951
|
+
runs.set(run.id, keptRun(run));
|
|
1877
1952
|
order.set(run.id, ++seq);
|
|
1878
1953
|
},
|
|
1879
1954
|
async updateRun(run) {
|
|
1880
1955
|
const existing = runs.get(run.id);
|
|
1881
1956
|
if (!existing) return;
|
|
1882
|
-
const { status, finishedAt, durationMs, error, output, metrics } =
|
|
1957
|
+
const { status, finishedAt, durationMs, error, output, metrics } = keptRun(run);
|
|
1883
1958
|
runs.set(run.id, { ...existing, status, finishedAt, durationMs, error, output, metrics });
|
|
1884
1959
|
},
|
|
1885
1960
|
async updateRunIf(run, fromStatuses) {
|
|
1886
1961
|
const existing = runs.get(run.id);
|
|
1887
1962
|
if (!existing || !fromStatuses.includes(existing.status)) return false;
|
|
1888
|
-
const { status, finishedAt, durationMs, error, output, metrics } =
|
|
1963
|
+
const { status, finishedAt, durationMs, error, output, metrics } = keptRun(run);
|
|
1889
1964
|
runs.set(run.id, { ...existing, status, finishedAt, durationMs, error, output, metrics });
|
|
1890
1965
|
return true;
|
|
1891
1966
|
},
|
|
@@ -1908,19 +1983,19 @@ function memory() {
|
|
|
1908
1983
|
return s ? clone(s) : null;
|
|
1909
1984
|
},
|
|
1910
1985
|
async setState(state) {
|
|
1911
|
-
states.set(state.job,
|
|
1986
|
+
states.set(state.job, kept(state));
|
|
1912
1987
|
},
|
|
1913
1988
|
async compareAndSetState(state, expectedVersion) {
|
|
1914
1989
|
if (stateVersion(states.get(state.job)) !== expectedVersion) return false;
|
|
1915
|
-
states.set(state.job,
|
|
1990
|
+
states.set(state.job, kept(state));
|
|
1916
1991
|
return true;
|
|
1917
1992
|
},
|
|
1918
1993
|
async prune(before) {
|
|
1919
|
-
const
|
|
1920
|
-
for (const run of runs.values())
|
|
1994
|
+
const newest2 = /* @__PURE__ */ new Map();
|
|
1995
|
+
for (const run of runs.values()) newest2.set(run.job, Math.max(newest2.get(run.job) ?? -Infinity, run.startedAt));
|
|
1921
1996
|
let n = 0;
|
|
1922
1997
|
for (const [id, run] of runs) {
|
|
1923
|
-
if (run.status !== "running" && run.startedAt < before && run.startedAt <
|
|
1998
|
+
if (run.status !== "running" && run.startedAt < before && run.startedAt < newest2.get(run.job)) {
|
|
1924
1999
|
runs.delete(id);
|
|
1925
2000
|
order.delete(id);
|
|
1926
2001
|
n++;
|
|
@@ -1936,7 +2011,6 @@ var NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$/;
|
|
|
1936
2011
|
var TRIAGE_TIMEOUT_MS = 25e3;
|
|
1937
2012
|
var CHANNEL_TIMEOUT_MS = 15e3;
|
|
1938
2013
|
var PRUNE_INTERVAL_MS = 60 * 6e4;
|
|
1939
|
-
var MAX_UNDELIVERED = 20;
|
|
1940
2014
|
var RETRY_BUDGET_MS = 2e4;
|
|
1941
2015
|
var STATE_ATTEMPTS = 10;
|
|
1942
2016
|
var HISTORY_PAGE = BASELINE_WINDOW + 5;
|
|
@@ -1977,9 +2051,6 @@ function validateDefinition(def) {
|
|
|
1977
2051
|
function sameState(a, b) {
|
|
1978
2052
|
return JSON.stringify(a) === JSON.stringify(b);
|
|
1979
2053
|
}
|
|
1980
|
-
function alertKey(alert) {
|
|
1981
|
-
return `${alert.type}|${alert.at}|${alert.run?.id ?? ""}`;
|
|
1982
|
-
}
|
|
1983
2054
|
var CronWatch = class {
|
|
1984
2055
|
store;
|
|
1985
2056
|
alerts;
|
|
@@ -2000,6 +2071,8 @@ var CronWatch = class {
|
|
|
2000
2071
|
synced = /* @__PURE__ */ new Set();
|
|
2001
2072
|
/** The tail of each job's queue of state updates. See serial(). */
|
|
2002
2073
|
queues = /* @__PURE__ */ new Map();
|
|
2074
|
+
/** The tail of each job's queue of declaration writes. See sync(). */
|
|
2075
|
+
syncing = /* @__PURE__ */ new Map();
|
|
2003
2076
|
/** start() calls with an id still in flight, so two at once in this process record one run. */
|
|
2004
2077
|
starting = /* @__PURE__ */ new Map();
|
|
2005
2078
|
ready = null;
|
|
@@ -2138,26 +2211,69 @@ var CronWatch = class {
|
|
|
2138
2211
|
}
|
|
2139
2212
|
await this.ready;
|
|
2140
2213
|
}
|
|
2141
|
-
|
|
2214
|
+
/**
|
|
2215
|
+
* Writes the declaration of `definition`'s name as it stands, unless the
|
|
2216
|
+
* store has it. A handle kept from an earlier declaration writes the one
|
|
2217
|
+
* that replaced it, never its own over it, and one forgotten since writes
|
|
2218
|
+
* its own. The writes of one name take turns, in the order they were asked
|
|
2219
|
+
* for, so one still under way cannot land after a later one; and a name
|
|
2220
|
+
* declared again while its write was under way is still to be written. A
|
|
2221
|
+
* name is marked as written only while that same declaration stands, so a
|
|
2222
|
+
* forget that lands during the write (deleting the row after it) leaves
|
|
2223
|
+
* the name to be written again, as does one forgotten before it.
|
|
2224
|
+
*
|
|
2225
|
+
* With `confirm`, as a run starts, a name already written is read back:
|
|
2226
|
+
* another process may have forgotten the job since, and a job still
|
|
2227
|
+
* declared here comes back on its next run.
|
|
2228
|
+
*/
|
|
2229
|
+
async sync(definition, confirm = false) {
|
|
2142
2230
|
await this.ensureReady();
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2231
|
+
const name = definition.name;
|
|
2232
|
+
if (this.synced.has(name)) {
|
|
2233
|
+
if (!confirm || await this.store.getJob(name)) return;
|
|
2234
|
+
this.synced.delete(name);
|
|
2235
|
+
}
|
|
2236
|
+
await this.serial(name, async () => {
|
|
2237
|
+
if (this.synced.has(name)) return;
|
|
2238
|
+
const standing = this.definitions.get(name) ?? definition;
|
|
2239
|
+
await this.store.upsertJob(toStored(standing), this.now());
|
|
2240
|
+
if (this.definitions.get(name) === standing) this.synced.add(name);
|
|
2241
|
+
}, this.syncing);
|
|
2242
|
+
}
|
|
2243
|
+
/**
|
|
2244
|
+
* Every stored job, once each declaration has been written. A job declared
|
|
2245
|
+
* here that the store no longer has was forgotten by another process after
|
|
2246
|
+
* this one wrote it: it is written again, as its next run would, so it is
|
|
2247
|
+
* checked and shown while any process still declares it.
|
|
2248
|
+
*/
|
|
2249
|
+
async storedJobs() {
|
|
2250
|
+
for (const definition of this.definitions.values()) await this.sync(definition);
|
|
2251
|
+
const jobs = await this.store.listJobs();
|
|
2252
|
+
const listed = new Set(jobs.map((job) => job.name));
|
|
2253
|
+
const missing = [...this.definitions.values()].filter((definition) => !listed.has(definition.name));
|
|
2254
|
+
if (missing.length === 0) return jobs;
|
|
2255
|
+
for (const definition of missing) {
|
|
2256
|
+
if (this.definitions.get(definition.name) !== definition) continue;
|
|
2257
|
+
this.synced.delete(definition.name);
|
|
2258
|
+
await this.sync(definition);
|
|
2259
|
+
}
|
|
2260
|
+
return this.store.listJobs();
|
|
2146
2261
|
}
|
|
2147
2262
|
/**
|
|
2148
2263
|
* Runs `fn` after every earlier state update for the same job has settled,
|
|
2149
2264
|
* so two runs (or a run and a check) in this process never read and write
|
|
2150
2265
|
* the job's state over each other. Other processes are coordinated by
|
|
2151
|
-
* updateState() instead.
|
|
2266
|
+
* updateState() instead. sync() queues a job's declaration writes the same
|
|
2267
|
+
* way, in `syncing`.
|
|
2152
2268
|
*/
|
|
2153
|
-
serial(job, fn) {
|
|
2154
|
-
const result = (
|
|
2269
|
+
serial(job, fn, queues = this.queues) {
|
|
2270
|
+
const result = (queues.get(job) ?? Promise.resolve()).then(fn);
|
|
2155
2271
|
const tail2 = result.then(() => {
|
|
2156
2272
|
}, () => {
|
|
2157
2273
|
});
|
|
2158
|
-
|
|
2274
|
+
queues.set(job, tail2);
|
|
2159
2275
|
void tail2.then(() => {
|
|
2160
|
-
if (
|
|
2276
|
+
if (queues.get(job) === tail2) queues.delete(job);
|
|
2161
2277
|
});
|
|
2162
2278
|
return result;
|
|
2163
2279
|
}
|
|
@@ -2218,7 +2334,7 @@ var CronWatch = class {
|
|
|
2218
2334
|
};
|
|
2219
2335
|
let recorded = false;
|
|
2220
2336
|
try {
|
|
2221
|
-
await this.sync(definition);
|
|
2337
|
+
await this.sync(definition, true);
|
|
2222
2338
|
await this.store.insertRun({ ...run });
|
|
2223
2339
|
recorded = true;
|
|
2224
2340
|
} catch (e) {
|
|
@@ -2243,7 +2359,7 @@ var CronWatch = class {
|
|
|
2243
2359
|
run.finishedAt = finishedAt;
|
|
2244
2360
|
run.durationMs = runDuration(startedAt, finishedAt);
|
|
2245
2361
|
run.metrics = recorder.metrics();
|
|
2246
|
-
run.output = recorder.output() ?? (typeof result === "string" ?
|
|
2362
|
+
run.output = recorder.output() ?? (typeof result === "string" ? result : null);
|
|
2247
2363
|
this.conclude(definition, run, result, error, threw, recorder.expectText() ?? (typeof result === "string" ? result : null));
|
|
2248
2364
|
await started;
|
|
2249
2365
|
try {
|
|
@@ -2256,12 +2372,13 @@ var CronWatch = class {
|
|
|
2256
2372
|
}
|
|
2257
2373
|
/**
|
|
2258
2374
|
* Sets a finished run's status and error from how it ended, then redacts
|
|
2259
|
-
* its output and error. Shared by execute()
|
|
2375
|
+
* its output and error and caps them, in that order. Shared by execute()
|
|
2376
|
+
* and RunHandle.finish().
|
|
2260
2377
|
*/
|
|
2261
2378
|
conclude(definition, run, result, error, threw, expectText) {
|
|
2262
2379
|
if (threw) {
|
|
2263
2380
|
run.status = "failed";
|
|
2264
|
-
run.error =
|
|
2381
|
+
run.error = describeError(error);
|
|
2265
2382
|
} else if (result instanceof Response && result.status >= 400) {
|
|
2266
2383
|
run.status = "failed";
|
|
2267
2384
|
run.error = `HTTP ${result.status}${result.statusText ? ` ${result.statusText}` : ""}`;
|
|
@@ -2274,8 +2391,8 @@ var CronWatch = class {
|
|
|
2274
2391
|
run.status = "ok";
|
|
2275
2392
|
}
|
|
2276
2393
|
}
|
|
2277
|
-
if (run.output !== null) run.output =
|
|
2278
|
-
if (run.error !== null) run.error =
|
|
2394
|
+
if (run.output !== null) run.output = redactAndCap(run.output, this.redact);
|
|
2395
|
+
if (run.error !== null) run.error = redactAndCap(run.error, this.redact);
|
|
2279
2396
|
}
|
|
2280
2397
|
/**
|
|
2281
2398
|
* Writes a finished run and evaluates it. `recorded` says whether its
|
|
@@ -2370,7 +2487,7 @@ ${id}`;
|
|
|
2370
2487
|
};
|
|
2371
2488
|
let recorded = false;
|
|
2372
2489
|
try {
|
|
2373
|
-
await this.sync(definition);
|
|
2490
|
+
await this.sync(definition, true);
|
|
2374
2491
|
await this.store.insertRun({ ...run });
|
|
2375
2492
|
recorded = true;
|
|
2376
2493
|
} catch (e) {
|
|
@@ -2470,14 +2587,15 @@ ${id}`;
|
|
|
2470
2587
|
const result = typeof outcome === "string" ? outcome : failed ? void 0 : outcome?.result;
|
|
2471
2588
|
const error = failed ? outcome.error : void 0;
|
|
2472
2589
|
const finishedAt = self.now();
|
|
2473
|
-
const added = recorder.output() ?? (typeof result === "string" ?
|
|
2590
|
+
const added = recorder.output() ?? (typeof result === "string" ? result : null);
|
|
2474
2591
|
const run = {
|
|
2475
2592
|
...from,
|
|
2476
2593
|
status: "running",
|
|
2477
2594
|
finishedAt,
|
|
2478
2595
|
durationMs: runDuration(from.startedAt, finishedAt),
|
|
2479
2596
|
error: null,
|
|
2480
|
-
|
|
2597
|
+
// Capped by conclude(), after it is redacted.
|
|
2598
|
+
output: joinLines(from.output, added),
|
|
2481
2599
|
metrics: { ...from.metrics, ...recorder.metrics() }
|
|
2482
2600
|
};
|
|
2483
2601
|
const expectText = joinLines(head, joinLines(from.output, recorder.expectText() ?? (typeof result === "string" ? result : null)));
|
|
@@ -2526,7 +2644,7 @@ ${id}`;
|
|
|
2526
2644
|
self.report(new Error(`run ${id} of ${name} belongs to job "${stored.job}"; ignored`), `flushing ${name}`);
|
|
2527
2645
|
return;
|
|
2528
2646
|
}
|
|
2529
|
-
const output = lines === null ? stored.output : joinOutput(stored.output,
|
|
2647
|
+
const output = lines === null ? stored.output : joinOutput(stored.output, redactAndCap(lines, self.redact));
|
|
2530
2648
|
if (!await self.writeRunIf({ ...stored, output, metrics: { ...stored.metrics, ...metrics } }, ["running"])) return putBack();
|
|
2531
2649
|
const text = taken.expectText();
|
|
2532
2650
|
if (text !== null && (head === null || head.length < OUTPUT_CAP)) head = (joinLines(head, text) ?? "").slice(0, OUTPUT_CAP);
|
|
@@ -2551,12 +2669,19 @@ ${id}`;
|
|
|
2551
2669
|
* alone and reported. A finished run is judged as if it had been wrapped
|
|
2552
2670
|
* here (expect, failures, duration, budgets) and its output and error are
|
|
2553
2671
|
* redacted the same way; one finishing after a check marked it timeout is
|
|
2554
|
-
* judged only when it succeeded, as RunHandle.finish() does.
|
|
2555
|
-
*
|
|
2672
|
+
* judged only when it succeeded, as RunHandle.finish() does. A metric that
|
|
2673
|
+
* is not a finite number throws before anything is written, as
|
|
2674
|
+
* job.metric() does. Returns the alerts it sent.
|
|
2556
2675
|
*/
|
|
2557
2676
|
async recordRun(input, options = {}) {
|
|
2558
2677
|
const declared = this.definitions.get(input.job);
|
|
2559
2678
|
if (!declared) throw new Error(`recordRun: job "${input.job}" is not declared; call job() first`);
|
|
2679
|
+
if (input.id.includes("\0")) throw new Error(`recordRun: run ids cannot contain a NUL character (job "${input.job}")`);
|
|
2680
|
+
for (const [metric, value] of Object.entries(input.metrics ?? {})) {
|
|
2681
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
2682
|
+
throw new Error(`recordRun: metric "${metric}" must be a finite number (job "${input.job}", run "${input.id}")`);
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2560
2685
|
await this.sync(declared);
|
|
2561
2686
|
const run = { ...input, metrics: { ...input.metrics } };
|
|
2562
2687
|
if (run.status === "ok") {
|
|
@@ -2566,8 +2691,8 @@ ${id}`;
|
|
|
2566
2691
|
run.error = unmet;
|
|
2567
2692
|
}
|
|
2568
2693
|
}
|
|
2569
|
-
if (run.output !== null) run.output =
|
|
2570
|
-
if (run.error !== null) run.error =
|
|
2694
|
+
if (run.output !== null) run.output = redactAndCap(run.output, this.redact);
|
|
2695
|
+
if (run.error !== null) run.error = redactAndCap(run.error, this.redact);
|
|
2571
2696
|
const evaluate = options.evaluate !== false;
|
|
2572
2697
|
const definition = toStored(declared);
|
|
2573
2698
|
const stored = await this.store.getRun(run.id);
|
|
@@ -2601,22 +2726,42 @@ ${id}`;
|
|
|
2601
2726
|
}
|
|
2602
2727
|
/**
|
|
2603
2728
|
* Evaluate a finished run (ok, failed, or timed out by a check), already
|
|
2604
|
-
* written, against the job's state and send what that produces.
|
|
2729
|
+
* written, against the job's state and send what that produces. The
|
|
2730
|
+
* alerts are written with that state (see outbox()). Never throws.
|
|
2605
2731
|
*/
|
|
2606
2732
|
async finishRun(definition, run, now) {
|
|
2607
|
-
let
|
|
2733
|
+
let held;
|
|
2608
2734
|
try {
|
|
2609
2735
|
let history = null;
|
|
2610
|
-
({ result:
|
|
2736
|
+
({ result: held } = await this.updateState(run.job, async (previous) => {
|
|
2611
2737
|
history ??= await this.history(run);
|
|
2612
|
-
|
|
2613
|
-
return { state: settled.state, result: settled.alerts };
|
|
2738
|
+
return this.outbox(applySilence(previous, onRunFinish(definition, run, previous, history, now), now), definition, now);
|
|
2614
2739
|
}));
|
|
2615
2740
|
} catch (e) {
|
|
2616
2741
|
this.onError(e, `evaluating ${run.job}`);
|
|
2617
2742
|
return [];
|
|
2618
2743
|
}
|
|
2619
|
-
|
|
2744
|
+
this.reportDropped(run.job, held.dropped);
|
|
2745
|
+
return this.dispatch(run.job, held.alerts, now);
|
|
2746
|
+
}
|
|
2747
|
+
/**
|
|
2748
|
+
* An evaluation as it is written: its drafts composed into alerts and held
|
|
2749
|
+
* in the same state (holdAlerts), so the write that opens a condition also
|
|
2750
|
+
* keeps its alerts, and a process that stops before sending them does not
|
|
2751
|
+
* lose them. Called inside updateState(), so it only computes.
|
|
2752
|
+
*/
|
|
2753
|
+
outbox(settled, definition, now) {
|
|
2754
|
+
const alerts = settled.alerts.map((draft) => composeAlert(draft, definition, now));
|
|
2755
|
+
const { state, dropped } = holdAlerts(settled.state, alerts, this.now() + SEND_LEASE_MS, this.deferDelivery);
|
|
2756
|
+
return { state, result: { alerts, dropped } };
|
|
2757
|
+
}
|
|
2758
|
+
/** Reports alerts let go because a job's queue was full. */
|
|
2759
|
+
reportDropped(name, dropped) {
|
|
2760
|
+
if (dropped <= 0) return;
|
|
2761
|
+
this.report(
|
|
2762
|
+
new Error(`${dropped} undelivered alert${dropped === 1 ? "" : "s"} for ${name} dropped: only the newest ${MAX_UNDELIVERED} are kept for retry`),
|
|
2763
|
+
`alert queue for ${name}`
|
|
2764
|
+
);
|
|
2620
2765
|
}
|
|
2621
2766
|
/**
|
|
2622
2767
|
* The runs before `run`, newest first, with up to BASELINE_WINDOW
|
|
@@ -2656,11 +2801,13 @@ ${id}`;
|
|
|
2656
2801
|
}
|
|
2657
2802
|
for (const definition of this.definitions.values()) await this.sync(definition);
|
|
2658
2803
|
const now = this.now();
|
|
2659
|
-
for (const
|
|
2804
|
+
for (const listed of await this.store.runningRuns()) {
|
|
2660
2805
|
try {
|
|
2661
|
-
const declared = this.definitions.get(
|
|
2662
|
-
const definition = declared ? toStored(declared) : (await this.store.getJob(
|
|
2663
|
-
if (!definition || !isStuck(definition,
|
|
2806
|
+
const declared = this.definitions.get(listed.job);
|
|
2807
|
+
const definition = declared ? toStored(declared) : (await this.store.getJob(listed.job))?.definition;
|
|
2808
|
+
if (!definition || !isStuck(definition, listed, now)) continue;
|
|
2809
|
+
const run = await this.store.getRun(listed.id);
|
|
2810
|
+
if (!run || run.status !== "running" || run.job !== listed.job) continue;
|
|
2664
2811
|
const timeout = timeoutMs(definition);
|
|
2665
2812
|
run.status = "timeout";
|
|
2666
2813
|
run.finishedAt = now;
|
|
@@ -2669,23 +2816,26 @@ ${id}`;
|
|
|
2669
2816
|
if (!await this.writeRunIf(run, ["running"])) continue;
|
|
2670
2817
|
alerts.push(...await this.finishRun(definition, run, now));
|
|
2671
2818
|
} catch (e) {
|
|
2672
|
-
this.onError(e, `checking ${
|
|
2819
|
+
this.onError(e, `checking ${listed.job}`);
|
|
2673
2820
|
}
|
|
2674
2821
|
}
|
|
2675
2822
|
const jobs = [];
|
|
2676
2823
|
const retries = { spentMs: 0 };
|
|
2677
|
-
for (const stored of await this.
|
|
2824
|
+
for (const stored of await this.storedJobs()) {
|
|
2678
2825
|
try {
|
|
2679
2826
|
const recent = await this.store.listRuns(stored.name, BASELINE_WINDOW);
|
|
2680
2827
|
let nextExpectedAt = null;
|
|
2681
|
-
const { state, result:
|
|
2828
|
+
const { state, result: held } = await this.updateState(stored.name, (previous) => {
|
|
2682
2829
|
const evaluation = onCheck(stored.definition, stored, recent[0] ?? null, previous, now);
|
|
2683
2830
|
nextExpectedAt = evaluation.nextExpectedAt;
|
|
2684
2831
|
const settled = applySilence(previous, evaluation, now);
|
|
2685
|
-
|
|
2832
|
+
const released = releaseSending(settled.state, this.now());
|
|
2833
|
+
const out = this.outbox({ state: released.state, alerts: settled.alerts }, stored.definition, now);
|
|
2834
|
+
return { state: out.state, result: { alerts: out.result.alerts, dropped: released.dropped + out.result.dropped } };
|
|
2686
2835
|
});
|
|
2836
|
+
this.reportDropped(stored.name, held.dropped);
|
|
2687
2837
|
alerts.push(...await this.retryUndelivered(stored.name, state, now, retries));
|
|
2688
|
-
alerts.push(...await this.dispatch(
|
|
2838
|
+
alerts.push(...await this.dispatch(stored.name, held.alerts, now));
|
|
2689
2839
|
jobs.push(summarize(stored, recent, state, nextExpectedAt, now));
|
|
2690
2840
|
} catch (e) {
|
|
2691
2841
|
this.onError(e, `checking ${stored.name}`);
|
|
@@ -2729,16 +2879,17 @@ ${id}`;
|
|
|
2729
2879
|
/** Every job's summary with its newest `limit` runs, read together. What the dashboard shows. */
|
|
2730
2880
|
async jobsWithRuns(limit = 20) {
|
|
2731
2881
|
await this.ensureReady();
|
|
2732
|
-
|
|
2882
|
+
const jobs = await this.storedJobs();
|
|
2733
2883
|
const now = this.now();
|
|
2734
2884
|
const out = [];
|
|
2735
|
-
for (const stored of
|
|
2885
|
+
for (const stored of jobs) out.push(await this.snapshot(stored, now, clampLimit(limit, 20, 0)));
|
|
2736
2886
|
return out;
|
|
2737
2887
|
}
|
|
2888
|
+
/** A job's summary, or null for one the store does not have. One declared here and forgotten elsewhere is written again, as storedJobs() does. */
|
|
2738
2889
|
async jobSummary(name) {
|
|
2739
2890
|
await this.ensureReady();
|
|
2740
2891
|
const definition = this.definitions.get(name);
|
|
2741
|
-
if (definition) await this.sync(definition);
|
|
2892
|
+
if (definition) await this.sync(definition, true);
|
|
2742
2893
|
const stored = await this.store.getJob(name);
|
|
2743
2894
|
if (!stored) return null;
|
|
2744
2895
|
return (await this.snapshot(stored, this.now(), 0)).job;
|
|
@@ -2752,11 +2903,14 @@ ${id}`;
|
|
|
2752
2903
|
await this.ensureReady();
|
|
2753
2904
|
return this.store.getRun(id);
|
|
2754
2905
|
}
|
|
2755
|
-
/**
|
|
2906
|
+
/**
|
|
2907
|
+
* Stop alerts for a job for a while. State keeps updating underneath. The
|
|
2908
|
+
* end is a whole millisecond, held at 2^53 - 1 (see silenceEnd).
|
|
2909
|
+
*/
|
|
2756
2910
|
async silence(name, duration) {
|
|
2757
2911
|
const ms = parseDuration(duration, "silence duration");
|
|
2758
2912
|
return this.patchState(name, (state) => {
|
|
2759
|
-
state.silencedUntil = this.now()
|
|
2913
|
+
state.silencedUntil = silenceEnd(this.now(), ms);
|
|
2760
2914
|
});
|
|
2761
2915
|
}
|
|
2762
2916
|
async unsilence(name) {
|
|
@@ -2774,7 +2928,11 @@ ${id}`;
|
|
|
2774
2928
|
});
|
|
2775
2929
|
return state;
|
|
2776
2930
|
}
|
|
2777
|
-
/**
|
|
2931
|
+
/**
|
|
2932
|
+
* Remove a job and its runs from the store. A job still declared in code
|
|
2933
|
+
* comes back: here on its next run, and in any other process that declares
|
|
2934
|
+
* it on its next run there, or at that process's next check or dashboard read.
|
|
2935
|
+
*/
|
|
2778
2936
|
async forget(name) {
|
|
2779
2937
|
await this.ensureReady();
|
|
2780
2938
|
this.definitions.delete(name);
|
|
@@ -2812,32 +2970,37 @@ ${id}`;
|
|
|
2812
2970
|
this.timer = null;
|
|
2813
2971
|
this.firstTick = null;
|
|
2814
2972
|
}
|
|
2973
|
+
/**
|
|
2974
|
+
* Stop the interval, wait for a check already under way (bounded by its
|
|
2975
|
+
* own channel, triage and retry timeouts; what it throws was reported to
|
|
2976
|
+
* whoever started it), then close the store, so that check neither writes
|
|
2977
|
+
* after the store is closed nor loses the alerts it would queue.
|
|
2978
|
+
*/
|
|
2815
2979
|
async close() {
|
|
2816
2980
|
this.stop();
|
|
2981
|
+
if (this.checking) await this.checking.catch(() => {
|
|
2982
|
+
});
|
|
2817
2983
|
if (this.store.close) await this.store.close();
|
|
2818
2984
|
}
|
|
2819
2985
|
/**
|
|
2820
|
-
*
|
|
2821
|
-
*
|
|
2822
|
-
* the delivery fields are written back, onto
|
|
2986
|
+
* Triage and send each alert the outbox holds (see outbox()). The state,
|
|
2987
|
+
* with the alerts in it, was saved before this, so a slow channel holds up
|
|
2988
|
+
* nothing else; afterwards only the delivery fields are written back, onto
|
|
2989
|
+
* a fresh read of the state, and the alerts leave `sending`. Triage is made
|
|
2990
|
+
* here, never stored with the held alert: the write that opens a condition
|
|
2991
|
+
* cannot wait for it, and a retry triages an alert that has none. With
|
|
2992
|
+
* deliver: "check" the alerts were queued for a check elsewhere instead.
|
|
2823
2993
|
*/
|
|
2824
|
-
async dispatch(
|
|
2825
|
-
if (
|
|
2826
|
-
const composed = [];
|
|
2994
|
+
async dispatch(name, alerts, now) {
|
|
2995
|
+
if (alerts.length === 0 || this.deferDelivery) return alerts;
|
|
2827
2996
|
const delivered = [];
|
|
2828
2997
|
const failed = [];
|
|
2829
|
-
for (const
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
failed.push(alert);
|
|
2833
|
-
} else {
|
|
2834
|
-
if (this.triage && alert.type !== "recovered") await this.addTriage(alert, TRIAGE_TIMEOUT_MS);
|
|
2835
|
-
(await this.deliver(alert) ? delivered : failed).push(alert);
|
|
2836
|
-
}
|
|
2837
|
-
composed.push(alert);
|
|
2998
|
+
for (const alert of alerts) {
|
|
2999
|
+
if (this.triage && alert.type !== "recovered") await this.addTriage(alert, TRIAGE_TIMEOUT_MS);
|
|
3000
|
+
(await this.deliver(alert) ? delivered : failed).push(alert);
|
|
2838
3001
|
}
|
|
2839
|
-
await this.recordDelivery(
|
|
2840
|
-
return
|
|
3002
|
+
await this.recordDelivery(name, delivered, failed, [], now);
|
|
3003
|
+
return alerts;
|
|
2841
3004
|
}
|
|
2842
3005
|
/**
|
|
2843
3006
|
* Send the alerts that no channel accepted last time, once each, oldest
|
|
@@ -2866,28 +3029,18 @@ ${id}`;
|
|
|
2866
3029
|
}
|
|
2867
3030
|
/**
|
|
2868
3031
|
* Mark delivered alerts done, drop stale ones, and keep failed ones for the
|
|
2869
|
-
* next check
|
|
2870
|
-
*
|
|
3032
|
+
* next check, taking them all out of `sending` (recordSent). A failed alert
|
|
3033
|
+
* replaces its stored copy, so a triage made on this attempt is kept.
|
|
3034
|
+
* lastAlertAt moves only on a delivery. When this write fails, alerts
|
|
3035
|
+
* still in `sending` are retried once their lease runs out.
|
|
2871
3036
|
*/
|
|
2872
3037
|
async recordDelivery(name, delivered, failed, dropped, now) {
|
|
2873
3038
|
try {
|
|
2874
3039
|
const { result: trimmed } = await this.updateState(name, (previous) => {
|
|
2875
|
-
const state = normalizeState(previous, name);
|
|
2876
|
-
|
|
2877
|
-
const retried = new Map(failed.map((a) => [alertKey(a), a]));
|
|
2878
|
-
const kept = state.undelivered.filter((a) => !done.has(alertKey(a))).map((a) => retried.get(alertKey(a)) ?? a);
|
|
2879
|
-
const known = new Set(kept.map(alertKey));
|
|
2880
|
-
kept.push(...failed.filter((a) => !known.has(alertKey(a))));
|
|
2881
|
-
state.undelivered = kept.slice(-MAX_UNDELIVERED);
|
|
2882
|
-
if (delivered.length > 0) state.lastAlertAt = now;
|
|
2883
|
-
return { state, result: Math.max(0, kept.length - MAX_UNDELIVERED) };
|
|
3040
|
+
const { state, dropped: letGo } = recordSent(normalizeState(previous, name), delivered, failed, dropped, now);
|
|
3041
|
+
return { state, result: letGo };
|
|
2884
3042
|
});
|
|
2885
|
-
|
|
2886
|
-
this.onError(
|
|
2887
|
-
new Error(`${trimmed} undelivered alert${trimmed === 1 ? "" : "s"} for ${name} dropped: only the newest ${MAX_UNDELIVERED} are kept for retry`),
|
|
2888
|
-
`alert queue for ${name}`
|
|
2889
|
-
);
|
|
2890
|
-
}
|
|
3043
|
+
this.reportDropped(name, trimmed);
|
|
2891
3044
|
} catch (e) {
|
|
2892
3045
|
this.onError(e, `recording alert delivery for ${name}`);
|
|
2893
3046
|
}
|
|
@@ -2927,6 +3080,7 @@ function checkRunId(job, id, method) {
|
|
|
2927
3080
|
if (typeof id !== "string" || id.length === 0 || id.length > 200) {
|
|
2928
3081
|
throw new Error(`job "${job}": ${method}() needs a run id of 1 to 200 characters (got ${typeof id === "string" ? `${id.length} characters` : typeof id})`);
|
|
2929
3082
|
}
|
|
3083
|
+
if (id.includes("\0")) throw new Error(`job "${job}": ${method}() cannot take a run id containing a NUL character`);
|
|
2930
3084
|
if (id.startsWith(RESERVED_RUN_ID_PREFIX)) {
|
|
2931
3085
|
throw new Error(`job "${job}": ${method}() cannot take a run id starting with "${RESERVED_RUN_ID_PREFIX}", which the pg_cron source uses for its runs`);
|
|
2932
3086
|
}
|