@mark1russell7/service 0.5.0 → 0.5.2
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/README.md +95 -4
- package/build/{builder-B_q-JeW8.d.ts → builder-BzjhWk2E.d.ts} +9 -2
- package/build/http.d.ts +1 -1
- package/build/http.js +3 -10
- package/build/http.js.map +1 -1
- package/build/index.d.ts +244 -7
- package/build/index.js +806 -25
- package/build/index.js.map +1 -1
- package/build/knobs-BxG-c7Zb.js +39 -0
- package/build/knobs-BxG-c7Zb.js.map +1 -0
- package/build/{lifecycle-DIj4YGmO.d.ts → lifecycle-DjEpgpRk.d.ts} +57 -8
- package/build/redis.d.ts +2 -2
- package/build/testing.d.ts +8 -2
- package/build/testing.js +23 -4
- package/build/testing.js.map +1 -1
- package/package.json +2 -2
package/build/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { detectionTime, heartbeat } from "./heartbeat.js";
|
|
|
3
3
|
import { a as recoverSpan, i as heartbeatDetection, n as asWindow, r as failSpan, t as asDebounce } from "./timing-DGRi3XCI.js";
|
|
4
4
|
import { n as operationProblems, t as mayReissue } from "./operation-DL5RxFds.js";
|
|
5
5
|
import { backoffBand, backoffDelay, backoffProblems } from "./backoff.js";
|
|
6
|
+
import { n as countOf, r as millisecondsOf, t as boundedMillisecondsOf } from "./knobs-BxG-c7Zb.js";
|
|
6
7
|
import { convertExact, convertible, quantity, rationals, valueIn } from "@mark1russell7/quantity";
|
|
7
8
|
import { replay } from "@mark1russell7/algebra/replay";
|
|
8
9
|
//#region src/description.ts
|
|
@@ -498,11 +499,37 @@ const EXIT_SIGNALS = [
|
|
|
498
499
|
"SIGQUIT"
|
|
499
500
|
];
|
|
500
501
|
/** These are the process triggers that a declaration can opt in to. */
|
|
501
|
-
const PROCESS_TRIGGERS = [
|
|
502
|
+
const PROCESS_TRIGGERS = [
|
|
503
|
+
"stdin-close",
|
|
504
|
+
"ipc-close",
|
|
505
|
+
"ipc-message"
|
|
506
|
+
];
|
|
502
507
|
/** This function tells if a number is an exit code of a process: a whole number from 0 to 255. */
|
|
503
508
|
const isExitCode = (code) => Number.isInteger(code) && code >= 0 && code <= 255;
|
|
504
509
|
/** This function tells if a trigger is an exit signal. A repeat signal obeys `onRepeatSignal`. */
|
|
505
510
|
const isExitSignal = (trigger) => EXIT_SIGNALS.includes(trigger);
|
|
511
|
+
/** This function tells if a value is a message shape: a value of JSON, with no function and no undefined. */
|
|
512
|
+
const isMessageShape = (value) => {
|
|
513
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
514
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
515
|
+
if (Array.isArray(value)) return value.every(isMessageShape);
|
|
516
|
+
if (typeof value !== "object") return false;
|
|
517
|
+
const prototype = Object.getPrototypeOf(value);
|
|
518
|
+
return (prototype === Object.prototype || prototype === null) && Object.values(value).every(isMessageShape);
|
|
519
|
+
};
|
|
520
|
+
/** This function tells if a message of the IPC channel matches a shape. */
|
|
521
|
+
const matchesMessage = (shape, message) => {
|
|
522
|
+
if (shape === null || typeof shape !== "object") return Object.is(shape, message);
|
|
523
|
+
if (Array.isArray(shape)) {
|
|
524
|
+
const items = shape;
|
|
525
|
+
return Array.isArray(message) && message.length === items.length && items.every((item, at) => matchesMessage(item, message[at]));
|
|
526
|
+
}
|
|
527
|
+
if (message === null || typeof message !== "object" || Array.isArray(message)) return false;
|
|
528
|
+
const fields = message;
|
|
529
|
+
return Object.entries(shape).every(([field, value]) => Object.hasOwn(fields, field) && matchesMessage(value, fields[field]));
|
|
530
|
+
};
|
|
531
|
+
/** This time is the interval of the timer of a hold, the most that a timer of Node accepts. */
|
|
532
|
+
const HOLD_INTERVAL = 2147483647;
|
|
506
533
|
/** This function adds a handler to some events of an emitter. The handler operates one time at most. */
|
|
507
534
|
const once = (emitter, events, handler) => {
|
|
508
535
|
let called = false;
|
|
@@ -556,6 +583,13 @@ const processPortOf = (find) => {
|
|
|
556
583
|
return once(input, ["end", "close"], handler);
|
|
557
584
|
}
|
|
558
585
|
if (trigger === "ipc-close") return once(host, ["disconnect"], handler);
|
|
586
|
+
if (trigger === "ipc-message") {
|
|
587
|
+
const receive = (message) => handler(message);
|
|
588
|
+
host.on("message", receive);
|
|
589
|
+
return () => {
|
|
590
|
+
host.off("message", receive);
|
|
591
|
+
};
|
|
592
|
+
}
|
|
559
593
|
host.on(trigger, handler);
|
|
560
594
|
return () => {
|
|
561
595
|
host.off(trigger, handler);
|
|
@@ -565,6 +599,16 @@ const processPortOf = (find) => {
|
|
|
565
599
|
const host = hostProcess();
|
|
566
600
|
await Promise.all([written(host.stdout), written(host.stderr)]);
|
|
567
601
|
},
|
|
602
|
+
hold() {
|
|
603
|
+
hostProcess();
|
|
604
|
+
const timer = setInterval(() => {}, HOLD_INTERVAL);
|
|
605
|
+
let held = true;
|
|
606
|
+
return () => {
|
|
607
|
+
if (!held) return;
|
|
608
|
+
held = false;
|
|
609
|
+
clearInterval(timer);
|
|
610
|
+
};
|
|
611
|
+
},
|
|
568
612
|
exit(code) {
|
|
569
613
|
hostProcess().exit(code);
|
|
570
614
|
}
|
|
@@ -572,6 +616,33 @@ const processPortOf = (find) => {
|
|
|
572
616
|
};
|
|
573
617
|
/** This port is the process of Node. */
|
|
574
618
|
const nodeProcess = processPortOf(() => globalThis.process);
|
|
619
|
+
/**
|
|
620
|
+
* This function gives a process port for a lifecycle in a process that another part owns. That
|
|
621
|
+
* part is a script, a test of an application, or a library entry. The port adds no handler of a signal or of
|
|
622
|
+
* a trigger, it does not hold the process, and its `exit` only records the code. The caller stops
|
|
623
|
+
* the lifecycle with `life.shutdown()`. A process with a service of its own gives its parts to
|
|
624
|
+
* that service, and does not use this port.
|
|
625
|
+
*/
|
|
626
|
+
const embeddedProcess = () => {
|
|
627
|
+
let code = null;
|
|
628
|
+
let resolve = () => {};
|
|
629
|
+
return {
|
|
630
|
+
on() {
|
|
631
|
+
return () => {};
|
|
632
|
+
},
|
|
633
|
+
exit(requested) {
|
|
634
|
+
if (code !== null) return;
|
|
635
|
+
code = requested;
|
|
636
|
+
resolve(requested);
|
|
637
|
+
},
|
|
638
|
+
get exitCode() {
|
|
639
|
+
return code;
|
|
640
|
+
},
|
|
641
|
+
exited: new Promise((done) => {
|
|
642
|
+
resolve = done;
|
|
643
|
+
})
|
|
644
|
+
};
|
|
645
|
+
};
|
|
575
646
|
//#endregion
|
|
576
647
|
//#region src/routes.ts
|
|
577
648
|
const NAME = /^[A-Za-z_][A-Za-z0-9_]*/;
|
|
@@ -648,7 +719,7 @@ const compilePattern = (pattern) => {
|
|
|
648
719
|
};
|
|
649
720
|
//#endregion
|
|
650
721
|
//#region src/requests.ts
|
|
651
|
-
const isAsyncIterable = (value) => value !== null && (typeof value === "object" || typeof value === "function") && typeof value[Symbol.asyncIterator] === "function";
|
|
722
|
+
const isAsyncIterable$1 = (value) => value !== null && (typeof value === "object" || typeof value === "function") && typeof value[Symbol.asyncIterator] === "function";
|
|
652
723
|
const isPromiseLike = (value) => value !== null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
|
|
653
724
|
/** This function gives a stream of events from a function that gives the next event. */
|
|
654
725
|
const streamOf = (next, stop) => {
|
|
@@ -793,7 +864,7 @@ const tracker = (controllers, clock, refuses, choices = /* @__PURE__ */ new Map(
|
|
|
793
864
|
/** This function takes the result of the work: a stream, or one value. */
|
|
794
865
|
const take = (result) => {
|
|
795
866
|
if (end !== null) return;
|
|
796
|
-
if (isAsyncIterable(result)) {
|
|
867
|
+
if (isAsyncIterable$1(result)) {
|
|
797
868
|
source = result[Symbol.asyncIterator]();
|
|
798
869
|
if (abort.signal.aborted) closeSource();
|
|
799
870
|
} else {
|
|
@@ -804,7 +875,7 @@ const tracker = (controllers, clock, refuses, choices = /* @__PURE__ */ new Map(
|
|
|
804
875
|
let ready;
|
|
805
876
|
try {
|
|
806
877
|
const result = work(abort.signal);
|
|
807
|
-
if (isAsyncIterable(result) || !isPromiseLike(result)) {
|
|
878
|
+
if (isAsyncIterable$1(result) || !isPromiseLike(result)) {
|
|
808
879
|
take(result);
|
|
809
880
|
ready = Promise.resolve();
|
|
810
881
|
} else ready = Promise.resolve(result).then(take, failed);
|
|
@@ -1039,7 +1110,8 @@ const lifecycle = (description, runtime, components, options = {}, bind = () =>
|
|
|
1039
1110
|
startupEnded = resolve;
|
|
1040
1111
|
});
|
|
1041
1112
|
let offs = [];
|
|
1042
|
-
|
|
1113
|
+
let release = () => {};
|
|
1114
|
+
/** This function ends the process with a code, one time, and removes each handler of a trigger and the hold. */
|
|
1043
1115
|
const end = (code, on) => {
|
|
1044
1116
|
state = "exited";
|
|
1045
1117
|
exitCode = code;
|
|
@@ -1047,22 +1119,34 @@ const lifecycle = (description, runtime, components, options = {}, bind = () =>
|
|
|
1047
1119
|
offs = [];
|
|
1048
1120
|
moveExit("exited", on, `exit ${code}`);
|
|
1049
1121
|
finish(code);
|
|
1122
|
+
release();
|
|
1050
1123
|
port.exit(code);
|
|
1051
1124
|
};
|
|
1052
|
-
/**
|
|
1053
|
-
|
|
1125
|
+
/**
|
|
1126
|
+
* This function receives a trigger of the process: the first one starts the shutdown. The cause
|
|
1127
|
+
* names the trigger, and a message that stands for a signal tells it.
|
|
1128
|
+
*/
|
|
1129
|
+
const triggered = (trigger, cause = trigger) => {
|
|
1054
1130
|
if (exitState === "exited") return;
|
|
1055
1131
|
if (shutdownStarted || exitState === "stopping") {
|
|
1056
1132
|
if (isExitSignal(trigger) && repeatPolicy === "exit") {
|
|
1057
|
-
fail(`${
|
|
1133
|
+
fail(`${cause} came again while the shutdown ran, and the declaration exits at once`);
|
|
1058
1134
|
end(1, "exit.repeat");
|
|
1059
1135
|
return;
|
|
1060
1136
|
}
|
|
1061
|
-
record("exit", exitState, exitState, "exit.repeat",
|
|
1137
|
+
record("exit", exitState, exitState, "exit.repeat", cause);
|
|
1062
1138
|
return;
|
|
1063
1139
|
}
|
|
1064
1140
|
const code = exitCodes[trigger];
|
|
1065
|
-
shutdown(
|
|
1141
|
+
shutdown(cause, code === void 0 ? {} : { code });
|
|
1142
|
+
};
|
|
1143
|
+
const messages = description.shutdown.messages ?? [];
|
|
1144
|
+
/** This function receives a message of the IPC channel. The first declared shape that it matches gives its trigger. */
|
|
1145
|
+
const received = (message) => {
|
|
1146
|
+
const found = messages.find((each) => matchesMessage(each.match, message));
|
|
1147
|
+
if (found === void 0) return;
|
|
1148
|
+
if (found.as === void 0) triggered("ipc-message");
|
|
1149
|
+
else triggered(found.as, `${found.as} (a message of the IPC channel)`);
|
|
1066
1150
|
};
|
|
1067
1151
|
/** This function does the work of one phase, until its cap. It gives true when the work ended in time. */
|
|
1068
1152
|
const runPhase = async (phase, hold, work) => {
|
|
@@ -1208,7 +1292,8 @@ const lifecycle = (description, runtime, components, options = {}, bind = () =>
|
|
|
1208
1292
|
keepAlive: () => state === "running" && runtime.verdict().ready
|
|
1209
1293
|
};
|
|
1210
1294
|
bind(life);
|
|
1211
|
-
|
|
1295
|
+
release = port.hold?.() ?? (() => {});
|
|
1296
|
+
offs = [...EXIT_SIGNALS, ...description.shutdown.triggers ?? []].map((trigger) => port.on(trigger, trigger === "ipc-message" ? received : () => triggered(trigger)));
|
|
1212
1297
|
(async () => {
|
|
1213
1298
|
const started = (what) => record("gate", "starting", "starting", "component.started", what);
|
|
1214
1299
|
try {
|
|
@@ -1301,8 +1386,8 @@ const watchdog = (clock, spec, trace) => {
|
|
|
1301
1386
|
};
|
|
1302
1387
|
//#endregion
|
|
1303
1388
|
//#region src/builder.ts
|
|
1304
|
-
const isTime = (amount) => convertible(amount.unit, "s");
|
|
1305
|
-
const above = (amount) => amount.value === "infinite" || isTime(amount) && valueIn(amount, "s") > 0;
|
|
1389
|
+
const isTime$1 = (amount) => convertible(amount.unit, "s");
|
|
1390
|
+
const above = (amount) => amount.value === "infinite" || isTime$1(amount) && valueIn(amount, "s") > 0;
|
|
1306
1391
|
const whole = (count) => Number.isInteger(count) && count >= 1;
|
|
1307
1392
|
/** This function gives the problems of a timing: a zero interval or count fails decoding. */
|
|
1308
1393
|
const timingProblems = (policy, timing) => {
|
|
@@ -1313,15 +1398,20 @@ const timingProblems = (policy, timing) => {
|
|
|
1313
1398
|
if (!whole(timing.recoverAfter)) problems.push(`the ${policy} debounce recovers after ${timing.recoverAfter} polls, not a whole number above zero`);
|
|
1314
1399
|
return problems;
|
|
1315
1400
|
}
|
|
1316
|
-
const negative = (amount) => amount !== void 0 && (!isTime(amount) || valueIn(amount, "s") < 0);
|
|
1401
|
+
const negative = (amount) => amount !== void 0 && (!isTime$1(amount) || valueIn(amount, "s") < 0);
|
|
1317
1402
|
return negative(timing.failAfter) || negative(timing.recoverAfter) ? [`the ${policy} window is not a time of zero or more`] : [];
|
|
1318
1403
|
};
|
|
1319
1404
|
/** This function gives the problems of the rules of the shutdown: an unknown name, or an exit code that a process cannot give. */
|
|
1320
|
-
const shutdownProblems = (floor, repeat, triggers, exitCodes) => {
|
|
1405
|
+
const shutdownProblems = (floor, repeat, triggers, exitCodes, messages) => {
|
|
1321
1406
|
const problems = [];
|
|
1322
1407
|
if (floor !== void 0 && floor !== "half" && floor !== "none") problems.push(`the cooperative floor ${String(floor)} is not "half" or "none"`);
|
|
1323
1408
|
if (repeat !== void 0 && repeat !== "ignore" && repeat !== "exit") problems.push(`the policy of a repeat signal ${String(repeat)} is not "ignore" or "exit"`);
|
|
1324
1409
|
for (const trigger of triggers) if (!PROCESS_TRIGGERS.includes(trigger)) problems.push(`the trigger ${String(trigger)} is not a process trigger (${PROCESS_TRIGGERS.join(", ")})`);
|
|
1410
|
+
if (triggers.includes("ipc-message") && messages.length === 0) problems.push(`the trigger ipc-message has no message to match: declare it with svc.trigger("ipc-message", { match })`);
|
|
1411
|
+
for (const message of messages) {
|
|
1412
|
+
if (!isMessageShape(message.match)) problems.push(`the match of a message trigger is not a value of JSON: ${String(message.match)}`);
|
|
1413
|
+
if (message.as !== void 0 && !isExitSignal(message.as)) problems.push(`a message trigger stands for ${String(message.as)}, not an exit signal (${EXIT_SIGNALS.join(", ")})`);
|
|
1414
|
+
}
|
|
1325
1415
|
for (const [trigger, code] of Object.entries(exitCodes)) {
|
|
1326
1416
|
if (!(EXIT_SIGNALS.includes(trigger) || triggers.includes(trigger))) problems.push(`the exit code of ${trigger} names no exit signal and no declared trigger`);
|
|
1327
1417
|
if (code === void 0 || !isExitCode(code)) problems.push(`the exit code of ${trigger} is ${String(code)}, not a whole number from 0 to 255`);
|
|
@@ -1346,6 +1436,7 @@ const service = (name) => {
|
|
|
1346
1436
|
let floor;
|
|
1347
1437
|
let repeat;
|
|
1348
1438
|
const triggers = [];
|
|
1439
|
+
const messages = [];
|
|
1349
1440
|
const exitCodes = {};
|
|
1350
1441
|
let started = false;
|
|
1351
1442
|
let partial = false;
|
|
@@ -1567,7 +1658,13 @@ const service = (name) => {
|
|
|
1567
1658
|
},
|
|
1568
1659
|
trigger(...given) {
|
|
1569
1660
|
composing("a trigger");
|
|
1570
|
-
|
|
1661
|
+
const [first, message] = given;
|
|
1662
|
+
if (first === "ipc-message" && typeof message === "object" && message !== null) {
|
|
1663
|
+
if (!triggers.includes("ipc-message")) triggers.push("ipc-message");
|
|
1664
|
+
messages.push({ ...message });
|
|
1665
|
+
return builder;
|
|
1666
|
+
}
|
|
1667
|
+
for (const each of given) if (typeof each === "string" && !triggers.includes(each)) triggers.push(each);
|
|
1571
1668
|
return builder;
|
|
1572
1669
|
},
|
|
1573
1670
|
exitCode(trigger, code) {
|
|
@@ -1586,6 +1683,7 @@ const service = (name) => {
|
|
|
1586
1683
|
if (given.cooperativeFloor !== void 0) builder.cooperativeFloor(given.cooperativeFloor);
|
|
1587
1684
|
if (given.onRepeatSignal !== void 0) builder.onRepeatSignal(given.onRepeatSignal);
|
|
1588
1685
|
if (given.triggers !== void 0) builder.trigger(...given.triggers);
|
|
1686
|
+
for (const message of given.messages ?? []) builder.trigger("ipc-message", message);
|
|
1589
1687
|
for (const [trigger, code] of Object.entries(given.exitCodes ?? {})) if (code !== void 0) builder.exitCode(trigger, code);
|
|
1590
1688
|
return builder;
|
|
1591
1689
|
},
|
|
@@ -1607,11 +1705,11 @@ const service = (name) => {
|
|
|
1607
1705
|
for (const gate of ["readiness", "liveness"]) problems.push(...timingProblems(gate, policies[gate]));
|
|
1608
1706
|
for (const check of checks) {
|
|
1609
1707
|
if (check.dependency !== null && !known.has(check.dependency)) problems.push(`check ${check.name} exercises ${check.dependency}, which is not a dependency`);
|
|
1610
|
-
if (!isTime(check.poll.interval) || !isTime(check.poll.timeout)) problems.push(`check ${check.name} polls in a unit that is not a time`);
|
|
1708
|
+
if (!isTime$1(check.poll.interval) || !isTime$1(check.poll.timeout)) problems.push(`check ${check.name} polls in a unit that is not a time`);
|
|
1611
1709
|
else if (!above(check.poll.interval) || !above(check.poll.timeout)) problems.push(`check ${check.name} polls with an interval or a timeout that is not above zero`);
|
|
1612
1710
|
if (!(check.poll.jitter >= 0 && check.poll.jitter < 1)) problems.push(`check ${check.name} has a jitter of ${check.poll.jitter}, not a fraction below 1`);
|
|
1613
1711
|
const timing = check.gates === null ? null : policies[check.gates];
|
|
1614
|
-
if (timing?.type === "debounce" && isTime(check.poll.interval) && above(timing.interval) && valueIn(check.poll.interval, "s") !== valueIn(timing.interval, "s")) problems.push(`check ${check.name} polls every ${valueIn(check.poll.interval, "s")} s, but the ${check.gates} debounce counts polls every ${valueIn(timing.interval, "s")} s`);
|
|
1712
|
+
if (timing?.type === "debounce" && isTime$1(check.poll.interval) && above(timing.interval) && valueIn(check.poll.interval, "s") !== valueIn(timing.interval, "s")) problems.push(`check ${check.name} polls every ${valueIn(check.poll.interval, "s")} s, but the ${check.gates} debounce counts polls every ${valueIn(timing.interval, "s")} s`);
|
|
1615
1713
|
}
|
|
1616
1714
|
for (const controller of controllers) {
|
|
1617
1715
|
for (const needed of controller.requires) if (!known.has(needed)) problems.push(`controller ${controller.name} requires ${needed}, which is not a dependency`);
|
|
@@ -1619,8 +1717,8 @@ const service = (name) => {
|
|
|
1619
1717
|
for (const declared of controller.patterns) problems.push(...routePatternProblems(`controller ${controller.name}`, declared));
|
|
1620
1718
|
}
|
|
1621
1719
|
for (const dependency of dependencies) for (const [name, operation] of Object.entries(dependency.operations)) problems.push(...operationProblems(`dependency ${dependency.name}`, name, operation));
|
|
1622
|
-
for (const phase of phases) if (!isTime(phase.cap)) problems.push(`phase ${phase.name}'s cap is not a time`);
|
|
1623
|
-
problems.push(...shutdownProblems(floor, repeat, triggers, exitCodes));
|
|
1720
|
+
for (const phase of phases) if (!isTime$1(phase.cap)) problems.push(`phase ${phase.name}'s cap is not a time`);
|
|
1721
|
+
problems.push(...shutdownProblems(floor, repeat, triggers, exitCodes, messages));
|
|
1624
1722
|
const ranked = phases.map((phase) => phase.name).filter((each) => stageRank(each) >= 0);
|
|
1625
1723
|
for (let at = 1; at < ranked.length; at += 1) if (stageRank(ranked[at]) < stageRank(ranked[at - 1])) problems.push(`the phase "${ranked[at]}" comes after "${ranked[at - 1]}", but the stages go ${STAGES.join(", ")}`);
|
|
1626
1724
|
if (problems.length > 0) throw new Error(`${name}: ${problems.join("; ")}`);
|
|
@@ -1663,6 +1761,7 @@ const service = (name) => {
|
|
|
1663
1761
|
...floor === void 0 ? {} : { cooperativeFloor: floor },
|
|
1664
1762
|
onRepeatSignal: repeat,
|
|
1665
1763
|
...triggers.length === 0 ? {} : { triggers: [...triggers] },
|
|
1764
|
+
...messages.length === 0 ? {} : { messages: messages.map((each) => ({ ...each })) },
|
|
1666
1765
|
...Object.keys(exitCodes).length === 0 ? {} : { exitCodes: { ...exitCodes } }
|
|
1667
1766
|
}
|
|
1668
1767
|
};
|
|
@@ -1807,6 +1906,7 @@ const SERVICE_RULES = [
|
|
|
1807
1906
|
"O17",
|
|
1808
1907
|
"O18",
|
|
1809
1908
|
"O19",
|
|
1909
|
+
"O20",
|
|
1810
1910
|
"O21",
|
|
1811
1911
|
"O22",
|
|
1812
1912
|
"O23",
|
|
@@ -1816,6 +1916,14 @@ const SERVICE_RULES = [
|
|
|
1816
1916
|
const secondsOf = (amount) => rationals.toNumber(convertExact(rationals.fromNumber(amount.value), amount.unit, "s"));
|
|
1817
1917
|
/** This function gives an amount, or no bound, as the law binds it. */
|
|
1818
1918
|
const boundOf = (amount) => amount.value === "infinite" ? "inf" : secondsOf(amount);
|
|
1919
|
+
/** This function tells if a unit is a time. A unit that the package does not know is not a time. */
|
|
1920
|
+
const isTime = (unit) => {
|
|
1921
|
+
try {
|
|
1922
|
+
return convertible(unit, "s");
|
|
1923
|
+
} catch {
|
|
1924
|
+
return false;
|
|
1925
|
+
}
|
|
1926
|
+
};
|
|
1819
1927
|
/** This function gives the largest of some numbers of seconds, or undefined when there are none. */
|
|
1820
1928
|
const most = (values) => values.length === 0 ? void 0 : Math.max(...values);
|
|
1821
1929
|
/** This function gives what the declaration tells about the service, as symbols of the law, and the source of each. */
|
|
@@ -1857,7 +1965,30 @@ const dependencyBindings = (dependency, scenario) => {
|
|
|
1857
1965
|
for (const [symbol, amount] of Object.entries(scenario.dependencies?.[dependency.name] ?? {})) bind[symbol.includes("@") ? symbol : `${symbol}@dependency`] = boundOf(amount);
|
|
1858
1966
|
return bind;
|
|
1859
1967
|
};
|
|
1968
|
+
/**
|
|
1969
|
+
* These are the hops of the law at which the knobs of a transport bind, by its kind. A worker does
|
|
1970
|
+
* its units as a consumer does, thus both bind at `consumer` (O20). A server binds at no hop yet.
|
|
1971
|
+
*/
|
|
1972
|
+
const TRANSPORT_HOPS = {
|
|
1973
|
+
server: null,
|
|
1974
|
+
consumer: "consumer",
|
|
1975
|
+
worker: "consumer"
|
|
1976
|
+
};
|
|
1977
|
+
/**
|
|
1978
|
+
* This function gives the time knobs of a transport at the hop of its kind, and the values of the
|
|
1979
|
+
* scenario for its source. A knob with a hop in its name binds at that hop. A count binds no symbol.
|
|
1980
|
+
*/
|
|
1981
|
+
const transportBindings = (transport, hop, scenario) => {
|
|
1982
|
+
const bind = {};
|
|
1983
|
+
for (const [knob, amount] of Object.entries(transport.knobs)) {
|
|
1984
|
+
if (!isTime(amount.unit)) continue;
|
|
1985
|
+
bind[knob.includes("@") ? knob : `${knob}@${hop}`] = boundOf(amount);
|
|
1986
|
+
}
|
|
1987
|
+
for (const [symbol, amount] of Object.entries(scenario.transports?.[transport.name] ?? {})) bind[symbol] = boundOf(amount);
|
|
1988
|
+
return bind;
|
|
1989
|
+
};
|
|
1860
1990
|
const SCOPED = /@(client|dependency|server)$/;
|
|
1991
|
+
const TRANSPORT_SCOPED = /@consumer$/;
|
|
1861
1992
|
/** This identifier is the rule of the findings about a field that the declaration does not give. */
|
|
1862
1993
|
const DECLARATION_RULE = "declaration";
|
|
1863
1994
|
/**
|
|
@@ -1875,9 +2006,9 @@ const declarationFindings = (svc) => svc.shutdown.onRepeatSignal === void 0 ? [{
|
|
|
1875
2006
|
unbound: ["shutdown.onRepeatSignal"]
|
|
1876
2007
|
}] : [];
|
|
1877
2008
|
/**
|
|
1878
|
-
* This function applies each service rule to a declaration and a scenario
|
|
1879
|
-
* service,
|
|
1880
|
-
* description does not give (`DECLARATION_RULE`).
|
|
2009
|
+
* This function applies each service rule to a declaration and a scenario. A rule applies one time
|
|
2010
|
+
* for the service, for each client, or for each consumer and worker. The function also gives a
|
|
2011
|
+
* finding for each necessary field that the description does not give (`DECLARATION_RULE`).
|
|
1881
2012
|
*/
|
|
1882
2013
|
const lint = (svc, scenario, rules) => {
|
|
1883
2014
|
const service = serviceBindings(svc, scenario).bind;
|
|
@@ -1885,7 +2016,13 @@ const lint = (svc, scenario, rules) => {
|
|
|
1885
2016
|
for (const rule of rules) {
|
|
1886
2017
|
if (!SERVICE_RULES.includes(rule.id)) continue;
|
|
1887
2018
|
const symbols = symbolsOf(rule);
|
|
1888
|
-
const scopes = symbols.some((symbol) =>
|
|
2019
|
+
const scopes = symbols.some((symbol) => TRANSPORT_SCOPED.test(symbol)) ? svc.transports.flatMap((transport) => {
|
|
2020
|
+
const hop = TRANSPORT_HOPS[transport.kind];
|
|
2021
|
+
return hop === null ? [] : [[transport.name, {
|
|
2022
|
+
...service,
|
|
2023
|
+
...transportBindings(transport, hop, scenario)
|
|
2024
|
+
}]];
|
|
2025
|
+
}) : symbols.some((symbol) => SCOPED.test(symbol)) ? svc.dependencies.map((dependency) => [dependency.name, {
|
|
1889
2026
|
...service,
|
|
1890
2027
|
...dependencyBindings(dependency, scenario)
|
|
1891
2028
|
}]) : [[svc.name, service]];
|
|
@@ -2022,6 +2159,650 @@ const kubernetes = (pod) => {
|
|
|
2022
2159
|
};
|
|
2023
2160
|
};
|
|
2024
2161
|
//#endregion
|
|
2162
|
+
//#region src/units.ts
|
|
2163
|
+
/** This function gives the cause of an abort as text. */
|
|
2164
|
+
const causeOf = (signal) => signal.reason instanceof Error ? signal.reason.message : String(signal.reason ?? "the unit got an abort");
|
|
2165
|
+
/**
|
|
2166
|
+
* This function does one unit of work, and gives its end. With a request, the unit goes through
|
|
2167
|
+
* `life.request`, thus the service counts it, drains it and can cut it. A throw after the abort of
|
|
2168
|
+
* the transport is a cut, not a failure (S6 A10). A throw after the bound is a failure.
|
|
2169
|
+
*/
|
|
2170
|
+
const runUnit = async (run) => {
|
|
2171
|
+
const { clock, bound, abort, life, request, unit, work } = run;
|
|
2172
|
+
const timeout = new AbortController();
|
|
2173
|
+
const cancel = Number.isFinite(bound) ? clock.after(bound, () => timeout.abort(/* @__PURE__ */ new Error(`the unit did not end in its unit_processing_time of ${bound} ms`))) : () => {};
|
|
2174
|
+
const call = async (tracker) => work(AbortSignal.any(tracker === void 0 ? [abort.signal, timeout.signal] : [
|
|
2175
|
+
abort.signal,
|
|
2176
|
+
timeout.signal,
|
|
2177
|
+
tracker
|
|
2178
|
+
]));
|
|
2179
|
+
const thrown = (error) => {
|
|
2180
|
+
if (abort.signal.aborted) return {
|
|
2181
|
+
kind: "cut",
|
|
2182
|
+
cause: causeOf(abort.signal),
|
|
2183
|
+
retryable: false
|
|
2184
|
+
};
|
|
2185
|
+
if (timeout.signal.aborted) return {
|
|
2186
|
+
kind: "failed",
|
|
2187
|
+
error: timeout.signal.reason
|
|
2188
|
+
};
|
|
2189
|
+
return {
|
|
2190
|
+
kind: "failed",
|
|
2191
|
+
error
|
|
2192
|
+
};
|
|
2193
|
+
};
|
|
2194
|
+
try {
|
|
2195
|
+
if (request === void 0) try {
|
|
2196
|
+
return {
|
|
2197
|
+
kind: "done",
|
|
2198
|
+
value: await call()
|
|
2199
|
+
};
|
|
2200
|
+
} catch (error) {
|
|
2201
|
+
return thrown(error);
|
|
2202
|
+
}
|
|
2203
|
+
let collected;
|
|
2204
|
+
try {
|
|
2205
|
+
const route = typeof request.route === "string" ? request.route : request.route(unit);
|
|
2206
|
+
const detail = request.detail?.(unit);
|
|
2207
|
+
collected = life.request(request.controller, route, (signal) => call(signal).then((value) => ({ value })), detail === void 0 ? {} : { detail }).collect();
|
|
2208
|
+
} catch (error) {
|
|
2209
|
+
return {
|
|
2210
|
+
kind: "failed",
|
|
2211
|
+
error
|
|
2212
|
+
};
|
|
2213
|
+
}
|
|
2214
|
+
const { items, end } = await collected;
|
|
2215
|
+
if (end.kind === "done") return {
|
|
2216
|
+
kind: "done",
|
|
2217
|
+
value: items[0]?.value
|
|
2218
|
+
};
|
|
2219
|
+
if (end.kind === "failed") return thrown(end.error);
|
|
2220
|
+
if (end.kind === "refused") return {
|
|
2221
|
+
kind: "refused",
|
|
2222
|
+
cause: end.cause
|
|
2223
|
+
};
|
|
2224
|
+
return {
|
|
2225
|
+
kind: "cut",
|
|
2226
|
+
cause: end.cause,
|
|
2227
|
+
retryable: end.retryable
|
|
2228
|
+
};
|
|
2229
|
+
} finally {
|
|
2230
|
+
cancel();
|
|
2231
|
+
}
|
|
2232
|
+
};
|
|
2233
|
+
/** This function makes a flight set on a clock. */
|
|
2234
|
+
const flights = (clock) => {
|
|
2235
|
+
const units = /* @__PURE__ */ new Set();
|
|
2236
|
+
const idle = /* @__PURE__ */ new Set();
|
|
2237
|
+
const lower = /* @__PURE__ */ new Set();
|
|
2238
|
+
let aborted = null;
|
|
2239
|
+
const settle = () => {
|
|
2240
|
+
for (const done of [...lower]) done();
|
|
2241
|
+
lower.clear();
|
|
2242
|
+
if (units.size === 0) for (const done of [...idle]) done();
|
|
2243
|
+
};
|
|
2244
|
+
const abort = (reason) => {
|
|
2245
|
+
aborted ??= { reason };
|
|
2246
|
+
for (const unit of [...units]) if (!unit.signal.aborted) unit.abort(aborted.reason);
|
|
2247
|
+
};
|
|
2248
|
+
return {
|
|
2249
|
+
get size() {
|
|
2250
|
+
return units.size;
|
|
2251
|
+
},
|
|
2252
|
+
add(unit) {
|
|
2253
|
+
units.add(unit);
|
|
2254
|
+
if (aborted !== null && !unit.signal.aborted) unit.abort(aborted.reason);
|
|
2255
|
+
return () => {
|
|
2256
|
+
if (units.delete(unit)) settle();
|
|
2257
|
+
};
|
|
2258
|
+
},
|
|
2259
|
+
abort,
|
|
2260
|
+
clear() {
|
|
2261
|
+
units.clear();
|
|
2262
|
+
settle();
|
|
2263
|
+
},
|
|
2264
|
+
drain(deadline, signal) {
|
|
2265
|
+
const cooperative = () => abort(signal.reason);
|
|
2266
|
+
if (signal.aborted) cooperative();
|
|
2267
|
+
else signal.addEventListener("abort", cooperative, { once: true });
|
|
2268
|
+
if (units.size === 0 || deadline <= clock.now()) {
|
|
2269
|
+
signal.removeEventListener("abort", cooperative);
|
|
2270
|
+
return Promise.resolve();
|
|
2271
|
+
}
|
|
2272
|
+
return new Promise((resolve) => {
|
|
2273
|
+
let cancel = () => {};
|
|
2274
|
+
const done = () => {
|
|
2275
|
+
idle.delete(done);
|
|
2276
|
+
cancel();
|
|
2277
|
+
signal.removeEventListener("abort", cooperative);
|
|
2278
|
+
resolve();
|
|
2279
|
+
};
|
|
2280
|
+
idle.add(done);
|
|
2281
|
+
cancel = clock.after(Math.max(0, deadline - clock.now()), done);
|
|
2282
|
+
});
|
|
2283
|
+
},
|
|
2284
|
+
less() {
|
|
2285
|
+
return new Promise((resolve) => lower.add(resolve));
|
|
2286
|
+
},
|
|
2287
|
+
release() {
|
|
2288
|
+
for (const done of [...lower]) done();
|
|
2289
|
+
lower.clear();
|
|
2290
|
+
}
|
|
2291
|
+
};
|
|
2292
|
+
};
|
|
2293
|
+
/** This function makes a sleeper on a clock. */
|
|
2294
|
+
const sleeper = (clock) => {
|
|
2295
|
+
let wakes = 0;
|
|
2296
|
+
let stopped = false;
|
|
2297
|
+
const sleeps = /* @__PURE__ */ new Set();
|
|
2298
|
+
const backoffs = /* @__PURE__ */ new Set();
|
|
2299
|
+
const wait = (ms, into) => stopped ? Promise.resolve() : new Promise((resolve) => {
|
|
2300
|
+
let cancel = () => {};
|
|
2301
|
+
const done = () => {
|
|
2302
|
+
if (!into.delete(done)) return;
|
|
2303
|
+
cancel();
|
|
2304
|
+
resolve();
|
|
2305
|
+
};
|
|
2306
|
+
into.add(done);
|
|
2307
|
+
if (Number.isFinite(ms)) cancel = clock.after(ms, done);
|
|
2308
|
+
});
|
|
2309
|
+
return {
|
|
2310
|
+
get wakes() {
|
|
2311
|
+
return wakes;
|
|
2312
|
+
},
|
|
2313
|
+
sleep(ms, since) {
|
|
2314
|
+
if (since !== wakes) return Promise.resolve();
|
|
2315
|
+
return wait(ms, sleeps);
|
|
2316
|
+
},
|
|
2317
|
+
backoff(ms) {
|
|
2318
|
+
return wait(ms, backoffs);
|
|
2319
|
+
},
|
|
2320
|
+
wake() {
|
|
2321
|
+
wakes += 1;
|
|
2322
|
+
const ended = sleeps.size;
|
|
2323
|
+
for (const done of [...sleeps]) done();
|
|
2324
|
+
return ended;
|
|
2325
|
+
},
|
|
2326
|
+
stop() {
|
|
2327
|
+
stopped = true;
|
|
2328
|
+
for (const done of [...sleeps, ...backoffs]) done();
|
|
2329
|
+
}
|
|
2330
|
+
};
|
|
2331
|
+
};
|
|
2332
|
+
/** This function waits for a work until a bound on the clock, in milliseconds. */
|
|
2333
|
+
const settleWithin = (clock, ms, work) => new Promise((resolve) => {
|
|
2334
|
+
const cancel = Number.isFinite(ms) ? clock.after(ms, () => resolve({ kind: "bound" })) : () => {};
|
|
2335
|
+
work.then(() => {
|
|
2336
|
+
cancel();
|
|
2337
|
+
resolve({ kind: "done" });
|
|
2338
|
+
}, (error) => {
|
|
2339
|
+
cancel();
|
|
2340
|
+
resolve({
|
|
2341
|
+
kind: "failed",
|
|
2342
|
+
error
|
|
2343
|
+
});
|
|
2344
|
+
});
|
|
2345
|
+
});
|
|
2346
|
+
/**
|
|
2347
|
+
* This function gives the backoff of the knobs of a loop. The first delay is `retry_base`, and each
|
|
2348
|
+
* delay is two times longer, up to `retry_cap`. Each delay has a jitter up to the base, as in the Redis client.
|
|
2349
|
+
*/
|
|
2350
|
+
const retryOf = (knobs, owner) => {
|
|
2351
|
+
const base = boundedMillisecondsOf(knobs, "retry_base", owner);
|
|
2352
|
+
const backoff = {
|
|
2353
|
+
base,
|
|
2354
|
+
factor: 2,
|
|
2355
|
+
cap: boundedMillisecondsOf(knobs, "retry_cap", owner),
|
|
2356
|
+
jitter: {
|
|
2357
|
+
kind: "add",
|
|
2358
|
+
amount: base
|
|
2359
|
+
}
|
|
2360
|
+
};
|
|
2361
|
+
const problems = backoffProblems(backoff);
|
|
2362
|
+
if (problems.length > 0) throw new Error(`${owner}: the retry_base and retry_cap give no backoff: ${problems.join(", ")}`);
|
|
2363
|
+
return backoff;
|
|
2364
|
+
};
|
|
2365
|
+
/**
|
|
2366
|
+
* This function gives the problems of the bounds of a loop in a declaration. The close bound fits
|
|
2367
|
+
* in the cap of `close transports`. The bound of a unit fits in the cap of `drain`, or the drain can cut a unit (S6 A10).
|
|
2368
|
+
*/
|
|
2369
|
+
const boundProblems = (owner, closeBound, unitBound, description) => {
|
|
2370
|
+
const capOf = (name) => {
|
|
2371
|
+
const phase = description.shutdown.phases.find((each) => each.name === name);
|
|
2372
|
+
return phase === void 0 ? void 0 : valueIn(phase.cap, "ms");
|
|
2373
|
+
};
|
|
2374
|
+
const found = [];
|
|
2375
|
+
const close = capOf("close transports");
|
|
2376
|
+
if (close === void 0) found.push(`${owner} needs the phase "close transports" to close`);
|
|
2377
|
+
else if (closeBound >= close) found.push(`the close_bound of ${owner}, ${closeBound} ms, does not fit in the cap of "close transports", ${close} ms`);
|
|
2378
|
+
const drain = capOf("drain");
|
|
2379
|
+
if (drain === void 0) found.push(`${owner} needs the phase "drain" to end its units`);
|
|
2380
|
+
else if (!Number.isFinite(unitBound)) found.push(`the unit_processing_time of ${owner} has no bound, thus the drain can cut a unit at its cap of ${drain} ms`);
|
|
2381
|
+
else if (unitBound >= drain) found.push(`the unit_processing_time of ${owner}, ${unitBound} ms, does not fit in the cap of "drain", ${drain} ms`);
|
|
2382
|
+
return found;
|
|
2383
|
+
};
|
|
2384
|
+
//#endregion
|
|
2385
|
+
//#region src/worker.ts
|
|
2386
|
+
const OWNER$1 = "workerLoop";
|
|
2387
|
+
/**
|
|
2388
|
+
* This function makes a worker loop as a transport (S6 A10). It has a setup, an iteration and a
|
|
2389
|
+
* teardown, and each lane does its iterations one at a time.
|
|
2390
|
+
* - The start does the setup. A failed setup gets its teardown, and the start throws.
|
|
2391
|
+
* - The first iteration of each lane comes after the startup ends. Each sleep and each backoff is on the clock.
|
|
2392
|
+
* - `stopIntake` starts no new iteration, and it ends each sleep and each backoff.
|
|
2393
|
+
* - `drain` waits for each iteration in flight until the deadline, and gives it the cooperative
|
|
2394
|
+
* abort. A throw after that abort is not a failure.
|
|
2395
|
+
* - `close` aborts each iteration, waits for its end, and does the teardown, all in `close_bound`.
|
|
2396
|
+
*/
|
|
2397
|
+
const workerLoop = (knobs, options) => {
|
|
2398
|
+
const sleep = millisecondsOf(knobs, "worker_sleep", OWNER$1);
|
|
2399
|
+
const bound = millisecondsOf(knobs, "unit_processing_time", OWNER$1);
|
|
2400
|
+
const backoff = retryOf(knobs, OWNER$1);
|
|
2401
|
+
const closeBound = boundedMillisecondsOf(knobs, "close_bound", OWNER$1);
|
|
2402
|
+
const lanes = countOf(knobs, "concurrency", OWNER$1);
|
|
2403
|
+
if (!Number.isFinite(lanes)) throw new Error(`${OWNER$1} needs a concurrency with a bound: each lane is a loop`);
|
|
2404
|
+
const clock = options.clock ?? realClock;
|
|
2405
|
+
const random = options.random ?? Math.random;
|
|
2406
|
+
const running = flights(clock);
|
|
2407
|
+
const rests = sleeper(clock);
|
|
2408
|
+
const intake = new AbortController();
|
|
2409
|
+
const pending = /* @__PURE__ */ new Set();
|
|
2410
|
+
let started = false;
|
|
2411
|
+
let setup = null;
|
|
2412
|
+
let state;
|
|
2413
|
+
let tornDown = null;
|
|
2414
|
+
let closing = null;
|
|
2415
|
+
const report = (error, failures) => {
|
|
2416
|
+
try {
|
|
2417
|
+
options.onError?.(error, failures);
|
|
2418
|
+
} catch {}
|
|
2419
|
+
};
|
|
2420
|
+
/** This function does the teardown one time, after the setup ends, and only when the setup started. */
|
|
2421
|
+
const teardown = () => {
|
|
2422
|
+
if (tornDown !== null) return tornDown;
|
|
2423
|
+
const release = options.teardown;
|
|
2424
|
+
const made = setup;
|
|
2425
|
+
tornDown = made === null || release === void 0 ? Promise.resolve() : made.then((given) => release(given), () => release(void 0));
|
|
2426
|
+
return tornDown;
|
|
2427
|
+
};
|
|
2428
|
+
const keep = (work) => {
|
|
2429
|
+
pending.add(work);
|
|
2430
|
+
const forget = () => {
|
|
2431
|
+
pending.delete(work);
|
|
2432
|
+
};
|
|
2433
|
+
work.then(forget, forget);
|
|
2434
|
+
return work;
|
|
2435
|
+
};
|
|
2436
|
+
/** This function is one lane: an iteration, then its sleep or its backoff, until the intake stops. */
|
|
2437
|
+
const lane = async (index, life) => {
|
|
2438
|
+
let failures = 0;
|
|
2439
|
+
let since = rests.wakes;
|
|
2440
|
+
await rests.sleep(0, since);
|
|
2441
|
+
for (;;) {
|
|
2442
|
+
if (intake.signal.aborted) return;
|
|
2443
|
+
since = rests.wakes;
|
|
2444
|
+
const abort = new AbortController();
|
|
2445
|
+
const remove = running.add(abort);
|
|
2446
|
+
const count = failures;
|
|
2447
|
+
const end = await runUnit({
|
|
2448
|
+
clock,
|
|
2449
|
+
bound,
|
|
2450
|
+
abort,
|
|
2451
|
+
life,
|
|
2452
|
+
request: options.request,
|
|
2453
|
+
unit: index,
|
|
2454
|
+
work: (signal) => keep((async () => options.iteration({
|
|
2455
|
+
signal,
|
|
2456
|
+
intake: intake.signal,
|
|
2457
|
+
state,
|
|
2458
|
+
lane: index,
|
|
2459
|
+
failures: count,
|
|
2460
|
+
life
|
|
2461
|
+
}))())
|
|
2462
|
+
});
|
|
2463
|
+
remove();
|
|
2464
|
+
if (end.kind === "cut") return;
|
|
2465
|
+
if (end.kind === "done") {
|
|
2466
|
+
failures = 0;
|
|
2467
|
+
await rests.sleep(end.value === "more" ? 0 : sleep, end.value === "more" ? rests.wakes : since);
|
|
2468
|
+
continue;
|
|
2469
|
+
}
|
|
2470
|
+
failures += 1;
|
|
2471
|
+
report(end.kind === "failed" ? end.error : /* @__PURE__ */ new Error(`the service refused the iteration: ${end.cause}`), failures);
|
|
2472
|
+
await rests.backoff(backoffDelay(backoff, failures, random));
|
|
2473
|
+
}
|
|
2474
|
+
};
|
|
2475
|
+
return {
|
|
2476
|
+
knobs: { ...knobs },
|
|
2477
|
+
async start(signal, life) {
|
|
2478
|
+
if (closing !== null) throw new Error(`${OWNER$1}: a closed loop does not start again`);
|
|
2479
|
+
if (started) throw new Error(`${OWNER$1}: the loop started already`);
|
|
2480
|
+
started = true;
|
|
2481
|
+
const make = options.setup;
|
|
2482
|
+
setup = make === void 0 ? Promise.resolve(void 0) : new Promise((resolve) => resolve(make(signal)));
|
|
2483
|
+
try {
|
|
2484
|
+
await setup.then((given) => {
|
|
2485
|
+
state = given;
|
|
2486
|
+
});
|
|
2487
|
+
} catch (error) {
|
|
2488
|
+
await settleWithin(clock, closeBound, teardown());
|
|
2489
|
+
throw error;
|
|
2490
|
+
}
|
|
2491
|
+
life.started.then(() => {
|
|
2492
|
+
if (intake.signal.aborted || life.state !== "running") return;
|
|
2493
|
+
for (let index = 0; index < lanes; index += 1) lane(index, life).catch((error) => report(error, 0));
|
|
2494
|
+
});
|
|
2495
|
+
},
|
|
2496
|
+
stopIntake() {
|
|
2497
|
+
if (intake.signal.aborted) return;
|
|
2498
|
+
intake.abort(/* @__PURE__ */ new Error(`${OWNER$1} stopped its intake`));
|
|
2499
|
+
rests.stop();
|
|
2500
|
+
},
|
|
2501
|
+
drain(deadline, signal) {
|
|
2502
|
+
return running.drain(deadline, signal);
|
|
2503
|
+
},
|
|
2504
|
+
inFlight: () => running.size,
|
|
2505
|
+
wake() {
|
|
2506
|
+
if (intake.signal.aborted) return false;
|
|
2507
|
+
rests.wake();
|
|
2508
|
+
return true;
|
|
2509
|
+
},
|
|
2510
|
+
close() {
|
|
2511
|
+
if (closing !== null) return closing;
|
|
2512
|
+
if (!intake.signal.aborted) intake.abort(/* @__PURE__ */ new Error(`${OWNER$1} closed`));
|
|
2513
|
+
rests.stop();
|
|
2514
|
+
running.abort(/* @__PURE__ */ new Error(`${OWNER$1} closed`));
|
|
2515
|
+
running.clear();
|
|
2516
|
+
closing = (async () => {
|
|
2517
|
+
const began = clock.now();
|
|
2518
|
+
const ended = await settleWithin(clock, closeBound, Promise.allSettled([...pending]));
|
|
2519
|
+
const down = await settleWithin(clock, Math.max(0, closeBound - (clock.now() - began)), teardown());
|
|
2520
|
+
if (ended.kind === "bound") throw new Error(`${OWNER$1}: an iteration did not end in the close_bound of ${closeBound} ms after its abort`);
|
|
2521
|
+
if (down.kind === "failed") throw down.error;
|
|
2522
|
+
if (down.kind === "bound") throw new Error(`${OWNER$1}: the teardown did not end in the close_bound of ${closeBound} ms`);
|
|
2523
|
+
})();
|
|
2524
|
+
return closing;
|
|
2525
|
+
},
|
|
2526
|
+
problems(description) {
|
|
2527
|
+
return boundProblems(OWNER$1, closeBound, bound, description);
|
|
2528
|
+
}
|
|
2529
|
+
};
|
|
2530
|
+
};
|
|
2531
|
+
//#endregion
|
|
2532
|
+
//#region src/queue.ts
|
|
2533
|
+
/**
|
|
2534
|
+
* This function gives the redelivery policy of S6 A11. A poison message gets `ack`. Each other job
|
|
2535
|
+
* goes back on the queue after its first failure, one time. After a later failure, `then` gives the
|
|
2536
|
+
* action: the declared policy of the queue.
|
|
2537
|
+
*/
|
|
2538
|
+
const redeliverOnce = (then, poison) => (failure) => {
|
|
2539
|
+
if (poison?.(failure.error, failure.job) === true) return "ack";
|
|
2540
|
+
if (failure.delivery <= 1) return "requeue";
|
|
2541
|
+
return typeof then === "function" ? then(failure) : then;
|
|
2542
|
+
};
|
|
2543
|
+
const OWNER = "queueDrain";
|
|
2544
|
+
const ACTIONS = [
|
|
2545
|
+
"requeue",
|
|
2546
|
+
"reject",
|
|
2547
|
+
"ack"
|
|
2548
|
+
];
|
|
2549
|
+
const STOPPED = Symbol("the intake stopped");
|
|
2550
|
+
const isAsyncIterable = (value) => value !== null && (typeof value === "object" || typeof value === "function") && typeof value[Symbol.asyncIterator] === "function";
|
|
2551
|
+
/**
|
|
2552
|
+
* This function makes a queue drain as a transport (S6 A11). One taker takes the jobs from the
|
|
2553
|
+
* source while a slot is free, and each job operates in its slot.
|
|
2554
|
+
* - A success gets `ack`. A failure gets the action of the redelivery policy.
|
|
2555
|
+
* - `stopIntake` takes no new job, and a job that the drain took and did not start goes back (`requeue`, `unstarted`).
|
|
2556
|
+
* - `drain` waits for each job in flight until the deadline, and gives it the cooperative abort.
|
|
2557
|
+
* - `close` aborts each job, waits for its end, and closes the source, all in `close_bound`. A job
|
|
2558
|
+
* that does not end in the bound gets `reject`.
|
|
2559
|
+
*/
|
|
2560
|
+
const queueDrain = (knobs, options) => {
|
|
2561
|
+
const poll = millisecondsOf(knobs, "queue_poll_interval", OWNER);
|
|
2562
|
+
const bound = millisecondsOf(knobs, "unit_processing_time", OWNER);
|
|
2563
|
+
const backoff = retryOf(knobs, OWNER);
|
|
2564
|
+
const closeBound = boundedMillisecondsOf(knobs, "close_bound", OWNER);
|
|
2565
|
+
const concurrency = countOf(knobs, "concurrency", OWNER);
|
|
2566
|
+
const { settle } = options;
|
|
2567
|
+
const clock = options.clock ?? realClock;
|
|
2568
|
+
const random = options.random ?? Math.random;
|
|
2569
|
+
const running = flights(clock);
|
|
2570
|
+
const rests = sleeper(clock);
|
|
2571
|
+
const intake = new AbortController();
|
|
2572
|
+
const stopped = new Promise((resolve) => intake.signal.addEventListener("abort", () => resolve(STOPPED), { once: true }));
|
|
2573
|
+
const pending = /* @__PURE__ */ new Set();
|
|
2574
|
+
const active = /* @__PURE__ */ new Set();
|
|
2575
|
+
let refusals = 0;
|
|
2576
|
+
let pausedUntil = Number.NEGATIVE_INFINITY;
|
|
2577
|
+
let cancelSource = () => {};
|
|
2578
|
+
let started = false;
|
|
2579
|
+
let closing = null;
|
|
2580
|
+
const report = (error, job) => {
|
|
2581
|
+
try {
|
|
2582
|
+
options.onError?.(error, job);
|
|
2583
|
+
} catch {}
|
|
2584
|
+
};
|
|
2585
|
+
const keep = (work) => {
|
|
2586
|
+
pending.add(work);
|
|
2587
|
+
const forget = () => {
|
|
2588
|
+
pending.delete(work);
|
|
2589
|
+
};
|
|
2590
|
+
work.then(forget, forget);
|
|
2591
|
+
};
|
|
2592
|
+
/** This function tells the queue the end of a job, one time. A throw of the queue is a failure. */
|
|
2593
|
+
const once = async (unit, tell) => {
|
|
2594
|
+
if (unit.settled) return;
|
|
2595
|
+
unit.settled = true;
|
|
2596
|
+
try {
|
|
2597
|
+
await tell();
|
|
2598
|
+
} catch (error) {
|
|
2599
|
+
report(error, unit.job);
|
|
2600
|
+
}
|
|
2601
|
+
};
|
|
2602
|
+
const handBack = (job) => keep((async () => {
|
|
2603
|
+
try {
|
|
2604
|
+
await settle.requeue(job, "unstarted");
|
|
2605
|
+
} catch (error) {
|
|
2606
|
+
report(error, job);
|
|
2607
|
+
}
|
|
2608
|
+
})());
|
|
2609
|
+
/** This function gives the action of the policy for a failed job. A policy that throws, or that gives no action, rejects the job. */
|
|
2610
|
+
const actionOf = (job, error, delivery) => {
|
|
2611
|
+
try {
|
|
2612
|
+
const action = options.redelivery({
|
|
2613
|
+
job,
|
|
2614
|
+
error,
|
|
2615
|
+
delivery
|
|
2616
|
+
});
|
|
2617
|
+
if (ACTIONS.includes(action)) return action;
|
|
2618
|
+
report(/* @__PURE__ */ new Error(`the redelivery policy gave ${String(action)}, not requeue, reject or ack`), job);
|
|
2619
|
+
} catch (thrown) {
|
|
2620
|
+
report(thrown, job);
|
|
2621
|
+
}
|
|
2622
|
+
return "reject";
|
|
2623
|
+
};
|
|
2624
|
+
const settleEnd = (unit, end, delivery) => {
|
|
2625
|
+
const { job } = unit;
|
|
2626
|
+
if (end.kind === "done") return once(unit, () => settle.ack(job));
|
|
2627
|
+
if (end.kind === "refused") {
|
|
2628
|
+
report(/* @__PURE__ */ new Error(`the service refused the job: ${end.cause}`), job);
|
|
2629
|
+
return once(unit, () => settle.requeue(job, "unstarted"));
|
|
2630
|
+
}
|
|
2631
|
+
if (end.kind === "cut") {
|
|
2632
|
+
if (end.retryable) return once(unit, () => settle.requeue(job, "cut"));
|
|
2633
|
+
const cut = new Error(end.cause);
|
|
2634
|
+
return once(unit, () => settle.reject(job, cut));
|
|
2635
|
+
}
|
|
2636
|
+
const { error } = end;
|
|
2637
|
+
report(error, job);
|
|
2638
|
+
const action = actionOf(job, error, delivery);
|
|
2639
|
+
if (action === "requeue") return once(unit, () => settle.requeue(job, "failed"));
|
|
2640
|
+
return once(unit, () => action === "ack" ? settle.ack(job) : settle.reject(job, error));
|
|
2641
|
+
};
|
|
2642
|
+
/** This function starts one job in a slot. The slot stays in flight until the queue has the end of the job. */
|
|
2643
|
+
const runJob = (job, life) => {
|
|
2644
|
+
const unit = {
|
|
2645
|
+
job,
|
|
2646
|
+
settled: false
|
|
2647
|
+
};
|
|
2648
|
+
active.add(unit);
|
|
2649
|
+
const abort = new AbortController();
|
|
2650
|
+
const remove = running.add(abort);
|
|
2651
|
+
let delivery;
|
|
2652
|
+
try {
|
|
2653
|
+
delivery = settle.delivery(job);
|
|
2654
|
+
} catch (error) {
|
|
2655
|
+
report(error, job);
|
|
2656
|
+
delivery = Number.POSITIVE_INFINITY;
|
|
2657
|
+
}
|
|
2658
|
+
keep((async () => {
|
|
2659
|
+
const end = await runUnit({
|
|
2660
|
+
clock,
|
|
2661
|
+
bound,
|
|
2662
|
+
abort,
|
|
2663
|
+
life,
|
|
2664
|
+
request: options.request,
|
|
2665
|
+
unit: job,
|
|
2666
|
+
work: (signal) => options.handle(job, {
|
|
2667
|
+
signal,
|
|
2668
|
+
intake: intake.signal,
|
|
2669
|
+
delivery,
|
|
2670
|
+
life
|
|
2671
|
+
})
|
|
2672
|
+
});
|
|
2673
|
+
if (end.kind === "refused") {
|
|
2674
|
+
refusals += 1;
|
|
2675
|
+
pausedUntil = clock.now() + backoffDelay(backoff, refusals, random);
|
|
2676
|
+
} else refusals = 0;
|
|
2677
|
+
await settleEnd(unit, end, delivery);
|
|
2678
|
+
})().finally(() => {
|
|
2679
|
+
active.delete(unit);
|
|
2680
|
+
remove();
|
|
2681
|
+
}));
|
|
2682
|
+
};
|
|
2683
|
+
/** This function gives the take of the source. */
|
|
2684
|
+
const takeOf = () => {
|
|
2685
|
+
const source = options.source;
|
|
2686
|
+
if (isAsyncIterable(source)) {
|
|
2687
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
2688
|
+
cancelSource = () => {
|
|
2689
|
+
try {
|
|
2690
|
+
Promise.resolve(iterator.return?.()).catch(() => {});
|
|
2691
|
+
} catch {}
|
|
2692
|
+
};
|
|
2693
|
+
return async () => {
|
|
2694
|
+
const step = await iterator.next();
|
|
2695
|
+
return step.done === true ? { kind: "end" } : {
|
|
2696
|
+
kind: "job",
|
|
2697
|
+
job: step.value
|
|
2698
|
+
};
|
|
2699
|
+
};
|
|
2700
|
+
}
|
|
2701
|
+
const pull = source;
|
|
2702
|
+
return () => new Promise((resolve) => resolve(pull(intake.signal))).then((job) => job === null || job === void 0 ? { kind: "none" } : {
|
|
2703
|
+
kind: "job",
|
|
2704
|
+
job
|
|
2705
|
+
});
|
|
2706
|
+
};
|
|
2707
|
+
/** This function is the taker: it takes a job while a slot is free, until the intake stops or the source ends. */
|
|
2708
|
+
const taker = async (life) => {
|
|
2709
|
+
const take = takeOf();
|
|
2710
|
+
let failures = 0;
|
|
2711
|
+
for (;;) {
|
|
2712
|
+
if (intake.signal.aborted) return;
|
|
2713
|
+
if (running.size >= concurrency) {
|
|
2714
|
+
await running.less();
|
|
2715
|
+
continue;
|
|
2716
|
+
}
|
|
2717
|
+
const pause = pausedUntil - clock.now();
|
|
2718
|
+
if (pause > 0) {
|
|
2719
|
+
await rests.backoff(pause);
|
|
2720
|
+
continue;
|
|
2721
|
+
}
|
|
2722
|
+
const since = rests.wakes;
|
|
2723
|
+
const taking = take().catch((error) => ({
|
|
2724
|
+
kind: "error",
|
|
2725
|
+
error
|
|
2726
|
+
}));
|
|
2727
|
+
const first = await Promise.race([taking, stopped]);
|
|
2728
|
+
if (first === STOPPED || intake.signal.aborted) {
|
|
2729
|
+
if (first === STOPPED) taking.then((late) => {
|
|
2730
|
+
if (late.kind === "job") handBack(late.job);
|
|
2731
|
+
});
|
|
2732
|
+
else if (first.kind === "job") handBack(first.job);
|
|
2733
|
+
return;
|
|
2734
|
+
}
|
|
2735
|
+
if (first.kind === "error") {
|
|
2736
|
+
failures += 1;
|
|
2737
|
+
report(first.error, void 0);
|
|
2738
|
+
await rests.backoff(backoffDelay(backoff, failures, random));
|
|
2739
|
+
continue;
|
|
2740
|
+
}
|
|
2741
|
+
failures = 0;
|
|
2742
|
+
if (first.kind === "end") {
|
|
2743
|
+
report(/* @__PURE__ */ new Error(`the source of ${OWNER} ended while its intake was open: it takes no more jobs`), void 0);
|
|
2744
|
+
return;
|
|
2745
|
+
}
|
|
2746
|
+
if (first.kind === "none") {
|
|
2747
|
+
await rests.sleep(poll, since);
|
|
2748
|
+
continue;
|
|
2749
|
+
}
|
|
2750
|
+
runJob(first.job, life);
|
|
2751
|
+
}
|
|
2752
|
+
};
|
|
2753
|
+
const stopTaking = (cause) => {
|
|
2754
|
+
if (intake.signal.aborted) return;
|
|
2755
|
+
intake.abort(new Error(cause));
|
|
2756
|
+
rests.stop();
|
|
2757
|
+
running.release();
|
|
2758
|
+
cancelSource();
|
|
2759
|
+
};
|
|
2760
|
+
return {
|
|
2761
|
+
knobs: { ...knobs },
|
|
2762
|
+
start(_signal, life) {
|
|
2763
|
+
if (closing !== null) throw new Error(`${OWNER}: a closed drain does not start again`);
|
|
2764
|
+
if (started) throw new Error(`${OWNER}: the drain started already`);
|
|
2765
|
+
started = true;
|
|
2766
|
+
life.started.then(() => {
|
|
2767
|
+
if (intake.signal.aborted || life.state !== "running") return;
|
|
2768
|
+
taker(life).catch((error) => report(error, void 0));
|
|
2769
|
+
});
|
|
2770
|
+
},
|
|
2771
|
+
stopIntake() {
|
|
2772
|
+
stopTaking(`${OWNER} stopped its intake`);
|
|
2773
|
+
},
|
|
2774
|
+
drain(deadline, signal) {
|
|
2775
|
+
return running.drain(deadline, signal);
|
|
2776
|
+
},
|
|
2777
|
+
inFlight: () => running.size,
|
|
2778
|
+
wake() {
|
|
2779
|
+
if (intake.signal.aborted) return false;
|
|
2780
|
+
rests.wake();
|
|
2781
|
+
return true;
|
|
2782
|
+
},
|
|
2783
|
+
close() {
|
|
2784
|
+
if (closing !== null) return closing;
|
|
2785
|
+
stopTaking(`${OWNER} closed`);
|
|
2786
|
+
running.abort(/* @__PURE__ */ new Error(`${OWNER} closed`));
|
|
2787
|
+
running.clear();
|
|
2788
|
+
closing = (async () => {
|
|
2789
|
+
const began = clock.now();
|
|
2790
|
+
const ended = await settleWithin(clock, closeBound, Promise.allSettled([...pending]));
|
|
2791
|
+
if (ended.kind === "bound") for (const unit of [...active]) once(unit, () => settle.reject(unit.job, /* @__PURE__ */ new Error(`${OWNER} closed with the job in flight`)));
|
|
2792
|
+
const shut = options.close;
|
|
2793
|
+
const down = await settleWithin(clock, Math.max(0, closeBound - (clock.now() - began)), shut === void 0 ? Promise.resolve() : (async () => shut())());
|
|
2794
|
+
if (ended.kind === "bound") throw new Error(`${OWNER}: a job did not end in the close_bound of ${closeBound} ms after its abort`);
|
|
2795
|
+
if (down.kind === "failed") throw down.error;
|
|
2796
|
+
if (down.kind === "bound") throw new Error(`${OWNER}: the close of the source did not end in the close_bound of ${closeBound} ms`);
|
|
2797
|
+
})();
|
|
2798
|
+
return closing;
|
|
2799
|
+
},
|
|
2800
|
+
problems(description) {
|
|
2801
|
+
return boundProblems(OWNER, closeBound, bound, description);
|
|
2802
|
+
}
|
|
2803
|
+
};
|
|
2804
|
+
};
|
|
2805
|
+
//#endregion
|
|
2025
2806
|
//#region src/machines.ts
|
|
2026
2807
|
/**
|
|
2027
2808
|
* The machines of the runtime as `mark1russell7.machine/1` documents. The documents are the
|
|
@@ -2471,6 +3252,6 @@ const MACHINES = {
|
|
|
2471
3252
|
watchdog: WATCHDOG_MACHINE
|
|
2472
3253
|
};
|
|
2473
3254
|
//#endregion
|
|
2474
|
-
export { DECLARATION_RULE, EXIT_MACHINE, EXIT_SIGNALS, FORMAT, GATE_MACHINE, LATCH_MACHINE, MACHINES, POLLER_MACHINE, PROCESS_TRIGGERS, SERVICE_RULES, STAGES, WATCHDOG_MACHINE, WINDOW_MACHINE, asDebounce, asWindow, backoffBand, backoffDelay, backoffProblems, capKnob, cooperativeAbortAt, daemon, detectionTime, drainMachine, ephemeral, evaluate, failSpan, heartbeat, heartbeatDetection, inequalities, isExitCode, isExitSignal, kubernetes, lint, localDev, mayReissue, nodeProcess, platformClock, processPortOf, realClock, recoverSpan, replay, service, serviceBindings, stageRank, symbolsOf, verdictOf, watchdog };
|
|
3255
|
+
export { DECLARATION_RULE, EXIT_MACHINE, EXIT_SIGNALS, FORMAT, GATE_MACHINE, LATCH_MACHINE, MACHINES, POLLER_MACHINE, PROCESS_TRIGGERS, SERVICE_RULES, STAGES, WATCHDOG_MACHINE, WINDOW_MACHINE, asDebounce, asWindow, backoffBand, backoffDelay, backoffProblems, capKnob, cooperativeAbortAt, daemon, detectionTime, drainMachine, embeddedProcess, ephemeral, evaluate, failSpan, heartbeat, heartbeatDetection, inequalities, isExitCode, isExitSignal, isMessageShape, kubernetes, lint, localDev, matchesMessage, mayReissue, nodeProcess, platformClock, processPortOf, queueDrain, realClock, recoverSpan, redeliverOnce, replay, service, serviceBindings, stageRank, symbolsOf, verdictOf, watchdog, workerLoop };
|
|
2475
3256
|
|
|
2476
3257
|
//# sourceMappingURL=index.js.map
|