@cronwatch/sdk 0.14.2 → 0.15.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/LICENSE +1 -1
- package/dist/anthropic.cjs +5 -1
- package/dist/anthropic.cjs.map +1 -1
- package/dist/anthropic.d.cts +6 -1
- package/dist/anthropic.d.ts +6 -1
- package/dist/anthropic.js +5 -1
- package/dist/anthropic.js.map +1 -1
- package/dist/{client-CHjrNHH-.d.cts → client-IuAn2-v9.d.cts} +7 -4
- package/dist/{client-C8j7B2Bo.d.ts → client-MLN3rDFE.d.ts} +7 -4
- package/dist/index.cjs +174 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +174 -57
- package/dist/index.js.map +1 -1
- package/dist/pg-cron.cjs.map +1 -1
- package/dist/pg-cron.d.cts +1 -1
- package/dist/pg-cron.d.ts +1 -1
- package/dist/pg-cron.js.map +1 -1
- package/dist/postgres.cjs +15 -1
- package/dist/postgres.cjs.map +1 -1
- package/dist/postgres.d.cts +29 -1
- package/dist/postgres.d.ts +29 -1
- package/dist/postgres.js +15 -1
- package/dist/postgres.js.map +1 -1
- package/dist/slack.cjs +7 -2
- package/dist/slack.cjs.map +1 -1
- package/dist/slack.js +7 -2
- package/dist/slack.js.map +1 -1
- package/dist/webhook.cjs +1 -8
- package/dist/webhook.cjs.map +1 -1
- package/dist/webhook.js +1 -8
- package/dist/webhook.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as CronwatchOptions, a as Cronwatch } from './client-
|
|
2
|
-
export { b as CronWatch, c as CronWatchOptions, F as FetchHandler, H as HandlerFn, d as HandlerOptions, J as JobContext, e as JobFn, f as JobHandle, R as RecordRunOptions, g as Routes, h as RoutesOptions, i as RunHandle, j as RunOutcome, S as Source, k as SourceHost, l as StartOptions, m as consoleChannel, n as createRoutes, o as custom } from './client-
|
|
1
|
+
import { C as CronwatchOptions, a as Cronwatch } from './client-IuAn2-v9.cjs';
|
|
2
|
+
export { b as CronWatch, c as CronWatchOptions, F as FetchHandler, H as HandlerFn, d as HandlerOptions, J as JobContext, e as JobFn, f as JobHandle, R as RecordRunOptions, g as Routes, h as RoutesOptions, i as RunHandle, j as RunOutcome, S as Source, k as SourceHost, l as StartOptions, m as consoleChannel, n as createRoutes, o as custom } from './client-IuAn2-v9.cjs';
|
|
3
3
|
import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-B8IFVK5k.cjs';
|
|
4
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, H as HeldAlert, h as JobDefinition, i as JobHealth, J 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-B8IFVK5k.cjs';
|
|
5
5
|
|
|
@@ -11,7 +11,7 @@ export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach
|
|
|
11
11
|
declare function memory(): Store & Required<Pick<Store, "lastRun">>;
|
|
12
12
|
|
|
13
13
|
/** This release of @cronwatch/sdk. scripts/release.mjs keeps it in step with package.json. */
|
|
14
|
-
declare const VERSION = "0.
|
|
14
|
+
declare const VERSION = "0.15.0";
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* "15m" -> 900000. Accepts a plain number of milliseconds, and compound
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as CronwatchOptions, a as Cronwatch } from './client-
|
|
2
|
-
export { b as CronWatch, c as CronWatchOptions, F as FetchHandler, H as HandlerFn, d as HandlerOptions, J as JobContext, e as JobFn, f as JobHandle, R as RecordRunOptions, g as Routes, h as RoutesOptions, i as RunHandle, j as RunOutcome, S as Source, k as SourceHost, l as StartOptions, m as consoleChannel, n as createRoutes, o as custom } from './client-
|
|
1
|
+
import { C as CronwatchOptions, a as Cronwatch } from './client-MLN3rDFE.js';
|
|
2
|
+
export { b as CronWatch, c as CronWatchOptions, F as FetchHandler, H as HandlerFn, d as HandlerOptions, J as JobContext, e as JobFn, f as JobHandle, R as RecordRunOptions, g as Routes, h as RoutesOptions, i as RunHandle, j as RunOutcome, S as Source, k as SourceHost, l as StartOptions, m as consoleChannel, n as createRoutes, o as custom } from './client-MLN3rDFE.js';
|
|
3
3
|
import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-B8IFVK5k.js';
|
|
4
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, H as HeldAlert, h as JobDefinition, i as JobHealth, J 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-B8IFVK5k.js';
|
|
5
5
|
|
|
@@ -11,7 +11,7 @@ export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach
|
|
|
11
11
|
declare function memory(): Store & Required<Pick<Store, "lastRun">>;
|
|
12
12
|
|
|
13
13
|
/** This release of @cronwatch/sdk. scripts/release.mjs keeps it in step with package.json. */
|
|
14
|
-
declare const VERSION = "0.
|
|
14
|
+
declare const VERSION = "0.15.0";
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* "15m" -> 900000. Accepts a plain number of milliseconds, and compound
|
package/dist/index.js
CHANGED
|
@@ -634,7 +634,6 @@ function catchesUp(def, run, now) {
|
|
|
634
634
|
function onRunStart(def, run, state, now) {
|
|
635
635
|
const next = cloneState(state);
|
|
636
636
|
if (catchesUp(def, run, now)) closeCondition(next, "missed");
|
|
637
|
-
closeCondition(next, "stuck");
|
|
638
637
|
return next;
|
|
639
638
|
}
|
|
640
639
|
function onRunFinish(def, run, state, history, now) {
|
|
@@ -722,6 +721,9 @@ function onCheck(def, stored, lastRun, state, now) {
|
|
|
722
721
|
function isStuck(def, run, now) {
|
|
723
722
|
return run.status === "running" && run.finishedAt === null && now - run.startedAt > timeoutMs(def);
|
|
724
723
|
}
|
|
724
|
+
function isStranded(run, now) {
|
|
725
|
+
return run.status === "running" && run.finishedAt !== null && now - run.finishedAt > SEND_LEASE_MS;
|
|
726
|
+
}
|
|
725
727
|
function muteOpens(previous, next) {
|
|
726
728
|
const muted = cloneState(next);
|
|
727
729
|
for (const condition of openConditions(muted)) {
|
|
@@ -897,6 +899,10 @@ function constantTimeEqual(sent, secret) {
|
|
|
897
899
|
for (let i = 0; i < sent.length; i++) diff |= sent.charCodeAt(i) ^ secret.charCodeAt(i % secret.length);
|
|
898
900
|
return diff === 0;
|
|
899
901
|
}
|
|
902
|
+
function bearerToken(header) {
|
|
903
|
+
const match = header === null ? null : /^Bearer\s+([\s\S]*)$/i.exec(header);
|
|
904
|
+
return match ? match[1] : null;
|
|
905
|
+
}
|
|
900
906
|
|
|
901
907
|
// src/output.ts
|
|
902
908
|
var OUTPUT_CAP = 16 * 1024;
|
|
@@ -990,15 +996,45 @@ function stringify(part) {
|
|
|
990
996
|
return String(part);
|
|
991
997
|
}
|
|
992
998
|
}
|
|
999
|
+
var LineWindow = class {
|
|
1000
|
+
items = [];
|
|
1001
|
+
start = 0;
|
|
1002
|
+
size = 0;
|
|
1003
|
+
get count() {
|
|
1004
|
+
return this.items.length - this.start;
|
|
1005
|
+
}
|
|
1006
|
+
push(line) {
|
|
1007
|
+
this.items.push(line);
|
|
1008
|
+
this.size += line.length + 1;
|
|
1009
|
+
}
|
|
1010
|
+
/** Drops the oldest lines while over `max`, always keeping the newest. */
|
|
1011
|
+
keepAtMost(max) {
|
|
1012
|
+
while (this.size > max && this.count > 1) this.drop();
|
|
1013
|
+
}
|
|
1014
|
+
/** Drops the oldest lines only while what is left still holds `min`. */
|
|
1015
|
+
keepAtLeast(min) {
|
|
1016
|
+
while (this.count > 1 && this.size - (this.items[this.start].length + 1) > min) this.drop();
|
|
1017
|
+
}
|
|
1018
|
+
join() {
|
|
1019
|
+
return this.items.slice(this.start).join("\n");
|
|
1020
|
+
}
|
|
1021
|
+
drop() {
|
|
1022
|
+
this.size -= this.items[this.start].length + 1;
|
|
1023
|
+
this.start += 1;
|
|
1024
|
+
if (this.start >= 1024 && this.start * 2 >= this.items.length) {
|
|
1025
|
+
this.items = this.items.slice(this.start);
|
|
1026
|
+
this.start = 0;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
993
1030
|
function createRecorder(run) {
|
|
994
1031
|
const window = OUTPUT_CAP + REDACT_EDGE + 2;
|
|
995
1032
|
const copy = (text) => JSON.parse(JSON.stringify(text));
|
|
996
|
-
const lines =
|
|
997
|
-
let size = 0;
|
|
1033
|
+
const lines = new LineWindow();
|
|
998
1034
|
const head = [];
|
|
999
1035
|
let headSize = 0;
|
|
1000
1036
|
let whole = "";
|
|
1001
|
-
|
|
1037
|
+
const raw = new LineWindow();
|
|
1002
1038
|
let logged = false;
|
|
1003
1039
|
const metrics = {};
|
|
1004
1040
|
const controller = new AbortController();
|
|
@@ -1012,7 +1048,8 @@ function createRecorder(run) {
|
|
|
1012
1048
|
const separator = logged ? "\n" : "";
|
|
1013
1049
|
logged = true;
|
|
1014
1050
|
if (whole !== null) whole = whole.length + separator.length + line.length <= 2 * OUTPUT_CAP ? whole + separator + line : null;
|
|
1015
|
-
|
|
1051
|
+
raw.push(copy(line.slice(-OUTPUT_CAP)));
|
|
1052
|
+
raw.keepAtLeast(OUTPUT_CAP);
|
|
1016
1053
|
if (headSize < OUTPUT_CAP) {
|
|
1017
1054
|
head.push(copy(line.slice(0, OUTPUT_CAP - headSize)));
|
|
1018
1055
|
headSize += line.length + 1;
|
|
@@ -1022,10 +1059,7 @@ function createRecorder(run) {
|
|
|
1022
1059
|
line = copy(line.slice(-68609));
|
|
1023
1060
|
}
|
|
1024
1061
|
lines.push(line);
|
|
1025
|
-
|
|
1026
|
-
while (size > window && lines.length > 1) {
|
|
1027
|
-
size -= lines.shift().length + 1;
|
|
1028
|
-
}
|
|
1062
|
+
lines.keepAtMost(window);
|
|
1029
1063
|
},
|
|
1030
1064
|
metric(name, value) {
|
|
1031
1065
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
@@ -1039,10 +1073,10 @@ function createRecorder(run) {
|
|
|
1039
1073
|
};
|
|
1040
1074
|
return {
|
|
1041
1075
|
context,
|
|
1042
|
-
output: () => lines.
|
|
1076
|
+
output: () => lines.count === 0 ? null : lines.join(),
|
|
1043
1077
|
expectText: () => {
|
|
1044
1078
|
if (!logged) return null;
|
|
1045
|
-
return whole ?? head.join("\n").slice(0, OUTPUT_CAP) + "\n" +
|
|
1079
|
+
return whole ?? head.join("\n").slice(0, OUTPUT_CAP) + "\n" + raw.join().slice(-OUTPUT_CAP);
|
|
1046
1080
|
},
|
|
1047
1081
|
metrics: () => ({ ...metrics }),
|
|
1048
1082
|
abort: () => controller.abort()
|
|
@@ -1216,7 +1250,7 @@ function staticAsset(path, base) {
|
|
|
1216
1250
|
}
|
|
1217
1251
|
|
|
1218
1252
|
// src/version.ts
|
|
1219
|
-
var VERSION = "0.
|
|
1253
|
+
var VERSION = "0.15.0";
|
|
1220
1254
|
var API_VERSION = 1;
|
|
1221
1255
|
|
|
1222
1256
|
// src/routes/timeline.ts
|
|
@@ -1983,10 +2017,6 @@ function forwardedOrigin(request, url) {
|
|
|
1983
2017
|
return url.origin;
|
|
1984
2018
|
}
|
|
1985
2019
|
}
|
|
1986
|
-
function bearerToken(header) {
|
|
1987
|
-
const match = header === null ? null : /^Bearer\s+([\s\S]*)$/i.exec(header);
|
|
1988
|
-
return match ? match[1] : null;
|
|
1989
|
-
}
|
|
1990
2020
|
function signInReturn(referer, publicOrigin, base) {
|
|
1991
2021
|
if (referer === null || !referer.startsWith(publicOrigin + "/")) return `${base}/`;
|
|
1992
2022
|
let url;
|
|
@@ -2155,13 +2185,14 @@ function buildRoutes(cw, options = {}) {
|
|
|
2155
2185
|
return wantsHtml ? html(messagePage("Bad request", "The path is not valid.", base), 400) : api({ ok: false, error: "Bad path" }, 400);
|
|
2156
2186
|
}
|
|
2157
2187
|
const parts = decoded;
|
|
2158
|
-
|
|
2188
|
+
const reads = method === "GET" || method === "HEAD";
|
|
2189
|
+
if (reads && path === "/") {
|
|
2159
2190
|
const entries = await cw.jobsWithRuns(BOARD_PAGE_RUNS);
|
|
2160
2191
|
const now = cw.now();
|
|
2161
2192
|
const runsByJob = new Map(entries.map((entry) => [entry.job.name, entry.runs]));
|
|
2162
2193
|
return html(dashboardPage(entries.map((entry) => entry.job), runsByJob, now, base, null, await boardLanes(cw, entries, now)));
|
|
2163
2194
|
}
|
|
2164
|
-
if (
|
|
2195
|
+
if (reads && parts[0] === "jobs" && parts.length === 2) {
|
|
2165
2196
|
const job = await cw.jobSummary(parts[1]);
|
|
2166
2197
|
if (!job) return html(messagePage("No such job", `${parts[1]} is not in the store.`, base), 404);
|
|
2167
2198
|
const now = cw.now();
|
|
@@ -2197,15 +2228,15 @@ function buildRoutes(cw, options = {}) {
|
|
|
2197
2228
|
}
|
|
2198
2229
|
if (parts[0] === "api") {
|
|
2199
2230
|
const rest = parts.slice(1);
|
|
2200
|
-
if (
|
|
2231
|
+
if (reads && rest.length === 0) {
|
|
2201
2232
|
return api({ ok: true, library: LIBRARY, language: LANGUAGE, version: VERSION, api: API_VERSION });
|
|
2202
2233
|
}
|
|
2203
|
-
if (
|
|
2234
|
+
if (reads && rest[0] === "jobs" && rest.length === 1) {
|
|
2204
2235
|
return api({ ok: true, jobs: await cw.jobs() });
|
|
2205
2236
|
}
|
|
2206
2237
|
if (rest[0] === "jobs" && rest.length === 2) {
|
|
2207
2238
|
const name = rest[1];
|
|
2208
|
-
if (
|
|
2239
|
+
if (reads) {
|
|
2209
2240
|
const job = await cw.jobSummary(name);
|
|
2210
2241
|
if (!job) return api({ ok: false, error: "No such job" }, 404);
|
|
2211
2242
|
return api({ ok: true, job, runs: await cw.runs(name, runsLimit(url.searchParams.get("runs"))) });
|
|
@@ -2246,7 +2277,7 @@ function buildRoutes(cw, options = {}) {
|
|
|
2246
2277
|
return api({ ok: true, ...result });
|
|
2247
2278
|
}
|
|
2248
2279
|
}
|
|
2249
|
-
if (
|
|
2280
|
+
if (reads && rest[0] === "runs" && rest.length === 2) {
|
|
2250
2281
|
const run = await cw.getRun(rest[1]);
|
|
2251
2282
|
return run ? api({ ok: true, run }) : api({ ok: false, error: "No such run" }, 404);
|
|
2252
2283
|
}
|
|
@@ -2261,7 +2292,8 @@ function buildRoutes(cw, options = {}) {
|
|
|
2261
2292
|
const path = stripBase(url.pathname, base);
|
|
2262
2293
|
if (path === null) return html(messagePage("Not found", url.pathname, base), 404);
|
|
2263
2294
|
wantsHtml = !path.startsWith("/api");
|
|
2264
|
-
|
|
2295
|
+
const response = await serve(request, url, path, wantsHtml);
|
|
2296
|
+
return request.method.toUpperCase() === "HEAD" ? new Response(null, { status: response.status, headers: response.headers }) : response;
|
|
2265
2297
|
} catch (e) {
|
|
2266
2298
|
try {
|
|
2267
2299
|
cw.onError(e, "routes");
|
|
@@ -2405,9 +2437,11 @@ var TRIAGE_TIMEOUT_MS = 25e3;
|
|
|
2405
2437
|
var CHANNEL_TIMEOUT_MS = 15e3;
|
|
2406
2438
|
var PRUNE_INTERVAL_MS = 60 * 6e4;
|
|
2407
2439
|
var RETRY_BUDGET_MS = 2e4;
|
|
2440
|
+
var START_DEADLINE_MS = 5e3;
|
|
2408
2441
|
var STATE_ATTEMPTS = 10;
|
|
2409
2442
|
var HISTORY_PAGE = BASELINE_WINDOW + 5;
|
|
2410
2443
|
var HISTORY_MAX = 200;
|
|
2444
|
+
var STRANDED_ERROR = "Imported, but its finish was never written; marked as timed out";
|
|
2411
2445
|
function validateDefinition(def) {
|
|
2412
2446
|
const name = def.name;
|
|
2413
2447
|
if (def.schedule !== void 0) {
|
|
@@ -2584,8 +2618,8 @@ var Cronwatch = class {
|
|
|
2584
2618
|
return json({ ok: false, error: "CRON_SECRET is not set, so this job will not run for an unauthenticated request. Set it, or pass secret: null to handler() to allow anyone." }, 503);
|
|
2585
2619
|
}
|
|
2586
2620
|
if (secret) {
|
|
2587
|
-
const
|
|
2588
|
-
if (!constantTimeEqual(
|
|
2621
|
+
const sent = bearerToken(request.headers.get("authorization"));
|
|
2622
|
+
if (sent === null || !constantTimeEqual(sent, secret)) {
|
|
2589
2623
|
return json({ ok: false, error: "Unauthorized" }, 401);
|
|
2590
2624
|
}
|
|
2591
2625
|
}
|
|
@@ -2647,8 +2681,8 @@ var Cronwatch = class {
|
|
|
2647
2681
|
/**
|
|
2648
2682
|
* Writes the declaration of `definition`'s name as it stands, unless the
|
|
2649
2683
|
* store has it. A handle kept from an earlier declaration writes the one
|
|
2650
|
-
* that replaced it, never its own over it, and one forgotten since
|
|
2651
|
-
*
|
|
2684
|
+
* that replaced it, never its own over it, and one forgotten since
|
|
2685
|
+
* declares the job again as it was last declared here. The writes of one name take turns, in the order they were asked
|
|
2652
2686
|
* for, so one still under way cannot land after a later one; and a name
|
|
2653
2687
|
* declared again while its write was under way is still to be written. A
|
|
2654
2688
|
* name is marked as written only while that same declaration stands, so a
|
|
@@ -2657,9 +2691,12 @@ var Cronwatch = class {
|
|
|
2657
2691
|
*
|
|
2658
2692
|
* With `confirm`, as a run starts, a name already written is read back:
|
|
2659
2693
|
* another process may have forgotten the job since, and a job still
|
|
2660
|
-
* declared here comes back on its next run.
|
|
2694
|
+
* declared here comes back on its next run. `running` says the write is
|
|
2695
|
+
* for a run (by default, whenever `confirm` is): a name forgotten here is
|
|
2696
|
+
* then declared again, where a check or a dashboard read, whose loop may
|
|
2697
|
+
* have picked the name up just before the forget, leaves it forgotten.
|
|
2661
2698
|
*/
|
|
2662
|
-
async sync(definition, confirm = false) {
|
|
2699
|
+
async sync(definition, confirm = false, running = confirm) {
|
|
2663
2700
|
await this.ensureReady();
|
|
2664
2701
|
const name = definition.name;
|
|
2665
2702
|
if (this.synced.has(name)) {
|
|
@@ -2668,6 +2705,10 @@ var Cronwatch = class {
|
|
|
2668
2705
|
}
|
|
2669
2706
|
await this.serial(name, async () => {
|
|
2670
2707
|
if (this.synced.has(name)) return;
|
|
2708
|
+
if (!this.definitions.has(name)) {
|
|
2709
|
+
if (!running) return;
|
|
2710
|
+
this.declared(name);
|
|
2711
|
+
}
|
|
2671
2712
|
const standing = this.definitions.get(name) ?? definition;
|
|
2672
2713
|
await this.store.upsertJob(toStored(standing), this.now());
|
|
2673
2714
|
if (this.definitions.get(name) === standing) this.synced.add(name);
|
|
@@ -2775,15 +2816,32 @@ var Cronwatch = class {
|
|
|
2775
2816
|
metrics: {},
|
|
2776
2817
|
trigger
|
|
2777
2818
|
};
|
|
2819
|
+
const begun = { ...run };
|
|
2778
2820
|
let recorded = false;
|
|
2779
|
-
|
|
2821
|
+
let landedLate = false;
|
|
2822
|
+
let concluded = false;
|
|
2823
|
+
const writing = (async () => {
|
|
2780
2824
|
await this.sync(definition, true);
|
|
2781
|
-
await this.store.insertRun({ ...
|
|
2782
|
-
|
|
2825
|
+
await this.store.insertRun({ ...begun });
|
|
2826
|
+
})();
|
|
2827
|
+
try {
|
|
2828
|
+
if (await within(writing, START_DEADLINE_MS) === TIMED_OUT) {
|
|
2829
|
+
this.report(new Error(`the store took longer than ${formatDuration(START_DEADLINE_MS)} to record the start of ${name}; running it unrecorded`), `recording ${name}`);
|
|
2830
|
+
void writing.then(
|
|
2831
|
+
() => {
|
|
2832
|
+
landedLate = true;
|
|
2833
|
+
if (concluded) return this.recordFinish(definition, run, true, run.finishedAt).then(() => {
|
|
2834
|
+
}, (e) => this.report(e, `recording ${name}`));
|
|
2835
|
+
},
|
|
2836
|
+
() => {
|
|
2837
|
+
}
|
|
2838
|
+
);
|
|
2839
|
+
} else {
|
|
2840
|
+
recorded = true;
|
|
2841
|
+
}
|
|
2783
2842
|
} catch (e) {
|
|
2784
2843
|
this.report(e, `recording ${name}`);
|
|
2785
2844
|
}
|
|
2786
|
-
const begun = { ...run };
|
|
2787
2845
|
const started = recorded ? this.updateState(name, (before) => ({ state: holdRun(onRunStart(definition, begun, before, this.now()), run.id), result: void 0 })).then(() => {
|
|
2788
2846
|
}, (e) => this.report(e, `starting ${name}`)) : Promise.resolve();
|
|
2789
2847
|
const recorder = createRecorder(run);
|
|
@@ -2806,11 +2864,23 @@ var Cronwatch = class {
|
|
|
2806
2864
|
run.output = recorder.output() ?? (typeof result === "string" ? result : null);
|
|
2807
2865
|
this.conclude(definition, run, result, error, threw, recorder.expectText() ?? (typeof result === "string" ? result : null));
|
|
2808
2866
|
await started;
|
|
2867
|
+
const ignore = (why) => {
|
|
2868
|
+
if (why) this.report(new Error(`run ${run.id} of ${name} ${why}; ignored`), `finishing ${name}`);
|
|
2869
|
+
};
|
|
2870
|
+
const over = recorded || landedLate;
|
|
2809
2871
|
try {
|
|
2810
|
-
|
|
2811
|
-
if (ignored) this.report(new Error(`run ${run.id} of ${name} ${ignored}; ignored`), `finishing ${name}`);
|
|
2872
|
+
ignore(await this.recordFinish(definition, run, over, finishedAt));
|
|
2812
2873
|
} catch (e) {
|
|
2813
2874
|
this.report(e, `recording ${name}`);
|
|
2875
|
+
if (!over && landedLate) {
|
|
2876
|
+
try {
|
|
2877
|
+
ignore(await this.recordFinish(definition, run, true, finishedAt));
|
|
2878
|
+
} catch (again) {
|
|
2879
|
+
this.report(again, `recording ${name}`);
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
} finally {
|
|
2883
|
+
concluded = true;
|
|
2814
2884
|
}
|
|
2815
2885
|
return { run, result, error, threw };
|
|
2816
2886
|
}
|
|
@@ -2847,7 +2917,7 @@ var Cronwatch = class {
|
|
|
2847
2917
|
*/
|
|
2848
2918
|
async recordFinish(definition, run, recorded, finishedAt) {
|
|
2849
2919
|
if (!recorded) {
|
|
2850
|
-
await this.sync(definition);
|
|
2920
|
+
await this.sync(definition, false, true);
|
|
2851
2921
|
try {
|
|
2852
2922
|
await this.store.insertRun(run);
|
|
2853
2923
|
await this.finishRun(toStored(definition), run, finishedAt);
|
|
@@ -2903,7 +2973,7 @@ ${id}`;
|
|
|
2903
2973
|
}
|
|
2904
2974
|
/**
|
|
2905
2975
|
* The start of execute() without the function: the run is inserted and
|
|
2906
|
-
* missed
|
|
2976
|
+
* missed closes (onRunStart). A store that fails is reported and
|
|
2907
2977
|
* the handle inserts the finished run instead, as execute() does.
|
|
2908
2978
|
*/
|
|
2909
2979
|
async recordStart(definition, trigger = "start", id) {
|
|
@@ -2981,6 +3051,21 @@ ${id}`;
|
|
|
2981
3051
|
let finished = inactive !== null;
|
|
2982
3052
|
let finishCalled = false;
|
|
2983
3053
|
let head = null;
|
|
3054
|
+
let flushed = null;
|
|
3055
|
+
const appendRedacted = (stored, lines) => {
|
|
3056
|
+
const added = stripNul(lines);
|
|
3057
|
+
const joins = flushed !== null && stored !== null && stored.endsWith(flushed.redacted);
|
|
3058
|
+
let raw = joins ? `${flushed.raw}
|
|
3059
|
+
${added}` : added;
|
|
3060
|
+
const redacted = redactAndCap(raw, self.redact);
|
|
3061
|
+
const output = joins ? stored.slice(0, stored.length - flushed.redacted.length) + redacted : joinLines(stored, redacted);
|
|
3062
|
+
if (raw.length <= OUTPUT_CAP) return { output, tail: { raw, redacted } };
|
|
3063
|
+
const cut = raw.indexOf("\n", raw.length - OUTPUT_CAP);
|
|
3064
|
+
if (cut === -1) return { output, tail: null };
|
|
3065
|
+
raw = raw.slice(cut + 1);
|
|
3066
|
+
const kept = redactAndCap(raw, self.redact);
|
|
3067
|
+
return { output, tail: redacted.endsWith(kept) ? { raw, redacted: kept } : null };
|
|
3068
|
+
};
|
|
2984
3069
|
let queue = Promise.resolve();
|
|
2985
3070
|
const inTurn = (fn) => {
|
|
2986
3071
|
const result = queue.then(fn);
|
|
@@ -3040,7 +3125,7 @@ ${id}`;
|
|
|
3040
3125
|
durationMs: runDuration(from.startedAt, finishedAt),
|
|
3041
3126
|
error: null,
|
|
3042
3127
|
// Capped by conclude(), after it is redacted.
|
|
3043
|
-
output:
|
|
3128
|
+
output: added === null ? from.output : appendRedacted(from.output, added).output,
|
|
3044
3129
|
metrics: { ...from.metrics, ...recorder.metrics() }
|
|
3045
3130
|
};
|
|
3046
3131
|
const expectText = joinLines(head, joinLines(from.output, recorder.expectText() ?? (typeof result === "string" ? result : null)));
|
|
@@ -3089,8 +3174,10 @@ ${id}`;
|
|
|
3089
3174
|
self.report(new Error(`run ${id} of ${name} belongs to job "${stored.job}"; ignored`), `flushing ${name}`);
|
|
3090
3175
|
return;
|
|
3091
3176
|
}
|
|
3092
|
-
const
|
|
3177
|
+
const next = lines === null ? null : appendRedacted(stored.output, lines);
|
|
3178
|
+
const output = next === null ? stored.output : next.output === null ? null : capOutput(next.output);
|
|
3093
3179
|
if (!await self.writeRunIf({ ...stored, output, metrics: { ...stored.metrics, ...metrics } }, ["running"])) return putBack();
|
|
3180
|
+
if (next !== null) flushed = next.tail !== null && output !== null && output.endsWith(next.tail.redacted) ? next.tail : null;
|
|
3094
3181
|
const text = taken.expectText();
|
|
3095
3182
|
if (text !== null && (head === null || head.length < OUTPUT_CAP)) head = (joinLines(head, text) ?? "").slice(0, OUTPUT_CAP);
|
|
3096
3183
|
} catch (e) {
|
|
@@ -3126,6 +3213,17 @@ ${id}`;
|
|
|
3126
3213
|
throw new Error(`recordRun: run ids must be 1 to ${MAX_RUN_ID} characters (got ${got}; job "${input.job}")`);
|
|
3127
3214
|
}
|
|
3128
3215
|
if (input.id.includes("\0")) throw new Error(`recordRun: run ids cannot contain a NUL character (job "${input.job}")`);
|
|
3216
|
+
const where = `job "${input.job}", run "${input.id}"`;
|
|
3217
|
+
if (!["running", "ok", "failed", "timeout"].includes(input.status)) {
|
|
3218
|
+
throw new Error(`recordRun: status must be "running", "ok", "failed", or "timeout" (${where})`);
|
|
3219
|
+
}
|
|
3220
|
+
if (!Number.isFinite(input.startedAt)) throw new Error(`recordRun: startedAt must be a finite number (${where})`);
|
|
3221
|
+
if (input.finishedAt !== null && !Number.isFinite(input.finishedAt)) throw new Error(`recordRun: finishedAt must be a finite number or null (${where})`);
|
|
3222
|
+
if (input.durationMs !== null && !Number.isFinite(input.durationMs)) throw new Error(`recordRun: durationMs must be a finite number or null (${where})`);
|
|
3223
|
+
for (const field of ["error", "output"]) {
|
|
3224
|
+
if (input[field] !== null && typeof input[field] !== "string") throw new Error(`recordRun: ${field} must be a string or null (${where})`);
|
|
3225
|
+
}
|
|
3226
|
+
if (typeof input.trigger !== "string") throw new Error(`recordRun: trigger must be a string (${where})`);
|
|
3129
3227
|
for (const [metric, value] of Object.entries(input.metrics ?? {})) {
|
|
3130
3228
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
3131
3229
|
throw new Error(`recordRun: metric "${metric}" must be a finite number (job "${input.job}", run "${input.id}")`);
|
|
@@ -3146,7 +3244,7 @@ ${id}`;
|
|
|
3146
3244
|
const definition = toStored(declared);
|
|
3147
3245
|
const stored = await this.store.getRun(run.id);
|
|
3148
3246
|
if (stored) return this.recordOver(definition, stored, run, evaluate);
|
|
3149
|
-
const inserted = evaluate ? { ...run, status: "running" } : run;
|
|
3247
|
+
const inserted = evaluate ? { ...run, status: "running", finishedAt: run.status === "running" ? null : this.now() } : run;
|
|
3150
3248
|
try {
|
|
3151
3249
|
await this.store.insertRun(inserted);
|
|
3152
3250
|
} catch (e) {
|
|
@@ -3179,7 +3277,7 @@ ${id}`;
|
|
|
3179
3277
|
return [];
|
|
3180
3278
|
}
|
|
3181
3279
|
if (!evaluate) {
|
|
3182
|
-
await this.updateState(run.job, (current) => ({ state: releaseRun(current, run.id), result: void 0 })).catch((e) => this.
|
|
3280
|
+
await this.updateState(run.job, (current) => ({ state: releaseRun(current, run.id), result: void 0 })).catch((e) => this.report(e, `recording ${run.job}`));
|
|
3183
3281
|
return [];
|
|
3184
3282
|
}
|
|
3185
3283
|
if (claim.lateAfterTimeout && run.status !== "ok") return [];
|
|
@@ -3259,7 +3357,7 @@ ${id}`;
|
|
|
3259
3357
|
try {
|
|
3260
3358
|
alerts.push(...await source.sync(this) ?? []);
|
|
3261
3359
|
} catch (e) {
|
|
3262
|
-
this.
|
|
3360
|
+
this.report(e, `source ${source.name}`);
|
|
3263
3361
|
}
|
|
3264
3362
|
}
|
|
3265
3363
|
for (const definition of this.definitions.values()) await this.sync(definition);
|
|
@@ -3271,18 +3369,27 @@ ${id}`;
|
|
|
3271
3369
|
const stored = found ? this.readJob(found) : null;
|
|
3272
3370
|
if (stored) this.evaluable(stored);
|
|
3273
3371
|
const definition = declared ? toStored(declared) : stored?.definition;
|
|
3274
|
-
|
|
3372
|
+
const stranded = isStranded(listed, now);
|
|
3373
|
+
if (!definition || !(stranded || isStuck(definition, listed, now))) continue;
|
|
3275
3374
|
const run = await this.store.getRun(listed.id);
|
|
3276
3375
|
if (!run || run.status !== "running" || run.job !== listed.job) continue;
|
|
3277
|
-
const timeout = timeoutMs(definition);
|
|
3278
3376
|
run.status = "timeout";
|
|
3279
|
-
run.
|
|
3280
|
-
|
|
3281
|
-
|
|
3377
|
+
if (stranded && run.error !== null) {
|
|
3378
|
+
run.status = "failed";
|
|
3379
|
+
if (run.durationMs !== null) run.finishedAt = run.startedAt + run.durationMs;
|
|
3380
|
+
else run.durationMs = runDuration(run.startedAt, run.finishedAt);
|
|
3381
|
+
} else if (stranded) {
|
|
3382
|
+
run.durationMs ??= runDuration(run.startedAt, run.finishedAt);
|
|
3383
|
+
run.error = STRANDED_ERROR;
|
|
3384
|
+
} else {
|
|
3385
|
+
run.finishedAt = now;
|
|
3386
|
+
run.durationMs = runDuration(run.startedAt, now);
|
|
3387
|
+
run.error = `Still running after ${formatDuration(timeoutMs(definition))}; marked as timed out`;
|
|
3388
|
+
}
|
|
3282
3389
|
if (!await this.writeRunIf(run, ["running"])) continue;
|
|
3283
3390
|
alerts.push(...await this.finishRun(definition, run, now));
|
|
3284
3391
|
} catch (e) {
|
|
3285
|
-
this.
|
|
3392
|
+
this.report(e, `checking ${listed.job}`);
|
|
3286
3393
|
}
|
|
3287
3394
|
}
|
|
3288
3395
|
const jobs = [];
|
|
@@ -3306,7 +3413,7 @@ ${id}`;
|
|
|
3306
3413
|
alerts.push(...await this.evaluatePending(stored, recent, state, now));
|
|
3307
3414
|
jobs.push(summarize(stored, recent, state, nextExpectedAt, now));
|
|
3308
3415
|
} catch (e) {
|
|
3309
|
-
this.
|
|
3416
|
+
this.report(e, `checking ${stored.name}`);
|
|
3310
3417
|
jobs.push(await this.unevaluable(stored, now));
|
|
3311
3418
|
}
|
|
3312
3419
|
}
|
|
@@ -3316,7 +3423,7 @@ ${id}`;
|
|
|
3316
3423
|
try {
|
|
3317
3424
|
pruned = await this.store.prune(now - this.retentionMs);
|
|
3318
3425
|
} catch (e) {
|
|
3319
|
-
this.
|
|
3426
|
+
this.report(e, "pruning");
|
|
3320
3427
|
}
|
|
3321
3428
|
}
|
|
3322
3429
|
return { checkedAt: now, jobs, alerts, pruned };
|
|
@@ -3342,7 +3449,7 @@ ${id}`;
|
|
|
3342
3449
|
if (run.status === "running" || typeof run.finishedAt !== "number" || run.finishedAt > now - SEND_LEASE_MS) continue;
|
|
3343
3450
|
alerts.push(...await this.finishRun(stored.definition, run, now, true));
|
|
3344
3451
|
} catch (e) {
|
|
3345
|
-
this.
|
|
3452
|
+
this.report(e, `evaluating run ${id} of ${stored.name}`);
|
|
3346
3453
|
}
|
|
3347
3454
|
}
|
|
3348
3455
|
return alerts;
|
|
@@ -3357,7 +3464,7 @@ ${id}`;
|
|
|
3357
3464
|
const { nextExpectedAt } = onCheck(stored.definition, stored, recent[0] ?? null, state, now);
|
|
3358
3465
|
return { job: summarize(stored, recent, state, nextExpectedAt, now), runs: recent.slice(0, runs) };
|
|
3359
3466
|
} catch (e) {
|
|
3360
|
-
this.
|
|
3467
|
+
this.report(e, `reading ${stored.name}`);
|
|
3361
3468
|
return { job: await this.unevaluable(stored, now), runs: recent.slice(0, runs) };
|
|
3362
3469
|
}
|
|
3363
3470
|
}
|
|
@@ -3384,7 +3491,7 @@ ${id}`;
|
|
|
3384
3491
|
async jobSummary(name) {
|
|
3385
3492
|
await this.ensureReady();
|
|
3386
3493
|
const definition = this.definitions.get(name);
|
|
3387
|
-
if (definition) await this.sync(definition, true);
|
|
3494
|
+
if (definition) await this.sync(definition, true, false);
|
|
3388
3495
|
const stored = await this.store.getJob(name);
|
|
3389
3496
|
if (!stored) return null;
|
|
3390
3497
|
return (await this.snapshot(this.readJob(stored), this.now(), 0)).job;
|
|
@@ -3643,10 +3750,6 @@ function joinLines(before, after2) {
|
|
|
3643
3750
|
return `${before}
|
|
3644
3751
|
${after2}`;
|
|
3645
3752
|
}
|
|
3646
|
-
function joinOutput(before, after2) {
|
|
3647
|
-
const joined = joinLines(before, after2);
|
|
3648
|
-
return joined === null ? null : capOutput(joined);
|
|
3649
|
-
}
|
|
3650
3753
|
function clampLimit(limit, fallback, min) {
|
|
3651
3754
|
const n = typeof limit === "number" && Number.isFinite(limit) ? Math.trunc(limit) : fallback;
|
|
3652
3755
|
return Math.min(500, Math.max(min, n));
|
|
@@ -3661,6 +3764,20 @@ function after(ms, fn) {
|
|
|
3661
3764
|
arm(ms);
|
|
3662
3765
|
return () => clearTimeout(timer);
|
|
3663
3766
|
}
|
|
3767
|
+
var TIMED_OUT = /* @__PURE__ */ Symbol("timed out");
|
|
3768
|
+
function within(promise, ms) {
|
|
3769
|
+
return new Promise((resolve, reject) => {
|
|
3770
|
+
const t = setTimeout(() => resolve(TIMED_OUT), ms);
|
|
3771
|
+
if (typeof t === "object" && "unref" in t) t.unref();
|
|
3772
|
+
promise.then((v) => {
|
|
3773
|
+
clearTimeout(t);
|
|
3774
|
+
resolve(v);
|
|
3775
|
+
}, (e) => {
|
|
3776
|
+
clearTimeout(t);
|
|
3777
|
+
reject(e);
|
|
3778
|
+
});
|
|
3779
|
+
});
|
|
3780
|
+
}
|
|
3664
3781
|
function withTimeout(promise, ms) {
|
|
3665
3782
|
return new Promise((resolve, reject) => {
|
|
3666
3783
|
const t = setTimeout(() => reject(new Error(`timed out after ${ms}ms`)), ms);
|