@mark1russell7/service 0.4.1 → 0.5.1
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 +358 -18
- package/build/builder-QVxu1WcL.d.ts +204 -0
- package/build/http.d.ts +277 -0
- package/build/http.js +782 -0
- package/build/http.js.map +1 -0
- package/build/index.d.ts +253 -6
- package/build/index.js +1506 -166
- 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/{builder-DnfUDW9u.d.ts → lifecycle-DtJZW09J.d.ts} +272 -181
- package/build/redis.d.ts +2 -1
- package/build/testing.d.ts +98 -4
- package/build/testing.js +321 -8
- package/build/testing.js.map +1 -1
- package/package.json +10 -3
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
|
|
@@ -497,101 +498,441 @@ const EXIT_SIGNALS = [
|
|
|
497
498
|
"SIGTERM",
|
|
498
499
|
"SIGQUIT"
|
|
499
500
|
];
|
|
500
|
-
/**
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
501
|
+
/** These are the process triggers that a declaration can opt in to. */
|
|
502
|
+
const PROCESS_TRIGGERS = [
|
|
503
|
+
"stdin-close",
|
|
504
|
+
"ipc-close",
|
|
505
|
+
"ipc-message"
|
|
506
|
+
];
|
|
507
|
+
/** This function tells if a number is an exit code of a process: a whole number from 0 to 255. */
|
|
508
|
+
const isExitCode = (code) => Number.isInteger(code) && code >= 0 && code <= 255;
|
|
509
|
+
/** This function tells if a trigger is an exit signal. A repeat signal obeys `onRepeatSignal`. */
|
|
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;
|
|
533
|
+
/** This function adds a handler to some events of an emitter. The handler operates one time at most. */
|
|
534
|
+
const once = (emitter, events, handler) => {
|
|
535
|
+
let called = false;
|
|
536
|
+
const fire = () => {
|
|
537
|
+
if (called) return;
|
|
538
|
+
called = true;
|
|
539
|
+
handler();
|
|
540
|
+
};
|
|
541
|
+
for (const event of events) emitter.on(event, fire);
|
|
542
|
+
return () => {
|
|
543
|
+
for (const event of events) emitter.off(event, fire);
|
|
544
|
+
};
|
|
545
|
+
};
|
|
546
|
+
/** This function gives a promise that ends when the data before it in a stream is written. */
|
|
547
|
+
const written = (stream) => new Promise((resolve) => {
|
|
548
|
+
if (stream === void 0 || stream.destroyed === true || stream.writable === false) {
|
|
549
|
+
resolve();
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
try {
|
|
553
|
+
stream.write("", () => resolve());
|
|
554
|
+
} catch {
|
|
555
|
+
resolve();
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
/**
|
|
559
|
+
* This function makes a process port on a host. `find` gives the host at each use, or undefined
|
|
560
|
+
* when the platform has no process. Then the use throws.
|
|
561
|
+
*/
|
|
562
|
+
const processPortOf = (find) => {
|
|
563
|
+
const hostProcess = () => {
|
|
564
|
+
const host = find();
|
|
512
565
|
if (host === void 0) throw new Error("no process: give the runtime a process port");
|
|
513
|
-
host
|
|
566
|
+
return host;
|
|
567
|
+
};
|
|
568
|
+
return {
|
|
569
|
+
on(trigger, handler) {
|
|
570
|
+
const host = hostProcess();
|
|
571
|
+
if (trigger === "stdin-close") {
|
|
572
|
+
const input = host.stdin;
|
|
573
|
+
if (input === void 0) return () => {};
|
|
574
|
+
if (input.readableEnded === true) {
|
|
575
|
+
let live = true;
|
|
576
|
+
queueMicrotask(() => {
|
|
577
|
+
if (live) handler();
|
|
578
|
+
});
|
|
579
|
+
return () => {
|
|
580
|
+
live = false;
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
return once(input, ["end", "close"], handler);
|
|
584
|
+
}
|
|
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
|
+
}
|
|
593
|
+
host.on(trigger, handler);
|
|
594
|
+
return () => {
|
|
595
|
+
host.off(trigger, handler);
|
|
596
|
+
};
|
|
597
|
+
},
|
|
598
|
+
async flush() {
|
|
599
|
+
const host = hostProcess();
|
|
600
|
+
await Promise.all([written(host.stdout), written(host.stderr)]);
|
|
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
|
+
},
|
|
612
|
+
exit(code) {
|
|
613
|
+
hostProcess().exit(code);
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
};
|
|
617
|
+
/** This port is the process of Node. */
|
|
618
|
+
const nodeProcess = processPortOf(() => globalThis.process);
|
|
619
|
+
//#endregion
|
|
620
|
+
//#region src/routes.ts
|
|
621
|
+
const NAME = /^[A-Za-z_][A-Za-z0-9_]*/;
|
|
622
|
+
/** This function gives the problems of the syntax of a pattern: an empty pattern, a part without a name, or a name two times. */
|
|
623
|
+
const patternProblems = (owner, pattern) => {
|
|
624
|
+
const problems = [];
|
|
625
|
+
if (pattern.length === 0) problems.push(`${owner} has an empty route pattern`);
|
|
626
|
+
const names = /* @__PURE__ */ new Set();
|
|
627
|
+
for (let at = 0; at < pattern.length; at += 1) {
|
|
628
|
+
if (pattern[at] !== ":") continue;
|
|
629
|
+
const name = NAME.exec(pattern.slice(at + 1))?.[0];
|
|
630
|
+
if (name === void 0) {
|
|
631
|
+
problems.push(`${owner}'s pattern ${pattern} has a ":" without a name at ${at}`);
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
if (names.has(name)) problems.push(`${owner}'s pattern ${pattern} names :${name} two times`);
|
|
635
|
+
names.add(name);
|
|
636
|
+
at += name.length;
|
|
637
|
+
}
|
|
638
|
+
return problems;
|
|
639
|
+
};
|
|
640
|
+
/** This function gives the problems of a pattern and of each of its operations, which `describe()` refuses. */
|
|
641
|
+
const routePatternProblems = (owner, declared) => {
|
|
642
|
+
const problems = patternProblems(owner, declared.pattern);
|
|
643
|
+
const names = Object.keys(declared.operations);
|
|
644
|
+
if (names.length === 0) problems.push(`${owner}'s pattern ${declared.pattern} declares no operation`);
|
|
645
|
+
for (const name of names) problems.push(...operationProblems(owner, `${declared.pattern} (${name})`, declared.operations[name]));
|
|
646
|
+
return problems;
|
|
647
|
+
};
|
|
648
|
+
/** This function compiles a pattern to a matcher. The match must cover the whole route. */
|
|
649
|
+
const compilePattern = (pattern) => {
|
|
650
|
+
const kinds = [];
|
|
651
|
+
let source = "^";
|
|
652
|
+
for (let at = 0; at < pattern.length; at += 1) {
|
|
653
|
+
const char = pattern[at];
|
|
654
|
+
if (char === "*") {
|
|
655
|
+
source += "([\\s\\S]*)";
|
|
656
|
+
kinds.push({ kind: "wildcard" });
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
if (char === ":") {
|
|
660
|
+
const name = NAME.exec(pattern.slice(at + 1))?.[0];
|
|
661
|
+
if (name !== void 0) {
|
|
662
|
+
source += "([^/]+)";
|
|
663
|
+
kinds.push({
|
|
664
|
+
kind: "param",
|
|
665
|
+
name
|
|
666
|
+
});
|
|
667
|
+
at += name.length;
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
source += char.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
514
672
|
}
|
|
673
|
+
const matcher = new RegExp(`${source}$`);
|
|
674
|
+
return {
|
|
675
|
+
pattern,
|
|
676
|
+
match(route) {
|
|
677
|
+
const found = matcher.exec(route);
|
|
678
|
+
if (found === null) return null;
|
|
679
|
+
const params = {};
|
|
680
|
+
const wildcards = [];
|
|
681
|
+
kinds.forEach((part, index) => {
|
|
682
|
+
const text = found[index + 1] ?? "";
|
|
683
|
+
if (part.kind === "param") params[part.name] = text;
|
|
684
|
+
else wildcards.push(text);
|
|
685
|
+
});
|
|
686
|
+
return {
|
|
687
|
+
params,
|
|
688
|
+
wildcards
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
};
|
|
515
692
|
};
|
|
516
693
|
//#endregion
|
|
517
694
|
//#region src/requests.ts
|
|
695
|
+
const isAsyncIterable$1 = (value) => value !== null && (typeof value === "object" || typeof value === "function") && typeof value[Symbol.asyncIterator] === "function";
|
|
696
|
+
const isPromiseLike = (value) => value !== null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
|
|
697
|
+
/** This function gives a stream of events from a function that gives the next event. */
|
|
698
|
+
const streamOf = (next, stop) => {
|
|
699
|
+
let chain = Promise.resolve();
|
|
700
|
+
const iterator = {
|
|
701
|
+
next() {
|
|
702
|
+
const step = chain.then(next).then((event) => event === null ? {
|
|
703
|
+
value: void 0,
|
|
704
|
+
done: true
|
|
705
|
+
} : {
|
|
706
|
+
value: event,
|
|
707
|
+
done: false
|
|
708
|
+
});
|
|
709
|
+
chain = step.catch(() => {});
|
|
710
|
+
return step;
|
|
711
|
+
},
|
|
712
|
+
return() {
|
|
713
|
+
stop();
|
|
714
|
+
return Promise.resolve({
|
|
715
|
+
value: void 0,
|
|
716
|
+
done: true
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
return {
|
|
721
|
+
[Symbol.asyncIterator]: () => iterator,
|
|
722
|
+
async collect() {
|
|
723
|
+
const items = [];
|
|
724
|
+
for (;;) {
|
|
725
|
+
const step = await iterator.next();
|
|
726
|
+
if (step.done === true) throw new Error("the stream ended without its end");
|
|
727
|
+
if (step.value.kind === "item") items.push(step.value.value);
|
|
728
|
+
else return {
|
|
729
|
+
items,
|
|
730
|
+
end: step.value
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
};
|
|
518
736
|
/**
|
|
519
737
|
* This function makes a tracker. `refuses` gives the reason that the service refuses a new request
|
|
520
|
-
* for a controller at this time, or null when it admits it.
|
|
738
|
+
* for a controller at this time, or null when it admits it. `choices` gives the function of each
|
|
739
|
+
* route pattern, by controller and by pattern.
|
|
521
740
|
*/
|
|
522
|
-
const tracker = (controllers, clock, refuses) => {
|
|
523
|
-
const byName = new Map(controllers.map((controller) => [controller.name,
|
|
741
|
+
const tracker = (controllers, clock, refuses, choices = /* @__PURE__ */ new Map()) => {
|
|
742
|
+
const byName = new Map(controllers.map((controller) => [controller.name, {
|
|
743
|
+
controller,
|
|
744
|
+
patterns: (controller.patterns ?? []).map((declared) => ({
|
|
745
|
+
declared,
|
|
746
|
+
compiled: compilePattern(declared.pattern)
|
|
747
|
+
}))
|
|
748
|
+
}]));
|
|
524
749
|
const flights = /* @__PURE__ */ new Set();
|
|
525
750
|
const idle = /* @__PURE__ */ new Set();
|
|
526
751
|
let intake = true;
|
|
752
|
+
/** This function gives the operation of a request: its exact route, else the first pattern that matches it. */
|
|
753
|
+
const operationOf = (controller, route, detail) => {
|
|
754
|
+
const held = byName.get(controller);
|
|
755
|
+
if (held === void 0) throw new Error(`there is no controller ${controller}`);
|
|
756
|
+
const exact = held.controller.routes[route];
|
|
757
|
+
if (exact !== void 0) return exact;
|
|
758
|
+
for (const { declared, compiled } of held.patterns) {
|
|
759
|
+
const found = compiled.match(route);
|
|
760
|
+
if (found === null) continue;
|
|
761
|
+
const names = Object.keys(declared.operations);
|
|
762
|
+
const choose = choices.get(controller)?.get(declared.pattern);
|
|
763
|
+
const name = choose === void 0 ? names[0] : choose({
|
|
764
|
+
controller,
|
|
765
|
+
route,
|
|
766
|
+
...found,
|
|
767
|
+
detail
|
|
768
|
+
});
|
|
769
|
+
const operation = declared.operations[name];
|
|
770
|
+
if (operation === void 0) throw new Error(`the pattern ${declared.pattern} of ${controller} declares no operation ${String(name)} for ${route}`);
|
|
771
|
+
return operation;
|
|
772
|
+
}
|
|
773
|
+
throw new Error(`controller ${controller} does not declare the route ${route}`);
|
|
774
|
+
};
|
|
527
775
|
const settle = (flight) => {
|
|
528
|
-
flights.delete(flight);
|
|
776
|
+
if (!flights.delete(flight)) return;
|
|
529
777
|
if (flights.size === 0) for (const done of [...idle]) done();
|
|
530
778
|
};
|
|
531
|
-
/** This function
|
|
532
|
-
const
|
|
533
|
-
const
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
779
|
+
/** This function starts the work of an admitted request, and gives its stream. */
|
|
780
|
+
const open = (controller, operation, work) => {
|
|
781
|
+
const started = clock.now();
|
|
782
|
+
const abort = new AbortController();
|
|
783
|
+
let delivered = 0;
|
|
784
|
+
let end = null;
|
|
785
|
+
let ended = false;
|
|
786
|
+
let returned = false;
|
|
787
|
+
let source = null;
|
|
788
|
+
let unary = null;
|
|
789
|
+
let interrupt = () => {};
|
|
790
|
+
const interrupted = new Promise((resolve) => {
|
|
791
|
+
interrupt = () => resolve("interrupted");
|
|
792
|
+
});
|
|
793
|
+
const cutEnd = (cause) => {
|
|
794
|
+
const since = quantity(Math.max(0, clock.now() - started) / 1e3, "s");
|
|
795
|
+
const retryable = mayReissue(operation, since);
|
|
796
|
+
return {
|
|
797
|
+
kind: "cut",
|
|
798
|
+
status: delivered > 0 ? null : retryable ? 503 : 500,
|
|
799
|
+
retryable,
|
|
800
|
+
cause,
|
|
801
|
+
delivered
|
|
802
|
+
};
|
|
541
803
|
};
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
804
|
+
/** This function ends the work of the request: the end is known, and the request leaves the flights. */
|
|
805
|
+
const finish = (given) => {
|
|
806
|
+
if (end !== null) return;
|
|
807
|
+
end = given;
|
|
808
|
+
settle(flight);
|
|
809
|
+
interrupt();
|
|
810
|
+
};
|
|
811
|
+
/** This function ends the work after a throw: a failure after the shutdown aborted it is a cut. */
|
|
812
|
+
const failed = (error) => finish(abort.signal.aborted ? cutEnd("the shutdown aborted it, and it failed") : {
|
|
813
|
+
kind: "failed",
|
|
814
|
+
error,
|
|
815
|
+
delivered
|
|
816
|
+
});
|
|
817
|
+
const closeSource = () => {
|
|
818
|
+
if (source === null || returned) return;
|
|
819
|
+
returned = true;
|
|
820
|
+
try {
|
|
821
|
+
Promise.resolve(source.return?.()).catch(() => {});
|
|
822
|
+
} catch {}
|
|
823
|
+
};
|
|
824
|
+
const flight = {
|
|
825
|
+
controller,
|
|
826
|
+
cooperative: () => {
|
|
827
|
+
abort.abort(/* @__PURE__ */ new Error("the cooperative abort of the drain"));
|
|
828
|
+
closeSource();
|
|
829
|
+
},
|
|
830
|
+
cut: (cause) => {
|
|
831
|
+
abort.abort(new Error(cause));
|
|
832
|
+
finish(cutEnd(cause));
|
|
833
|
+
closeSource();
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
flights.add(flight);
|
|
837
|
+
/** This function takes the result of the work: a stream, or one value. */
|
|
838
|
+
const take = (result) => {
|
|
839
|
+
if (end !== null) return;
|
|
840
|
+
if (isAsyncIterable$1(result)) {
|
|
841
|
+
source = result[Symbol.asyncIterator]();
|
|
842
|
+
if (abort.signal.aborted) closeSource();
|
|
843
|
+
} else {
|
|
844
|
+
unary = { value: result };
|
|
845
|
+
settle(flight);
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
let ready;
|
|
849
|
+
try {
|
|
850
|
+
const result = work(abort.signal);
|
|
851
|
+
if (isAsyncIterable$1(result) || !isPromiseLike(result)) {
|
|
852
|
+
take(result);
|
|
853
|
+
ready = Promise.resolve();
|
|
854
|
+
} else ready = Promise.resolve(result).then(take, failed);
|
|
855
|
+
} catch (error) {
|
|
856
|
+
failed(error);
|
|
857
|
+
ready = Promise.resolve();
|
|
858
|
+
}
|
|
859
|
+
const next = async () => {
|
|
860
|
+
if (ended) return null;
|
|
861
|
+
if (end === null) await Promise.race([ready, interrupted]);
|
|
862
|
+
if (end === null && unary !== null) {
|
|
863
|
+
const { value } = unary;
|
|
864
|
+
unary = null;
|
|
865
|
+
delivered = 1;
|
|
866
|
+
end = {
|
|
867
|
+
kind: "done",
|
|
868
|
+
delivered
|
|
869
|
+
};
|
|
870
|
+
return {
|
|
871
|
+
kind: "item",
|
|
872
|
+
value
|
|
568
873
|
};
|
|
569
|
-
|
|
570
|
-
|
|
874
|
+
}
|
|
875
|
+
if (end === null && source !== null) {
|
|
876
|
+
const pending = source.next();
|
|
877
|
+
let step;
|
|
571
878
|
try {
|
|
572
|
-
|
|
879
|
+
step = await Promise.race([pending, interrupted]);
|
|
573
880
|
} catch (error) {
|
|
574
|
-
|
|
881
|
+
failed(error);
|
|
882
|
+
step = "interrupted";
|
|
575
883
|
}
|
|
576
|
-
|
|
577
|
-
if (
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
884
|
+
if (step !== "interrupted") {
|
|
885
|
+
if (step.done !== true) {
|
|
886
|
+
delivered += 1;
|
|
887
|
+
return {
|
|
888
|
+
kind: "item",
|
|
889
|
+
value: step.value
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
finish(abort.signal.aborted ? cutEnd("the shutdown ended the stream") : {
|
|
581
893
|
kind: "done",
|
|
582
|
-
|
|
894
|
+
delivered
|
|
583
895
|
});
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
ended = true;
|
|
899
|
+
return end;
|
|
900
|
+
};
|
|
901
|
+
const stop = () => {
|
|
902
|
+
if (end === null) {
|
|
903
|
+
abort.abort(/* @__PURE__ */ new Error("the reader stopped the stream"));
|
|
904
|
+
finish({
|
|
905
|
+
kind: "done",
|
|
906
|
+
delivered
|
|
593
907
|
});
|
|
594
|
-
|
|
908
|
+
closeSource();
|
|
909
|
+
}
|
|
910
|
+
ended = true;
|
|
911
|
+
};
|
|
912
|
+
return streamOf(next, stop);
|
|
913
|
+
};
|
|
914
|
+
return {
|
|
915
|
+
request(controller, route, work, options = {}) {
|
|
916
|
+
const operation = operationOf(controller, route, options.detail);
|
|
917
|
+
const refusal = intake ? refuses(controller) : "the service stopped its intake";
|
|
918
|
+
if (refusal !== null) {
|
|
919
|
+
let given = false;
|
|
920
|
+
const refused = {
|
|
921
|
+
kind: "refused",
|
|
922
|
+
status: 503,
|
|
923
|
+
retryable: true,
|
|
924
|
+
cause: refusal,
|
|
925
|
+
delivered: 0
|
|
926
|
+
};
|
|
927
|
+
return streamOf(() => {
|
|
928
|
+
if (given) return Promise.resolve(null);
|
|
929
|
+
given = true;
|
|
930
|
+
return Promise.resolve(refused);
|
|
931
|
+
}, () => {
|
|
932
|
+
given = true;
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
return open(controller, operation, work);
|
|
595
936
|
},
|
|
596
937
|
inFlight(controller) {
|
|
597
938
|
let count = 0;
|
|
@@ -602,7 +943,7 @@ const tracker = (controllers, clock, refuses) => {
|
|
|
602
943
|
intake = false;
|
|
603
944
|
},
|
|
604
945
|
cooperativeAbort() {
|
|
605
|
-
for (const flight of flights) flight.
|
|
946
|
+
for (const flight of [...flights]) flight.cooperative();
|
|
606
947
|
},
|
|
607
948
|
drain(deadline) {
|
|
608
949
|
if (flights.size === 0) return Promise.resolve();
|
|
@@ -617,10 +958,7 @@ const tracker = (controllers, clock, refuses) => {
|
|
|
617
958
|
};
|
|
618
959
|
idle.add(done);
|
|
619
960
|
const cancel = clock.after(Math.max(0, deadline - clock.now()), () => {
|
|
620
|
-
for (const flight of [...flights])
|
|
621
|
-
flight.abort.abort(/* @__PURE__ */ new Error("the drain reached its deadline"));
|
|
622
|
-
flight.answer(cut(flight, "the drain reached its deadline"));
|
|
623
|
-
}
|
|
961
|
+
for (const flight of [...flights]) flight.cut("the drain reached its deadline");
|
|
624
962
|
done();
|
|
625
963
|
});
|
|
626
964
|
});
|
|
@@ -641,7 +979,9 @@ const tracker = (controllers, clock, refuses) => {
|
|
|
641
979
|
* - `close transports`: each transport closes.
|
|
642
980
|
* - `stop the poller`: each check and watchdog stops. Then the signal of the service aborts.
|
|
643
981
|
* - `tear down dependencies`: each dependency closes, in the reverse sequence of its connection.
|
|
644
|
-
* - `close the health server`: the probes stop
|
|
982
|
+
* - `close the health server`: the probes stop.
|
|
983
|
+
* - `flush the output`: the work of the owners of this phase ends, and then the process port
|
|
984
|
+
* writes the buffered output. Without this phase, the process exits without a wait.
|
|
645
985
|
*/
|
|
646
986
|
const STAGES = [
|
|
647
987
|
"fail readiness",
|
|
@@ -650,7 +990,8 @@ const STAGES = [
|
|
|
650
990
|
"close transports",
|
|
651
991
|
"stop the poller",
|
|
652
992
|
"tear down dependencies",
|
|
653
|
-
"close the health server"
|
|
993
|
+
"close the health server",
|
|
994
|
+
"flush the output"
|
|
654
995
|
];
|
|
655
996
|
/** This function gives the place of a stage in the sequence, or -1 for a phase of another name. */
|
|
656
997
|
const stageRank = (name) => STAGES.indexOf(name);
|
|
@@ -658,12 +999,12 @@ const stageRank = (name) => STAGES.indexOf(name);
|
|
|
658
999
|
const ABORT_AFTER = stageRank("stop the poller");
|
|
659
1000
|
/**
|
|
660
1001
|
* This function gives the time of the cooperative abort, in milliseconds after the drain starts. It
|
|
661
|
-
* is the cap less the cooperative window
|
|
662
|
-
* gives half the cap.
|
|
1002
|
+
* is the cap less the cooperative window. With the floor `half`, it is not less than half the cap,
|
|
1003
|
+
* and a window without a bound gives half the cap. With the floor `none`, it is not less than 0.
|
|
663
1004
|
*/
|
|
664
|
-
const cooperativeAbortAt = (cap, window) => {
|
|
1005
|
+
const cooperativeAbortAt = (cap, window, floor = "half") => {
|
|
665
1006
|
const before = window.value === "infinite" ? Number.POSITIVE_INFINITY : valueIn(window, "ms");
|
|
666
|
-
return Math.max(cap - before, cap / 2);
|
|
1007
|
+
return Math.max(cap - before, floor === "none" ? 0 : cap / 2);
|
|
667
1008
|
};
|
|
668
1009
|
const message = (error) => error instanceof Error ? error.message : String(error);
|
|
669
1010
|
/**
|
|
@@ -688,12 +1029,12 @@ const lifecycleProblems = (description, components) => {
|
|
|
688
1029
|
};
|
|
689
1030
|
/**
|
|
690
1031
|
* This function starts the lifecycle of a service. The startup has a sequence: the health server
|
|
691
|
-
* first, then each dependency, then each transport, and the startup gate last. A
|
|
1032
|
+
* first, then each dependency, then each transport, and the startup gate last. A trigger during the
|
|
692
1033
|
* startup gets its shutdown after the startup ends. The shutdown does the declared phases in their
|
|
693
|
-
* sequence, each one capped. Only the lifecycle ends the process
|
|
694
|
-
* after an error.
|
|
1034
|
+
* sequence, each one capped. Only the lifecycle ends the process. Its exit code is the code of the
|
|
1035
|
+
* exit request, else 1 after an error, else 0.
|
|
695
1036
|
*/
|
|
696
|
-
const lifecycle = (description, runtime, components, options = {}) => {
|
|
1037
|
+
const lifecycle = (description, runtime, components, options = {}, bind = () => {}) => {
|
|
697
1038
|
const clock = options.clock ?? realClock;
|
|
698
1039
|
const port = options.process ?? nodeProcess;
|
|
699
1040
|
const record = (machine, from, to, on, detail) => options.record?.({
|
|
@@ -707,8 +1048,11 @@ const lifecycle = (description, runtime, components, options = {}) => {
|
|
|
707
1048
|
});
|
|
708
1049
|
const service = new AbortController();
|
|
709
1050
|
const errors = [];
|
|
1051
|
+
const exitCodes = description.shutdown.exitCodes ?? {};
|
|
1052
|
+
const repeatPolicy = description.shutdown.onRepeatSignal;
|
|
710
1053
|
let state = "starting";
|
|
711
1054
|
let exitCode = null;
|
|
1055
|
+
let requested;
|
|
712
1056
|
let intakeStopped = false;
|
|
713
1057
|
let shutdownStarted = false;
|
|
714
1058
|
let failedStartup = false;
|
|
@@ -719,14 +1063,14 @@ const lifecycle = (description, runtime, components, options = {}) => {
|
|
|
719
1063
|
};
|
|
720
1064
|
const fail = (cause) => {
|
|
721
1065
|
errors.push(cause);
|
|
722
|
-
record("exit", exitState, exitState, "error", cause);
|
|
1066
|
+
if (exitState !== "exited") record("exit", exitState, exitState, "error", cause);
|
|
723
1067
|
};
|
|
724
1068
|
const requests = tracker(description.controllers, clock, (controller) => {
|
|
725
1069
|
if (state === "starting") return "the service has not started";
|
|
726
1070
|
if (state === "exited") return "the service has exited";
|
|
727
1071
|
if (state === "draining") return null;
|
|
728
1072
|
return runtime.verdict().controllers[controller] === true ? null : `controller ${controller} is not ready`;
|
|
729
|
-
});
|
|
1073
|
+
}, components.choices);
|
|
730
1074
|
let finish = () => {};
|
|
731
1075
|
const done = new Promise((resolve) => {
|
|
732
1076
|
finish = resolve;
|
|
@@ -734,45 +1078,49 @@ const lifecycle = (description, runtime, components, options = {}) => {
|
|
|
734
1078
|
let healthListening = false;
|
|
735
1079
|
const connected = [];
|
|
736
1080
|
const startedTransports = [];
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
1081
|
+
let startupEnded = () => {};
|
|
1082
|
+
const startup = new Promise((resolve) => {
|
|
1083
|
+
startupEnded = resolve;
|
|
1084
|
+
});
|
|
1085
|
+
let offs = [];
|
|
1086
|
+
let release = () => {};
|
|
1087
|
+
/** This function ends the process with a code, one time, and removes each handler of a trigger and the hold. */
|
|
1088
|
+
const end = (code, on) => {
|
|
1089
|
+
state = "exited";
|
|
1090
|
+
exitCode = code;
|
|
1091
|
+
for (const off of offs) off();
|
|
1092
|
+
offs = [];
|
|
1093
|
+
moveExit("exited", on, `exit ${code}`);
|
|
1094
|
+
finish(code);
|
|
1095
|
+
release();
|
|
1096
|
+
port.exit(code);
|
|
1097
|
+
};
|
|
1098
|
+
/**
|
|
1099
|
+
* This function receives a trigger of the process: the first one starts the shutdown. The cause
|
|
1100
|
+
* names the trigger, and a message that stands for a signal tells it.
|
|
1101
|
+
*/
|
|
1102
|
+
const triggered = (trigger, cause = trigger) => {
|
|
1103
|
+
if (exitState === "exited") return;
|
|
1104
|
+
if (shutdownStarted || exitState === "stopping") {
|
|
1105
|
+
if (isExitSignal(trigger) && repeatPolicy === "exit") {
|
|
1106
|
+
fail(`${cause} came again while the shutdown ran, and the declaration exits at once`);
|
|
1107
|
+
end(1, "exit.repeat");
|
|
1108
|
+
return;
|
|
761
1109
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
} catch (error) {
|
|
765
|
-
const cause = `the startup failed: ${message(error)}`;
|
|
766
|
-
failedStartup = true;
|
|
767
|
-
runtime.failStartup(cause);
|
|
768
|
-
fail(cause);
|
|
769
|
-
if (exitState === "running") moveExit("stopping", "startup.fail", cause);
|
|
770
|
-
else record("exit", exitState, exitState, "startup.fail", cause);
|
|
1110
|
+
record("exit", exitState, exitState, "exit.repeat", cause);
|
|
1111
|
+
return;
|
|
771
1112
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
1113
|
+
const code = exitCodes[trigger];
|
|
1114
|
+
shutdown(cause, code === void 0 ? {} : { code });
|
|
1115
|
+
};
|
|
1116
|
+
const messages = description.shutdown.messages ?? [];
|
|
1117
|
+
/** This function receives a message of the IPC channel. The first declared shape that it matches gives its trigger. */
|
|
1118
|
+
const received = (message) => {
|
|
1119
|
+
const found = messages.find((each) => matchesMessage(each.match, message));
|
|
1120
|
+
if (found === void 0) return;
|
|
1121
|
+
if (found.as === void 0) triggered("ipc-message");
|
|
1122
|
+
else triggered(found.as, `${found.as} (a message of the IPC channel)`);
|
|
1123
|
+
};
|
|
776
1124
|
/** This function does the work of one phase, until its cap. It gives true when the work ended in time. */
|
|
777
1125
|
const runPhase = async (phase, hold, work) => {
|
|
778
1126
|
const cap = valueIn(phase.cap, "ms");
|
|
@@ -802,14 +1150,22 @@ const lifecycle = (description, runtime, components, options = {}) => {
|
|
|
802
1150
|
capped.abort(/* @__PURE__ */ new Error(`the phase ${phase.name} reached its cap`));
|
|
803
1151
|
return pending === 0;
|
|
804
1152
|
};
|
|
805
|
-
async function shutdown(cause = "the service asked for it") {
|
|
806
|
-
if (
|
|
1153
|
+
async function shutdown(cause = "the service asked for it", request = {}) {
|
|
1154
|
+
if (request.code !== void 0 && !isExitCode(request.code)) throw new Error(`${description.name}: the exit code ${request.code} is not a whole number from 0 to 255`);
|
|
1155
|
+
if (exitState === "exited") return done;
|
|
1156
|
+
if (request.code !== void 0 && requested === void 0) requested = request.code;
|
|
1157
|
+
if (shutdownStarted) {
|
|
807
1158
|
if (exitState === "stopping") record("exit", exitState, exitState, "exit.repeat", cause);
|
|
808
1159
|
return done;
|
|
809
1160
|
}
|
|
810
1161
|
shutdownStarted = true;
|
|
811
1162
|
if (exitState === "running") moveExit("stopping", "exit.request", cause);
|
|
812
1163
|
await startup;
|
|
1164
|
+
if (exitState === "exited") {
|
|
1165
|
+
runtime.stop();
|
|
1166
|
+
components.stopPollers();
|
|
1167
|
+
return exitCode;
|
|
1168
|
+
}
|
|
813
1169
|
state = "draining";
|
|
814
1170
|
runtime.drain();
|
|
815
1171
|
let drainState = "running";
|
|
@@ -825,10 +1181,12 @@ const lifecycle = (description, runtime, components, options = {}) => {
|
|
|
825
1181
|
const phases = description.shutdown.phases;
|
|
826
1182
|
let on = "exit.request";
|
|
827
1183
|
for (const phase of phases) {
|
|
1184
|
+
if (exitState === "exited") break;
|
|
828
1185
|
const rank = stageRank(phase.name);
|
|
829
1186
|
moveDrain(phase.name, on);
|
|
830
1187
|
if (rank > ABORT_AFTER) abortService();
|
|
831
1188
|
const work = [];
|
|
1189
|
+
const owned = components.phaseWork.get(phase.name) ?? [];
|
|
832
1190
|
if (phase.name === "stop intake") {
|
|
833
1191
|
work.push(() => {
|
|
834
1192
|
intakeStopped = true;
|
|
@@ -840,7 +1198,7 @@ const lifecycle = (description, runtime, components, options = {}) => {
|
|
|
840
1198
|
if (phase.name === "drain") {
|
|
841
1199
|
const cap = valueIn(phase.cap, "ms");
|
|
842
1200
|
const cooperative = new AbortController();
|
|
843
|
-
const fireAt = cooperativeAbortAt(cap, description.shutdown.cooperativeWindow);
|
|
1201
|
+
const fireAt = cooperativeAbortAt(cap, description.shutdown.cooperativeWindow, description.shutdown.cooperativeFloor);
|
|
844
1202
|
cancelAbort = clock.after(fireAt, () => {
|
|
845
1203
|
cooperative.abort(/* @__PURE__ */ new Error("the cooperative abort of the drain"));
|
|
846
1204
|
requests.cooperativeAbort();
|
|
@@ -861,26 +1219,33 @@ const lifecycle = (description, runtime, components, options = {}) => {
|
|
|
861
1219
|
const server = components.healthServer;
|
|
862
1220
|
work.push(() => server.close());
|
|
863
1221
|
}
|
|
864
|
-
|
|
1222
|
+
if (phase.name === "flush the output") work.push(async (context) => {
|
|
1223
|
+
await Promise.all(owned.map(async (each) => {
|
|
1224
|
+
try {
|
|
1225
|
+
await each(context);
|
|
1226
|
+
} catch (error) {
|
|
1227
|
+
fail(`the phase ${phase.name} failed: ${message(error)}`);
|
|
1228
|
+
}
|
|
1229
|
+
}));
|
|
1230
|
+
await port.flush?.();
|
|
1231
|
+
});
|
|
1232
|
+
else for (const each of owned) work.push(each);
|
|
865
1233
|
const inTime = await runPhase(phase, phase.name === "fail readiness", work);
|
|
866
1234
|
cancelAbort();
|
|
867
1235
|
if (!inTime) fail(`the phase ${phase.name} did not end in its cap of ${valueIn(phase.cap, "s")} s`);
|
|
868
1236
|
on = inTime ? "phase.done" : "phase.cap";
|
|
869
1237
|
}
|
|
870
|
-
moveDrain("done", on);
|
|
1238
|
+
if (exitState !== "exited") moveDrain("done", on);
|
|
871
1239
|
abortService();
|
|
872
1240
|
runtime.stop();
|
|
873
1241
|
components.stopPollers();
|
|
874
1242
|
if (!intakeStopped) requests.stopIntake();
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
finish(exitCode);
|
|
880
|
-
port.exit(exitCode);
|
|
881
|
-
return exitCode;
|
|
1243
|
+
if (exitState === "exited") return exitCode;
|
|
1244
|
+
const code = requested !== void 0 && requested !== 0 ? requested : errors.length > 0 ? 1 : 0;
|
|
1245
|
+
end(code, "shutdown.done");
|
|
1246
|
+
return code;
|
|
882
1247
|
}
|
|
883
|
-
|
|
1248
|
+
const life = {
|
|
884
1249
|
health: runtime,
|
|
885
1250
|
get state() {
|
|
886
1251
|
return state;
|
|
@@ -895,10 +1260,52 @@ const lifecycle = (description, runtime, components, options = {}) => {
|
|
|
895
1260
|
done,
|
|
896
1261
|
signal: service.signal,
|
|
897
1262
|
shutdown,
|
|
898
|
-
request: (controller, route, work) => requests.request(controller, route, work),
|
|
1263
|
+
request: (controller, route, work, request) => requests.request(controller, route, work, request),
|
|
899
1264
|
inFlight: (controller) => requests.inFlight(controller),
|
|
900
1265
|
keepAlive: () => state === "running" && runtime.verdict().ready
|
|
901
1266
|
};
|
|
1267
|
+
bind(life);
|
|
1268
|
+
release = port.hold?.() ?? (() => {});
|
|
1269
|
+
offs = [...EXIT_SIGNALS, ...description.shutdown.triggers ?? []].map((trigger) => port.on(trigger, trigger === "ipc-message" ? received : () => triggered(trigger)));
|
|
1270
|
+
(async () => {
|
|
1271
|
+
const started = (what) => record("gate", "starting", "starting", "component.started", what);
|
|
1272
|
+
try {
|
|
1273
|
+
runtime.beginStartup();
|
|
1274
|
+
if (components.healthServer !== null) {
|
|
1275
|
+
await components.healthServer.listen(runtime);
|
|
1276
|
+
healthListening = true;
|
|
1277
|
+
started("the health server");
|
|
1278
|
+
}
|
|
1279
|
+
for (const dependency of description.dependencies) {
|
|
1280
|
+
const handle = components.dependencies.get(dependency.name);
|
|
1281
|
+
if (handle === void 0) continue;
|
|
1282
|
+
await handle.connect(service.signal);
|
|
1283
|
+
connected.push([dependency.name, handle]);
|
|
1284
|
+
started(`dependency ${dependency.name}`);
|
|
1285
|
+
}
|
|
1286
|
+
for (const transport of description.transports) {
|
|
1287
|
+
const impl = components.transports.get(transport.name);
|
|
1288
|
+
await impl.start(service.signal, life);
|
|
1289
|
+
startedTransports.push([transport.name, impl]);
|
|
1290
|
+
started(`${transport.kind} ${transport.name}`);
|
|
1291
|
+
}
|
|
1292
|
+
await runtime.warmUp();
|
|
1293
|
+
if (state === "starting") state = "running";
|
|
1294
|
+
} catch (error) {
|
|
1295
|
+
const cause = `the startup failed: ${message(error)}`;
|
|
1296
|
+
failedStartup = true;
|
|
1297
|
+
runtime.failStartup(cause);
|
|
1298
|
+
fail(cause);
|
|
1299
|
+
if (exitState === "running") moveExit("stopping", "startup.fail", cause);
|
|
1300
|
+
else if (exitState === "stopping") record("exit", exitState, exitState, "startup.fail", cause);
|
|
1301
|
+
} finally {
|
|
1302
|
+
startupEnded();
|
|
1303
|
+
}
|
|
1304
|
+
})();
|
|
1305
|
+
startup.then(() => {
|
|
1306
|
+
if (failedStartup && !shutdownStarted && exitState !== "exited") shutdown("the startup failed");
|
|
1307
|
+
});
|
|
1308
|
+
return life;
|
|
902
1309
|
};
|
|
903
1310
|
//#endregion
|
|
904
1311
|
//#region src/watchdog.ts
|
|
@@ -952,8 +1359,8 @@ const watchdog = (clock, spec, trace) => {
|
|
|
952
1359
|
};
|
|
953
1360
|
//#endregion
|
|
954
1361
|
//#region src/builder.ts
|
|
955
|
-
const isTime = (amount) => convertible(amount.unit, "s");
|
|
956
|
-
const above = (amount) => amount.value === "infinite" || isTime(amount) && valueIn(amount, "s") > 0;
|
|
1362
|
+
const isTime$1 = (amount) => convertible(amount.unit, "s");
|
|
1363
|
+
const above = (amount) => amount.value === "infinite" || isTime$1(amount) && valueIn(amount, "s") > 0;
|
|
957
1364
|
const whole = (count) => Number.isInteger(count) && count >= 1;
|
|
958
1365
|
/** This function gives the problems of a timing: a zero interval or count fails decoding. */
|
|
959
1366
|
const timingProblems = (policy, timing) => {
|
|
@@ -964,15 +1371,33 @@ const timingProblems = (policy, timing) => {
|
|
|
964
1371
|
if (!whole(timing.recoverAfter)) problems.push(`the ${policy} debounce recovers after ${timing.recoverAfter} polls, not a whole number above zero`);
|
|
965
1372
|
return problems;
|
|
966
1373
|
}
|
|
967
|
-
const negative = (amount) => amount !== void 0 && (!isTime(amount) || valueIn(amount, "s") < 0);
|
|
1374
|
+
const negative = (amount) => amount !== void 0 && (!isTime$1(amount) || valueIn(amount, "s") < 0);
|
|
968
1375
|
return negative(timing.failAfter) || negative(timing.recoverAfter) ? [`the ${policy} window is not a time of zero or more`] : [];
|
|
969
1376
|
};
|
|
1377
|
+
/** This function gives the problems of the rules of the shutdown: an unknown name, or an exit code that a process cannot give. */
|
|
1378
|
+
const shutdownProblems = (floor, repeat, triggers, exitCodes, messages) => {
|
|
1379
|
+
const problems = [];
|
|
1380
|
+
if (floor !== void 0 && floor !== "half" && floor !== "none") problems.push(`the cooperative floor ${String(floor)} is not "half" or "none"`);
|
|
1381
|
+
if (repeat !== void 0 && repeat !== "ignore" && repeat !== "exit") problems.push(`the policy of a repeat signal ${String(repeat)} is not "ignore" or "exit"`);
|
|
1382
|
+
for (const trigger of triggers) if (!PROCESS_TRIGGERS.includes(trigger)) problems.push(`the trigger ${String(trigger)} is not a process trigger (${PROCESS_TRIGGERS.join(", ")})`);
|
|
1383
|
+
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 })`);
|
|
1384
|
+
for (const message of messages) {
|
|
1385
|
+
if (!isMessageShape(message.match)) problems.push(`the match of a message trigger is not a value of JSON: ${String(message.match)}`);
|
|
1386
|
+
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(", ")})`);
|
|
1387
|
+
}
|
|
1388
|
+
for (const [trigger, code] of Object.entries(exitCodes)) {
|
|
1389
|
+
if (!(EXIT_SIGNALS.includes(trigger) || triggers.includes(trigger))) problems.push(`the exit code of ${trigger} names no exit signal and no declared trigger`);
|
|
1390
|
+
if (code === void 0 || !isExitCode(code)) problems.push(`the exit code of ${trigger} is ${String(code)}, not a whole number from 0 to 255`);
|
|
1391
|
+
}
|
|
1392
|
+
return problems;
|
|
1393
|
+
};
|
|
970
1394
|
/** This function starts the composition of a service. */
|
|
971
1395
|
const service = (name) => {
|
|
972
1396
|
if (name.trim().length === 0) throw new Error("a service needs a name");
|
|
973
1397
|
const transports = [];
|
|
974
1398
|
const dependencies = [];
|
|
975
1399
|
const controllers = [];
|
|
1400
|
+
const choices = /* @__PURE__ */ new Map();
|
|
976
1401
|
const checks = [];
|
|
977
1402
|
const runs = /* @__PURE__ */ new Map();
|
|
978
1403
|
const couplings = [];
|
|
@@ -981,11 +1406,17 @@ const service = (name) => {
|
|
|
981
1406
|
let liveness;
|
|
982
1407
|
let startup;
|
|
983
1408
|
let cooperative;
|
|
1409
|
+
let floor;
|
|
1410
|
+
let repeat;
|
|
1411
|
+
const triggers = [];
|
|
1412
|
+
const messages = [];
|
|
1413
|
+
const exitCodes = {};
|
|
984
1414
|
let started = false;
|
|
985
1415
|
let partial = false;
|
|
986
1416
|
let runtime;
|
|
987
1417
|
let live;
|
|
988
1418
|
let running;
|
|
1419
|
+
let life;
|
|
989
1420
|
const transportImpls = /* @__PURE__ */ new Map();
|
|
990
1421
|
const handles = /* @__PURE__ */ new Map();
|
|
991
1422
|
const phaseWork = /* @__PURE__ */ new Map();
|
|
@@ -1054,6 +1485,9 @@ const service = (name) => {
|
|
|
1054
1485
|
get health() {
|
|
1055
1486
|
return live ?? null;
|
|
1056
1487
|
},
|
|
1488
|
+
get lifecycle() {
|
|
1489
|
+
return life ?? null;
|
|
1490
|
+
},
|
|
1057
1491
|
dependency(entry, spec, handle) {
|
|
1058
1492
|
once(dependencies, entry, "dependency");
|
|
1059
1493
|
if (handle !== void 0 && !started) handles.set(entry, handle);
|
|
@@ -1078,7 +1512,8 @@ const service = (name) => {
|
|
|
1078
1512
|
const held = {
|
|
1079
1513
|
name: entry,
|
|
1080
1514
|
requires: [],
|
|
1081
|
-
routes: {}
|
|
1515
|
+
routes: {},
|
|
1516
|
+
patterns: []
|
|
1082
1517
|
};
|
|
1083
1518
|
controllers.push(held);
|
|
1084
1519
|
const handle = {
|
|
@@ -1092,6 +1527,21 @@ const service = (name) => {
|
|
|
1092
1527
|
if (held.routes[served] !== void 0) throw new Error(`${entry} already serves ${served}`);
|
|
1093
1528
|
held.routes[served] = { ...operation };
|
|
1094
1529
|
return handle;
|
|
1530
|
+
},
|
|
1531
|
+
pattern(pattern, operations, operationOf) {
|
|
1532
|
+
composing(`${entry}'s route patterns`);
|
|
1533
|
+
if (held.patterns.some((each) => each.pattern === pattern)) throw new Error(`${entry} already has the route pattern ${pattern}`);
|
|
1534
|
+
if (operationOf === void 0 && Object.keys(operations).length > 1) throw new Error(`${entry}'s pattern ${pattern} has ${Object.keys(operations).length} operations, thus it needs the function that picks one`);
|
|
1535
|
+
held.patterns.push({
|
|
1536
|
+
pattern,
|
|
1537
|
+
operations: Object.fromEntries(Object.entries(operations).map(([name, operation]) => [name, { ...operation }]))
|
|
1538
|
+
});
|
|
1539
|
+
if (operationOf !== void 0) {
|
|
1540
|
+
const mine = choices.get(entry) ?? /* @__PURE__ */ new Map();
|
|
1541
|
+
mine.set(pattern, operationOf);
|
|
1542
|
+
choices.set(entry, mine);
|
|
1543
|
+
}
|
|
1544
|
+
return handle;
|
|
1095
1545
|
}
|
|
1096
1546
|
};
|
|
1097
1547
|
return handle;
|
|
@@ -1169,6 +1619,47 @@ const service = (name) => {
|
|
|
1169
1619
|
cooperative = window;
|
|
1170
1620
|
return builder;
|
|
1171
1621
|
},
|
|
1622
|
+
cooperativeFloor(given) {
|
|
1623
|
+
composing("the cooperative floor");
|
|
1624
|
+
floor = given;
|
|
1625
|
+
return builder;
|
|
1626
|
+
},
|
|
1627
|
+
onRepeatSignal(policy) {
|
|
1628
|
+
composing("the policy of a repeat signal");
|
|
1629
|
+
repeat = policy;
|
|
1630
|
+
return builder;
|
|
1631
|
+
},
|
|
1632
|
+
trigger(...given) {
|
|
1633
|
+
composing("a trigger");
|
|
1634
|
+
const [first, message] = given;
|
|
1635
|
+
if (first === "ipc-message" && typeof message === "object" && message !== null) {
|
|
1636
|
+
if (!triggers.includes("ipc-message")) triggers.push("ipc-message");
|
|
1637
|
+
messages.push({ ...message });
|
|
1638
|
+
return builder;
|
|
1639
|
+
}
|
|
1640
|
+
for (const each of given) if (typeof each === "string" && !triggers.includes(each)) triggers.push(each);
|
|
1641
|
+
return builder;
|
|
1642
|
+
},
|
|
1643
|
+
exitCode(trigger, code) {
|
|
1644
|
+
composing(`the exit code of ${trigger}`);
|
|
1645
|
+
exitCodes[trigger] = code;
|
|
1646
|
+
return builder;
|
|
1647
|
+
},
|
|
1648
|
+
profile(given) {
|
|
1649
|
+
composing("a profile");
|
|
1650
|
+
if (given.readiness !== void 0) builder.readiness(given.readiness);
|
|
1651
|
+
if (given.liveness !== void 0) builder.liveness(given.liveness);
|
|
1652
|
+
if (given.startup !== void 0) builder.startup(given.startup);
|
|
1653
|
+
for (const coupling of given.couplings ?? []) builder.couple(coupling);
|
|
1654
|
+
for (const each of given.phases ?? []) builder.phase(each.name, each.cap);
|
|
1655
|
+
if (given.cooperativeWindow !== void 0) builder.cooperativeWindow(given.cooperativeWindow);
|
|
1656
|
+
if (given.cooperativeFloor !== void 0) builder.cooperativeFloor(given.cooperativeFloor);
|
|
1657
|
+
if (given.onRepeatSignal !== void 0) builder.onRepeatSignal(given.onRepeatSignal);
|
|
1658
|
+
if (given.triggers !== void 0) builder.trigger(...given.triggers);
|
|
1659
|
+
for (const message of given.messages ?? []) builder.trigger("ipc-message", message);
|
|
1660
|
+
for (const [trigger, code] of Object.entries(given.exitCodes ?? {})) if (code !== void 0) builder.exitCode(trigger, code);
|
|
1661
|
+
return builder;
|
|
1662
|
+
},
|
|
1172
1663
|
describe() {
|
|
1173
1664
|
const missing = [];
|
|
1174
1665
|
if (readiness === void 0) missing.push("readiness (svc.readiness)");
|
|
@@ -1176,6 +1667,7 @@ const service = (name) => {
|
|
|
1176
1667
|
if (startup === void 0) missing.push("a startup budget (svc.startup)");
|
|
1177
1668
|
if (phases.length === 0) missing.push("its shutdown phases (svc.phase)");
|
|
1178
1669
|
if (cooperative === void 0) missing.push("a cooperative window, or no bound (svc.cooperativeWindow)");
|
|
1670
|
+
if (repeat === void 0) missing.push(`what a repeat signal does, "ignore" or "exit" (svc.onRepeatSignal)`);
|
|
1179
1671
|
if (missing.length > 0) throw new Error(`${name} does not declare ${missing.join("; ")}: the runtime has no defaults`);
|
|
1180
1672
|
const known = new Set(dependencies.map((entry) => entry.name));
|
|
1181
1673
|
const problems = [];
|
|
@@ -1186,25 +1678,31 @@ const service = (name) => {
|
|
|
1186
1678
|
for (const gate of ["readiness", "liveness"]) problems.push(...timingProblems(gate, policies[gate]));
|
|
1187
1679
|
for (const check of checks) {
|
|
1188
1680
|
if (check.dependency !== null && !known.has(check.dependency)) problems.push(`check ${check.name} exercises ${check.dependency}, which is not a dependency`);
|
|
1189
|
-
if (!isTime(check.poll.interval) || !isTime(check.poll.timeout)) problems.push(`check ${check.name} polls in a unit that is not a time`);
|
|
1681
|
+
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`);
|
|
1190
1682
|
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`);
|
|
1191
1683
|
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`);
|
|
1192
1684
|
const timing = check.gates === null ? null : policies[check.gates];
|
|
1193
|
-
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`);
|
|
1685
|
+
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`);
|
|
1194
1686
|
}
|
|
1195
1687
|
for (const controller of controllers) {
|
|
1196
1688
|
for (const needed of controller.requires) if (!known.has(needed)) problems.push(`controller ${controller.name} requires ${needed}, which is not a dependency`);
|
|
1197
1689
|
for (const [route, operation] of Object.entries(controller.routes)) problems.push(...operationProblems(`controller ${controller.name}`, route, operation));
|
|
1690
|
+
for (const declared of controller.patterns) problems.push(...routePatternProblems(`controller ${controller.name}`, declared));
|
|
1198
1691
|
}
|
|
1199
1692
|
for (const dependency of dependencies) for (const [name, operation] of Object.entries(dependency.operations)) problems.push(...operationProblems(`dependency ${dependency.name}`, name, operation));
|
|
1200
|
-
for (const phase of phases) if (!isTime(phase.cap)) problems.push(`phase ${phase.name}'s cap is not a time`);
|
|
1693
|
+
for (const phase of phases) if (!isTime$1(phase.cap)) problems.push(`phase ${phase.name}'s cap is not a time`);
|
|
1694
|
+
problems.push(...shutdownProblems(floor, repeat, triggers, exitCodes, messages));
|
|
1201
1695
|
const ranked = phases.map((phase) => phase.name).filter((each) => stageRank(each) >= 0);
|
|
1202
1696
|
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(", ")}`);
|
|
1203
1697
|
if (problems.length > 0) throw new Error(`${name}: ${problems.join("; ")}`);
|
|
1204
1698
|
const controllersOut = controllers.map((entry) => ({
|
|
1205
1699
|
name: entry.name,
|
|
1206
1700
|
requires: [...entry.requires],
|
|
1207
|
-
routes: { ...entry.routes }
|
|
1701
|
+
routes: { ...entry.routes },
|
|
1702
|
+
...entry.patterns.length === 0 ? {} : { patterns: entry.patterns.map((each) => ({
|
|
1703
|
+
pattern: each.pattern,
|
|
1704
|
+
operations: { ...each.operations }
|
|
1705
|
+
})) }
|
|
1208
1706
|
}));
|
|
1209
1707
|
return {
|
|
1210
1708
|
format: FORMAT,
|
|
@@ -1232,7 +1730,12 @@ const service = (name) => {
|
|
|
1232
1730
|
couplings: couplings.map((entry) => ({ ...entry })),
|
|
1233
1731
|
shutdown: {
|
|
1234
1732
|
phases: phases.map((entry) => ({ ...entry })),
|
|
1235
|
-
cooperativeWindow: cooperative
|
|
1733
|
+
cooperativeWindow: cooperative,
|
|
1734
|
+
...floor === void 0 ? {} : { cooperativeFloor: floor },
|
|
1735
|
+
onRepeatSignal: repeat,
|
|
1736
|
+
...triggers.length === 0 ? {} : { triggers: [...triggers] },
|
|
1737
|
+
...messages.length === 0 ? {} : { messages: messages.map((each) => ({ ...each })) },
|
|
1738
|
+
...Object.keys(exitCodes).length === 0 ? {} : { exitCodes: { ...exitCodes } }
|
|
1236
1739
|
}
|
|
1237
1740
|
};
|
|
1238
1741
|
},
|
|
@@ -1253,7 +1756,6 @@ const service = (name) => {
|
|
|
1253
1756
|
run(options = {}) {
|
|
1254
1757
|
if (running !== void 0) return running;
|
|
1255
1758
|
if (runtime !== void 0) return Promise.reject(/* @__PURE__ */ new Error(`${name} started its health only: run() starts the whole lifecycle`));
|
|
1256
|
-
let life;
|
|
1257
1759
|
try {
|
|
1258
1760
|
const description = builder.describe();
|
|
1259
1761
|
const components = {
|
|
@@ -1261,6 +1763,7 @@ const service = (name) => {
|
|
|
1261
1763
|
dependencies: handles,
|
|
1262
1764
|
healthServer: probes,
|
|
1263
1765
|
phaseWork,
|
|
1766
|
+
choices,
|
|
1264
1767
|
stopPollers: () => {}
|
|
1265
1768
|
};
|
|
1266
1769
|
const problems = lifecycleProblems(description, components);
|
|
@@ -1268,14 +1771,16 @@ const service = (name) => {
|
|
|
1268
1771
|
const made = makeRuntime(options);
|
|
1269
1772
|
started = true;
|
|
1270
1773
|
live = made.runtime;
|
|
1271
|
-
|
|
1774
|
+
const ran = lifecycle(made.description, made.runtime, {
|
|
1272
1775
|
...components,
|
|
1273
1776
|
stopPollers: made.stopPollers
|
|
1274
|
-
}, options)
|
|
1777
|
+
}, options, (given) => {
|
|
1778
|
+
life = given;
|
|
1779
|
+
});
|
|
1780
|
+
running = ran.started.then(() => ran);
|
|
1275
1781
|
} catch (error) {
|
|
1276
1782
|
return Promise.reject(error);
|
|
1277
1783
|
}
|
|
1278
|
-
running = life.started.then(() => life);
|
|
1279
1784
|
return running;
|
|
1280
1785
|
}
|
|
1281
1786
|
};
|
|
@@ -1374,6 +1879,7 @@ const SERVICE_RULES = [
|
|
|
1374
1879
|
"O17",
|
|
1375
1880
|
"O18",
|
|
1376
1881
|
"O19",
|
|
1882
|
+
"O20",
|
|
1377
1883
|
"O21",
|
|
1378
1884
|
"O22",
|
|
1379
1885
|
"O23",
|
|
@@ -1383,6 +1889,14 @@ const SERVICE_RULES = [
|
|
|
1383
1889
|
const secondsOf = (amount) => rationals.toNumber(convertExact(rationals.fromNumber(amount.value), amount.unit, "s"));
|
|
1384
1890
|
/** This function gives an amount, or no bound, as the law binds it. */
|
|
1385
1891
|
const boundOf = (amount) => amount.value === "infinite" ? "inf" : secondsOf(amount);
|
|
1892
|
+
/** This function tells if a unit is a time. A unit that the package does not know is not a time. */
|
|
1893
|
+
const isTime = (unit) => {
|
|
1894
|
+
try {
|
|
1895
|
+
return convertible(unit, "s");
|
|
1896
|
+
} catch {
|
|
1897
|
+
return false;
|
|
1898
|
+
}
|
|
1899
|
+
};
|
|
1386
1900
|
/** This function gives the largest of some numbers of seconds, or undefined when there are none. */
|
|
1387
1901
|
const most = (values) => values.length === 0 ? void 0 : Math.max(...values);
|
|
1388
1902
|
/** This function gives what the declaration tells about the service, as symbols of the law, and the source of each. */
|
|
@@ -1424,15 +1938,64 @@ const dependencyBindings = (dependency, scenario) => {
|
|
|
1424
1938
|
for (const [symbol, amount] of Object.entries(scenario.dependencies?.[dependency.name] ?? {})) bind[symbol.includes("@") ? symbol : `${symbol}@dependency`] = boundOf(amount);
|
|
1425
1939
|
return bind;
|
|
1426
1940
|
};
|
|
1941
|
+
/**
|
|
1942
|
+
* These are the hops of the law at which the knobs of a transport bind, by its kind. A worker does
|
|
1943
|
+
* its units as a consumer does, thus both bind at `consumer` (O20). A server binds at no hop yet.
|
|
1944
|
+
*/
|
|
1945
|
+
const TRANSPORT_HOPS = {
|
|
1946
|
+
server: null,
|
|
1947
|
+
consumer: "consumer",
|
|
1948
|
+
worker: "consumer"
|
|
1949
|
+
};
|
|
1950
|
+
/**
|
|
1951
|
+
* This function gives the time knobs of a transport at the hop of its kind, and the values of the
|
|
1952
|
+
* scenario for its source. A knob with a hop in its name binds at that hop. A count binds no symbol.
|
|
1953
|
+
*/
|
|
1954
|
+
const transportBindings = (transport, hop, scenario) => {
|
|
1955
|
+
const bind = {};
|
|
1956
|
+
for (const [knob, amount] of Object.entries(transport.knobs)) {
|
|
1957
|
+
if (!isTime(amount.unit)) continue;
|
|
1958
|
+
bind[knob.includes("@") ? knob : `${knob}@${hop}`] = boundOf(amount);
|
|
1959
|
+
}
|
|
1960
|
+
for (const [symbol, amount] of Object.entries(scenario.transports?.[transport.name] ?? {})) bind[symbol] = boundOf(amount);
|
|
1961
|
+
return bind;
|
|
1962
|
+
};
|
|
1427
1963
|
const SCOPED = /@(client|dependency|server)$/;
|
|
1428
|
-
|
|
1964
|
+
const TRANSPORT_SCOPED = /@consumer$/;
|
|
1965
|
+
/** This identifier is the rule of the findings about a field that the declaration does not give. */
|
|
1966
|
+
const DECLARATION_RULE = "declaration";
|
|
1967
|
+
/**
|
|
1968
|
+
* This function gives a finding for each field of the shutdown that a description does not give.
|
|
1969
|
+
* `describe()` refuses such a declaration. A description from another source, for example the
|
|
1970
|
+
* JSON of 0.4, can lack the field. The verdict is `depends`, and `unbound` names the field.
|
|
1971
|
+
*/
|
|
1972
|
+
const declarationFindings = (svc) => svc.shutdown.onRepeatSignal === void 0 ? [{
|
|
1973
|
+
rule: DECLARATION_RULE,
|
|
1974
|
+
scope: svc.name,
|
|
1975
|
+
verdict: "depends",
|
|
1976
|
+
statement: "the declaration tells what a signal does while the shutdown runs: ignore or exit",
|
|
1977
|
+
window: "a repeat signal has no declared effect",
|
|
1978
|
+
evaluated: [],
|
|
1979
|
+
unbound: ["shutdown.onRepeatSignal"]
|
|
1980
|
+
}] : [];
|
|
1981
|
+
/**
|
|
1982
|
+
* This function applies each service rule to a declaration and a scenario. A rule applies one time
|
|
1983
|
+
* for the service, for each client, or for each consumer and worker. The function also gives a
|
|
1984
|
+
* finding for each necessary field that the description does not give (`DECLARATION_RULE`).
|
|
1985
|
+
*/
|
|
1429
1986
|
const lint = (svc, scenario, rules) => {
|
|
1430
1987
|
const service = serviceBindings(svc, scenario).bind;
|
|
1431
|
-
const findings = [];
|
|
1988
|
+
const findings = [...declarationFindings(svc)];
|
|
1432
1989
|
for (const rule of rules) {
|
|
1433
1990
|
if (!SERVICE_RULES.includes(rule.id)) continue;
|
|
1434
1991
|
const symbols = symbolsOf(rule);
|
|
1435
|
-
const scopes = symbols.some((symbol) =>
|
|
1992
|
+
const scopes = symbols.some((symbol) => TRANSPORT_SCOPED.test(symbol)) ? svc.transports.flatMap((transport) => {
|
|
1993
|
+
const hop = TRANSPORT_HOPS[transport.kind];
|
|
1994
|
+
return hop === null ? [] : [[transport.name, {
|
|
1995
|
+
...service,
|
|
1996
|
+
...transportBindings(transport, hop, scenario)
|
|
1997
|
+
}]];
|
|
1998
|
+
}) : symbols.some((symbol) => SCOPED.test(symbol)) ? svc.dependencies.map((dependency) => [dependency.name, {
|
|
1436
1999
|
...service,
|
|
1437
2000
|
...dependencyBindings(dependency, scenario)
|
|
1438
2001
|
}]) : [[svc.name, service]];
|
|
@@ -1452,6 +2015,767 @@ const lint = (svc, scenario, rules) => {
|
|
|
1452
2015
|
return findings;
|
|
1453
2016
|
};
|
|
1454
2017
|
//#endregion
|
|
2018
|
+
//#region src/profiles.ts
|
|
2019
|
+
const s = (value) => ({
|
|
2020
|
+
value,
|
|
2021
|
+
unit: "s"
|
|
2022
|
+
});
|
|
2023
|
+
const phase = (name, cap) => ({
|
|
2024
|
+
name,
|
|
2025
|
+
cap
|
|
2026
|
+
});
|
|
2027
|
+
/**
|
|
2028
|
+
* This profile is for a service on the computer of a developer. No load balancer routes to it,
|
|
2029
|
+
* thus it has no readiness hold. Its caps sum to 10 s, and a second signal exits at once. It
|
|
2030
|
+
* declares no checks, thus the caller declares readiness, liveness and the startup budget.
|
|
2031
|
+
*/
|
|
2032
|
+
const localDev = {
|
|
2033
|
+
phases: [
|
|
2034
|
+
phase("stop intake", s(1)),
|
|
2035
|
+
phase("drain", s(4)),
|
|
2036
|
+
phase("close transports", s(1)),
|
|
2037
|
+
phase("stop the poller", s(1)),
|
|
2038
|
+
phase("tear down dependencies", s(1)),
|
|
2039
|
+
phase("close the health server", s(1)),
|
|
2040
|
+
phase("flush the output", s(1))
|
|
2041
|
+
],
|
|
2042
|
+
cooperativeWindow: s(2),
|
|
2043
|
+
cooperativeFloor: "half",
|
|
2044
|
+
onRepeatSignal: "exit"
|
|
2045
|
+
};
|
|
2046
|
+
/**
|
|
2047
|
+
* This profile is for a background host on the computer of a developer, under a supervisor or a
|
|
2048
|
+
* command that stops it. It has the caps of `localDev`. A repeat signal has no effect, thus the
|
|
2049
|
+
* host ends its drain. It has no process trigger: a background host has no parent on stdin.
|
|
2050
|
+
*/
|
|
2051
|
+
const daemon = {
|
|
2052
|
+
phases: localDev.phases.map((each) => ({ ...each })),
|
|
2053
|
+
cooperativeWindow: localDev.cooperativeWindow,
|
|
2054
|
+
cooperativeFloor: "half",
|
|
2055
|
+
onRepeatSignal: "ignore"
|
|
2056
|
+
};
|
|
2057
|
+
/**
|
|
2058
|
+
* This profile is for a small process: a command that runs one time, or a child process on stdio.
|
|
2059
|
+
* It has no checks and no probes, and its policies gate nothing. The cooperative abort occurs at the
|
|
2060
|
+
* start of the drain, thus a first Ctrl-C stops the work at once. A second signal exits at once.
|
|
2061
|
+
* The exit codes tell the shell how the process stopped: 130 for SIGINT, 143 for SIGTERM and 131
|
|
2062
|
+
* for SIGQUIT. A stdio child also declares `triggers: ["stdin-close"]`.
|
|
2063
|
+
*/
|
|
2064
|
+
const ephemeral = {
|
|
2065
|
+
readiness: {
|
|
2066
|
+
type: "window",
|
|
2067
|
+
failAfter: s(0)
|
|
2068
|
+
},
|
|
2069
|
+
liveness: {
|
|
2070
|
+
type: "window",
|
|
2071
|
+
failAfter: s(0)
|
|
2072
|
+
},
|
|
2073
|
+
startup: s(10),
|
|
2074
|
+
phases: [
|
|
2075
|
+
phase("stop intake", s(1)),
|
|
2076
|
+
phase("drain", s(2)),
|
|
2077
|
+
phase("close transports", s(1)),
|
|
2078
|
+
phase("stop the poller", s(1)),
|
|
2079
|
+
phase("tear down dependencies", s(2)),
|
|
2080
|
+
phase("flush the output", s(1))
|
|
2081
|
+
],
|
|
2082
|
+
cooperativeWindow: s(2),
|
|
2083
|
+
cooperativeFloor: "none",
|
|
2084
|
+
onRepeatSignal: "exit",
|
|
2085
|
+
exitCodes: {
|
|
2086
|
+
SIGINT: 130,
|
|
2087
|
+
SIGTERM: 143,
|
|
2088
|
+
SIGQUIT: 131
|
|
2089
|
+
}
|
|
2090
|
+
};
|
|
2091
|
+
/** These are the parts of the budget of each stage in the profile `kubernetes`, in thirtieths. */
|
|
2092
|
+
const KUBERNETES_SHARES = [
|
|
2093
|
+
["fail readiness", 2],
|
|
2094
|
+
["stop intake", 5],
|
|
2095
|
+
["drain", 15],
|
|
2096
|
+
["close transports", 2],
|
|
2097
|
+
["stop the poller", 1],
|
|
2098
|
+
["tear down dependencies", 2],
|
|
2099
|
+
["close the health server", 1]
|
|
2100
|
+
];
|
|
2101
|
+
/** This part of the budget is the cooperative window, in thirtieths. */
|
|
2102
|
+
const KUBERNETES_WINDOW = 5;
|
|
2103
|
+
/**
|
|
2104
|
+
* This function gives the profile of a pod in Kubernetes. The budget is the grace less the preStop
|
|
2105
|
+
* sleep: the time from SIGTERM to SIGKILL. Each stage gets a part of the budget, rounded down to
|
|
2106
|
+
* the millisecond.
|
|
2107
|
+
* - The parts in thirtieths are 2, 5, 15, 2, 1, 2 and 1, in the sequence of the stages. Thus 2
|
|
2108
|
+
* thirtieths of the budget stay as a margin before SIGKILL.
|
|
2109
|
+
* - The cooperative window is 5 thirtieths of the budget.
|
|
2110
|
+
* - The kubelet sends SIGTERM one time, thus a repeat signal has no effect.
|
|
2111
|
+
*
|
|
2112
|
+
* At the defaults of Kubernetes (a grace of 30 s and no preStop hook), the caps are 2, 5, 15, 2, 1,
|
|
2113
|
+
* 2 and 1 s. Thus the sum of the caps and the preStop sleep fits in the grace (O10). The window is
|
|
2114
|
+
* shorter than the drain, which is shorter than the budget (O16).
|
|
2115
|
+
*/
|
|
2116
|
+
const kubernetes = (pod) => {
|
|
2117
|
+
const time = (amount, what) => {
|
|
2118
|
+
if (!convertible(amount.unit, "s")) throw new Error(`the ${what} of the pod is not a time`);
|
|
2119
|
+
return valueIn(amount, "ms");
|
|
2120
|
+
};
|
|
2121
|
+
const grace = time(pod.grace, "grace");
|
|
2122
|
+
const preStop = time(pod.preStop, "preStop sleep");
|
|
2123
|
+
if (preStop < 0) throw new Error("the preStop sleep of the pod is less than 0");
|
|
2124
|
+
const budget = grace - preStop;
|
|
2125
|
+
if (!(budget > 0)) throw new Error(`the grace of the pod (${grace} ms) is not longer than its preStop sleep (${preStop} ms)`);
|
|
2126
|
+
const part = (thirtieths) => s(Math.floor(budget * thirtieths / 30) / 1e3);
|
|
2127
|
+
return {
|
|
2128
|
+
phases: KUBERNETES_SHARES.map(([name, thirtieths]) => phase(name, part(thirtieths))),
|
|
2129
|
+
cooperativeWindow: part(KUBERNETES_WINDOW),
|
|
2130
|
+
cooperativeFloor: "half",
|
|
2131
|
+
onRepeatSignal: "ignore"
|
|
2132
|
+
};
|
|
2133
|
+
};
|
|
2134
|
+
//#endregion
|
|
2135
|
+
//#region src/units.ts
|
|
2136
|
+
/** This function gives the cause of an abort as text. */
|
|
2137
|
+
const causeOf = (signal) => signal.reason instanceof Error ? signal.reason.message : String(signal.reason ?? "the unit got an abort");
|
|
2138
|
+
/**
|
|
2139
|
+
* This function does one unit of work, and gives its end. With a request, the unit goes through
|
|
2140
|
+
* `life.request`, thus the service counts it, drains it and can cut it. A throw after the abort of
|
|
2141
|
+
* the transport is a cut, not a failure (S6 A10). A throw after the bound is a failure.
|
|
2142
|
+
*/
|
|
2143
|
+
const runUnit = async (run) => {
|
|
2144
|
+
const { clock, bound, abort, life, request, unit, work } = run;
|
|
2145
|
+
const timeout = new AbortController();
|
|
2146
|
+
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`))) : () => {};
|
|
2147
|
+
const call = async (tracker) => work(AbortSignal.any(tracker === void 0 ? [abort.signal, timeout.signal] : [
|
|
2148
|
+
abort.signal,
|
|
2149
|
+
timeout.signal,
|
|
2150
|
+
tracker
|
|
2151
|
+
]));
|
|
2152
|
+
const thrown = (error) => {
|
|
2153
|
+
if (abort.signal.aborted) return {
|
|
2154
|
+
kind: "cut",
|
|
2155
|
+
cause: causeOf(abort.signal),
|
|
2156
|
+
retryable: false
|
|
2157
|
+
};
|
|
2158
|
+
if (timeout.signal.aborted) return {
|
|
2159
|
+
kind: "failed",
|
|
2160
|
+
error: timeout.signal.reason
|
|
2161
|
+
};
|
|
2162
|
+
return {
|
|
2163
|
+
kind: "failed",
|
|
2164
|
+
error
|
|
2165
|
+
};
|
|
2166
|
+
};
|
|
2167
|
+
try {
|
|
2168
|
+
if (request === void 0) try {
|
|
2169
|
+
return {
|
|
2170
|
+
kind: "done",
|
|
2171
|
+
value: await call()
|
|
2172
|
+
};
|
|
2173
|
+
} catch (error) {
|
|
2174
|
+
return thrown(error);
|
|
2175
|
+
}
|
|
2176
|
+
let collected;
|
|
2177
|
+
try {
|
|
2178
|
+
const route = typeof request.route === "string" ? request.route : request.route(unit);
|
|
2179
|
+
const detail = request.detail?.(unit);
|
|
2180
|
+
collected = life.request(request.controller, route, (signal) => call(signal).then((value) => ({ value })), detail === void 0 ? {} : { detail }).collect();
|
|
2181
|
+
} catch (error) {
|
|
2182
|
+
return {
|
|
2183
|
+
kind: "failed",
|
|
2184
|
+
error
|
|
2185
|
+
};
|
|
2186
|
+
}
|
|
2187
|
+
const { items, end } = await collected;
|
|
2188
|
+
if (end.kind === "done") return {
|
|
2189
|
+
kind: "done",
|
|
2190
|
+
value: items[0]?.value
|
|
2191
|
+
};
|
|
2192
|
+
if (end.kind === "failed") return thrown(end.error);
|
|
2193
|
+
if (end.kind === "refused") return {
|
|
2194
|
+
kind: "refused",
|
|
2195
|
+
cause: end.cause
|
|
2196
|
+
};
|
|
2197
|
+
return {
|
|
2198
|
+
kind: "cut",
|
|
2199
|
+
cause: end.cause,
|
|
2200
|
+
retryable: end.retryable
|
|
2201
|
+
};
|
|
2202
|
+
} finally {
|
|
2203
|
+
cancel();
|
|
2204
|
+
}
|
|
2205
|
+
};
|
|
2206
|
+
/** This function makes a flight set on a clock. */
|
|
2207
|
+
const flights = (clock) => {
|
|
2208
|
+
const units = /* @__PURE__ */ new Set();
|
|
2209
|
+
const idle = /* @__PURE__ */ new Set();
|
|
2210
|
+
const lower = /* @__PURE__ */ new Set();
|
|
2211
|
+
let aborted = null;
|
|
2212
|
+
const settle = () => {
|
|
2213
|
+
for (const done of [...lower]) done();
|
|
2214
|
+
lower.clear();
|
|
2215
|
+
if (units.size === 0) for (const done of [...idle]) done();
|
|
2216
|
+
};
|
|
2217
|
+
const abort = (reason) => {
|
|
2218
|
+
aborted ??= { reason };
|
|
2219
|
+
for (const unit of [...units]) if (!unit.signal.aborted) unit.abort(aborted.reason);
|
|
2220
|
+
};
|
|
2221
|
+
return {
|
|
2222
|
+
get size() {
|
|
2223
|
+
return units.size;
|
|
2224
|
+
},
|
|
2225
|
+
add(unit) {
|
|
2226
|
+
units.add(unit);
|
|
2227
|
+
if (aborted !== null && !unit.signal.aborted) unit.abort(aborted.reason);
|
|
2228
|
+
return () => {
|
|
2229
|
+
if (units.delete(unit)) settle();
|
|
2230
|
+
};
|
|
2231
|
+
},
|
|
2232
|
+
abort,
|
|
2233
|
+
clear() {
|
|
2234
|
+
units.clear();
|
|
2235
|
+
settle();
|
|
2236
|
+
},
|
|
2237
|
+
drain(deadline, signal) {
|
|
2238
|
+
const cooperative = () => abort(signal.reason);
|
|
2239
|
+
if (signal.aborted) cooperative();
|
|
2240
|
+
else signal.addEventListener("abort", cooperative, { once: true });
|
|
2241
|
+
if (units.size === 0 || deadline <= clock.now()) {
|
|
2242
|
+
signal.removeEventListener("abort", cooperative);
|
|
2243
|
+
return Promise.resolve();
|
|
2244
|
+
}
|
|
2245
|
+
return new Promise((resolve) => {
|
|
2246
|
+
let cancel = () => {};
|
|
2247
|
+
const done = () => {
|
|
2248
|
+
idle.delete(done);
|
|
2249
|
+
cancel();
|
|
2250
|
+
signal.removeEventListener("abort", cooperative);
|
|
2251
|
+
resolve();
|
|
2252
|
+
};
|
|
2253
|
+
idle.add(done);
|
|
2254
|
+
cancel = clock.after(Math.max(0, deadline - clock.now()), done);
|
|
2255
|
+
});
|
|
2256
|
+
},
|
|
2257
|
+
less() {
|
|
2258
|
+
return new Promise((resolve) => lower.add(resolve));
|
|
2259
|
+
},
|
|
2260
|
+
release() {
|
|
2261
|
+
for (const done of [...lower]) done();
|
|
2262
|
+
lower.clear();
|
|
2263
|
+
}
|
|
2264
|
+
};
|
|
2265
|
+
};
|
|
2266
|
+
/** This function makes a sleeper on a clock. */
|
|
2267
|
+
const sleeper = (clock) => {
|
|
2268
|
+
let wakes = 0;
|
|
2269
|
+
let stopped = false;
|
|
2270
|
+
const sleeps = /* @__PURE__ */ new Set();
|
|
2271
|
+
const backoffs = /* @__PURE__ */ new Set();
|
|
2272
|
+
const wait = (ms, into) => stopped ? Promise.resolve() : new Promise((resolve) => {
|
|
2273
|
+
let cancel = () => {};
|
|
2274
|
+
const done = () => {
|
|
2275
|
+
if (!into.delete(done)) return;
|
|
2276
|
+
cancel();
|
|
2277
|
+
resolve();
|
|
2278
|
+
};
|
|
2279
|
+
into.add(done);
|
|
2280
|
+
if (Number.isFinite(ms)) cancel = clock.after(ms, done);
|
|
2281
|
+
});
|
|
2282
|
+
return {
|
|
2283
|
+
get wakes() {
|
|
2284
|
+
return wakes;
|
|
2285
|
+
},
|
|
2286
|
+
sleep(ms, since) {
|
|
2287
|
+
if (since !== wakes) return Promise.resolve();
|
|
2288
|
+
return wait(ms, sleeps);
|
|
2289
|
+
},
|
|
2290
|
+
backoff(ms) {
|
|
2291
|
+
return wait(ms, backoffs);
|
|
2292
|
+
},
|
|
2293
|
+
wake() {
|
|
2294
|
+
wakes += 1;
|
|
2295
|
+
const ended = sleeps.size;
|
|
2296
|
+
for (const done of [...sleeps]) done();
|
|
2297
|
+
return ended;
|
|
2298
|
+
},
|
|
2299
|
+
stop() {
|
|
2300
|
+
stopped = true;
|
|
2301
|
+
for (const done of [...sleeps, ...backoffs]) done();
|
|
2302
|
+
}
|
|
2303
|
+
};
|
|
2304
|
+
};
|
|
2305
|
+
/** This function waits for a work until a bound on the clock, in milliseconds. */
|
|
2306
|
+
const settleWithin = (clock, ms, work) => new Promise((resolve) => {
|
|
2307
|
+
const cancel = Number.isFinite(ms) ? clock.after(ms, () => resolve({ kind: "bound" })) : () => {};
|
|
2308
|
+
work.then(() => {
|
|
2309
|
+
cancel();
|
|
2310
|
+
resolve({ kind: "done" });
|
|
2311
|
+
}, (error) => {
|
|
2312
|
+
cancel();
|
|
2313
|
+
resolve({
|
|
2314
|
+
kind: "failed",
|
|
2315
|
+
error
|
|
2316
|
+
});
|
|
2317
|
+
});
|
|
2318
|
+
});
|
|
2319
|
+
/**
|
|
2320
|
+
* This function gives the backoff of the knobs of a loop. The first delay is `retry_base`, and each
|
|
2321
|
+
* delay is two times longer, up to `retry_cap`. Each delay has a jitter up to the base, as in the Redis client.
|
|
2322
|
+
*/
|
|
2323
|
+
const retryOf = (knobs, owner) => {
|
|
2324
|
+
const base = boundedMillisecondsOf(knobs, "retry_base", owner);
|
|
2325
|
+
const backoff = {
|
|
2326
|
+
base,
|
|
2327
|
+
factor: 2,
|
|
2328
|
+
cap: boundedMillisecondsOf(knobs, "retry_cap", owner),
|
|
2329
|
+
jitter: {
|
|
2330
|
+
kind: "add",
|
|
2331
|
+
amount: base
|
|
2332
|
+
}
|
|
2333
|
+
};
|
|
2334
|
+
const problems = backoffProblems(backoff);
|
|
2335
|
+
if (problems.length > 0) throw new Error(`${owner}: the retry_base and retry_cap give no backoff: ${problems.join(", ")}`);
|
|
2336
|
+
return backoff;
|
|
2337
|
+
};
|
|
2338
|
+
/**
|
|
2339
|
+
* This function gives the problems of the bounds of a loop in a declaration. The close bound fits
|
|
2340
|
+
* 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).
|
|
2341
|
+
*/
|
|
2342
|
+
const boundProblems = (owner, closeBound, unitBound, description) => {
|
|
2343
|
+
const capOf = (name) => {
|
|
2344
|
+
const phase = description.shutdown.phases.find((each) => each.name === name);
|
|
2345
|
+
return phase === void 0 ? void 0 : valueIn(phase.cap, "ms");
|
|
2346
|
+
};
|
|
2347
|
+
const found = [];
|
|
2348
|
+
const close = capOf("close transports");
|
|
2349
|
+
if (close === void 0) found.push(`${owner} needs the phase "close transports" to close`);
|
|
2350
|
+
else if (closeBound >= close) found.push(`the close_bound of ${owner}, ${closeBound} ms, does not fit in the cap of "close transports", ${close} ms`);
|
|
2351
|
+
const drain = capOf("drain");
|
|
2352
|
+
if (drain === void 0) found.push(`${owner} needs the phase "drain" to end its units`);
|
|
2353
|
+
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`);
|
|
2354
|
+
else if (unitBound >= drain) found.push(`the unit_processing_time of ${owner}, ${unitBound} ms, does not fit in the cap of "drain", ${drain} ms`);
|
|
2355
|
+
return found;
|
|
2356
|
+
};
|
|
2357
|
+
//#endregion
|
|
2358
|
+
//#region src/worker.ts
|
|
2359
|
+
const OWNER$1 = "workerLoop";
|
|
2360
|
+
/**
|
|
2361
|
+
* This function makes a worker loop as a transport (S6 A10). It has a setup, an iteration and a
|
|
2362
|
+
* teardown, and each lane does its iterations one at a time.
|
|
2363
|
+
* - The start does the setup. A failed setup gets its teardown, and the start throws.
|
|
2364
|
+
* - The first iteration of each lane comes after the startup ends. Each sleep and each backoff is on the clock.
|
|
2365
|
+
* - `stopIntake` starts no new iteration, and it ends each sleep and each backoff.
|
|
2366
|
+
* - `drain` waits for each iteration in flight until the deadline, and gives it the cooperative
|
|
2367
|
+
* abort. A throw after that abort is not a failure.
|
|
2368
|
+
* - `close` aborts each iteration, waits for its end, and does the teardown, all in `close_bound`.
|
|
2369
|
+
*/
|
|
2370
|
+
const workerLoop = (knobs, options) => {
|
|
2371
|
+
const sleep = millisecondsOf(knobs, "worker_sleep", OWNER$1);
|
|
2372
|
+
const bound = millisecondsOf(knobs, "unit_processing_time", OWNER$1);
|
|
2373
|
+
const backoff = retryOf(knobs, OWNER$1);
|
|
2374
|
+
const closeBound = boundedMillisecondsOf(knobs, "close_bound", OWNER$1);
|
|
2375
|
+
const lanes = countOf(knobs, "concurrency", OWNER$1);
|
|
2376
|
+
if (!Number.isFinite(lanes)) throw new Error(`${OWNER$1} needs a concurrency with a bound: each lane is a loop`);
|
|
2377
|
+
const clock = options.clock ?? realClock;
|
|
2378
|
+
const random = options.random ?? Math.random;
|
|
2379
|
+
const running = flights(clock);
|
|
2380
|
+
const rests = sleeper(clock);
|
|
2381
|
+
const intake = new AbortController();
|
|
2382
|
+
const pending = /* @__PURE__ */ new Set();
|
|
2383
|
+
let started = false;
|
|
2384
|
+
let setup = null;
|
|
2385
|
+
let state;
|
|
2386
|
+
let tornDown = null;
|
|
2387
|
+
let closing = null;
|
|
2388
|
+
const report = (error, failures) => {
|
|
2389
|
+
try {
|
|
2390
|
+
options.onError?.(error, failures);
|
|
2391
|
+
} catch {}
|
|
2392
|
+
};
|
|
2393
|
+
/** This function does the teardown one time, after the setup ends, and only when the setup started. */
|
|
2394
|
+
const teardown = () => {
|
|
2395
|
+
if (tornDown !== null) return tornDown;
|
|
2396
|
+
const release = options.teardown;
|
|
2397
|
+
const made = setup;
|
|
2398
|
+
tornDown = made === null || release === void 0 ? Promise.resolve() : made.then((given) => release(given), () => release(void 0));
|
|
2399
|
+
return tornDown;
|
|
2400
|
+
};
|
|
2401
|
+
const keep = (work) => {
|
|
2402
|
+
pending.add(work);
|
|
2403
|
+
const forget = () => {
|
|
2404
|
+
pending.delete(work);
|
|
2405
|
+
};
|
|
2406
|
+
work.then(forget, forget);
|
|
2407
|
+
return work;
|
|
2408
|
+
};
|
|
2409
|
+
/** This function is one lane: an iteration, then its sleep or its backoff, until the intake stops. */
|
|
2410
|
+
const lane = async (index, life) => {
|
|
2411
|
+
let failures = 0;
|
|
2412
|
+
let since = rests.wakes;
|
|
2413
|
+
await rests.sleep(0, since);
|
|
2414
|
+
for (;;) {
|
|
2415
|
+
if (intake.signal.aborted) return;
|
|
2416
|
+
since = rests.wakes;
|
|
2417
|
+
const abort = new AbortController();
|
|
2418
|
+
const remove = running.add(abort);
|
|
2419
|
+
const count = failures;
|
|
2420
|
+
const end = await runUnit({
|
|
2421
|
+
clock,
|
|
2422
|
+
bound,
|
|
2423
|
+
abort,
|
|
2424
|
+
life,
|
|
2425
|
+
request: options.request,
|
|
2426
|
+
unit: index,
|
|
2427
|
+
work: (signal) => keep((async () => options.iteration({
|
|
2428
|
+
signal,
|
|
2429
|
+
intake: intake.signal,
|
|
2430
|
+
state,
|
|
2431
|
+
lane: index,
|
|
2432
|
+
failures: count,
|
|
2433
|
+
life
|
|
2434
|
+
}))())
|
|
2435
|
+
});
|
|
2436
|
+
remove();
|
|
2437
|
+
if (end.kind === "cut") return;
|
|
2438
|
+
if (end.kind === "done") {
|
|
2439
|
+
failures = 0;
|
|
2440
|
+
await rests.sleep(end.value === "more" ? 0 : sleep, end.value === "more" ? rests.wakes : since);
|
|
2441
|
+
continue;
|
|
2442
|
+
}
|
|
2443
|
+
failures += 1;
|
|
2444
|
+
report(end.kind === "failed" ? end.error : /* @__PURE__ */ new Error(`the service refused the iteration: ${end.cause}`), failures);
|
|
2445
|
+
await rests.backoff(backoffDelay(backoff, failures, random));
|
|
2446
|
+
}
|
|
2447
|
+
};
|
|
2448
|
+
return {
|
|
2449
|
+
knobs: { ...knobs },
|
|
2450
|
+
async start(signal, life) {
|
|
2451
|
+
if (closing !== null) throw new Error(`${OWNER$1}: a closed loop does not start again`);
|
|
2452
|
+
if (started) throw new Error(`${OWNER$1}: the loop started already`);
|
|
2453
|
+
started = true;
|
|
2454
|
+
const make = options.setup;
|
|
2455
|
+
setup = make === void 0 ? Promise.resolve(void 0) : new Promise((resolve) => resolve(make(signal)));
|
|
2456
|
+
try {
|
|
2457
|
+
await setup.then((given) => {
|
|
2458
|
+
state = given;
|
|
2459
|
+
});
|
|
2460
|
+
} catch (error) {
|
|
2461
|
+
await settleWithin(clock, closeBound, teardown());
|
|
2462
|
+
throw error;
|
|
2463
|
+
}
|
|
2464
|
+
life.started.then(() => {
|
|
2465
|
+
if (intake.signal.aborted || life.state !== "running") return;
|
|
2466
|
+
for (let index = 0; index < lanes; index += 1) lane(index, life).catch((error) => report(error, 0));
|
|
2467
|
+
});
|
|
2468
|
+
},
|
|
2469
|
+
stopIntake() {
|
|
2470
|
+
if (intake.signal.aborted) return;
|
|
2471
|
+
intake.abort(/* @__PURE__ */ new Error(`${OWNER$1} stopped its intake`));
|
|
2472
|
+
rests.stop();
|
|
2473
|
+
},
|
|
2474
|
+
drain(deadline, signal) {
|
|
2475
|
+
return running.drain(deadline, signal);
|
|
2476
|
+
},
|
|
2477
|
+
inFlight: () => running.size,
|
|
2478
|
+
wake() {
|
|
2479
|
+
if (intake.signal.aborted) return false;
|
|
2480
|
+
rests.wake();
|
|
2481
|
+
return true;
|
|
2482
|
+
},
|
|
2483
|
+
close() {
|
|
2484
|
+
if (closing !== null) return closing;
|
|
2485
|
+
if (!intake.signal.aborted) intake.abort(/* @__PURE__ */ new Error(`${OWNER$1} closed`));
|
|
2486
|
+
rests.stop();
|
|
2487
|
+
running.abort(/* @__PURE__ */ new Error(`${OWNER$1} closed`));
|
|
2488
|
+
running.clear();
|
|
2489
|
+
closing = (async () => {
|
|
2490
|
+
const began = clock.now();
|
|
2491
|
+
const ended = await settleWithin(clock, closeBound, Promise.allSettled([...pending]));
|
|
2492
|
+
const down = await settleWithin(clock, Math.max(0, closeBound - (clock.now() - began)), teardown());
|
|
2493
|
+
if (ended.kind === "bound") throw new Error(`${OWNER$1}: an iteration did not end in the close_bound of ${closeBound} ms after its abort`);
|
|
2494
|
+
if (down.kind === "failed") throw down.error;
|
|
2495
|
+
if (down.kind === "bound") throw new Error(`${OWNER$1}: the teardown did not end in the close_bound of ${closeBound} ms`);
|
|
2496
|
+
})();
|
|
2497
|
+
return closing;
|
|
2498
|
+
},
|
|
2499
|
+
problems(description) {
|
|
2500
|
+
return boundProblems(OWNER$1, closeBound, bound, description);
|
|
2501
|
+
}
|
|
2502
|
+
};
|
|
2503
|
+
};
|
|
2504
|
+
//#endregion
|
|
2505
|
+
//#region src/queue.ts
|
|
2506
|
+
/**
|
|
2507
|
+
* This function gives the redelivery policy of S6 A11. A poison message gets `ack`. Each other job
|
|
2508
|
+
* goes back on the queue after its first failure, one time. After a later failure, `then` gives the
|
|
2509
|
+
* action: the declared policy of the queue.
|
|
2510
|
+
*/
|
|
2511
|
+
const redeliverOnce = (then, poison) => (failure) => {
|
|
2512
|
+
if (poison?.(failure.error, failure.job) === true) return "ack";
|
|
2513
|
+
if (failure.delivery <= 1) return "requeue";
|
|
2514
|
+
return typeof then === "function" ? then(failure) : then;
|
|
2515
|
+
};
|
|
2516
|
+
const OWNER = "queueDrain";
|
|
2517
|
+
const ACTIONS = [
|
|
2518
|
+
"requeue",
|
|
2519
|
+
"reject",
|
|
2520
|
+
"ack"
|
|
2521
|
+
];
|
|
2522
|
+
const STOPPED = Symbol("the intake stopped");
|
|
2523
|
+
const isAsyncIterable = (value) => value !== null && (typeof value === "object" || typeof value === "function") && typeof value[Symbol.asyncIterator] === "function";
|
|
2524
|
+
/**
|
|
2525
|
+
* This function makes a queue drain as a transport (S6 A11). One taker takes the jobs from the
|
|
2526
|
+
* source while a slot is free, and each job operates in its slot.
|
|
2527
|
+
* - A success gets `ack`. A failure gets the action of the redelivery policy.
|
|
2528
|
+
* - `stopIntake` takes no new job, and a job that the drain took and did not start goes back (`requeue`, `unstarted`).
|
|
2529
|
+
* - `drain` waits for each job in flight until the deadline, and gives it the cooperative abort.
|
|
2530
|
+
* - `close` aborts each job, waits for its end, and closes the source, all in `close_bound`. A job
|
|
2531
|
+
* that does not end in the bound gets `reject`.
|
|
2532
|
+
*/
|
|
2533
|
+
const queueDrain = (knobs, options) => {
|
|
2534
|
+
const poll = millisecondsOf(knobs, "queue_poll_interval", OWNER);
|
|
2535
|
+
const bound = millisecondsOf(knobs, "unit_processing_time", OWNER);
|
|
2536
|
+
const backoff = retryOf(knobs, OWNER);
|
|
2537
|
+
const closeBound = boundedMillisecondsOf(knobs, "close_bound", OWNER);
|
|
2538
|
+
const concurrency = countOf(knobs, "concurrency", OWNER);
|
|
2539
|
+
const { settle } = options;
|
|
2540
|
+
const clock = options.clock ?? realClock;
|
|
2541
|
+
const random = options.random ?? Math.random;
|
|
2542
|
+
const running = flights(clock);
|
|
2543
|
+
const rests = sleeper(clock);
|
|
2544
|
+
const intake = new AbortController();
|
|
2545
|
+
const stopped = new Promise((resolve) => intake.signal.addEventListener("abort", () => resolve(STOPPED), { once: true }));
|
|
2546
|
+
const pending = /* @__PURE__ */ new Set();
|
|
2547
|
+
const active = /* @__PURE__ */ new Set();
|
|
2548
|
+
let refusals = 0;
|
|
2549
|
+
let pausedUntil = Number.NEGATIVE_INFINITY;
|
|
2550
|
+
let cancelSource = () => {};
|
|
2551
|
+
let started = false;
|
|
2552
|
+
let closing = null;
|
|
2553
|
+
const report = (error, job) => {
|
|
2554
|
+
try {
|
|
2555
|
+
options.onError?.(error, job);
|
|
2556
|
+
} catch {}
|
|
2557
|
+
};
|
|
2558
|
+
const keep = (work) => {
|
|
2559
|
+
pending.add(work);
|
|
2560
|
+
const forget = () => {
|
|
2561
|
+
pending.delete(work);
|
|
2562
|
+
};
|
|
2563
|
+
work.then(forget, forget);
|
|
2564
|
+
};
|
|
2565
|
+
/** This function tells the queue the end of a job, one time. A throw of the queue is a failure. */
|
|
2566
|
+
const once = async (unit, tell) => {
|
|
2567
|
+
if (unit.settled) return;
|
|
2568
|
+
unit.settled = true;
|
|
2569
|
+
try {
|
|
2570
|
+
await tell();
|
|
2571
|
+
} catch (error) {
|
|
2572
|
+
report(error, unit.job);
|
|
2573
|
+
}
|
|
2574
|
+
};
|
|
2575
|
+
const handBack = (job) => keep((async () => {
|
|
2576
|
+
try {
|
|
2577
|
+
await settle.requeue(job, "unstarted");
|
|
2578
|
+
} catch (error) {
|
|
2579
|
+
report(error, job);
|
|
2580
|
+
}
|
|
2581
|
+
})());
|
|
2582
|
+
/** This function gives the action of the policy for a failed job. A policy that throws, or that gives no action, rejects the job. */
|
|
2583
|
+
const actionOf = (job, error, delivery) => {
|
|
2584
|
+
try {
|
|
2585
|
+
const action = options.redelivery({
|
|
2586
|
+
job,
|
|
2587
|
+
error,
|
|
2588
|
+
delivery
|
|
2589
|
+
});
|
|
2590
|
+
if (ACTIONS.includes(action)) return action;
|
|
2591
|
+
report(/* @__PURE__ */ new Error(`the redelivery policy gave ${String(action)}, not requeue, reject or ack`), job);
|
|
2592
|
+
} catch (thrown) {
|
|
2593
|
+
report(thrown, job);
|
|
2594
|
+
}
|
|
2595
|
+
return "reject";
|
|
2596
|
+
};
|
|
2597
|
+
const settleEnd = (unit, end, delivery) => {
|
|
2598
|
+
const { job } = unit;
|
|
2599
|
+
if (end.kind === "done") return once(unit, () => settle.ack(job));
|
|
2600
|
+
if (end.kind === "refused") {
|
|
2601
|
+
report(/* @__PURE__ */ new Error(`the service refused the job: ${end.cause}`), job);
|
|
2602
|
+
return once(unit, () => settle.requeue(job, "unstarted"));
|
|
2603
|
+
}
|
|
2604
|
+
if (end.kind === "cut") {
|
|
2605
|
+
if (end.retryable) return once(unit, () => settle.requeue(job, "cut"));
|
|
2606
|
+
const cut = new Error(end.cause);
|
|
2607
|
+
return once(unit, () => settle.reject(job, cut));
|
|
2608
|
+
}
|
|
2609
|
+
const { error } = end;
|
|
2610
|
+
report(error, job);
|
|
2611
|
+
const action = actionOf(job, error, delivery);
|
|
2612
|
+
if (action === "requeue") return once(unit, () => settle.requeue(job, "failed"));
|
|
2613
|
+
return once(unit, () => action === "ack" ? settle.ack(job) : settle.reject(job, error));
|
|
2614
|
+
};
|
|
2615
|
+
/** This function starts one job in a slot. The slot stays in flight until the queue has the end of the job. */
|
|
2616
|
+
const runJob = (job, life) => {
|
|
2617
|
+
const unit = {
|
|
2618
|
+
job,
|
|
2619
|
+
settled: false
|
|
2620
|
+
};
|
|
2621
|
+
active.add(unit);
|
|
2622
|
+
const abort = new AbortController();
|
|
2623
|
+
const remove = running.add(abort);
|
|
2624
|
+
let delivery;
|
|
2625
|
+
try {
|
|
2626
|
+
delivery = settle.delivery(job);
|
|
2627
|
+
} catch (error) {
|
|
2628
|
+
report(error, job);
|
|
2629
|
+
delivery = Number.POSITIVE_INFINITY;
|
|
2630
|
+
}
|
|
2631
|
+
keep((async () => {
|
|
2632
|
+
const end = await runUnit({
|
|
2633
|
+
clock,
|
|
2634
|
+
bound,
|
|
2635
|
+
abort,
|
|
2636
|
+
life,
|
|
2637
|
+
request: options.request,
|
|
2638
|
+
unit: job,
|
|
2639
|
+
work: (signal) => options.handle(job, {
|
|
2640
|
+
signal,
|
|
2641
|
+
intake: intake.signal,
|
|
2642
|
+
delivery,
|
|
2643
|
+
life
|
|
2644
|
+
})
|
|
2645
|
+
});
|
|
2646
|
+
if (end.kind === "refused") {
|
|
2647
|
+
refusals += 1;
|
|
2648
|
+
pausedUntil = clock.now() + backoffDelay(backoff, refusals, random);
|
|
2649
|
+
} else refusals = 0;
|
|
2650
|
+
await settleEnd(unit, end, delivery);
|
|
2651
|
+
})().finally(() => {
|
|
2652
|
+
active.delete(unit);
|
|
2653
|
+
remove();
|
|
2654
|
+
}));
|
|
2655
|
+
};
|
|
2656
|
+
/** This function gives the take of the source. */
|
|
2657
|
+
const takeOf = () => {
|
|
2658
|
+
const source = options.source;
|
|
2659
|
+
if (isAsyncIterable(source)) {
|
|
2660
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
2661
|
+
cancelSource = () => {
|
|
2662
|
+
try {
|
|
2663
|
+
Promise.resolve(iterator.return?.()).catch(() => {});
|
|
2664
|
+
} catch {}
|
|
2665
|
+
};
|
|
2666
|
+
return async () => {
|
|
2667
|
+
const step = await iterator.next();
|
|
2668
|
+
return step.done === true ? { kind: "end" } : {
|
|
2669
|
+
kind: "job",
|
|
2670
|
+
job: step.value
|
|
2671
|
+
};
|
|
2672
|
+
};
|
|
2673
|
+
}
|
|
2674
|
+
const pull = source;
|
|
2675
|
+
return () => new Promise((resolve) => resolve(pull(intake.signal))).then((job) => job === null || job === void 0 ? { kind: "none" } : {
|
|
2676
|
+
kind: "job",
|
|
2677
|
+
job
|
|
2678
|
+
});
|
|
2679
|
+
};
|
|
2680
|
+
/** This function is the taker: it takes a job while a slot is free, until the intake stops or the source ends. */
|
|
2681
|
+
const taker = async (life) => {
|
|
2682
|
+
const take = takeOf();
|
|
2683
|
+
let failures = 0;
|
|
2684
|
+
for (;;) {
|
|
2685
|
+
if (intake.signal.aborted) return;
|
|
2686
|
+
if (running.size >= concurrency) {
|
|
2687
|
+
await running.less();
|
|
2688
|
+
continue;
|
|
2689
|
+
}
|
|
2690
|
+
const pause = pausedUntil - clock.now();
|
|
2691
|
+
if (pause > 0) {
|
|
2692
|
+
await rests.backoff(pause);
|
|
2693
|
+
continue;
|
|
2694
|
+
}
|
|
2695
|
+
const since = rests.wakes;
|
|
2696
|
+
const taking = take().catch((error) => ({
|
|
2697
|
+
kind: "error",
|
|
2698
|
+
error
|
|
2699
|
+
}));
|
|
2700
|
+
const first = await Promise.race([taking, stopped]);
|
|
2701
|
+
if (first === STOPPED || intake.signal.aborted) {
|
|
2702
|
+
if (first === STOPPED) taking.then((late) => {
|
|
2703
|
+
if (late.kind === "job") handBack(late.job);
|
|
2704
|
+
});
|
|
2705
|
+
else if (first.kind === "job") handBack(first.job);
|
|
2706
|
+
return;
|
|
2707
|
+
}
|
|
2708
|
+
if (first.kind === "error") {
|
|
2709
|
+
failures += 1;
|
|
2710
|
+
report(first.error, void 0);
|
|
2711
|
+
await rests.backoff(backoffDelay(backoff, failures, random));
|
|
2712
|
+
continue;
|
|
2713
|
+
}
|
|
2714
|
+
failures = 0;
|
|
2715
|
+
if (first.kind === "end") {
|
|
2716
|
+
report(/* @__PURE__ */ new Error(`the source of ${OWNER} ended while its intake was open: it takes no more jobs`), void 0);
|
|
2717
|
+
return;
|
|
2718
|
+
}
|
|
2719
|
+
if (first.kind === "none") {
|
|
2720
|
+
await rests.sleep(poll, since);
|
|
2721
|
+
continue;
|
|
2722
|
+
}
|
|
2723
|
+
runJob(first.job, life);
|
|
2724
|
+
}
|
|
2725
|
+
};
|
|
2726
|
+
const stopTaking = (cause) => {
|
|
2727
|
+
if (intake.signal.aborted) return;
|
|
2728
|
+
intake.abort(new Error(cause));
|
|
2729
|
+
rests.stop();
|
|
2730
|
+
running.release();
|
|
2731
|
+
cancelSource();
|
|
2732
|
+
};
|
|
2733
|
+
return {
|
|
2734
|
+
knobs: { ...knobs },
|
|
2735
|
+
start(_signal, life) {
|
|
2736
|
+
if (closing !== null) throw new Error(`${OWNER}: a closed drain does not start again`);
|
|
2737
|
+
if (started) throw new Error(`${OWNER}: the drain started already`);
|
|
2738
|
+
started = true;
|
|
2739
|
+
life.started.then(() => {
|
|
2740
|
+
if (intake.signal.aborted || life.state !== "running") return;
|
|
2741
|
+
taker(life).catch((error) => report(error, void 0));
|
|
2742
|
+
});
|
|
2743
|
+
},
|
|
2744
|
+
stopIntake() {
|
|
2745
|
+
stopTaking(`${OWNER} stopped its intake`);
|
|
2746
|
+
},
|
|
2747
|
+
drain(deadline, signal) {
|
|
2748
|
+
return running.drain(deadline, signal);
|
|
2749
|
+
},
|
|
2750
|
+
inFlight: () => running.size,
|
|
2751
|
+
wake() {
|
|
2752
|
+
if (intake.signal.aborted) return false;
|
|
2753
|
+
rests.wake();
|
|
2754
|
+
return true;
|
|
2755
|
+
},
|
|
2756
|
+
close() {
|
|
2757
|
+
if (closing !== null) return closing;
|
|
2758
|
+
stopTaking(`${OWNER} closed`);
|
|
2759
|
+
running.abort(/* @__PURE__ */ new Error(`${OWNER} closed`));
|
|
2760
|
+
running.clear();
|
|
2761
|
+
closing = (async () => {
|
|
2762
|
+
const began = clock.now();
|
|
2763
|
+
const ended = await settleWithin(clock, closeBound, Promise.allSettled([...pending]));
|
|
2764
|
+
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`)));
|
|
2765
|
+
const shut = options.close;
|
|
2766
|
+
const down = await settleWithin(clock, Math.max(0, closeBound - (clock.now() - began)), shut === void 0 ? Promise.resolve() : (async () => shut())());
|
|
2767
|
+
if (ended.kind === "bound") throw new Error(`${OWNER}: a job did not end in the close_bound of ${closeBound} ms after its abort`);
|
|
2768
|
+
if (down.kind === "failed") throw down.error;
|
|
2769
|
+
if (down.kind === "bound") throw new Error(`${OWNER}: the close of the source did not end in the close_bound of ${closeBound} ms`);
|
|
2770
|
+
})();
|
|
2771
|
+
return closing;
|
|
2772
|
+
},
|
|
2773
|
+
problems(description) {
|
|
2774
|
+
return boundProblems(OWNER, closeBound, bound, description);
|
|
2775
|
+
}
|
|
2776
|
+
};
|
|
2777
|
+
};
|
|
2778
|
+
//#endregion
|
|
1455
2779
|
//#region src/machines.ts
|
|
1456
2780
|
/**
|
|
1457
2781
|
* The machines of the runtime as `mark1russell7.machine/1` documents. The documents are the
|
|
@@ -1728,7 +3052,9 @@ const POLLER_MACHINE = {
|
|
|
1728
3052
|
};
|
|
1729
3053
|
/**
|
|
1730
3054
|
* The exit manager is the only part that ends the process. The first exit request starts the
|
|
1731
|
-
* shutdown
|
|
3055
|
+
* shutdown. A later request has no effect, but a repeat signal exits at once when the declaration
|
|
3056
|
+
* says `onRepeatSignal: "exit"`. The exit code is the code of the exit request, else 1 after an
|
|
3057
|
+
* error, else 0.
|
|
1732
3058
|
*/
|
|
1733
3059
|
const EXIT_MACHINE = {
|
|
1734
3060
|
...base("exit", "The exit manager"),
|
|
@@ -1740,23 +3066,35 @@ const EXIT_MACHINE = {
|
|
|
1740
3066
|
events: {
|
|
1741
3067
|
"exit.request": {
|
|
1742
3068
|
kind: "app",
|
|
1743
|
-
note: "SIGINT, SIGTERM, SIGQUIT or shutdown()"
|
|
3069
|
+
note: "SIGINT, SIGTERM, SIGQUIT, a declared process trigger or shutdown()"
|
|
3070
|
+
},
|
|
3071
|
+
"exit.repeat": {
|
|
3072
|
+
kind: "app",
|
|
3073
|
+
note: "a trigger or shutdown() while the shutdown runs"
|
|
1744
3074
|
},
|
|
1745
|
-
"exit.repeat": { kind: "app" },
|
|
1746
3075
|
"startup.fail": { kind: "local" },
|
|
1747
3076
|
error: { kind: "local" },
|
|
1748
3077
|
"shutdown.done": { kind: "local" }
|
|
1749
3078
|
},
|
|
1750
|
-
knobs: {
|
|
3079
|
+
knobs: { on_repeat_signal: {
|
|
3080
|
+
key: "shutdown.onRepeatSignal",
|
|
3081
|
+
scope: "service",
|
|
3082
|
+
unit: null,
|
|
3083
|
+
default: null
|
|
3084
|
+
} },
|
|
1751
3085
|
timers: {},
|
|
1752
3086
|
transitions: [
|
|
1753
3087
|
t("request", "running", "exit.request", "stopping"),
|
|
1754
3088
|
t("startup-fail", "running", "startup.fail", "stopping"),
|
|
1755
3089
|
t("error-running", "running", "error", "running"),
|
|
1756
|
-
t("repeat", "stopping", "exit.repeat", "stopping"),
|
|
3090
|
+
t("repeat", "stopping", "exit.repeat", "stopping", { guard: "a process trigger or shutdown() repeats, or a signal repeats and on_repeat_signal is not exit" }),
|
|
3091
|
+
t("repeat-exit", "stopping", "exit.repeat", "exited", {
|
|
3092
|
+
guard: "a signal repeats and on_repeat_signal is exit",
|
|
3093
|
+
note: "the process exits at once with the code 1"
|
|
3094
|
+
}),
|
|
1757
3095
|
t("startup-fail-stopping", "stopping", "startup.fail", "stopping"),
|
|
1758
3096
|
t("error", "stopping", "error", "stopping"),
|
|
1759
|
-
t("exit", "stopping", "shutdown.done", "exited", { note: "the exit code is 1 after an error, else 0" })
|
|
3097
|
+
t("exit", "stopping", "shutdown.done", "exited", { note: "the exit code is the code of the exit request, else 1 after an error, else 0" })
|
|
1760
3098
|
]
|
|
1761
3099
|
};
|
|
1762
3100
|
/** A watchdog measures how late its own tick comes, and a late tick shows a blocked event loop. */
|
|
@@ -1807,7 +3145,8 @@ const capKnob = (phase) => `${phase.replace(/[^A-Za-z0-9]+/g, "_")}_cap`;
|
|
|
1807
3145
|
/**
|
|
1808
3146
|
* This function gives the drain of a declaration as a machine. It has one state for each declared
|
|
1809
3147
|
* phase, in its sequence, and each state has a timer at its cap. The drain also has the cooperative abort: the
|
|
1810
|
-
* cap of the drain less the cooperative window
|
|
3148
|
+
* cap of the drain less the cooperative window. With the floor `half`, it is half that cap at the
|
|
3149
|
+
* least. With the floor `none`, it is 0 at the least.
|
|
1811
3150
|
*/
|
|
1812
3151
|
const drainMachine = (description) => {
|
|
1813
3152
|
const phases = description.shutdown.phases;
|
|
@@ -1835,9 +3174,10 @@ const drainMachine = (description) => {
|
|
|
1835
3174
|
cancels: ["phase.done"],
|
|
1836
3175
|
fires: "phase.cap"
|
|
1837
3176
|
};
|
|
3177
|
+
const floor = description.shutdown.cooperativeFloor === "none" ? "0" : "drain_cap / 2";
|
|
1838
3178
|
if (phases.some((phase) => phase.name === "drain")) timers["cooperative_abort"] = {
|
|
1839
3179
|
duration: {
|
|
1840
|
-
expr:
|
|
3180
|
+
expr: `max(drain_cap - cooperative_window, ${floor})`,
|
|
1841
3181
|
unit: "s"
|
|
1842
3182
|
},
|
|
1843
3183
|
arms: ["enter:drain"],
|
|
@@ -1885,6 +3225,6 @@ const MACHINES = {
|
|
|
1885
3225
|
watchdog: WATCHDOG_MACHINE
|
|
1886
3226
|
};
|
|
1887
3227
|
//#endregion
|
|
1888
|
-
export { EXIT_MACHINE, EXIT_SIGNALS, FORMAT, GATE_MACHINE, LATCH_MACHINE, MACHINES, POLLER_MACHINE, SERVICE_RULES, STAGES, WATCHDOG_MACHINE, WINDOW_MACHINE, asDebounce, asWindow, backoffBand, backoffDelay, backoffProblems, capKnob, cooperativeAbortAt, detectionTime, drainMachine, evaluate, failSpan, heartbeat, heartbeatDetection, inequalities, lint, mayReissue, nodeProcess, platformClock, realClock, recoverSpan, replay, service, serviceBindings, stageRank, symbolsOf, verdictOf, watchdog };
|
|
3228
|
+
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, isMessageShape, kubernetes, lint, localDev, matchesMessage, mayReissue, nodeProcess, platformClock, processPortOf, queueDrain, realClock, recoverSpan, redeliverOnce, replay, service, serviceBindings, stageRank, symbolsOf, verdictOf, watchdog, workerLoop };
|
|
1889
3229
|
|
|
1890
3230
|
//# sourceMappingURL=index.js.map
|