@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.cjs
CHANGED
|
@@ -636,7 +636,6 @@ function catchesUp(def, run, now) {
|
|
|
636
636
|
function onRunStart(def, run, state, now) {
|
|
637
637
|
const next = cloneState(state);
|
|
638
638
|
if (catchesUp(def, run, now)) closeCondition(next, "missed");
|
|
639
|
-
closeCondition(next, "stuck");
|
|
640
639
|
return next;
|
|
641
640
|
}
|
|
642
641
|
function onRunFinish(def, run, state, history, now) {
|
|
@@ -724,6 +723,9 @@ function onCheck(def, stored, lastRun, state, now) {
|
|
|
724
723
|
function isStuck(def, run, now) {
|
|
725
724
|
return run.status === "running" && run.finishedAt === null && now - run.startedAt > timeoutMs(def);
|
|
726
725
|
}
|
|
726
|
+
function isStranded(run, now) {
|
|
727
|
+
return run.status === "running" && run.finishedAt !== null && now - run.finishedAt > SEND_LEASE_MS;
|
|
728
|
+
}
|
|
727
729
|
function muteOpens(previous, next) {
|
|
728
730
|
const muted = cloneState(next);
|
|
729
731
|
for (const condition of openConditions(muted)) {
|
|
@@ -899,6 +901,10 @@ function constantTimeEqual(sent, secret) {
|
|
|
899
901
|
for (let i = 0; i < sent.length; i++) diff |= sent.charCodeAt(i) ^ secret.charCodeAt(i % secret.length);
|
|
900
902
|
return diff === 0;
|
|
901
903
|
}
|
|
904
|
+
function bearerToken(header) {
|
|
905
|
+
const match = header === null ? null : /^Bearer\s+([\s\S]*)$/i.exec(header);
|
|
906
|
+
return match ? match[1] : null;
|
|
907
|
+
}
|
|
902
908
|
|
|
903
909
|
// src/output.ts
|
|
904
910
|
var OUTPUT_CAP = 16 * 1024;
|
|
@@ -992,15 +998,45 @@ function stringify(part) {
|
|
|
992
998
|
return String(part);
|
|
993
999
|
}
|
|
994
1000
|
}
|
|
1001
|
+
var LineWindow = class {
|
|
1002
|
+
items = [];
|
|
1003
|
+
start = 0;
|
|
1004
|
+
size = 0;
|
|
1005
|
+
get count() {
|
|
1006
|
+
return this.items.length - this.start;
|
|
1007
|
+
}
|
|
1008
|
+
push(line) {
|
|
1009
|
+
this.items.push(line);
|
|
1010
|
+
this.size += line.length + 1;
|
|
1011
|
+
}
|
|
1012
|
+
/** Drops the oldest lines while over `max`, always keeping the newest. */
|
|
1013
|
+
keepAtMost(max) {
|
|
1014
|
+
while (this.size > max && this.count > 1) this.drop();
|
|
1015
|
+
}
|
|
1016
|
+
/** Drops the oldest lines only while what is left still holds `min`. */
|
|
1017
|
+
keepAtLeast(min) {
|
|
1018
|
+
while (this.count > 1 && this.size - (this.items[this.start].length + 1) > min) this.drop();
|
|
1019
|
+
}
|
|
1020
|
+
join() {
|
|
1021
|
+
return this.items.slice(this.start).join("\n");
|
|
1022
|
+
}
|
|
1023
|
+
drop() {
|
|
1024
|
+
this.size -= this.items[this.start].length + 1;
|
|
1025
|
+
this.start += 1;
|
|
1026
|
+
if (this.start >= 1024 && this.start * 2 >= this.items.length) {
|
|
1027
|
+
this.items = this.items.slice(this.start);
|
|
1028
|
+
this.start = 0;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
995
1032
|
function createRecorder(run) {
|
|
996
1033
|
const window = OUTPUT_CAP + REDACT_EDGE + 2;
|
|
997
1034
|
const copy = (text) => JSON.parse(JSON.stringify(text));
|
|
998
|
-
const lines =
|
|
999
|
-
let size = 0;
|
|
1035
|
+
const lines = new LineWindow();
|
|
1000
1036
|
const head = [];
|
|
1001
1037
|
let headSize = 0;
|
|
1002
1038
|
let whole = "";
|
|
1003
|
-
|
|
1039
|
+
const raw = new LineWindow();
|
|
1004
1040
|
let logged = false;
|
|
1005
1041
|
const metrics = {};
|
|
1006
1042
|
const controller = new AbortController();
|
|
@@ -1014,7 +1050,8 @@ function createRecorder(run) {
|
|
|
1014
1050
|
const separator = logged ? "\n" : "";
|
|
1015
1051
|
logged = true;
|
|
1016
1052
|
if (whole !== null) whole = whole.length + separator.length + line.length <= 2 * OUTPUT_CAP ? whole + separator + line : null;
|
|
1017
|
-
|
|
1053
|
+
raw.push(copy(line.slice(-OUTPUT_CAP)));
|
|
1054
|
+
raw.keepAtLeast(OUTPUT_CAP);
|
|
1018
1055
|
if (headSize < OUTPUT_CAP) {
|
|
1019
1056
|
head.push(copy(line.slice(0, OUTPUT_CAP - headSize)));
|
|
1020
1057
|
headSize += line.length + 1;
|
|
@@ -1024,10 +1061,7 @@ function createRecorder(run) {
|
|
|
1024
1061
|
line = copy(line.slice(-68609));
|
|
1025
1062
|
}
|
|
1026
1063
|
lines.push(line);
|
|
1027
|
-
|
|
1028
|
-
while (size > window && lines.length > 1) {
|
|
1029
|
-
size -= lines.shift().length + 1;
|
|
1030
|
-
}
|
|
1064
|
+
lines.keepAtMost(window);
|
|
1031
1065
|
},
|
|
1032
1066
|
metric(name, value) {
|
|
1033
1067
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
@@ -1041,10 +1075,10 @@ function createRecorder(run) {
|
|
|
1041
1075
|
};
|
|
1042
1076
|
return {
|
|
1043
1077
|
context,
|
|
1044
|
-
output: () => lines.
|
|
1078
|
+
output: () => lines.count === 0 ? null : lines.join(),
|
|
1045
1079
|
expectText: () => {
|
|
1046
1080
|
if (!logged) return null;
|
|
1047
|
-
return whole ?? head.join("\n").slice(0, OUTPUT_CAP) + "\n" +
|
|
1081
|
+
return whole ?? head.join("\n").slice(0, OUTPUT_CAP) + "\n" + raw.join().slice(-OUTPUT_CAP);
|
|
1048
1082
|
},
|
|
1049
1083
|
metrics: () => ({ ...metrics }),
|
|
1050
1084
|
abort: () => controller.abort()
|
|
@@ -1218,7 +1252,7 @@ function staticAsset(path, base) {
|
|
|
1218
1252
|
}
|
|
1219
1253
|
|
|
1220
1254
|
// src/version.ts
|
|
1221
|
-
var VERSION = "0.
|
|
1255
|
+
var VERSION = "0.15.0";
|
|
1222
1256
|
var API_VERSION = 1;
|
|
1223
1257
|
|
|
1224
1258
|
// src/routes/timeline.ts
|
|
@@ -1985,10 +2019,6 @@ function forwardedOrigin(request, url) {
|
|
|
1985
2019
|
return url.origin;
|
|
1986
2020
|
}
|
|
1987
2021
|
}
|
|
1988
|
-
function bearerToken(header) {
|
|
1989
|
-
const match = header === null ? null : /^Bearer\s+([\s\S]*)$/i.exec(header);
|
|
1990
|
-
return match ? match[1] : null;
|
|
1991
|
-
}
|
|
1992
2022
|
function signInReturn(referer, publicOrigin, base) {
|
|
1993
2023
|
if (referer === null || !referer.startsWith(publicOrigin + "/")) return `${base}/`;
|
|
1994
2024
|
let url;
|
|
@@ -2157,13 +2187,14 @@ function buildRoutes(cw, options = {}) {
|
|
|
2157
2187
|
return wantsHtml ? html(messagePage("Bad request", "The path is not valid.", base), 400) : api({ ok: false, error: "Bad path" }, 400);
|
|
2158
2188
|
}
|
|
2159
2189
|
const parts = decoded;
|
|
2160
|
-
|
|
2190
|
+
const reads = method === "GET" || method === "HEAD";
|
|
2191
|
+
if (reads && path === "/") {
|
|
2161
2192
|
const entries = await cw.jobsWithRuns(BOARD_PAGE_RUNS);
|
|
2162
2193
|
const now = cw.now();
|
|
2163
2194
|
const runsByJob = new Map(entries.map((entry) => [entry.job.name, entry.runs]));
|
|
2164
2195
|
return html(dashboardPage(entries.map((entry) => entry.job), runsByJob, now, base, null, await boardLanes(cw, entries, now)));
|
|
2165
2196
|
}
|
|
2166
|
-
if (
|
|
2197
|
+
if (reads && parts[0] === "jobs" && parts.length === 2) {
|
|
2167
2198
|
const job = await cw.jobSummary(parts[1]);
|
|
2168
2199
|
if (!job) return html(messagePage("No such job", `${parts[1]} is not in the store.`, base), 404);
|
|
2169
2200
|
const now = cw.now();
|
|
@@ -2199,15 +2230,15 @@ function buildRoutes(cw, options = {}) {
|
|
|
2199
2230
|
}
|
|
2200
2231
|
if (parts[0] === "api") {
|
|
2201
2232
|
const rest = parts.slice(1);
|
|
2202
|
-
if (
|
|
2233
|
+
if (reads && rest.length === 0) {
|
|
2203
2234
|
return api({ ok: true, library: LIBRARY, language: LANGUAGE, version: VERSION, api: API_VERSION });
|
|
2204
2235
|
}
|
|
2205
|
-
if (
|
|
2236
|
+
if (reads && rest[0] === "jobs" && rest.length === 1) {
|
|
2206
2237
|
return api({ ok: true, jobs: await cw.jobs() });
|
|
2207
2238
|
}
|
|
2208
2239
|
if (rest[0] === "jobs" && rest.length === 2) {
|
|
2209
2240
|
const name = rest[1];
|
|
2210
|
-
if (
|
|
2241
|
+
if (reads) {
|
|
2211
2242
|
const job = await cw.jobSummary(name);
|
|
2212
2243
|
if (!job) return api({ ok: false, error: "No such job" }, 404);
|
|
2213
2244
|
return api({ ok: true, job, runs: await cw.runs(name, runsLimit(url.searchParams.get("runs"))) });
|
|
@@ -2248,7 +2279,7 @@ function buildRoutes(cw, options = {}) {
|
|
|
2248
2279
|
return api({ ok: true, ...result });
|
|
2249
2280
|
}
|
|
2250
2281
|
}
|
|
2251
|
-
if (
|
|
2282
|
+
if (reads && rest[0] === "runs" && rest.length === 2) {
|
|
2252
2283
|
const run = await cw.getRun(rest[1]);
|
|
2253
2284
|
return run ? api({ ok: true, run }) : api({ ok: false, error: "No such run" }, 404);
|
|
2254
2285
|
}
|
|
@@ -2263,7 +2294,8 @@ function buildRoutes(cw, options = {}) {
|
|
|
2263
2294
|
const path = stripBase(url.pathname, base);
|
|
2264
2295
|
if (path === null) return html(messagePage("Not found", url.pathname, base), 404);
|
|
2265
2296
|
wantsHtml = !path.startsWith("/api");
|
|
2266
|
-
|
|
2297
|
+
const response = await serve(request, url, path, wantsHtml);
|
|
2298
|
+
return request.method.toUpperCase() === "HEAD" ? new Response(null, { status: response.status, headers: response.headers }) : response;
|
|
2267
2299
|
} catch (e) {
|
|
2268
2300
|
try {
|
|
2269
2301
|
cw.onError(e, "routes");
|
|
@@ -2407,9 +2439,11 @@ var TRIAGE_TIMEOUT_MS = 25e3;
|
|
|
2407
2439
|
var CHANNEL_TIMEOUT_MS = 15e3;
|
|
2408
2440
|
var PRUNE_INTERVAL_MS = 60 * 6e4;
|
|
2409
2441
|
var RETRY_BUDGET_MS = 2e4;
|
|
2442
|
+
var START_DEADLINE_MS = 5e3;
|
|
2410
2443
|
var STATE_ATTEMPTS = 10;
|
|
2411
2444
|
var HISTORY_PAGE = BASELINE_WINDOW + 5;
|
|
2412
2445
|
var HISTORY_MAX = 200;
|
|
2446
|
+
var STRANDED_ERROR = "Imported, but its finish was never written; marked as timed out";
|
|
2413
2447
|
function validateDefinition(def) {
|
|
2414
2448
|
const name = def.name;
|
|
2415
2449
|
if (def.schedule !== void 0) {
|
|
@@ -2586,8 +2620,8 @@ var Cronwatch = class {
|
|
|
2586
2620
|
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);
|
|
2587
2621
|
}
|
|
2588
2622
|
if (secret) {
|
|
2589
|
-
const
|
|
2590
|
-
if (!constantTimeEqual(
|
|
2623
|
+
const sent = bearerToken(request.headers.get("authorization"));
|
|
2624
|
+
if (sent === null || !constantTimeEqual(sent, secret)) {
|
|
2591
2625
|
return json({ ok: false, error: "Unauthorized" }, 401);
|
|
2592
2626
|
}
|
|
2593
2627
|
}
|
|
@@ -2649,8 +2683,8 @@ var Cronwatch = class {
|
|
|
2649
2683
|
/**
|
|
2650
2684
|
* Writes the declaration of `definition`'s name as it stands, unless the
|
|
2651
2685
|
* store has it. A handle kept from an earlier declaration writes the one
|
|
2652
|
-
* that replaced it, never its own over it, and one forgotten since
|
|
2653
|
-
*
|
|
2686
|
+
* that replaced it, never its own over it, and one forgotten since
|
|
2687
|
+
* declares the job again as it was last declared here. The writes of one name take turns, in the order they were asked
|
|
2654
2688
|
* for, so one still under way cannot land after a later one; and a name
|
|
2655
2689
|
* declared again while its write was under way is still to be written. A
|
|
2656
2690
|
* name is marked as written only while that same declaration stands, so a
|
|
@@ -2659,9 +2693,12 @@ var Cronwatch = class {
|
|
|
2659
2693
|
*
|
|
2660
2694
|
* With `confirm`, as a run starts, a name already written is read back:
|
|
2661
2695
|
* another process may have forgotten the job since, and a job still
|
|
2662
|
-
* declared here comes back on its next run.
|
|
2696
|
+
* declared here comes back on its next run. `running` says the write is
|
|
2697
|
+
* for a run (by default, whenever `confirm` is): a name forgotten here is
|
|
2698
|
+
* then declared again, where a check or a dashboard read, whose loop may
|
|
2699
|
+
* have picked the name up just before the forget, leaves it forgotten.
|
|
2663
2700
|
*/
|
|
2664
|
-
async sync(definition, confirm = false) {
|
|
2701
|
+
async sync(definition, confirm = false, running = confirm) {
|
|
2665
2702
|
await this.ensureReady();
|
|
2666
2703
|
const name = definition.name;
|
|
2667
2704
|
if (this.synced.has(name)) {
|
|
@@ -2670,6 +2707,10 @@ var Cronwatch = class {
|
|
|
2670
2707
|
}
|
|
2671
2708
|
await this.serial(name, async () => {
|
|
2672
2709
|
if (this.synced.has(name)) return;
|
|
2710
|
+
if (!this.definitions.has(name)) {
|
|
2711
|
+
if (!running) return;
|
|
2712
|
+
this.declared(name);
|
|
2713
|
+
}
|
|
2673
2714
|
const standing = this.definitions.get(name) ?? definition;
|
|
2674
2715
|
await this.store.upsertJob(toStored(standing), this.now());
|
|
2675
2716
|
if (this.definitions.get(name) === standing) this.synced.add(name);
|
|
@@ -2777,15 +2818,32 @@ var Cronwatch = class {
|
|
|
2777
2818
|
metrics: {},
|
|
2778
2819
|
trigger
|
|
2779
2820
|
};
|
|
2821
|
+
const begun = { ...run };
|
|
2780
2822
|
let recorded = false;
|
|
2781
|
-
|
|
2823
|
+
let landedLate = false;
|
|
2824
|
+
let concluded = false;
|
|
2825
|
+
const writing = (async () => {
|
|
2782
2826
|
await this.sync(definition, true);
|
|
2783
|
-
await this.store.insertRun({ ...
|
|
2784
|
-
|
|
2827
|
+
await this.store.insertRun({ ...begun });
|
|
2828
|
+
})();
|
|
2829
|
+
try {
|
|
2830
|
+
if (await within(writing, START_DEADLINE_MS) === TIMED_OUT) {
|
|
2831
|
+
this.report(new Error(`the store took longer than ${formatDuration(START_DEADLINE_MS)} to record the start of ${name}; running it unrecorded`), `recording ${name}`);
|
|
2832
|
+
void writing.then(
|
|
2833
|
+
() => {
|
|
2834
|
+
landedLate = true;
|
|
2835
|
+
if (concluded) return this.recordFinish(definition, run, true, run.finishedAt).then(() => {
|
|
2836
|
+
}, (e) => this.report(e, `recording ${name}`));
|
|
2837
|
+
},
|
|
2838
|
+
() => {
|
|
2839
|
+
}
|
|
2840
|
+
);
|
|
2841
|
+
} else {
|
|
2842
|
+
recorded = true;
|
|
2843
|
+
}
|
|
2785
2844
|
} catch (e) {
|
|
2786
2845
|
this.report(e, `recording ${name}`);
|
|
2787
2846
|
}
|
|
2788
|
-
const begun = { ...run };
|
|
2789
2847
|
const started = recorded ? this.updateState(name, (before) => ({ state: holdRun(onRunStart(definition, begun, before, this.now()), run.id), result: void 0 })).then(() => {
|
|
2790
2848
|
}, (e) => this.report(e, `starting ${name}`)) : Promise.resolve();
|
|
2791
2849
|
const recorder = createRecorder(run);
|
|
@@ -2808,11 +2866,23 @@ var Cronwatch = class {
|
|
|
2808
2866
|
run.output = recorder.output() ?? (typeof result === "string" ? result : null);
|
|
2809
2867
|
this.conclude(definition, run, result, error, threw, recorder.expectText() ?? (typeof result === "string" ? result : null));
|
|
2810
2868
|
await started;
|
|
2869
|
+
const ignore = (why) => {
|
|
2870
|
+
if (why) this.report(new Error(`run ${run.id} of ${name} ${why}; ignored`), `finishing ${name}`);
|
|
2871
|
+
};
|
|
2872
|
+
const over = recorded || landedLate;
|
|
2811
2873
|
try {
|
|
2812
|
-
|
|
2813
|
-
if (ignored) this.report(new Error(`run ${run.id} of ${name} ${ignored}; ignored`), `finishing ${name}`);
|
|
2874
|
+
ignore(await this.recordFinish(definition, run, over, finishedAt));
|
|
2814
2875
|
} catch (e) {
|
|
2815
2876
|
this.report(e, `recording ${name}`);
|
|
2877
|
+
if (!over && landedLate) {
|
|
2878
|
+
try {
|
|
2879
|
+
ignore(await this.recordFinish(definition, run, true, finishedAt));
|
|
2880
|
+
} catch (again) {
|
|
2881
|
+
this.report(again, `recording ${name}`);
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
} finally {
|
|
2885
|
+
concluded = true;
|
|
2816
2886
|
}
|
|
2817
2887
|
return { run, result, error, threw };
|
|
2818
2888
|
}
|
|
@@ -2849,7 +2919,7 @@ var Cronwatch = class {
|
|
|
2849
2919
|
*/
|
|
2850
2920
|
async recordFinish(definition, run, recorded, finishedAt) {
|
|
2851
2921
|
if (!recorded) {
|
|
2852
|
-
await this.sync(definition);
|
|
2922
|
+
await this.sync(definition, false, true);
|
|
2853
2923
|
try {
|
|
2854
2924
|
await this.store.insertRun(run);
|
|
2855
2925
|
await this.finishRun(toStored(definition), run, finishedAt);
|
|
@@ -2905,7 +2975,7 @@ ${id}`;
|
|
|
2905
2975
|
}
|
|
2906
2976
|
/**
|
|
2907
2977
|
* The start of execute() without the function: the run is inserted and
|
|
2908
|
-
* missed
|
|
2978
|
+
* missed closes (onRunStart). A store that fails is reported and
|
|
2909
2979
|
* the handle inserts the finished run instead, as execute() does.
|
|
2910
2980
|
*/
|
|
2911
2981
|
async recordStart(definition, trigger = "start", id) {
|
|
@@ -2983,6 +3053,21 @@ ${id}`;
|
|
|
2983
3053
|
let finished = inactive !== null;
|
|
2984
3054
|
let finishCalled = false;
|
|
2985
3055
|
let head = null;
|
|
3056
|
+
let flushed = null;
|
|
3057
|
+
const appendRedacted = (stored, lines) => {
|
|
3058
|
+
const added = stripNul(lines);
|
|
3059
|
+
const joins = flushed !== null && stored !== null && stored.endsWith(flushed.redacted);
|
|
3060
|
+
let raw = joins ? `${flushed.raw}
|
|
3061
|
+
${added}` : added;
|
|
3062
|
+
const redacted = redactAndCap(raw, self.redact);
|
|
3063
|
+
const output = joins ? stored.slice(0, stored.length - flushed.redacted.length) + redacted : joinLines(stored, redacted);
|
|
3064
|
+
if (raw.length <= OUTPUT_CAP) return { output, tail: { raw, redacted } };
|
|
3065
|
+
const cut = raw.indexOf("\n", raw.length - OUTPUT_CAP);
|
|
3066
|
+
if (cut === -1) return { output, tail: null };
|
|
3067
|
+
raw = raw.slice(cut + 1);
|
|
3068
|
+
const kept = redactAndCap(raw, self.redact);
|
|
3069
|
+
return { output, tail: redacted.endsWith(kept) ? { raw, redacted: kept } : null };
|
|
3070
|
+
};
|
|
2986
3071
|
let queue = Promise.resolve();
|
|
2987
3072
|
const inTurn = (fn) => {
|
|
2988
3073
|
const result = queue.then(fn);
|
|
@@ -3042,7 +3127,7 @@ ${id}`;
|
|
|
3042
3127
|
durationMs: runDuration(from.startedAt, finishedAt),
|
|
3043
3128
|
error: null,
|
|
3044
3129
|
// Capped by conclude(), after it is redacted.
|
|
3045
|
-
output:
|
|
3130
|
+
output: added === null ? from.output : appendRedacted(from.output, added).output,
|
|
3046
3131
|
metrics: { ...from.metrics, ...recorder.metrics() }
|
|
3047
3132
|
};
|
|
3048
3133
|
const expectText = joinLines(head, joinLines(from.output, recorder.expectText() ?? (typeof result === "string" ? result : null)));
|
|
@@ -3091,8 +3176,10 @@ ${id}`;
|
|
|
3091
3176
|
self.report(new Error(`run ${id} of ${name} belongs to job "${stored.job}"; ignored`), `flushing ${name}`);
|
|
3092
3177
|
return;
|
|
3093
3178
|
}
|
|
3094
|
-
const
|
|
3179
|
+
const next = lines === null ? null : appendRedacted(stored.output, lines);
|
|
3180
|
+
const output = next === null ? stored.output : next.output === null ? null : capOutput(next.output);
|
|
3095
3181
|
if (!await self.writeRunIf({ ...stored, output, metrics: { ...stored.metrics, ...metrics } }, ["running"])) return putBack();
|
|
3182
|
+
if (next !== null) flushed = next.tail !== null && output !== null && output.endsWith(next.tail.redacted) ? next.tail : null;
|
|
3096
3183
|
const text = taken.expectText();
|
|
3097
3184
|
if (text !== null && (head === null || head.length < OUTPUT_CAP)) head = (joinLines(head, text) ?? "").slice(0, OUTPUT_CAP);
|
|
3098
3185
|
} catch (e) {
|
|
@@ -3128,6 +3215,17 @@ ${id}`;
|
|
|
3128
3215
|
throw new Error(`recordRun: run ids must be 1 to ${MAX_RUN_ID} characters (got ${got}; job "${input.job}")`);
|
|
3129
3216
|
}
|
|
3130
3217
|
if (input.id.includes("\0")) throw new Error(`recordRun: run ids cannot contain a NUL character (job "${input.job}")`);
|
|
3218
|
+
const where = `job "${input.job}", run "${input.id}"`;
|
|
3219
|
+
if (!["running", "ok", "failed", "timeout"].includes(input.status)) {
|
|
3220
|
+
throw new Error(`recordRun: status must be "running", "ok", "failed", or "timeout" (${where})`);
|
|
3221
|
+
}
|
|
3222
|
+
if (!Number.isFinite(input.startedAt)) throw new Error(`recordRun: startedAt must be a finite number (${where})`);
|
|
3223
|
+
if (input.finishedAt !== null && !Number.isFinite(input.finishedAt)) throw new Error(`recordRun: finishedAt must be a finite number or null (${where})`);
|
|
3224
|
+
if (input.durationMs !== null && !Number.isFinite(input.durationMs)) throw new Error(`recordRun: durationMs must be a finite number or null (${where})`);
|
|
3225
|
+
for (const field of ["error", "output"]) {
|
|
3226
|
+
if (input[field] !== null && typeof input[field] !== "string") throw new Error(`recordRun: ${field} must be a string or null (${where})`);
|
|
3227
|
+
}
|
|
3228
|
+
if (typeof input.trigger !== "string") throw new Error(`recordRun: trigger must be a string (${where})`);
|
|
3131
3229
|
for (const [metric, value] of Object.entries(input.metrics ?? {})) {
|
|
3132
3230
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
3133
3231
|
throw new Error(`recordRun: metric "${metric}" must be a finite number (job "${input.job}", run "${input.id}")`);
|
|
@@ -3148,7 +3246,7 @@ ${id}`;
|
|
|
3148
3246
|
const definition = toStored(declared);
|
|
3149
3247
|
const stored = await this.store.getRun(run.id);
|
|
3150
3248
|
if (stored) return this.recordOver(definition, stored, run, evaluate);
|
|
3151
|
-
const inserted = evaluate ? { ...run, status: "running" } : run;
|
|
3249
|
+
const inserted = evaluate ? { ...run, status: "running", finishedAt: run.status === "running" ? null : this.now() } : run;
|
|
3152
3250
|
try {
|
|
3153
3251
|
await this.store.insertRun(inserted);
|
|
3154
3252
|
} catch (e) {
|
|
@@ -3181,7 +3279,7 @@ ${id}`;
|
|
|
3181
3279
|
return [];
|
|
3182
3280
|
}
|
|
3183
3281
|
if (!evaluate) {
|
|
3184
|
-
await this.updateState(run.job, (current) => ({ state: releaseRun(current, run.id), result: void 0 })).catch((e) => this.
|
|
3282
|
+
await this.updateState(run.job, (current) => ({ state: releaseRun(current, run.id), result: void 0 })).catch((e) => this.report(e, `recording ${run.job}`));
|
|
3185
3283
|
return [];
|
|
3186
3284
|
}
|
|
3187
3285
|
if (claim.lateAfterTimeout && run.status !== "ok") return [];
|
|
@@ -3261,7 +3359,7 @@ ${id}`;
|
|
|
3261
3359
|
try {
|
|
3262
3360
|
alerts.push(...await source.sync(this) ?? []);
|
|
3263
3361
|
} catch (e) {
|
|
3264
|
-
this.
|
|
3362
|
+
this.report(e, `source ${source.name}`);
|
|
3265
3363
|
}
|
|
3266
3364
|
}
|
|
3267
3365
|
for (const definition of this.definitions.values()) await this.sync(definition);
|
|
@@ -3273,18 +3371,27 @@ ${id}`;
|
|
|
3273
3371
|
const stored = found ? this.readJob(found) : null;
|
|
3274
3372
|
if (stored) this.evaluable(stored);
|
|
3275
3373
|
const definition = declared ? toStored(declared) : stored?.definition;
|
|
3276
|
-
|
|
3374
|
+
const stranded = isStranded(listed, now);
|
|
3375
|
+
if (!definition || !(stranded || isStuck(definition, listed, now))) continue;
|
|
3277
3376
|
const run = await this.store.getRun(listed.id);
|
|
3278
3377
|
if (!run || run.status !== "running" || run.job !== listed.job) continue;
|
|
3279
|
-
const timeout = timeoutMs(definition);
|
|
3280
3378
|
run.status = "timeout";
|
|
3281
|
-
run.
|
|
3282
|
-
|
|
3283
|
-
|
|
3379
|
+
if (stranded && run.error !== null) {
|
|
3380
|
+
run.status = "failed";
|
|
3381
|
+
if (run.durationMs !== null) run.finishedAt = run.startedAt + run.durationMs;
|
|
3382
|
+
else run.durationMs = runDuration(run.startedAt, run.finishedAt);
|
|
3383
|
+
} else if (stranded) {
|
|
3384
|
+
run.durationMs ??= runDuration(run.startedAt, run.finishedAt);
|
|
3385
|
+
run.error = STRANDED_ERROR;
|
|
3386
|
+
} else {
|
|
3387
|
+
run.finishedAt = now;
|
|
3388
|
+
run.durationMs = runDuration(run.startedAt, now);
|
|
3389
|
+
run.error = `Still running after ${formatDuration(timeoutMs(definition))}; marked as timed out`;
|
|
3390
|
+
}
|
|
3284
3391
|
if (!await this.writeRunIf(run, ["running"])) continue;
|
|
3285
3392
|
alerts.push(...await this.finishRun(definition, run, now));
|
|
3286
3393
|
} catch (e) {
|
|
3287
|
-
this.
|
|
3394
|
+
this.report(e, `checking ${listed.job}`);
|
|
3288
3395
|
}
|
|
3289
3396
|
}
|
|
3290
3397
|
const jobs = [];
|
|
@@ -3308,7 +3415,7 @@ ${id}`;
|
|
|
3308
3415
|
alerts.push(...await this.evaluatePending(stored, recent, state, now));
|
|
3309
3416
|
jobs.push(summarize(stored, recent, state, nextExpectedAt, now));
|
|
3310
3417
|
} catch (e) {
|
|
3311
|
-
this.
|
|
3418
|
+
this.report(e, `checking ${stored.name}`);
|
|
3312
3419
|
jobs.push(await this.unevaluable(stored, now));
|
|
3313
3420
|
}
|
|
3314
3421
|
}
|
|
@@ -3318,7 +3425,7 @@ ${id}`;
|
|
|
3318
3425
|
try {
|
|
3319
3426
|
pruned = await this.store.prune(now - this.retentionMs);
|
|
3320
3427
|
} catch (e) {
|
|
3321
|
-
this.
|
|
3428
|
+
this.report(e, "pruning");
|
|
3322
3429
|
}
|
|
3323
3430
|
}
|
|
3324
3431
|
return { checkedAt: now, jobs, alerts, pruned };
|
|
@@ -3344,7 +3451,7 @@ ${id}`;
|
|
|
3344
3451
|
if (run.status === "running" || typeof run.finishedAt !== "number" || run.finishedAt > now - SEND_LEASE_MS) continue;
|
|
3345
3452
|
alerts.push(...await this.finishRun(stored.definition, run, now, true));
|
|
3346
3453
|
} catch (e) {
|
|
3347
|
-
this.
|
|
3454
|
+
this.report(e, `evaluating run ${id} of ${stored.name}`);
|
|
3348
3455
|
}
|
|
3349
3456
|
}
|
|
3350
3457
|
return alerts;
|
|
@@ -3359,7 +3466,7 @@ ${id}`;
|
|
|
3359
3466
|
const { nextExpectedAt } = onCheck(stored.definition, stored, recent[0] ?? null, state, now);
|
|
3360
3467
|
return { job: summarize(stored, recent, state, nextExpectedAt, now), runs: recent.slice(0, runs) };
|
|
3361
3468
|
} catch (e) {
|
|
3362
|
-
this.
|
|
3469
|
+
this.report(e, `reading ${stored.name}`);
|
|
3363
3470
|
return { job: await this.unevaluable(stored, now), runs: recent.slice(0, runs) };
|
|
3364
3471
|
}
|
|
3365
3472
|
}
|
|
@@ -3386,7 +3493,7 @@ ${id}`;
|
|
|
3386
3493
|
async jobSummary(name) {
|
|
3387
3494
|
await this.ensureReady();
|
|
3388
3495
|
const definition = this.definitions.get(name);
|
|
3389
|
-
if (definition) await this.sync(definition, true);
|
|
3496
|
+
if (definition) await this.sync(definition, true, false);
|
|
3390
3497
|
const stored = await this.store.getJob(name);
|
|
3391
3498
|
if (!stored) return null;
|
|
3392
3499
|
return (await this.snapshot(this.readJob(stored), this.now(), 0)).job;
|
|
@@ -3645,10 +3752,6 @@ function joinLines(before, after2) {
|
|
|
3645
3752
|
return `${before}
|
|
3646
3753
|
${after2}`;
|
|
3647
3754
|
}
|
|
3648
|
-
function joinOutput(before, after2) {
|
|
3649
|
-
const joined = joinLines(before, after2);
|
|
3650
|
-
return joined === null ? null : capOutput(joined);
|
|
3651
|
-
}
|
|
3652
3755
|
function clampLimit(limit, fallback, min) {
|
|
3653
3756
|
const n = typeof limit === "number" && Number.isFinite(limit) ? Math.trunc(limit) : fallback;
|
|
3654
3757
|
return Math.min(500, Math.max(min, n));
|
|
@@ -3663,6 +3766,20 @@ function after(ms, fn) {
|
|
|
3663
3766
|
arm(ms);
|
|
3664
3767
|
return () => clearTimeout(timer);
|
|
3665
3768
|
}
|
|
3769
|
+
var TIMED_OUT = /* @__PURE__ */ Symbol("timed out");
|
|
3770
|
+
function within(promise, ms) {
|
|
3771
|
+
return new Promise((resolve, reject) => {
|
|
3772
|
+
const t = setTimeout(() => resolve(TIMED_OUT), ms);
|
|
3773
|
+
if (typeof t === "object" && "unref" in t) t.unref();
|
|
3774
|
+
promise.then((v) => {
|
|
3775
|
+
clearTimeout(t);
|
|
3776
|
+
resolve(v);
|
|
3777
|
+
}, (e) => {
|
|
3778
|
+
clearTimeout(t);
|
|
3779
|
+
reject(e);
|
|
3780
|
+
});
|
|
3781
|
+
});
|
|
3782
|
+
}
|
|
3666
3783
|
function withTimeout(promise, ms) {
|
|
3667
3784
|
return new Promise((resolve, reject) => {
|
|
3668
3785
|
const t = setTimeout(() => reject(new Error(`timed out after ${ms}ms`)), ms);
|