@cronwatch/sdk 0.9.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-CQm-8-mR.d.ts → client-BcIjVBiZ.d.ts} +68 -13
- package/dist/{client-D35cfnNV.d.cts → client-DUoSea7X.d.cts} +68 -13
- 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.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-D1d5niMy.d.cts → email-JZs3vxwq.d.cts} +1 -1
- package/dist/{email-BxCp9RQV.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 +207 -88
- 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 +207 -88
- 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 +2 -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 +2 -1
- package/dist/pg-cron.js.map +1 -1
- 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.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.map +1 -1
- package/dist/sqlite.d.cts +1 -1
- package/dist/sqlite.d.ts +1 -1
- package/dist/sqlite.js.map +1 -1
- package/dist/twilio.d.cts +1 -1
- package/dist/twilio.d.ts +1 -1
- package/dist/{types-Cz47Boks.d.cts → types-DyWCxlSi.d.cts} +14 -1
- package/dist/{types-Cz47Boks.d.ts → types-DyWCxlSi.d.ts} +14 -1
- package/dist/webhook.d.cts +1 -1
- package/dist/webhook.d.ts +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { C as CronWatchOptions, a as CronWatch } from './client-
|
|
2
|
-
export { F as FetchHandler, H as HandlerFn, b as HandlerOptions, J as JobContext, c as JobFn, d as JobHandle, R as RecordRunOptions, e as Routes, f as RoutesOptions, g as RunHandle, h as RunOutcome, S as Source, i as SourceHost, j as StartOptions, k as consoleChannel, l as createRoutes, m as custom } from './client-
|
|
3
|
-
import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-
|
|
4
|
-
export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach, C as ChannelContext, f as CheckResult, g as Condition, E as ExpectRule, J as JobDefinition, h as JobHealth, i as JobOptions, j as JobState, k as JobSummary, R as Run, l as RunStatus, m as StoredJob, T as TriageContext,
|
|
1
|
+
import { C as CronWatchOptions, a as CronWatch } from './client-DUoSea7X.cjs';
|
|
2
|
+
export { F as FetchHandler, H as HandlerFn, b as HandlerOptions, J as JobContext, c as JobFn, d as JobHandle, R as RecordRunOptions, e as Routes, f as RoutesOptions, g as RunHandle, h as RunOutcome, S as Source, i as SourceHost, j as StartOptions, k as consoleChannel, l as createRoutes, m as custom } from './client-DUoSea7X.cjs';
|
|
3
|
+
import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-DyWCxlSi.cjs';
|
|
4
|
+
export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach, C as ChannelContext, f as CheckResult, g as Condition, E as ExpectRule, J as JobDefinition, h as JobHealth, i as JobOptions, j as JobState, k as JobSummary, R as Run, l as RunStatus, m as SendingAlert, n as StoredJob, T as TriageContext, o as TriageFn } from './types-DyWCxlSi.cjs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Keeps everything in process memory. The default when no store is given,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { C as CronWatchOptions, a as CronWatch } from './client-
|
|
2
|
-
export { F as FetchHandler, H as HandlerFn, b as HandlerOptions, J as JobContext, c as JobFn, d as JobHandle, R as RecordRunOptions, e as Routes, f as RoutesOptions, g as RunHandle, h as RunOutcome, S as Source, i as SourceHost, j as StartOptions, k as consoleChannel, l as createRoutes, m as custom } from './client-
|
|
3
|
-
import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-
|
|
4
|
-
export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach, C as ChannelContext, f as CheckResult, g as Condition, E as ExpectRule, J as JobDefinition, h as JobHealth, i as JobOptions, j as JobState, k as JobSummary, R as Run, l as RunStatus, m as StoredJob, T as TriageContext,
|
|
1
|
+
import { C as CronWatchOptions, a as CronWatch } from './client-BcIjVBiZ.js';
|
|
2
|
+
export { F as FetchHandler, H as HandlerFn, b as HandlerOptions, J as JobContext, c as JobFn, d as JobHandle, R as RecordRunOptions, e as Routes, f as RoutesOptions, g as RunHandle, h as RunOutcome, S as Source, i as SourceHost, j as StartOptions, k as consoleChannel, l as createRoutes, m as custom } from './client-BcIjVBiZ.js';
|
|
3
|
+
import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-DyWCxlSi.js';
|
|
4
|
+
export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach, C as ChannelContext, f as CheckResult, g as Condition, E as ExpectRule, J as JobDefinition, h as JobHealth, i as JobOptions, j as JobState, k as JobSummary, R as Run, l as RunStatus, m as SendingAlert, n as StoredJob, T as TriageContext, o as TriageFn } from './types-DyWCxlSi.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Keeps everything in process memory. The default when no store is given,
|
package/dist/index.js
CHANGED
|
@@ -266,6 +266,9 @@ function runDuration(startedAt, finishedAt) {
|
|
|
266
266
|
const ms = finishedAt - startedAt;
|
|
267
267
|
return ms > 0 ? Math.min(ms, MAX_DURATION_MS) : 0;
|
|
268
268
|
}
|
|
269
|
+
function silenceEnd(now, ms) {
|
|
270
|
+
return Math.min(now + Math.floor(Math.min(ms, MAX_DURATION_MS)), MAX_DURATION_MS);
|
|
271
|
+
}
|
|
269
272
|
function stateVersion(state) {
|
|
270
273
|
const version = state?.version;
|
|
271
274
|
return typeof version === "number" && Number.isSafeInteger(version) && version >= 0 ? version + 0 : 0;
|
|
@@ -280,15 +283,64 @@ function emptyState(job) {
|
|
|
280
283
|
}
|
|
281
284
|
function normalizeState(state, job) {
|
|
282
285
|
if (!state) return emptyState(job);
|
|
286
|
+
const { sending, ...rest } = state;
|
|
283
287
|
return {
|
|
284
288
|
...emptyState(job),
|
|
285
|
-
...
|
|
289
|
+
...rest,
|
|
286
290
|
open: { ...state.open },
|
|
287
291
|
consecutiveFailures: failureCount(state),
|
|
288
292
|
pendingRecovery: [...state.pendingRecovery ?? []],
|
|
289
|
-
undelivered: [...state.undelivered ?? []]
|
|
293
|
+
undelivered: [...state.undelivered ?? []],
|
|
294
|
+
...Array.isArray(sending) && sending.length > 0 ? { sending: [...sending] } : {}
|
|
290
295
|
};
|
|
291
296
|
}
|
|
297
|
+
var MAX_UNDELIVERED = 20;
|
|
298
|
+
var SEND_LEASE_MS = 5 * 6e4;
|
|
299
|
+
function alertKey(alert) {
|
|
300
|
+
return `${alert.type}|${alert.at}|${alert.run?.id ?? ""}`;
|
|
301
|
+
}
|
|
302
|
+
function newest(list, max) {
|
|
303
|
+
return { kept: list.slice(-max), dropped: Math.max(0, list.length - max) };
|
|
304
|
+
}
|
|
305
|
+
function queueUndelivered(state, alerts) {
|
|
306
|
+
const next = cloneState(state);
|
|
307
|
+
const byKey = new Map(alerts.map((alert) => [alertKey(alert), alert]));
|
|
308
|
+
const queue = next.undelivered.map((alert) => byKey.get(alertKey(alert)) ?? alert);
|
|
309
|
+
const known = new Set(queue.map(alertKey));
|
|
310
|
+
queue.push(...alerts.filter((alert) => !known.has(alertKey(alert))));
|
|
311
|
+
const { kept, dropped } = newest(queue, MAX_UNDELIVERED);
|
|
312
|
+
next.undelivered = kept;
|
|
313
|
+
return { state: next, dropped };
|
|
314
|
+
}
|
|
315
|
+
function holdAlerts(state, alerts, until, deferred) {
|
|
316
|
+
if (alerts.length === 0) return { state, dropped: 0 };
|
|
317
|
+
if (deferred) return queueUndelivered(state, alerts);
|
|
318
|
+
const next = cloneState(state);
|
|
319
|
+
const { kept, dropped } = newest([...next.sending ?? [], ...alerts.map((alert) => ({ until, alert }))], MAX_UNDELIVERED);
|
|
320
|
+
next.sending = kept;
|
|
321
|
+
return { state: next, dropped };
|
|
322
|
+
}
|
|
323
|
+
function releaseSending(state, now) {
|
|
324
|
+
const sending = state.sending ?? [];
|
|
325
|
+
const lapsed = sending.filter((entry) => !(typeof entry?.until === "number" && entry.until > now));
|
|
326
|
+
if (lapsed.length === 0) return { state, dropped: 0 };
|
|
327
|
+
const held = sending.filter((entry) => !lapsed.includes(entry));
|
|
328
|
+
return queueUndelivered(
|
|
329
|
+
{ ...state, sending: held },
|
|
330
|
+
lapsed.filter((entry) => typeof entry?.alert === "object" && entry.alert !== null).map((entry) => entry.alert)
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
function recordSent(state, delivered, failed, stale, now) {
|
|
334
|
+
const next = cloneState(state);
|
|
335
|
+
const done = new Set([...delivered, ...stale].map(alertKey));
|
|
336
|
+
next.undelivered = next.undelivered.filter((alert) => !done.has(alertKey(alert)));
|
|
337
|
+
const sent = new Set([...delivered, ...failed, ...stale].map(alertKey));
|
|
338
|
+
const held = (next.sending ?? []).filter((entry) => !(typeof entry?.alert === "object" && entry.alert !== null && sent.has(alertKey(entry.alert))));
|
|
339
|
+
if (held.length > 0) next.sending = held;
|
|
340
|
+
else delete next.sending;
|
|
341
|
+
if (delivered.length > 0) next.lastAlertAt = now;
|
|
342
|
+
return queueUndelivered(next, failed);
|
|
343
|
+
}
|
|
292
344
|
function cloneState(state) {
|
|
293
345
|
return normalizeState(state, state.job);
|
|
294
346
|
}
|
|
@@ -592,7 +644,16 @@ var OUTPUT_CAP = 16 * 1024;
|
|
|
592
644
|
function capOutput(text) {
|
|
593
645
|
const clean = stripNul(text);
|
|
594
646
|
if (clean.length <= OUTPUT_CAP) return clean;
|
|
595
|
-
return
|
|
647
|
+
return TRIMMED + clean.slice(clean.length - OUTPUT_CAP);
|
|
648
|
+
}
|
|
649
|
+
var TRIMMED = "[earlier output trimmed]\n";
|
|
650
|
+
var REDACT_EDGE = 3 * (OUTPUT_CAP + 1024);
|
|
651
|
+
function redactAndCap(text, redact) {
|
|
652
|
+
const clean = stripNul(text);
|
|
653
|
+
const from = clean.length - (OUTPUT_CAP + REDACT_EDGE);
|
|
654
|
+
if (from <= 0) return capOutput(redact(clean));
|
|
655
|
+
const redacted = stripNul(redact(clean.slice(from)));
|
|
656
|
+
return TRIMMED + redacted.slice(Math.max(redacted.length - OUTPUT_CAP, REDACT_EDGE));
|
|
596
657
|
}
|
|
597
658
|
function stripNul(text) {
|
|
598
659
|
return text.includes("\0") ? text.replace(/\u0000/g, "") : text;
|
|
@@ -600,9 +661,6 @@ function stripNul(text) {
|
|
|
600
661
|
function stripJsonNul(json2) {
|
|
601
662
|
return json2.includes("\\u0000") ? json2.replace(/\\(u0000|[\s\S])/g, (escape, next) => next === "u0000" ? "" : escape) : json2;
|
|
602
663
|
}
|
|
603
|
-
function errorMessage(error) {
|
|
604
|
-
return capOutput(describeError(error));
|
|
605
|
-
}
|
|
606
664
|
function describeError(error) {
|
|
607
665
|
if (error instanceof Error) {
|
|
608
666
|
const frames = (error.stack ?? "").split("\n").filter((line) => /^\s+at /.test(line)).slice(0, 5);
|
|
@@ -711,7 +769,7 @@ function createRecorder(run) {
|
|
|
711
769
|
};
|
|
712
770
|
return {
|
|
713
771
|
context,
|
|
714
|
-
output: () => lines.length === 0 ? null :
|
|
772
|
+
output: () => lines.length === 0 ? null : lines.join("\n"),
|
|
715
773
|
expectText: () => {
|
|
716
774
|
if (lines.length === 0) return null;
|
|
717
775
|
const all = lines.join("\n");
|
|
@@ -1452,7 +1510,7 @@ function jobPage(job, runs, now, base, complete = true) {
|
|
|
1452
1510
|
run.error ? `<details class="out error" open><summary>error</summary><pre>${h(run.error)}</pre></details>` : "",
|
|
1453
1511
|
run.output ? `<details class="out"${run.status === "ok" ? "" : " open"}><summary>output</summary><pre>${h(run.output)}</pre></details>` : ""
|
|
1454
1512
|
].join("");
|
|
1455
|
-
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("");
|
|
1513
|
+
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("");
|
|
1456
1514
|
return `<tr${detail ? ` class="has-detail"` : ""}>
|
|
1457
1515
|
<td class="nowrap">${runState(run)}</td>
|
|
1458
1516
|
<td class="nowrap">${h(when2(run.startedAt, now))} <span class="muted">UTC</span><span class="sub">${stamp(run.startedAt, now)}</span></td>
|
|
@@ -1931,11 +1989,11 @@ function memory() {
|
|
|
1931
1989
|
return true;
|
|
1932
1990
|
},
|
|
1933
1991
|
async prune(before) {
|
|
1934
|
-
const
|
|
1935
|
-
for (const run of runs.values())
|
|
1992
|
+
const newest2 = /* @__PURE__ */ new Map();
|
|
1993
|
+
for (const run of runs.values()) newest2.set(run.job, Math.max(newest2.get(run.job) ?? -Infinity, run.startedAt));
|
|
1936
1994
|
let n = 0;
|
|
1937
1995
|
for (const [id, run] of runs) {
|
|
1938
|
-
if (run.status !== "running" && run.startedAt < before && run.startedAt <
|
|
1996
|
+
if (run.status !== "running" && run.startedAt < before && run.startedAt < newest2.get(run.job)) {
|
|
1939
1997
|
runs.delete(id);
|
|
1940
1998
|
order.delete(id);
|
|
1941
1999
|
n++;
|
|
@@ -1951,7 +2009,6 @@ var NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$/;
|
|
|
1951
2009
|
var TRIAGE_TIMEOUT_MS = 25e3;
|
|
1952
2010
|
var CHANNEL_TIMEOUT_MS = 15e3;
|
|
1953
2011
|
var PRUNE_INTERVAL_MS = 60 * 6e4;
|
|
1954
|
-
var MAX_UNDELIVERED = 20;
|
|
1955
2012
|
var RETRY_BUDGET_MS = 2e4;
|
|
1956
2013
|
var STATE_ATTEMPTS = 10;
|
|
1957
2014
|
var HISTORY_PAGE = BASELINE_WINDOW + 5;
|
|
@@ -1992,9 +2049,6 @@ function validateDefinition(def) {
|
|
|
1992
2049
|
function sameState(a, b) {
|
|
1993
2050
|
return JSON.stringify(a) === JSON.stringify(b);
|
|
1994
2051
|
}
|
|
1995
|
-
function alertKey(alert) {
|
|
1996
|
-
return `${alert.type}|${alert.at}|${alert.run?.id ?? ""}`;
|
|
1997
|
-
}
|
|
1998
2052
|
var CronWatch = class {
|
|
1999
2053
|
store;
|
|
2000
2054
|
alerts;
|
|
@@ -2161,19 +2215,48 @@ var CronWatch = class {
|
|
|
2161
2215
|
* that replaced it, never its own over it, and one forgotten since writes
|
|
2162
2216
|
* its own. The writes of one name take turns, in the order they were asked
|
|
2163
2217
|
* for, so one still under way cannot land after a later one; and a name
|
|
2164
|
-
* declared again while its write was under way is still to be written.
|
|
2218
|
+
* declared again while its write was under way is still to be written. A
|
|
2219
|
+
* name is marked as written only while that same declaration stands, so a
|
|
2220
|
+
* forget that lands during the write (deleting the row after it) leaves
|
|
2221
|
+
* the name to be written again, as does one forgotten before it.
|
|
2222
|
+
*
|
|
2223
|
+
* With `confirm`, as a run starts, a name already written is read back:
|
|
2224
|
+
* another process may have forgotten the job since, and a job still
|
|
2225
|
+
* declared here comes back on its next run.
|
|
2165
2226
|
*/
|
|
2166
|
-
async sync(definition) {
|
|
2227
|
+
async sync(definition, confirm = false) {
|
|
2167
2228
|
await this.ensureReady();
|
|
2168
2229
|
const name = definition.name;
|
|
2169
|
-
if (this.synced.has(name))
|
|
2230
|
+
if (this.synced.has(name)) {
|
|
2231
|
+
if (!confirm || await this.store.getJob(name)) return;
|
|
2232
|
+
this.synced.delete(name);
|
|
2233
|
+
}
|
|
2170
2234
|
await this.serial(name, async () => {
|
|
2171
2235
|
if (this.synced.has(name)) return;
|
|
2172
2236
|
const standing = this.definitions.get(name) ?? definition;
|
|
2173
2237
|
await this.store.upsertJob(toStored(standing), this.now());
|
|
2174
|
-
if (
|
|
2238
|
+
if (this.definitions.get(name) === standing) this.synced.add(name);
|
|
2175
2239
|
}, this.syncing);
|
|
2176
2240
|
}
|
|
2241
|
+
/**
|
|
2242
|
+
* Every stored job, once each declaration has been written. A job declared
|
|
2243
|
+
* here that the store no longer has was forgotten by another process after
|
|
2244
|
+
* this one wrote it: it is written again, as its next run would, so it is
|
|
2245
|
+
* checked and shown while any process still declares it.
|
|
2246
|
+
*/
|
|
2247
|
+
async storedJobs() {
|
|
2248
|
+
for (const definition of this.definitions.values()) await this.sync(definition);
|
|
2249
|
+
const jobs = await this.store.listJobs();
|
|
2250
|
+
const listed = new Set(jobs.map((job) => job.name));
|
|
2251
|
+
const missing = [...this.definitions.values()].filter((definition) => !listed.has(definition.name));
|
|
2252
|
+
if (missing.length === 0) return jobs;
|
|
2253
|
+
for (const definition of missing) {
|
|
2254
|
+
if (this.definitions.get(definition.name) !== definition) continue;
|
|
2255
|
+
this.synced.delete(definition.name);
|
|
2256
|
+
await this.sync(definition);
|
|
2257
|
+
}
|
|
2258
|
+
return this.store.listJobs();
|
|
2259
|
+
}
|
|
2177
2260
|
/**
|
|
2178
2261
|
* Runs `fn` after every earlier state update for the same job has settled,
|
|
2179
2262
|
* so two runs (or a run and a check) in this process never read and write
|
|
@@ -2249,7 +2332,7 @@ var CronWatch = class {
|
|
|
2249
2332
|
};
|
|
2250
2333
|
let recorded = false;
|
|
2251
2334
|
try {
|
|
2252
|
-
await this.sync(definition);
|
|
2335
|
+
await this.sync(definition, true);
|
|
2253
2336
|
await this.store.insertRun({ ...run });
|
|
2254
2337
|
recorded = true;
|
|
2255
2338
|
} catch (e) {
|
|
@@ -2274,7 +2357,7 @@ var CronWatch = class {
|
|
|
2274
2357
|
run.finishedAt = finishedAt;
|
|
2275
2358
|
run.durationMs = runDuration(startedAt, finishedAt);
|
|
2276
2359
|
run.metrics = recorder.metrics();
|
|
2277
|
-
run.output = recorder.output() ?? (typeof result === "string" ?
|
|
2360
|
+
run.output = recorder.output() ?? (typeof result === "string" ? result : null);
|
|
2278
2361
|
this.conclude(definition, run, result, error, threw, recorder.expectText() ?? (typeof result === "string" ? result : null));
|
|
2279
2362
|
await started;
|
|
2280
2363
|
try {
|
|
@@ -2287,12 +2370,13 @@ var CronWatch = class {
|
|
|
2287
2370
|
}
|
|
2288
2371
|
/**
|
|
2289
2372
|
* Sets a finished run's status and error from how it ended, then redacts
|
|
2290
|
-
* its output and error. Shared by execute()
|
|
2373
|
+
* its output and error and caps them, in that order. Shared by execute()
|
|
2374
|
+
* and RunHandle.finish().
|
|
2291
2375
|
*/
|
|
2292
2376
|
conclude(definition, run, result, error, threw, expectText) {
|
|
2293
2377
|
if (threw) {
|
|
2294
2378
|
run.status = "failed";
|
|
2295
|
-
run.error =
|
|
2379
|
+
run.error = describeError(error);
|
|
2296
2380
|
} else if (result instanceof Response && result.status >= 400) {
|
|
2297
2381
|
run.status = "failed";
|
|
2298
2382
|
run.error = `HTTP ${result.status}${result.statusText ? ` ${result.statusText}` : ""}`;
|
|
@@ -2305,8 +2389,8 @@ var CronWatch = class {
|
|
|
2305
2389
|
run.status = "ok";
|
|
2306
2390
|
}
|
|
2307
2391
|
}
|
|
2308
|
-
if (run.output !== null) run.output =
|
|
2309
|
-
if (run.error !== null) run.error =
|
|
2392
|
+
if (run.output !== null) run.output = redactAndCap(run.output, this.redact);
|
|
2393
|
+
if (run.error !== null) run.error = redactAndCap(run.error, this.redact);
|
|
2310
2394
|
}
|
|
2311
2395
|
/**
|
|
2312
2396
|
* Writes a finished run and evaluates it. `recorded` says whether its
|
|
@@ -2401,7 +2485,7 @@ ${id}`;
|
|
|
2401
2485
|
};
|
|
2402
2486
|
let recorded = false;
|
|
2403
2487
|
try {
|
|
2404
|
-
await this.sync(definition);
|
|
2488
|
+
await this.sync(definition, true);
|
|
2405
2489
|
await this.store.insertRun({ ...run });
|
|
2406
2490
|
recorded = true;
|
|
2407
2491
|
} catch (e) {
|
|
@@ -2501,14 +2585,15 @@ ${id}`;
|
|
|
2501
2585
|
const result = typeof outcome === "string" ? outcome : failed ? void 0 : outcome?.result;
|
|
2502
2586
|
const error = failed ? outcome.error : void 0;
|
|
2503
2587
|
const finishedAt = self.now();
|
|
2504
|
-
const added = recorder.output() ?? (typeof result === "string" ?
|
|
2588
|
+
const added = recorder.output() ?? (typeof result === "string" ? result : null);
|
|
2505
2589
|
const run = {
|
|
2506
2590
|
...from,
|
|
2507
2591
|
status: "running",
|
|
2508
2592
|
finishedAt,
|
|
2509
2593
|
durationMs: runDuration(from.startedAt, finishedAt),
|
|
2510
2594
|
error: null,
|
|
2511
|
-
|
|
2595
|
+
// Capped by conclude(), after it is redacted.
|
|
2596
|
+
output: joinLines(from.output, added),
|
|
2512
2597
|
metrics: { ...from.metrics, ...recorder.metrics() }
|
|
2513
2598
|
};
|
|
2514
2599
|
const expectText = joinLines(head, joinLines(from.output, recorder.expectText() ?? (typeof result === "string" ? result : null)));
|
|
@@ -2557,7 +2642,7 @@ ${id}`;
|
|
|
2557
2642
|
self.report(new Error(`run ${id} of ${name} belongs to job "${stored.job}"; ignored`), `flushing ${name}`);
|
|
2558
2643
|
return;
|
|
2559
2644
|
}
|
|
2560
|
-
const output = lines === null ? stored.output : joinOutput(stored.output,
|
|
2645
|
+
const output = lines === null ? stored.output : joinOutput(stored.output, redactAndCap(lines, self.redact));
|
|
2561
2646
|
if (!await self.writeRunIf({ ...stored, output, metrics: { ...stored.metrics, ...metrics } }, ["running"])) return putBack();
|
|
2562
2647
|
const text = taken.expectText();
|
|
2563
2648
|
if (text !== null && (head === null || head.length < OUTPUT_CAP)) head = (joinLines(head, text) ?? "").slice(0, OUTPUT_CAP);
|
|
@@ -2582,13 +2667,19 @@ ${id}`;
|
|
|
2582
2667
|
* alone and reported. A finished run is judged as if it had been wrapped
|
|
2583
2668
|
* here (expect, failures, duration, budgets) and its output and error are
|
|
2584
2669
|
* redacted the same way; one finishing after a check marked it timeout is
|
|
2585
|
-
* judged only when it succeeded, as RunHandle.finish() does.
|
|
2586
|
-
*
|
|
2670
|
+
* judged only when it succeeded, as RunHandle.finish() does. A metric that
|
|
2671
|
+
* is not a finite number throws before anything is written, as
|
|
2672
|
+
* job.metric() does. Returns the alerts it sent.
|
|
2587
2673
|
*/
|
|
2588
2674
|
async recordRun(input, options = {}) {
|
|
2589
2675
|
const declared = this.definitions.get(input.job);
|
|
2590
2676
|
if (!declared) throw new Error(`recordRun: job "${input.job}" is not declared; call job() first`);
|
|
2591
2677
|
if (input.id.includes("\0")) throw new Error(`recordRun: run ids cannot contain a NUL character (job "${input.job}")`);
|
|
2678
|
+
for (const [metric, value] of Object.entries(input.metrics ?? {})) {
|
|
2679
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
2680
|
+
throw new Error(`recordRun: metric "${metric}" must be a finite number (job "${input.job}", run "${input.id}")`);
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2592
2683
|
await this.sync(declared);
|
|
2593
2684
|
const run = { ...input, metrics: { ...input.metrics } };
|
|
2594
2685
|
if (run.status === "ok") {
|
|
@@ -2598,8 +2689,8 @@ ${id}`;
|
|
|
2598
2689
|
run.error = unmet;
|
|
2599
2690
|
}
|
|
2600
2691
|
}
|
|
2601
|
-
if (run.output !== null) run.output =
|
|
2602
|
-
if (run.error !== null) run.error =
|
|
2692
|
+
if (run.output !== null) run.output = redactAndCap(run.output, this.redact);
|
|
2693
|
+
if (run.error !== null) run.error = redactAndCap(run.error, this.redact);
|
|
2603
2694
|
const evaluate = options.evaluate !== false;
|
|
2604
2695
|
const definition = toStored(declared);
|
|
2605
2696
|
const stored = await this.store.getRun(run.id);
|
|
@@ -2633,22 +2724,42 @@ ${id}`;
|
|
|
2633
2724
|
}
|
|
2634
2725
|
/**
|
|
2635
2726
|
* Evaluate a finished run (ok, failed, or timed out by a check), already
|
|
2636
|
-
* written, against the job's state and send what that produces.
|
|
2727
|
+
* written, against the job's state and send what that produces. The
|
|
2728
|
+
* alerts are written with that state (see outbox()). Never throws.
|
|
2637
2729
|
*/
|
|
2638
2730
|
async finishRun(definition, run, now) {
|
|
2639
|
-
let
|
|
2731
|
+
let held;
|
|
2640
2732
|
try {
|
|
2641
2733
|
let history = null;
|
|
2642
|
-
({ result:
|
|
2734
|
+
({ result: held } = await this.updateState(run.job, async (previous) => {
|
|
2643
2735
|
history ??= await this.history(run);
|
|
2644
|
-
|
|
2645
|
-
return { state: settled.state, result: settled.alerts };
|
|
2736
|
+
return this.outbox(applySilence(previous, onRunFinish(definition, run, previous, history, now), now), definition, now);
|
|
2646
2737
|
}));
|
|
2647
2738
|
} catch (e) {
|
|
2648
2739
|
this.onError(e, `evaluating ${run.job}`);
|
|
2649
2740
|
return [];
|
|
2650
2741
|
}
|
|
2651
|
-
|
|
2742
|
+
this.reportDropped(run.job, held.dropped);
|
|
2743
|
+
return this.dispatch(run.job, held.alerts, now);
|
|
2744
|
+
}
|
|
2745
|
+
/**
|
|
2746
|
+
* An evaluation as it is written: its drafts composed into alerts and held
|
|
2747
|
+
* in the same state (holdAlerts), so the write that opens a condition also
|
|
2748
|
+
* keeps its alerts, and a process that stops before sending them does not
|
|
2749
|
+
* lose them. Called inside updateState(), so it only computes.
|
|
2750
|
+
*/
|
|
2751
|
+
outbox(settled, definition, now) {
|
|
2752
|
+
const alerts = settled.alerts.map((draft) => composeAlert(draft, definition, now));
|
|
2753
|
+
const { state, dropped } = holdAlerts(settled.state, alerts, this.now() + SEND_LEASE_MS, this.deferDelivery);
|
|
2754
|
+
return { state, result: { alerts, dropped } };
|
|
2755
|
+
}
|
|
2756
|
+
/** Reports alerts let go because a job's queue was full. */
|
|
2757
|
+
reportDropped(name, dropped) {
|
|
2758
|
+
if (dropped <= 0) return;
|
|
2759
|
+
this.report(
|
|
2760
|
+
new Error(`${dropped} undelivered alert${dropped === 1 ? "" : "s"} for ${name} dropped: only the newest ${MAX_UNDELIVERED} are kept for retry`),
|
|
2761
|
+
`alert queue for ${name}`
|
|
2762
|
+
);
|
|
2652
2763
|
}
|
|
2653
2764
|
/**
|
|
2654
2765
|
* The runs before `run`, newest first, with up to BASELINE_WINDOW
|
|
@@ -2688,11 +2799,13 @@ ${id}`;
|
|
|
2688
2799
|
}
|
|
2689
2800
|
for (const definition of this.definitions.values()) await this.sync(definition);
|
|
2690
2801
|
const now = this.now();
|
|
2691
|
-
for (const
|
|
2802
|
+
for (const listed of await this.store.runningRuns()) {
|
|
2692
2803
|
try {
|
|
2693
|
-
const declared = this.definitions.get(
|
|
2694
|
-
const definition = declared ? toStored(declared) : (await this.store.getJob(
|
|
2695
|
-
if (!definition || !isStuck(definition,
|
|
2804
|
+
const declared = this.definitions.get(listed.job);
|
|
2805
|
+
const definition = declared ? toStored(declared) : (await this.store.getJob(listed.job))?.definition;
|
|
2806
|
+
if (!definition || !isStuck(definition, listed, now)) continue;
|
|
2807
|
+
const run = await this.store.getRun(listed.id);
|
|
2808
|
+
if (!run || run.status !== "running" || run.job !== listed.job) continue;
|
|
2696
2809
|
const timeout = timeoutMs(definition);
|
|
2697
2810
|
run.status = "timeout";
|
|
2698
2811
|
run.finishedAt = now;
|
|
@@ -2701,23 +2814,26 @@ ${id}`;
|
|
|
2701
2814
|
if (!await this.writeRunIf(run, ["running"])) continue;
|
|
2702
2815
|
alerts.push(...await this.finishRun(definition, run, now));
|
|
2703
2816
|
} catch (e) {
|
|
2704
|
-
this.onError(e, `checking ${
|
|
2817
|
+
this.onError(e, `checking ${listed.job}`);
|
|
2705
2818
|
}
|
|
2706
2819
|
}
|
|
2707
2820
|
const jobs = [];
|
|
2708
2821
|
const retries = { spentMs: 0 };
|
|
2709
|
-
for (const stored of await this.
|
|
2822
|
+
for (const stored of await this.storedJobs()) {
|
|
2710
2823
|
try {
|
|
2711
2824
|
const recent = await this.store.listRuns(stored.name, BASELINE_WINDOW);
|
|
2712
2825
|
let nextExpectedAt = null;
|
|
2713
|
-
const { state, result:
|
|
2826
|
+
const { state, result: held } = await this.updateState(stored.name, (previous) => {
|
|
2714
2827
|
const evaluation = onCheck(stored.definition, stored, recent[0] ?? null, previous, now);
|
|
2715
2828
|
nextExpectedAt = evaluation.nextExpectedAt;
|
|
2716
2829
|
const settled = applySilence(previous, evaluation, now);
|
|
2717
|
-
|
|
2830
|
+
const released = releaseSending(settled.state, this.now());
|
|
2831
|
+
const out = this.outbox({ state: released.state, alerts: settled.alerts }, stored.definition, now);
|
|
2832
|
+
return { state: out.state, result: { alerts: out.result.alerts, dropped: released.dropped + out.result.dropped } };
|
|
2718
2833
|
});
|
|
2834
|
+
this.reportDropped(stored.name, held.dropped);
|
|
2719
2835
|
alerts.push(...await this.retryUndelivered(stored.name, state, now, retries));
|
|
2720
|
-
alerts.push(...await this.dispatch(
|
|
2836
|
+
alerts.push(...await this.dispatch(stored.name, held.alerts, now));
|
|
2721
2837
|
jobs.push(summarize(stored, recent, state, nextExpectedAt, now));
|
|
2722
2838
|
} catch (e) {
|
|
2723
2839
|
this.onError(e, `checking ${stored.name}`);
|
|
@@ -2761,16 +2877,17 @@ ${id}`;
|
|
|
2761
2877
|
/** Every job's summary with its newest `limit` runs, read together. What the dashboard shows. */
|
|
2762
2878
|
async jobsWithRuns(limit = 20) {
|
|
2763
2879
|
await this.ensureReady();
|
|
2764
|
-
|
|
2880
|
+
const jobs = await this.storedJobs();
|
|
2765
2881
|
const now = this.now();
|
|
2766
2882
|
const out = [];
|
|
2767
|
-
for (const stored of
|
|
2883
|
+
for (const stored of jobs) out.push(await this.snapshot(stored, now, clampLimit(limit, 20, 0)));
|
|
2768
2884
|
return out;
|
|
2769
2885
|
}
|
|
2886
|
+
/** 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. */
|
|
2770
2887
|
async jobSummary(name) {
|
|
2771
2888
|
await this.ensureReady();
|
|
2772
2889
|
const definition = this.definitions.get(name);
|
|
2773
|
-
if (definition) await this.sync(definition);
|
|
2890
|
+
if (definition) await this.sync(definition, true);
|
|
2774
2891
|
const stored = await this.store.getJob(name);
|
|
2775
2892
|
if (!stored) return null;
|
|
2776
2893
|
return (await this.snapshot(stored, this.now(), 0)).job;
|
|
@@ -2784,11 +2901,14 @@ ${id}`;
|
|
|
2784
2901
|
await this.ensureReady();
|
|
2785
2902
|
return this.store.getRun(id);
|
|
2786
2903
|
}
|
|
2787
|
-
/**
|
|
2904
|
+
/**
|
|
2905
|
+
* Stop alerts for a job for a while. State keeps updating underneath. The
|
|
2906
|
+
* end is a whole millisecond, held at 2^53 - 1 (see silenceEnd).
|
|
2907
|
+
*/
|
|
2788
2908
|
async silence(name, duration) {
|
|
2789
2909
|
const ms = parseDuration(duration, "silence duration");
|
|
2790
2910
|
return this.patchState(name, (state) => {
|
|
2791
|
-
state.silencedUntil = this.now()
|
|
2911
|
+
state.silencedUntil = silenceEnd(this.now(), ms);
|
|
2792
2912
|
});
|
|
2793
2913
|
}
|
|
2794
2914
|
async unsilence(name) {
|
|
@@ -2806,7 +2926,11 @@ ${id}`;
|
|
|
2806
2926
|
});
|
|
2807
2927
|
return state;
|
|
2808
2928
|
}
|
|
2809
|
-
/**
|
|
2929
|
+
/**
|
|
2930
|
+
* Remove a job and its runs from the store. A job still declared in code
|
|
2931
|
+
* comes back: here on its next run, and in any other process that declares
|
|
2932
|
+
* it on its next run there, or at that process's next check or dashboard read.
|
|
2933
|
+
*/
|
|
2810
2934
|
async forget(name) {
|
|
2811
2935
|
await this.ensureReady();
|
|
2812
2936
|
this.definitions.delete(name);
|
|
@@ -2844,32 +2968,37 @@ ${id}`;
|
|
|
2844
2968
|
this.timer = null;
|
|
2845
2969
|
this.firstTick = null;
|
|
2846
2970
|
}
|
|
2971
|
+
/**
|
|
2972
|
+
* Stop the interval, wait for a check already under way (bounded by its
|
|
2973
|
+
* own channel, triage and retry timeouts; what it throws was reported to
|
|
2974
|
+
* whoever started it), then close the store, so that check neither writes
|
|
2975
|
+
* after the store is closed nor loses the alerts it would queue.
|
|
2976
|
+
*/
|
|
2847
2977
|
async close() {
|
|
2848
2978
|
this.stop();
|
|
2979
|
+
if (this.checking) await this.checking.catch(() => {
|
|
2980
|
+
});
|
|
2849
2981
|
if (this.store.close) await this.store.close();
|
|
2850
2982
|
}
|
|
2851
2983
|
/**
|
|
2852
|
-
*
|
|
2853
|
-
*
|
|
2854
|
-
* the delivery fields are written back, onto
|
|
2984
|
+
* Triage and send each alert the outbox holds (see outbox()). The state,
|
|
2985
|
+
* with the alerts in it, was saved before this, so a slow channel holds up
|
|
2986
|
+
* nothing else; afterwards only the delivery fields are written back, onto
|
|
2987
|
+
* a fresh read of the state, and the alerts leave `sending`. Triage is made
|
|
2988
|
+
* here, never stored with the held alert: the write that opens a condition
|
|
2989
|
+
* cannot wait for it, and a retry triages an alert that has none. With
|
|
2990
|
+
* deliver: "check" the alerts were queued for a check elsewhere instead.
|
|
2855
2991
|
*/
|
|
2856
|
-
async dispatch(
|
|
2857
|
-
if (
|
|
2858
|
-
const composed = [];
|
|
2992
|
+
async dispatch(name, alerts, now) {
|
|
2993
|
+
if (alerts.length === 0 || this.deferDelivery) return alerts;
|
|
2859
2994
|
const delivered = [];
|
|
2860
2995
|
const failed = [];
|
|
2861
|
-
for (const
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
failed.push(alert);
|
|
2865
|
-
} else {
|
|
2866
|
-
if (this.triage && alert.type !== "recovered") await this.addTriage(alert, TRIAGE_TIMEOUT_MS);
|
|
2867
|
-
(await this.deliver(alert) ? delivered : failed).push(alert);
|
|
2868
|
-
}
|
|
2869
|
-
composed.push(alert);
|
|
2996
|
+
for (const alert of alerts) {
|
|
2997
|
+
if (this.triage && alert.type !== "recovered") await this.addTriage(alert, TRIAGE_TIMEOUT_MS);
|
|
2998
|
+
(await this.deliver(alert) ? delivered : failed).push(alert);
|
|
2870
2999
|
}
|
|
2871
|
-
await this.recordDelivery(
|
|
2872
|
-
return
|
|
3000
|
+
await this.recordDelivery(name, delivered, failed, [], now);
|
|
3001
|
+
return alerts;
|
|
2873
3002
|
}
|
|
2874
3003
|
/**
|
|
2875
3004
|
* Send the alerts that no channel accepted last time, once each, oldest
|
|
@@ -2898,28 +3027,18 @@ ${id}`;
|
|
|
2898
3027
|
}
|
|
2899
3028
|
/**
|
|
2900
3029
|
* Mark delivered alerts done, drop stale ones, and keep failed ones for the
|
|
2901
|
-
* next check
|
|
2902
|
-
*
|
|
3030
|
+
* next check, taking them all out of `sending` (recordSent). A failed alert
|
|
3031
|
+
* replaces its stored copy, so a triage made on this attempt is kept.
|
|
3032
|
+
* lastAlertAt moves only on a delivery. When this write fails, alerts
|
|
3033
|
+
* still in `sending` are retried once their lease runs out.
|
|
2903
3034
|
*/
|
|
2904
3035
|
async recordDelivery(name, delivered, failed, dropped, now) {
|
|
2905
3036
|
try {
|
|
2906
3037
|
const { result: trimmed } = await this.updateState(name, (previous) => {
|
|
2907
|
-
const state = normalizeState(previous, name);
|
|
2908
|
-
|
|
2909
|
-
const retried = new Map(failed.map((a) => [alertKey(a), a]));
|
|
2910
|
-
const kept = state.undelivered.filter((a) => !done.has(alertKey(a))).map((a) => retried.get(alertKey(a)) ?? a);
|
|
2911
|
-
const known = new Set(kept.map(alertKey));
|
|
2912
|
-
kept.push(...failed.filter((a) => !known.has(alertKey(a))));
|
|
2913
|
-
state.undelivered = kept.slice(-MAX_UNDELIVERED);
|
|
2914
|
-
if (delivered.length > 0) state.lastAlertAt = now;
|
|
2915
|
-
return { state, result: Math.max(0, kept.length - MAX_UNDELIVERED) };
|
|
3038
|
+
const { state, dropped: letGo } = recordSent(normalizeState(previous, name), delivered, failed, dropped, now);
|
|
3039
|
+
return { state, result: letGo };
|
|
2916
3040
|
});
|
|
2917
|
-
|
|
2918
|
-
this.onError(
|
|
2919
|
-
new Error(`${trimmed} undelivered alert${trimmed === 1 ? "" : "s"} for ${name} dropped: only the newest ${MAX_UNDELIVERED} are kept for retry`),
|
|
2920
|
-
`alert queue for ${name}`
|
|
2921
|
-
);
|
|
2922
|
-
}
|
|
3041
|
+
this.reportDropped(name, trimmed);
|
|
2923
3042
|
} catch (e) {
|
|
2924
3043
|
this.onError(e, `recording alert delivery for ${name}`);
|
|
2925
3044
|
}
|