@mark1russell7/service 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +268 -18
- package/build/builder-B_q-JeW8.d.ts +197 -0
- package/build/http.d.ts +277 -0
- package/build/http.js +789 -0
- package/build/http.js.map +1 -0
- package/build/index.d.ts +15 -5
- package/build/index.js +745 -159
- package/build/index.js.map +1 -1
- package/build/{builder-DnfUDW9u.d.ts → lifecycle-DIj4YGmO.d.ts} +238 -181
- package/build/redis.d.ts +2 -1
- package/build/testing.d.ts +92 -4
- package/build/testing.js +302 -8
- package/build/testing.js.map +1 -1
- package/package.json +10 -3
package/build/index.js
CHANGED
|
@@ -497,101 +497,398 @@ const EXIT_SIGNALS = [
|
|
|
497
497
|
"SIGTERM",
|
|
498
498
|
"SIGQUIT"
|
|
499
499
|
];
|
|
500
|
-
/**
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
500
|
+
/** These are the process triggers that a declaration can opt in to. */
|
|
501
|
+
const PROCESS_TRIGGERS = ["stdin-close", "ipc-close"];
|
|
502
|
+
/** This function tells if a number is an exit code of a process: a whole number from 0 to 255. */
|
|
503
|
+
const isExitCode = (code) => Number.isInteger(code) && code >= 0 && code <= 255;
|
|
504
|
+
/** This function tells if a trigger is an exit signal. A repeat signal obeys `onRepeatSignal`. */
|
|
505
|
+
const isExitSignal = (trigger) => EXIT_SIGNALS.includes(trigger);
|
|
506
|
+
/** This function adds a handler to some events of an emitter. The handler operates one time at most. */
|
|
507
|
+
const once = (emitter, events, handler) => {
|
|
508
|
+
let called = false;
|
|
509
|
+
const fire = () => {
|
|
510
|
+
if (called) return;
|
|
511
|
+
called = true;
|
|
512
|
+
handler();
|
|
513
|
+
};
|
|
514
|
+
for (const event of events) emitter.on(event, fire);
|
|
515
|
+
return () => {
|
|
516
|
+
for (const event of events) emitter.off(event, fire);
|
|
517
|
+
};
|
|
518
|
+
};
|
|
519
|
+
/** This function gives a promise that ends when the data before it in a stream is written. */
|
|
520
|
+
const written = (stream) => new Promise((resolve) => {
|
|
521
|
+
if (stream === void 0 || stream.destroyed === true || stream.writable === false) {
|
|
522
|
+
resolve();
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
try {
|
|
526
|
+
stream.write("", () => resolve());
|
|
527
|
+
} catch {
|
|
528
|
+
resolve();
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
/**
|
|
532
|
+
* This function makes a process port on a host. `find` gives the host at each use, or undefined
|
|
533
|
+
* when the platform has no process. Then the use throws.
|
|
534
|
+
*/
|
|
535
|
+
const processPortOf = (find) => {
|
|
536
|
+
const hostProcess = () => {
|
|
537
|
+
const host = find();
|
|
512
538
|
if (host === void 0) throw new Error("no process: give the runtime a process port");
|
|
513
|
-
host
|
|
539
|
+
return host;
|
|
540
|
+
};
|
|
541
|
+
return {
|
|
542
|
+
on(trigger, handler) {
|
|
543
|
+
const host = hostProcess();
|
|
544
|
+
if (trigger === "stdin-close") {
|
|
545
|
+
const input = host.stdin;
|
|
546
|
+
if (input === void 0) return () => {};
|
|
547
|
+
if (input.readableEnded === true) {
|
|
548
|
+
let live = true;
|
|
549
|
+
queueMicrotask(() => {
|
|
550
|
+
if (live) handler();
|
|
551
|
+
});
|
|
552
|
+
return () => {
|
|
553
|
+
live = false;
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
return once(input, ["end", "close"], handler);
|
|
557
|
+
}
|
|
558
|
+
if (trigger === "ipc-close") return once(host, ["disconnect"], handler);
|
|
559
|
+
host.on(trigger, handler);
|
|
560
|
+
return () => {
|
|
561
|
+
host.off(trigger, handler);
|
|
562
|
+
};
|
|
563
|
+
},
|
|
564
|
+
async flush() {
|
|
565
|
+
const host = hostProcess();
|
|
566
|
+
await Promise.all([written(host.stdout), written(host.stderr)]);
|
|
567
|
+
},
|
|
568
|
+
exit(code) {
|
|
569
|
+
hostProcess().exit(code);
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
};
|
|
573
|
+
/** This port is the process of Node. */
|
|
574
|
+
const nodeProcess = processPortOf(() => globalThis.process);
|
|
575
|
+
//#endregion
|
|
576
|
+
//#region src/routes.ts
|
|
577
|
+
const NAME = /^[A-Za-z_][A-Za-z0-9_]*/;
|
|
578
|
+
/** This function gives the problems of the syntax of a pattern: an empty pattern, a part without a name, or a name two times. */
|
|
579
|
+
const patternProblems = (owner, pattern) => {
|
|
580
|
+
const problems = [];
|
|
581
|
+
if (pattern.length === 0) problems.push(`${owner} has an empty route pattern`);
|
|
582
|
+
const names = /* @__PURE__ */ new Set();
|
|
583
|
+
for (let at = 0; at < pattern.length; at += 1) {
|
|
584
|
+
if (pattern[at] !== ":") continue;
|
|
585
|
+
const name = NAME.exec(pattern.slice(at + 1))?.[0];
|
|
586
|
+
if (name === void 0) {
|
|
587
|
+
problems.push(`${owner}'s pattern ${pattern} has a ":" without a name at ${at}`);
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
if (names.has(name)) problems.push(`${owner}'s pattern ${pattern} names :${name} two times`);
|
|
591
|
+
names.add(name);
|
|
592
|
+
at += name.length;
|
|
514
593
|
}
|
|
594
|
+
return problems;
|
|
595
|
+
};
|
|
596
|
+
/** This function gives the problems of a pattern and of each of its operations, which `describe()` refuses. */
|
|
597
|
+
const routePatternProblems = (owner, declared) => {
|
|
598
|
+
const problems = patternProblems(owner, declared.pattern);
|
|
599
|
+
const names = Object.keys(declared.operations);
|
|
600
|
+
if (names.length === 0) problems.push(`${owner}'s pattern ${declared.pattern} declares no operation`);
|
|
601
|
+
for (const name of names) problems.push(...operationProblems(owner, `${declared.pattern} (${name})`, declared.operations[name]));
|
|
602
|
+
return problems;
|
|
603
|
+
};
|
|
604
|
+
/** This function compiles a pattern to a matcher. The match must cover the whole route. */
|
|
605
|
+
const compilePattern = (pattern) => {
|
|
606
|
+
const kinds = [];
|
|
607
|
+
let source = "^";
|
|
608
|
+
for (let at = 0; at < pattern.length; at += 1) {
|
|
609
|
+
const char = pattern[at];
|
|
610
|
+
if (char === "*") {
|
|
611
|
+
source += "([\\s\\S]*)";
|
|
612
|
+
kinds.push({ kind: "wildcard" });
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
if (char === ":") {
|
|
616
|
+
const name = NAME.exec(pattern.slice(at + 1))?.[0];
|
|
617
|
+
if (name !== void 0) {
|
|
618
|
+
source += "([^/]+)";
|
|
619
|
+
kinds.push({
|
|
620
|
+
kind: "param",
|
|
621
|
+
name
|
|
622
|
+
});
|
|
623
|
+
at += name.length;
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
source += char.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
628
|
+
}
|
|
629
|
+
const matcher = new RegExp(`${source}$`);
|
|
630
|
+
return {
|
|
631
|
+
pattern,
|
|
632
|
+
match(route) {
|
|
633
|
+
const found = matcher.exec(route);
|
|
634
|
+
if (found === null) return null;
|
|
635
|
+
const params = {};
|
|
636
|
+
const wildcards = [];
|
|
637
|
+
kinds.forEach((part, index) => {
|
|
638
|
+
const text = found[index + 1] ?? "";
|
|
639
|
+
if (part.kind === "param") params[part.name] = text;
|
|
640
|
+
else wildcards.push(text);
|
|
641
|
+
});
|
|
642
|
+
return {
|
|
643
|
+
params,
|
|
644
|
+
wildcards
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
};
|
|
515
648
|
};
|
|
516
649
|
//#endregion
|
|
517
650
|
//#region src/requests.ts
|
|
651
|
+
const isAsyncIterable = (value) => value !== null && (typeof value === "object" || typeof value === "function") && typeof value[Symbol.asyncIterator] === "function";
|
|
652
|
+
const isPromiseLike = (value) => value !== null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
|
|
653
|
+
/** This function gives a stream of events from a function that gives the next event. */
|
|
654
|
+
const streamOf = (next, stop) => {
|
|
655
|
+
let chain = Promise.resolve();
|
|
656
|
+
const iterator = {
|
|
657
|
+
next() {
|
|
658
|
+
const step = chain.then(next).then((event) => event === null ? {
|
|
659
|
+
value: void 0,
|
|
660
|
+
done: true
|
|
661
|
+
} : {
|
|
662
|
+
value: event,
|
|
663
|
+
done: false
|
|
664
|
+
});
|
|
665
|
+
chain = step.catch(() => {});
|
|
666
|
+
return step;
|
|
667
|
+
},
|
|
668
|
+
return() {
|
|
669
|
+
stop();
|
|
670
|
+
return Promise.resolve({
|
|
671
|
+
value: void 0,
|
|
672
|
+
done: true
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
return {
|
|
677
|
+
[Symbol.asyncIterator]: () => iterator,
|
|
678
|
+
async collect() {
|
|
679
|
+
const items = [];
|
|
680
|
+
for (;;) {
|
|
681
|
+
const step = await iterator.next();
|
|
682
|
+
if (step.done === true) throw new Error("the stream ended without its end");
|
|
683
|
+
if (step.value.kind === "item") items.push(step.value.value);
|
|
684
|
+
else return {
|
|
685
|
+
items,
|
|
686
|
+
end: step.value
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
};
|
|
518
692
|
/**
|
|
519
693
|
* 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.
|
|
694
|
+
* for a controller at this time, or null when it admits it. `choices` gives the function of each
|
|
695
|
+
* route pattern, by controller and by pattern.
|
|
521
696
|
*/
|
|
522
|
-
const tracker = (controllers, clock, refuses) => {
|
|
523
|
-
const byName = new Map(controllers.map((controller) => [controller.name,
|
|
697
|
+
const tracker = (controllers, clock, refuses, choices = /* @__PURE__ */ new Map()) => {
|
|
698
|
+
const byName = new Map(controllers.map((controller) => [controller.name, {
|
|
699
|
+
controller,
|
|
700
|
+
patterns: (controller.patterns ?? []).map((declared) => ({
|
|
701
|
+
declared,
|
|
702
|
+
compiled: compilePattern(declared.pattern)
|
|
703
|
+
}))
|
|
704
|
+
}]));
|
|
524
705
|
const flights = /* @__PURE__ */ new Set();
|
|
525
706
|
const idle = /* @__PURE__ */ new Set();
|
|
526
707
|
let intake = true;
|
|
708
|
+
/** This function gives the operation of a request: its exact route, else the first pattern that matches it. */
|
|
709
|
+
const operationOf = (controller, route, detail) => {
|
|
710
|
+
const held = byName.get(controller);
|
|
711
|
+
if (held === void 0) throw new Error(`there is no controller ${controller}`);
|
|
712
|
+
const exact = held.controller.routes[route];
|
|
713
|
+
if (exact !== void 0) return exact;
|
|
714
|
+
for (const { declared, compiled } of held.patterns) {
|
|
715
|
+
const found = compiled.match(route);
|
|
716
|
+
if (found === null) continue;
|
|
717
|
+
const names = Object.keys(declared.operations);
|
|
718
|
+
const choose = choices.get(controller)?.get(declared.pattern);
|
|
719
|
+
const name = choose === void 0 ? names[0] : choose({
|
|
720
|
+
controller,
|
|
721
|
+
route,
|
|
722
|
+
...found,
|
|
723
|
+
detail
|
|
724
|
+
});
|
|
725
|
+
const operation = declared.operations[name];
|
|
726
|
+
if (operation === void 0) throw new Error(`the pattern ${declared.pattern} of ${controller} declares no operation ${String(name)} for ${route}`);
|
|
727
|
+
return operation;
|
|
728
|
+
}
|
|
729
|
+
throw new Error(`controller ${controller} does not declare the route ${route}`);
|
|
730
|
+
};
|
|
527
731
|
const settle = (flight) => {
|
|
528
|
-
flights.delete(flight);
|
|
732
|
+
if (!flights.delete(flight)) return;
|
|
529
733
|
if (flights.size === 0) for (const done of [...idle]) done();
|
|
530
734
|
};
|
|
531
|
-
/** This function
|
|
532
|
-
const
|
|
533
|
-
const
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
735
|
+
/** This function starts the work of an admitted request, and gives its stream. */
|
|
736
|
+
const open = (controller, operation, work) => {
|
|
737
|
+
const started = clock.now();
|
|
738
|
+
const abort = new AbortController();
|
|
739
|
+
let delivered = 0;
|
|
740
|
+
let end = null;
|
|
741
|
+
let ended = false;
|
|
742
|
+
let returned = false;
|
|
743
|
+
let source = null;
|
|
744
|
+
let unary = null;
|
|
745
|
+
let interrupt = () => {};
|
|
746
|
+
const interrupted = new Promise((resolve) => {
|
|
747
|
+
interrupt = () => resolve("interrupted");
|
|
748
|
+
});
|
|
749
|
+
const cutEnd = (cause) => {
|
|
750
|
+
const since = quantity(Math.max(0, clock.now() - started) / 1e3, "s");
|
|
751
|
+
const retryable = mayReissue(operation, since);
|
|
752
|
+
return {
|
|
753
|
+
kind: "cut",
|
|
754
|
+
status: delivered > 0 ? null : retryable ? 503 : 500,
|
|
755
|
+
retryable,
|
|
756
|
+
cause,
|
|
757
|
+
delivered
|
|
758
|
+
};
|
|
541
759
|
};
|
|
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
|
-
|
|
760
|
+
/** This function ends the work of the request: the end is known, and the request leaves the flights. */
|
|
761
|
+
const finish = (given) => {
|
|
762
|
+
if (end !== null) return;
|
|
763
|
+
end = given;
|
|
764
|
+
settle(flight);
|
|
765
|
+
interrupt();
|
|
766
|
+
};
|
|
767
|
+
/** This function ends the work after a throw: a failure after the shutdown aborted it is a cut. */
|
|
768
|
+
const failed = (error) => finish(abort.signal.aborted ? cutEnd("the shutdown aborted it, and it failed") : {
|
|
769
|
+
kind: "failed",
|
|
770
|
+
error,
|
|
771
|
+
delivered
|
|
772
|
+
});
|
|
773
|
+
const closeSource = () => {
|
|
774
|
+
if (source === null || returned) return;
|
|
775
|
+
returned = true;
|
|
776
|
+
try {
|
|
777
|
+
Promise.resolve(source.return?.()).catch(() => {});
|
|
778
|
+
} catch {}
|
|
779
|
+
};
|
|
780
|
+
const flight = {
|
|
781
|
+
controller,
|
|
782
|
+
cooperative: () => {
|
|
783
|
+
abort.abort(/* @__PURE__ */ new Error("the cooperative abort of the drain"));
|
|
784
|
+
closeSource();
|
|
785
|
+
},
|
|
786
|
+
cut: (cause) => {
|
|
787
|
+
abort.abort(new Error(cause));
|
|
788
|
+
finish(cutEnd(cause));
|
|
789
|
+
closeSource();
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
flights.add(flight);
|
|
793
|
+
/** This function takes the result of the work: a stream, or one value. */
|
|
794
|
+
const take = (result) => {
|
|
795
|
+
if (end !== null) return;
|
|
796
|
+
if (isAsyncIterable(result)) {
|
|
797
|
+
source = result[Symbol.asyncIterator]();
|
|
798
|
+
if (abort.signal.aborted) closeSource();
|
|
799
|
+
} else {
|
|
800
|
+
unary = { value: result };
|
|
801
|
+
settle(flight);
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
let ready;
|
|
805
|
+
try {
|
|
806
|
+
const result = work(abort.signal);
|
|
807
|
+
if (isAsyncIterable(result) || !isPromiseLike(result)) {
|
|
808
|
+
take(result);
|
|
809
|
+
ready = Promise.resolve();
|
|
810
|
+
} else ready = Promise.resolve(result).then(take, failed);
|
|
811
|
+
} catch (error) {
|
|
812
|
+
failed(error);
|
|
813
|
+
ready = Promise.resolve();
|
|
814
|
+
}
|
|
815
|
+
const next = async () => {
|
|
816
|
+
if (ended) return null;
|
|
817
|
+
if (end === null) await Promise.race([ready, interrupted]);
|
|
818
|
+
if (end === null && unary !== null) {
|
|
819
|
+
const { value } = unary;
|
|
820
|
+
unary = null;
|
|
821
|
+
delivered = 1;
|
|
822
|
+
end = {
|
|
823
|
+
kind: "done",
|
|
824
|
+
delivered
|
|
568
825
|
};
|
|
569
|
-
|
|
570
|
-
|
|
826
|
+
return {
|
|
827
|
+
kind: "item",
|
|
828
|
+
value
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
if (end === null && source !== null) {
|
|
832
|
+
const pending = source.next();
|
|
833
|
+
let step;
|
|
571
834
|
try {
|
|
572
|
-
|
|
835
|
+
step = await Promise.race([pending, interrupted]);
|
|
573
836
|
} catch (error) {
|
|
574
|
-
|
|
837
|
+
failed(error);
|
|
838
|
+
step = "interrupted";
|
|
575
839
|
}
|
|
576
|
-
|
|
577
|
-
if (
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
840
|
+
if (step !== "interrupted") {
|
|
841
|
+
if (step.done !== true) {
|
|
842
|
+
delivered += 1;
|
|
843
|
+
return {
|
|
844
|
+
kind: "item",
|
|
845
|
+
value: step.value
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
finish(abort.signal.aborted ? cutEnd("the shutdown ended the stream") : {
|
|
581
849
|
kind: "done",
|
|
582
|
-
|
|
850
|
+
delivered
|
|
583
851
|
});
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
ended = true;
|
|
855
|
+
return end;
|
|
856
|
+
};
|
|
857
|
+
const stop = () => {
|
|
858
|
+
if (end === null) {
|
|
859
|
+
abort.abort(/* @__PURE__ */ new Error("the reader stopped the stream"));
|
|
860
|
+
finish({
|
|
861
|
+
kind: "done",
|
|
862
|
+
delivered
|
|
593
863
|
});
|
|
594
|
-
|
|
864
|
+
closeSource();
|
|
865
|
+
}
|
|
866
|
+
ended = true;
|
|
867
|
+
};
|
|
868
|
+
return streamOf(next, stop);
|
|
869
|
+
};
|
|
870
|
+
return {
|
|
871
|
+
request(controller, route, work, options = {}) {
|
|
872
|
+
const operation = operationOf(controller, route, options.detail);
|
|
873
|
+
const refusal = intake ? refuses(controller) : "the service stopped its intake";
|
|
874
|
+
if (refusal !== null) {
|
|
875
|
+
let given = false;
|
|
876
|
+
const refused = {
|
|
877
|
+
kind: "refused",
|
|
878
|
+
status: 503,
|
|
879
|
+
retryable: true,
|
|
880
|
+
cause: refusal,
|
|
881
|
+
delivered: 0
|
|
882
|
+
};
|
|
883
|
+
return streamOf(() => {
|
|
884
|
+
if (given) return Promise.resolve(null);
|
|
885
|
+
given = true;
|
|
886
|
+
return Promise.resolve(refused);
|
|
887
|
+
}, () => {
|
|
888
|
+
given = true;
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
return open(controller, operation, work);
|
|
595
892
|
},
|
|
596
893
|
inFlight(controller) {
|
|
597
894
|
let count = 0;
|
|
@@ -602,7 +899,7 @@ const tracker = (controllers, clock, refuses) => {
|
|
|
602
899
|
intake = false;
|
|
603
900
|
},
|
|
604
901
|
cooperativeAbort() {
|
|
605
|
-
for (const flight of flights) flight.
|
|
902
|
+
for (const flight of [...flights]) flight.cooperative();
|
|
606
903
|
},
|
|
607
904
|
drain(deadline) {
|
|
608
905
|
if (flights.size === 0) return Promise.resolve();
|
|
@@ -617,10 +914,7 @@ const tracker = (controllers, clock, refuses) => {
|
|
|
617
914
|
};
|
|
618
915
|
idle.add(done);
|
|
619
916
|
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
|
-
}
|
|
917
|
+
for (const flight of [...flights]) flight.cut("the drain reached its deadline");
|
|
624
918
|
done();
|
|
625
919
|
});
|
|
626
920
|
});
|
|
@@ -641,7 +935,9 @@ const tracker = (controllers, clock, refuses) => {
|
|
|
641
935
|
* - `close transports`: each transport closes.
|
|
642
936
|
* - `stop the poller`: each check and watchdog stops. Then the signal of the service aborts.
|
|
643
937
|
* - `tear down dependencies`: each dependency closes, in the reverse sequence of its connection.
|
|
644
|
-
* - `close the health server`: the probes stop
|
|
938
|
+
* - `close the health server`: the probes stop.
|
|
939
|
+
* - `flush the output`: the work of the owners of this phase ends, and then the process port
|
|
940
|
+
* writes the buffered output. Without this phase, the process exits without a wait.
|
|
645
941
|
*/
|
|
646
942
|
const STAGES = [
|
|
647
943
|
"fail readiness",
|
|
@@ -650,7 +946,8 @@ const STAGES = [
|
|
|
650
946
|
"close transports",
|
|
651
947
|
"stop the poller",
|
|
652
948
|
"tear down dependencies",
|
|
653
|
-
"close the health server"
|
|
949
|
+
"close the health server",
|
|
950
|
+
"flush the output"
|
|
654
951
|
];
|
|
655
952
|
/** This function gives the place of a stage in the sequence, or -1 for a phase of another name. */
|
|
656
953
|
const stageRank = (name) => STAGES.indexOf(name);
|
|
@@ -658,12 +955,12 @@ const stageRank = (name) => STAGES.indexOf(name);
|
|
|
658
955
|
const ABORT_AFTER = stageRank("stop the poller");
|
|
659
956
|
/**
|
|
660
957
|
* 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.
|
|
958
|
+
* is the cap less the cooperative window. With the floor `half`, it is not less than half the cap,
|
|
959
|
+
* and a window without a bound gives half the cap. With the floor `none`, it is not less than 0.
|
|
663
960
|
*/
|
|
664
|
-
const cooperativeAbortAt = (cap, window) => {
|
|
961
|
+
const cooperativeAbortAt = (cap, window, floor = "half") => {
|
|
665
962
|
const before = window.value === "infinite" ? Number.POSITIVE_INFINITY : valueIn(window, "ms");
|
|
666
|
-
return Math.max(cap - before, cap / 2);
|
|
963
|
+
return Math.max(cap - before, floor === "none" ? 0 : cap / 2);
|
|
667
964
|
};
|
|
668
965
|
const message = (error) => error instanceof Error ? error.message : String(error);
|
|
669
966
|
/**
|
|
@@ -688,12 +985,12 @@ const lifecycleProblems = (description, components) => {
|
|
|
688
985
|
};
|
|
689
986
|
/**
|
|
690
987
|
* 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
|
|
988
|
+
* first, then each dependency, then each transport, and the startup gate last. A trigger during the
|
|
692
989
|
* 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.
|
|
990
|
+
* sequence, each one capped. Only the lifecycle ends the process. Its exit code is the code of the
|
|
991
|
+
* exit request, else 1 after an error, else 0.
|
|
695
992
|
*/
|
|
696
|
-
const lifecycle = (description, runtime, components, options = {}) => {
|
|
993
|
+
const lifecycle = (description, runtime, components, options = {}, bind = () => {}) => {
|
|
697
994
|
const clock = options.clock ?? realClock;
|
|
698
995
|
const port = options.process ?? nodeProcess;
|
|
699
996
|
const record = (machine, from, to, on, detail) => options.record?.({
|
|
@@ -707,8 +1004,11 @@ const lifecycle = (description, runtime, components, options = {}) => {
|
|
|
707
1004
|
});
|
|
708
1005
|
const service = new AbortController();
|
|
709
1006
|
const errors = [];
|
|
1007
|
+
const exitCodes = description.shutdown.exitCodes ?? {};
|
|
1008
|
+
const repeatPolicy = description.shutdown.onRepeatSignal;
|
|
710
1009
|
let state = "starting";
|
|
711
1010
|
let exitCode = null;
|
|
1011
|
+
let requested;
|
|
712
1012
|
let intakeStopped = false;
|
|
713
1013
|
let shutdownStarted = false;
|
|
714
1014
|
let failedStartup = false;
|
|
@@ -719,14 +1019,14 @@ const lifecycle = (description, runtime, components, options = {}) => {
|
|
|
719
1019
|
};
|
|
720
1020
|
const fail = (cause) => {
|
|
721
1021
|
errors.push(cause);
|
|
722
|
-
record("exit", exitState, exitState, "error", cause);
|
|
1022
|
+
if (exitState !== "exited") record("exit", exitState, exitState, "error", cause);
|
|
723
1023
|
};
|
|
724
1024
|
const requests = tracker(description.controllers, clock, (controller) => {
|
|
725
1025
|
if (state === "starting") return "the service has not started";
|
|
726
1026
|
if (state === "exited") return "the service has exited";
|
|
727
1027
|
if (state === "draining") return null;
|
|
728
1028
|
return runtime.verdict().controllers[controller] === true ? null : `controller ${controller} is not ready`;
|
|
729
|
-
});
|
|
1029
|
+
}, components.choices);
|
|
730
1030
|
let finish = () => {};
|
|
731
1031
|
const done = new Promise((resolve) => {
|
|
732
1032
|
finish = resolve;
|
|
@@ -734,45 +1034,36 @@ const lifecycle = (description, runtime, components, options = {}) => {
|
|
|
734
1034
|
let healthListening = false;
|
|
735
1035
|
const connected = [];
|
|
736
1036
|
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
|
-
started(`${transport.kind} ${transport.name}`);
|
|
1037
|
+
let startupEnded = () => {};
|
|
1038
|
+
const startup = new Promise((resolve) => {
|
|
1039
|
+
startupEnded = resolve;
|
|
1040
|
+
});
|
|
1041
|
+
let offs = [];
|
|
1042
|
+
/** This function ends the process with a code, one time, and removes each handler of a trigger. */
|
|
1043
|
+
const end = (code, on) => {
|
|
1044
|
+
state = "exited";
|
|
1045
|
+
exitCode = code;
|
|
1046
|
+
for (const off of offs) off();
|
|
1047
|
+
offs = [];
|
|
1048
|
+
moveExit("exited", on, `exit ${code}`);
|
|
1049
|
+
finish(code);
|
|
1050
|
+
port.exit(code);
|
|
1051
|
+
};
|
|
1052
|
+
/** This function receives a trigger of the process: the first one starts the shutdown. */
|
|
1053
|
+
const triggered = (trigger) => {
|
|
1054
|
+
if (exitState === "exited") return;
|
|
1055
|
+
if (shutdownStarted || exitState === "stopping") {
|
|
1056
|
+
if (isExitSignal(trigger) && repeatPolicy === "exit") {
|
|
1057
|
+
fail(`${trigger} came again while the shutdown ran, and the declaration exits at once`);
|
|
1058
|
+
end(1, "exit.repeat");
|
|
1059
|
+
return;
|
|
761
1060
|
}
|
|
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);
|
|
1061
|
+
record("exit", exitState, exitState, "exit.repeat", trigger);
|
|
1062
|
+
return;
|
|
771
1063
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
});
|
|
1064
|
+
const code = exitCodes[trigger];
|
|
1065
|
+
shutdown(trigger, code === void 0 ? {} : { code });
|
|
1066
|
+
};
|
|
776
1067
|
/** This function does the work of one phase, until its cap. It gives true when the work ended in time. */
|
|
777
1068
|
const runPhase = async (phase, hold, work) => {
|
|
778
1069
|
const cap = valueIn(phase.cap, "ms");
|
|
@@ -802,14 +1093,22 @@ const lifecycle = (description, runtime, components, options = {}) => {
|
|
|
802
1093
|
capped.abort(/* @__PURE__ */ new Error(`the phase ${phase.name} reached its cap`));
|
|
803
1094
|
return pending === 0;
|
|
804
1095
|
};
|
|
805
|
-
async function shutdown(cause = "the service asked for it") {
|
|
806
|
-
if (
|
|
1096
|
+
async function shutdown(cause = "the service asked for it", request = {}) {
|
|
1097
|
+
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`);
|
|
1098
|
+
if (exitState === "exited") return done;
|
|
1099
|
+
if (request.code !== void 0 && requested === void 0) requested = request.code;
|
|
1100
|
+
if (shutdownStarted) {
|
|
807
1101
|
if (exitState === "stopping") record("exit", exitState, exitState, "exit.repeat", cause);
|
|
808
1102
|
return done;
|
|
809
1103
|
}
|
|
810
1104
|
shutdownStarted = true;
|
|
811
1105
|
if (exitState === "running") moveExit("stopping", "exit.request", cause);
|
|
812
1106
|
await startup;
|
|
1107
|
+
if (exitState === "exited") {
|
|
1108
|
+
runtime.stop();
|
|
1109
|
+
components.stopPollers();
|
|
1110
|
+
return exitCode;
|
|
1111
|
+
}
|
|
813
1112
|
state = "draining";
|
|
814
1113
|
runtime.drain();
|
|
815
1114
|
let drainState = "running";
|
|
@@ -825,10 +1124,12 @@ const lifecycle = (description, runtime, components, options = {}) => {
|
|
|
825
1124
|
const phases = description.shutdown.phases;
|
|
826
1125
|
let on = "exit.request";
|
|
827
1126
|
for (const phase of phases) {
|
|
1127
|
+
if (exitState === "exited") break;
|
|
828
1128
|
const rank = stageRank(phase.name);
|
|
829
1129
|
moveDrain(phase.name, on);
|
|
830
1130
|
if (rank > ABORT_AFTER) abortService();
|
|
831
1131
|
const work = [];
|
|
1132
|
+
const owned = components.phaseWork.get(phase.name) ?? [];
|
|
832
1133
|
if (phase.name === "stop intake") {
|
|
833
1134
|
work.push(() => {
|
|
834
1135
|
intakeStopped = true;
|
|
@@ -840,7 +1141,7 @@ const lifecycle = (description, runtime, components, options = {}) => {
|
|
|
840
1141
|
if (phase.name === "drain") {
|
|
841
1142
|
const cap = valueIn(phase.cap, "ms");
|
|
842
1143
|
const cooperative = new AbortController();
|
|
843
|
-
const fireAt = cooperativeAbortAt(cap, description.shutdown.cooperativeWindow);
|
|
1144
|
+
const fireAt = cooperativeAbortAt(cap, description.shutdown.cooperativeWindow, description.shutdown.cooperativeFloor);
|
|
844
1145
|
cancelAbort = clock.after(fireAt, () => {
|
|
845
1146
|
cooperative.abort(/* @__PURE__ */ new Error("the cooperative abort of the drain"));
|
|
846
1147
|
requests.cooperativeAbort();
|
|
@@ -861,26 +1162,33 @@ const lifecycle = (description, runtime, components, options = {}) => {
|
|
|
861
1162
|
const server = components.healthServer;
|
|
862
1163
|
work.push(() => server.close());
|
|
863
1164
|
}
|
|
864
|
-
|
|
1165
|
+
if (phase.name === "flush the output") work.push(async (context) => {
|
|
1166
|
+
await Promise.all(owned.map(async (each) => {
|
|
1167
|
+
try {
|
|
1168
|
+
await each(context);
|
|
1169
|
+
} catch (error) {
|
|
1170
|
+
fail(`the phase ${phase.name} failed: ${message(error)}`);
|
|
1171
|
+
}
|
|
1172
|
+
}));
|
|
1173
|
+
await port.flush?.();
|
|
1174
|
+
});
|
|
1175
|
+
else for (const each of owned) work.push(each);
|
|
865
1176
|
const inTime = await runPhase(phase, phase.name === "fail readiness", work);
|
|
866
1177
|
cancelAbort();
|
|
867
1178
|
if (!inTime) fail(`the phase ${phase.name} did not end in its cap of ${valueIn(phase.cap, "s")} s`);
|
|
868
1179
|
on = inTime ? "phase.done" : "phase.cap";
|
|
869
1180
|
}
|
|
870
|
-
moveDrain("done", on);
|
|
1181
|
+
if (exitState !== "exited") moveDrain("done", on);
|
|
871
1182
|
abortService();
|
|
872
1183
|
runtime.stop();
|
|
873
1184
|
components.stopPollers();
|
|
874
1185
|
if (!intakeStopped) requests.stopIntake();
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
finish(exitCode);
|
|
880
|
-
port.exit(exitCode);
|
|
881
|
-
return exitCode;
|
|
1186
|
+
if (exitState === "exited") return exitCode;
|
|
1187
|
+
const code = requested !== void 0 && requested !== 0 ? requested : errors.length > 0 ? 1 : 0;
|
|
1188
|
+
end(code, "shutdown.done");
|
|
1189
|
+
return code;
|
|
882
1190
|
}
|
|
883
|
-
|
|
1191
|
+
const life = {
|
|
884
1192
|
health: runtime,
|
|
885
1193
|
get state() {
|
|
886
1194
|
return state;
|
|
@@ -895,10 +1203,51 @@ const lifecycle = (description, runtime, components, options = {}) => {
|
|
|
895
1203
|
done,
|
|
896
1204
|
signal: service.signal,
|
|
897
1205
|
shutdown,
|
|
898
|
-
request: (controller, route, work) => requests.request(controller, route, work),
|
|
1206
|
+
request: (controller, route, work, request) => requests.request(controller, route, work, request),
|
|
899
1207
|
inFlight: (controller) => requests.inFlight(controller),
|
|
900
1208
|
keepAlive: () => state === "running" && runtime.verdict().ready
|
|
901
1209
|
};
|
|
1210
|
+
bind(life);
|
|
1211
|
+
offs = [...EXIT_SIGNALS, ...description.shutdown.triggers ?? []].map((trigger) => port.on(trigger, () => triggered(trigger)));
|
|
1212
|
+
(async () => {
|
|
1213
|
+
const started = (what) => record("gate", "starting", "starting", "component.started", what);
|
|
1214
|
+
try {
|
|
1215
|
+
runtime.beginStartup();
|
|
1216
|
+
if (components.healthServer !== null) {
|
|
1217
|
+
await components.healthServer.listen(runtime);
|
|
1218
|
+
healthListening = true;
|
|
1219
|
+
started("the health server");
|
|
1220
|
+
}
|
|
1221
|
+
for (const dependency of description.dependencies) {
|
|
1222
|
+
const handle = components.dependencies.get(dependency.name);
|
|
1223
|
+
if (handle === void 0) continue;
|
|
1224
|
+
await handle.connect(service.signal);
|
|
1225
|
+
connected.push([dependency.name, handle]);
|
|
1226
|
+
started(`dependency ${dependency.name}`);
|
|
1227
|
+
}
|
|
1228
|
+
for (const transport of description.transports) {
|
|
1229
|
+
const impl = components.transports.get(transport.name);
|
|
1230
|
+
await impl.start(service.signal, life);
|
|
1231
|
+
startedTransports.push([transport.name, impl]);
|
|
1232
|
+
started(`${transport.kind} ${transport.name}`);
|
|
1233
|
+
}
|
|
1234
|
+
await runtime.warmUp();
|
|
1235
|
+
if (state === "starting") state = "running";
|
|
1236
|
+
} catch (error) {
|
|
1237
|
+
const cause = `the startup failed: ${message(error)}`;
|
|
1238
|
+
failedStartup = true;
|
|
1239
|
+
runtime.failStartup(cause);
|
|
1240
|
+
fail(cause);
|
|
1241
|
+
if (exitState === "running") moveExit("stopping", "startup.fail", cause);
|
|
1242
|
+
else if (exitState === "stopping") record("exit", exitState, exitState, "startup.fail", cause);
|
|
1243
|
+
} finally {
|
|
1244
|
+
startupEnded();
|
|
1245
|
+
}
|
|
1246
|
+
})();
|
|
1247
|
+
startup.then(() => {
|
|
1248
|
+
if (failedStartup && !shutdownStarted && exitState !== "exited") shutdown("the startup failed");
|
|
1249
|
+
});
|
|
1250
|
+
return life;
|
|
902
1251
|
};
|
|
903
1252
|
//#endregion
|
|
904
1253
|
//#region src/watchdog.ts
|
|
@@ -967,12 +1316,25 @@ const timingProblems = (policy, timing) => {
|
|
|
967
1316
|
const negative = (amount) => amount !== void 0 && (!isTime(amount) || valueIn(amount, "s") < 0);
|
|
968
1317
|
return negative(timing.failAfter) || negative(timing.recoverAfter) ? [`the ${policy} window is not a time of zero or more`] : [];
|
|
969
1318
|
};
|
|
1319
|
+
/** This function gives the problems of the rules of the shutdown: an unknown name, or an exit code that a process cannot give. */
|
|
1320
|
+
const shutdownProblems = (floor, repeat, triggers, exitCodes) => {
|
|
1321
|
+
const problems = [];
|
|
1322
|
+
if (floor !== void 0 && floor !== "half" && floor !== "none") problems.push(`the cooperative floor ${String(floor)} is not "half" or "none"`);
|
|
1323
|
+
if (repeat !== void 0 && repeat !== "ignore" && repeat !== "exit") problems.push(`the policy of a repeat signal ${String(repeat)} is not "ignore" or "exit"`);
|
|
1324
|
+
for (const trigger of triggers) if (!PROCESS_TRIGGERS.includes(trigger)) problems.push(`the trigger ${String(trigger)} is not a process trigger (${PROCESS_TRIGGERS.join(", ")})`);
|
|
1325
|
+
for (const [trigger, code] of Object.entries(exitCodes)) {
|
|
1326
|
+
if (!(EXIT_SIGNALS.includes(trigger) || triggers.includes(trigger))) problems.push(`the exit code of ${trigger} names no exit signal and no declared trigger`);
|
|
1327
|
+
if (code === void 0 || !isExitCode(code)) problems.push(`the exit code of ${trigger} is ${String(code)}, not a whole number from 0 to 255`);
|
|
1328
|
+
}
|
|
1329
|
+
return problems;
|
|
1330
|
+
};
|
|
970
1331
|
/** This function starts the composition of a service. */
|
|
971
1332
|
const service = (name) => {
|
|
972
1333
|
if (name.trim().length === 0) throw new Error("a service needs a name");
|
|
973
1334
|
const transports = [];
|
|
974
1335
|
const dependencies = [];
|
|
975
1336
|
const controllers = [];
|
|
1337
|
+
const choices = /* @__PURE__ */ new Map();
|
|
976
1338
|
const checks = [];
|
|
977
1339
|
const runs = /* @__PURE__ */ new Map();
|
|
978
1340
|
const couplings = [];
|
|
@@ -981,11 +1343,16 @@ const service = (name) => {
|
|
|
981
1343
|
let liveness;
|
|
982
1344
|
let startup;
|
|
983
1345
|
let cooperative;
|
|
1346
|
+
let floor;
|
|
1347
|
+
let repeat;
|
|
1348
|
+
const triggers = [];
|
|
1349
|
+
const exitCodes = {};
|
|
984
1350
|
let started = false;
|
|
985
1351
|
let partial = false;
|
|
986
1352
|
let runtime;
|
|
987
1353
|
let live;
|
|
988
1354
|
let running;
|
|
1355
|
+
let life;
|
|
989
1356
|
const transportImpls = /* @__PURE__ */ new Map();
|
|
990
1357
|
const handles = /* @__PURE__ */ new Map();
|
|
991
1358
|
const phaseWork = /* @__PURE__ */ new Map();
|
|
@@ -1054,6 +1421,9 @@ const service = (name) => {
|
|
|
1054
1421
|
get health() {
|
|
1055
1422
|
return live ?? null;
|
|
1056
1423
|
},
|
|
1424
|
+
get lifecycle() {
|
|
1425
|
+
return life ?? null;
|
|
1426
|
+
},
|
|
1057
1427
|
dependency(entry, spec, handle) {
|
|
1058
1428
|
once(dependencies, entry, "dependency");
|
|
1059
1429
|
if (handle !== void 0 && !started) handles.set(entry, handle);
|
|
@@ -1078,7 +1448,8 @@ const service = (name) => {
|
|
|
1078
1448
|
const held = {
|
|
1079
1449
|
name: entry,
|
|
1080
1450
|
requires: [],
|
|
1081
|
-
routes: {}
|
|
1451
|
+
routes: {},
|
|
1452
|
+
patterns: []
|
|
1082
1453
|
};
|
|
1083
1454
|
controllers.push(held);
|
|
1084
1455
|
const handle = {
|
|
@@ -1092,6 +1463,21 @@ const service = (name) => {
|
|
|
1092
1463
|
if (held.routes[served] !== void 0) throw new Error(`${entry} already serves ${served}`);
|
|
1093
1464
|
held.routes[served] = { ...operation };
|
|
1094
1465
|
return handle;
|
|
1466
|
+
},
|
|
1467
|
+
pattern(pattern, operations, operationOf) {
|
|
1468
|
+
composing(`${entry}'s route patterns`);
|
|
1469
|
+
if (held.patterns.some((each) => each.pattern === pattern)) throw new Error(`${entry} already has the route pattern ${pattern}`);
|
|
1470
|
+
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`);
|
|
1471
|
+
held.patterns.push({
|
|
1472
|
+
pattern,
|
|
1473
|
+
operations: Object.fromEntries(Object.entries(operations).map(([name, operation]) => [name, { ...operation }]))
|
|
1474
|
+
});
|
|
1475
|
+
if (operationOf !== void 0) {
|
|
1476
|
+
const mine = choices.get(entry) ?? /* @__PURE__ */ new Map();
|
|
1477
|
+
mine.set(pattern, operationOf);
|
|
1478
|
+
choices.set(entry, mine);
|
|
1479
|
+
}
|
|
1480
|
+
return handle;
|
|
1095
1481
|
}
|
|
1096
1482
|
};
|
|
1097
1483
|
return handle;
|
|
@@ -1169,6 +1555,40 @@ const service = (name) => {
|
|
|
1169
1555
|
cooperative = window;
|
|
1170
1556
|
return builder;
|
|
1171
1557
|
},
|
|
1558
|
+
cooperativeFloor(given) {
|
|
1559
|
+
composing("the cooperative floor");
|
|
1560
|
+
floor = given;
|
|
1561
|
+
return builder;
|
|
1562
|
+
},
|
|
1563
|
+
onRepeatSignal(policy) {
|
|
1564
|
+
composing("the policy of a repeat signal");
|
|
1565
|
+
repeat = policy;
|
|
1566
|
+
return builder;
|
|
1567
|
+
},
|
|
1568
|
+
trigger(...given) {
|
|
1569
|
+
composing("a trigger");
|
|
1570
|
+
for (const each of given) if (!triggers.includes(each)) triggers.push(each);
|
|
1571
|
+
return builder;
|
|
1572
|
+
},
|
|
1573
|
+
exitCode(trigger, code) {
|
|
1574
|
+
composing(`the exit code of ${trigger}`);
|
|
1575
|
+
exitCodes[trigger] = code;
|
|
1576
|
+
return builder;
|
|
1577
|
+
},
|
|
1578
|
+
profile(given) {
|
|
1579
|
+
composing("a profile");
|
|
1580
|
+
if (given.readiness !== void 0) builder.readiness(given.readiness);
|
|
1581
|
+
if (given.liveness !== void 0) builder.liveness(given.liveness);
|
|
1582
|
+
if (given.startup !== void 0) builder.startup(given.startup);
|
|
1583
|
+
for (const coupling of given.couplings ?? []) builder.couple(coupling);
|
|
1584
|
+
for (const each of given.phases ?? []) builder.phase(each.name, each.cap);
|
|
1585
|
+
if (given.cooperativeWindow !== void 0) builder.cooperativeWindow(given.cooperativeWindow);
|
|
1586
|
+
if (given.cooperativeFloor !== void 0) builder.cooperativeFloor(given.cooperativeFloor);
|
|
1587
|
+
if (given.onRepeatSignal !== void 0) builder.onRepeatSignal(given.onRepeatSignal);
|
|
1588
|
+
if (given.triggers !== void 0) builder.trigger(...given.triggers);
|
|
1589
|
+
for (const [trigger, code] of Object.entries(given.exitCodes ?? {})) if (code !== void 0) builder.exitCode(trigger, code);
|
|
1590
|
+
return builder;
|
|
1591
|
+
},
|
|
1172
1592
|
describe() {
|
|
1173
1593
|
const missing = [];
|
|
1174
1594
|
if (readiness === void 0) missing.push("readiness (svc.readiness)");
|
|
@@ -1176,6 +1596,7 @@ const service = (name) => {
|
|
|
1176
1596
|
if (startup === void 0) missing.push("a startup budget (svc.startup)");
|
|
1177
1597
|
if (phases.length === 0) missing.push("its shutdown phases (svc.phase)");
|
|
1178
1598
|
if (cooperative === void 0) missing.push("a cooperative window, or no bound (svc.cooperativeWindow)");
|
|
1599
|
+
if (repeat === void 0) missing.push(`what a repeat signal does, "ignore" or "exit" (svc.onRepeatSignal)`);
|
|
1179
1600
|
if (missing.length > 0) throw new Error(`${name} does not declare ${missing.join("; ")}: the runtime has no defaults`);
|
|
1180
1601
|
const known = new Set(dependencies.map((entry) => entry.name));
|
|
1181
1602
|
const problems = [];
|
|
@@ -1195,16 +1616,22 @@ const service = (name) => {
|
|
|
1195
1616
|
for (const controller of controllers) {
|
|
1196
1617
|
for (const needed of controller.requires) if (!known.has(needed)) problems.push(`controller ${controller.name} requires ${needed}, which is not a dependency`);
|
|
1197
1618
|
for (const [route, operation] of Object.entries(controller.routes)) problems.push(...operationProblems(`controller ${controller.name}`, route, operation));
|
|
1619
|
+
for (const declared of controller.patterns) problems.push(...routePatternProblems(`controller ${controller.name}`, declared));
|
|
1198
1620
|
}
|
|
1199
1621
|
for (const dependency of dependencies) for (const [name, operation] of Object.entries(dependency.operations)) problems.push(...operationProblems(`dependency ${dependency.name}`, name, operation));
|
|
1200
1622
|
for (const phase of phases) if (!isTime(phase.cap)) problems.push(`phase ${phase.name}'s cap is not a time`);
|
|
1623
|
+
problems.push(...shutdownProblems(floor, repeat, triggers, exitCodes));
|
|
1201
1624
|
const ranked = phases.map((phase) => phase.name).filter((each) => stageRank(each) >= 0);
|
|
1202
1625
|
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
1626
|
if (problems.length > 0) throw new Error(`${name}: ${problems.join("; ")}`);
|
|
1204
1627
|
const controllersOut = controllers.map((entry) => ({
|
|
1205
1628
|
name: entry.name,
|
|
1206
1629
|
requires: [...entry.requires],
|
|
1207
|
-
routes: { ...entry.routes }
|
|
1630
|
+
routes: { ...entry.routes },
|
|
1631
|
+
...entry.patterns.length === 0 ? {} : { patterns: entry.patterns.map((each) => ({
|
|
1632
|
+
pattern: each.pattern,
|
|
1633
|
+
operations: { ...each.operations }
|
|
1634
|
+
})) }
|
|
1208
1635
|
}));
|
|
1209
1636
|
return {
|
|
1210
1637
|
format: FORMAT,
|
|
@@ -1232,7 +1659,11 @@ const service = (name) => {
|
|
|
1232
1659
|
couplings: couplings.map((entry) => ({ ...entry })),
|
|
1233
1660
|
shutdown: {
|
|
1234
1661
|
phases: phases.map((entry) => ({ ...entry })),
|
|
1235
|
-
cooperativeWindow: cooperative
|
|
1662
|
+
cooperativeWindow: cooperative,
|
|
1663
|
+
...floor === void 0 ? {} : { cooperativeFloor: floor },
|
|
1664
|
+
onRepeatSignal: repeat,
|
|
1665
|
+
...triggers.length === 0 ? {} : { triggers: [...triggers] },
|
|
1666
|
+
...Object.keys(exitCodes).length === 0 ? {} : { exitCodes: { ...exitCodes } }
|
|
1236
1667
|
}
|
|
1237
1668
|
};
|
|
1238
1669
|
},
|
|
@@ -1253,7 +1684,6 @@ const service = (name) => {
|
|
|
1253
1684
|
run(options = {}) {
|
|
1254
1685
|
if (running !== void 0) return running;
|
|
1255
1686
|
if (runtime !== void 0) return Promise.reject(/* @__PURE__ */ new Error(`${name} started its health only: run() starts the whole lifecycle`));
|
|
1256
|
-
let life;
|
|
1257
1687
|
try {
|
|
1258
1688
|
const description = builder.describe();
|
|
1259
1689
|
const components = {
|
|
@@ -1261,6 +1691,7 @@ const service = (name) => {
|
|
|
1261
1691
|
dependencies: handles,
|
|
1262
1692
|
healthServer: probes,
|
|
1263
1693
|
phaseWork,
|
|
1694
|
+
choices,
|
|
1264
1695
|
stopPollers: () => {}
|
|
1265
1696
|
};
|
|
1266
1697
|
const problems = lifecycleProblems(description, components);
|
|
@@ -1268,14 +1699,16 @@ const service = (name) => {
|
|
|
1268
1699
|
const made = makeRuntime(options);
|
|
1269
1700
|
started = true;
|
|
1270
1701
|
live = made.runtime;
|
|
1271
|
-
|
|
1702
|
+
const ran = lifecycle(made.description, made.runtime, {
|
|
1272
1703
|
...components,
|
|
1273
1704
|
stopPollers: made.stopPollers
|
|
1274
|
-
}, options)
|
|
1705
|
+
}, options, (given) => {
|
|
1706
|
+
life = given;
|
|
1707
|
+
});
|
|
1708
|
+
running = ran.started.then(() => ran);
|
|
1275
1709
|
} catch (error) {
|
|
1276
1710
|
return Promise.reject(error);
|
|
1277
1711
|
}
|
|
1278
|
-
running = life.started.then(() => life);
|
|
1279
1712
|
return running;
|
|
1280
1713
|
}
|
|
1281
1714
|
};
|
|
@@ -1425,10 +1858,30 @@ const dependencyBindings = (dependency, scenario) => {
|
|
|
1425
1858
|
return bind;
|
|
1426
1859
|
};
|
|
1427
1860
|
const SCOPED = /@(client|dependency|server)$/;
|
|
1428
|
-
/** This
|
|
1861
|
+
/** This identifier is the rule of the findings about a field that the declaration does not give. */
|
|
1862
|
+
const DECLARATION_RULE = "declaration";
|
|
1863
|
+
/**
|
|
1864
|
+
* This function gives a finding for each field of the shutdown that a description does not give.
|
|
1865
|
+
* `describe()` refuses such a declaration. A description from another source, for example the
|
|
1866
|
+
* JSON of 0.4, can lack the field. The verdict is `depends`, and `unbound` names the field.
|
|
1867
|
+
*/
|
|
1868
|
+
const declarationFindings = (svc) => svc.shutdown.onRepeatSignal === void 0 ? [{
|
|
1869
|
+
rule: DECLARATION_RULE,
|
|
1870
|
+
scope: svc.name,
|
|
1871
|
+
verdict: "depends",
|
|
1872
|
+
statement: "the declaration tells what a signal does while the shutdown runs: ignore or exit",
|
|
1873
|
+
window: "a repeat signal has no declared effect",
|
|
1874
|
+
evaluated: [],
|
|
1875
|
+
unbound: ["shutdown.onRepeatSignal"]
|
|
1876
|
+
}] : [];
|
|
1877
|
+
/**
|
|
1878
|
+
* This function applies each service rule to a declaration and a scenario: one time for the
|
|
1879
|
+
* service, and one time for each client. It also gives a finding for each necessary field that the
|
|
1880
|
+
* description does not give (`DECLARATION_RULE`).
|
|
1881
|
+
*/
|
|
1429
1882
|
const lint = (svc, scenario, rules) => {
|
|
1430
1883
|
const service = serviceBindings(svc, scenario).bind;
|
|
1431
|
-
const findings = [];
|
|
1884
|
+
const findings = [...declarationFindings(svc)];
|
|
1432
1885
|
for (const rule of rules) {
|
|
1433
1886
|
if (!SERVICE_RULES.includes(rule.id)) continue;
|
|
1434
1887
|
const symbols = symbolsOf(rule);
|
|
@@ -1452,6 +1905,123 @@ const lint = (svc, scenario, rules) => {
|
|
|
1452
1905
|
return findings;
|
|
1453
1906
|
};
|
|
1454
1907
|
//#endregion
|
|
1908
|
+
//#region src/profiles.ts
|
|
1909
|
+
const s = (value) => ({
|
|
1910
|
+
value,
|
|
1911
|
+
unit: "s"
|
|
1912
|
+
});
|
|
1913
|
+
const phase = (name, cap) => ({
|
|
1914
|
+
name,
|
|
1915
|
+
cap
|
|
1916
|
+
});
|
|
1917
|
+
/**
|
|
1918
|
+
* This profile is for a service on the computer of a developer. No load balancer routes to it,
|
|
1919
|
+
* thus it has no readiness hold. Its caps sum to 10 s, and a second signal exits at once. It
|
|
1920
|
+
* declares no checks, thus the caller declares readiness, liveness and the startup budget.
|
|
1921
|
+
*/
|
|
1922
|
+
const localDev = {
|
|
1923
|
+
phases: [
|
|
1924
|
+
phase("stop intake", s(1)),
|
|
1925
|
+
phase("drain", s(4)),
|
|
1926
|
+
phase("close transports", s(1)),
|
|
1927
|
+
phase("stop the poller", s(1)),
|
|
1928
|
+
phase("tear down dependencies", s(1)),
|
|
1929
|
+
phase("close the health server", s(1)),
|
|
1930
|
+
phase("flush the output", s(1))
|
|
1931
|
+
],
|
|
1932
|
+
cooperativeWindow: s(2),
|
|
1933
|
+
cooperativeFloor: "half",
|
|
1934
|
+
onRepeatSignal: "exit"
|
|
1935
|
+
};
|
|
1936
|
+
/**
|
|
1937
|
+
* This profile is for a background host on the computer of a developer, under a supervisor or a
|
|
1938
|
+
* command that stops it. It has the caps of `localDev`. A repeat signal has no effect, thus the
|
|
1939
|
+
* host ends its drain. It has no process trigger: a background host has no parent on stdin.
|
|
1940
|
+
*/
|
|
1941
|
+
const daemon = {
|
|
1942
|
+
phases: localDev.phases.map((each) => ({ ...each })),
|
|
1943
|
+
cooperativeWindow: localDev.cooperativeWindow,
|
|
1944
|
+
cooperativeFloor: "half",
|
|
1945
|
+
onRepeatSignal: "ignore"
|
|
1946
|
+
};
|
|
1947
|
+
/**
|
|
1948
|
+
* This profile is for a small process: a command that runs one time, or a child process on stdio.
|
|
1949
|
+
* It has no checks and no probes, and its policies gate nothing. The cooperative abort occurs at the
|
|
1950
|
+
* start of the drain, thus a first Ctrl-C stops the work at once. A second signal exits at once.
|
|
1951
|
+
* The exit codes tell the shell how the process stopped: 130 for SIGINT, 143 for SIGTERM and 131
|
|
1952
|
+
* for SIGQUIT. A stdio child also declares `triggers: ["stdin-close"]`.
|
|
1953
|
+
*/
|
|
1954
|
+
const ephemeral = {
|
|
1955
|
+
readiness: {
|
|
1956
|
+
type: "window",
|
|
1957
|
+
failAfter: s(0)
|
|
1958
|
+
},
|
|
1959
|
+
liveness: {
|
|
1960
|
+
type: "window",
|
|
1961
|
+
failAfter: s(0)
|
|
1962
|
+
},
|
|
1963
|
+
startup: s(10),
|
|
1964
|
+
phases: [
|
|
1965
|
+
phase("stop intake", s(1)),
|
|
1966
|
+
phase("drain", s(2)),
|
|
1967
|
+
phase("close transports", s(1)),
|
|
1968
|
+
phase("stop the poller", s(1)),
|
|
1969
|
+
phase("tear down dependencies", s(2)),
|
|
1970
|
+
phase("flush the output", s(1))
|
|
1971
|
+
],
|
|
1972
|
+
cooperativeWindow: s(2),
|
|
1973
|
+
cooperativeFloor: "none",
|
|
1974
|
+
onRepeatSignal: "exit",
|
|
1975
|
+
exitCodes: {
|
|
1976
|
+
SIGINT: 130,
|
|
1977
|
+
SIGTERM: 143,
|
|
1978
|
+
SIGQUIT: 131
|
|
1979
|
+
}
|
|
1980
|
+
};
|
|
1981
|
+
/** These are the parts of the budget of each stage in the profile `kubernetes`, in thirtieths. */
|
|
1982
|
+
const KUBERNETES_SHARES = [
|
|
1983
|
+
["fail readiness", 2],
|
|
1984
|
+
["stop intake", 5],
|
|
1985
|
+
["drain", 15],
|
|
1986
|
+
["close transports", 2],
|
|
1987
|
+
["stop the poller", 1],
|
|
1988
|
+
["tear down dependencies", 2],
|
|
1989
|
+
["close the health server", 1]
|
|
1990
|
+
];
|
|
1991
|
+
/** This part of the budget is the cooperative window, in thirtieths. */
|
|
1992
|
+
const KUBERNETES_WINDOW = 5;
|
|
1993
|
+
/**
|
|
1994
|
+
* This function gives the profile of a pod in Kubernetes. The budget is the grace less the preStop
|
|
1995
|
+
* sleep: the time from SIGTERM to SIGKILL. Each stage gets a part of the budget, rounded down to
|
|
1996
|
+
* the millisecond.
|
|
1997
|
+
* - The parts in thirtieths are 2, 5, 15, 2, 1, 2 and 1, in the sequence of the stages. Thus 2
|
|
1998
|
+
* thirtieths of the budget stay as a margin before SIGKILL.
|
|
1999
|
+
* - The cooperative window is 5 thirtieths of the budget.
|
|
2000
|
+
* - The kubelet sends SIGTERM one time, thus a repeat signal has no effect.
|
|
2001
|
+
*
|
|
2002
|
+
* At the defaults of Kubernetes (a grace of 30 s and no preStop hook), the caps are 2, 5, 15, 2, 1,
|
|
2003
|
+
* 2 and 1 s. Thus the sum of the caps and the preStop sleep fits in the grace (O10). The window is
|
|
2004
|
+
* shorter than the drain, which is shorter than the budget (O16).
|
|
2005
|
+
*/
|
|
2006
|
+
const kubernetes = (pod) => {
|
|
2007
|
+
const time = (amount, what) => {
|
|
2008
|
+
if (!convertible(amount.unit, "s")) throw new Error(`the ${what} of the pod is not a time`);
|
|
2009
|
+
return valueIn(amount, "ms");
|
|
2010
|
+
};
|
|
2011
|
+
const grace = time(pod.grace, "grace");
|
|
2012
|
+
const preStop = time(pod.preStop, "preStop sleep");
|
|
2013
|
+
if (preStop < 0) throw new Error("the preStop sleep of the pod is less than 0");
|
|
2014
|
+
const budget = grace - preStop;
|
|
2015
|
+
if (!(budget > 0)) throw new Error(`the grace of the pod (${grace} ms) is not longer than its preStop sleep (${preStop} ms)`);
|
|
2016
|
+
const part = (thirtieths) => s(Math.floor(budget * thirtieths / 30) / 1e3);
|
|
2017
|
+
return {
|
|
2018
|
+
phases: KUBERNETES_SHARES.map(([name, thirtieths]) => phase(name, part(thirtieths))),
|
|
2019
|
+
cooperativeWindow: part(KUBERNETES_WINDOW),
|
|
2020
|
+
cooperativeFloor: "half",
|
|
2021
|
+
onRepeatSignal: "ignore"
|
|
2022
|
+
};
|
|
2023
|
+
};
|
|
2024
|
+
//#endregion
|
|
1455
2025
|
//#region src/machines.ts
|
|
1456
2026
|
/**
|
|
1457
2027
|
* The machines of the runtime as `mark1russell7.machine/1` documents. The documents are the
|
|
@@ -1728,7 +2298,9 @@ const POLLER_MACHINE = {
|
|
|
1728
2298
|
};
|
|
1729
2299
|
/**
|
|
1730
2300
|
* The exit manager is the only part that ends the process. The first exit request starts the
|
|
1731
|
-
* shutdown
|
|
2301
|
+
* shutdown. A later request has no effect, but a repeat signal exits at once when the declaration
|
|
2302
|
+
* says `onRepeatSignal: "exit"`. The exit code is the code of the exit request, else 1 after an
|
|
2303
|
+
* error, else 0.
|
|
1732
2304
|
*/
|
|
1733
2305
|
const EXIT_MACHINE = {
|
|
1734
2306
|
...base("exit", "The exit manager"),
|
|
@@ -1740,23 +2312,35 @@ const EXIT_MACHINE = {
|
|
|
1740
2312
|
events: {
|
|
1741
2313
|
"exit.request": {
|
|
1742
2314
|
kind: "app",
|
|
1743
|
-
note: "SIGINT, SIGTERM, SIGQUIT or shutdown()"
|
|
2315
|
+
note: "SIGINT, SIGTERM, SIGQUIT, a declared process trigger or shutdown()"
|
|
2316
|
+
},
|
|
2317
|
+
"exit.repeat": {
|
|
2318
|
+
kind: "app",
|
|
2319
|
+
note: "a trigger or shutdown() while the shutdown runs"
|
|
1744
2320
|
},
|
|
1745
|
-
"exit.repeat": { kind: "app" },
|
|
1746
2321
|
"startup.fail": { kind: "local" },
|
|
1747
2322
|
error: { kind: "local" },
|
|
1748
2323
|
"shutdown.done": { kind: "local" }
|
|
1749
2324
|
},
|
|
1750
|
-
knobs: {
|
|
2325
|
+
knobs: { on_repeat_signal: {
|
|
2326
|
+
key: "shutdown.onRepeatSignal",
|
|
2327
|
+
scope: "service",
|
|
2328
|
+
unit: null,
|
|
2329
|
+
default: null
|
|
2330
|
+
} },
|
|
1751
2331
|
timers: {},
|
|
1752
2332
|
transitions: [
|
|
1753
2333
|
t("request", "running", "exit.request", "stopping"),
|
|
1754
2334
|
t("startup-fail", "running", "startup.fail", "stopping"),
|
|
1755
2335
|
t("error-running", "running", "error", "running"),
|
|
1756
|
-
t("repeat", "stopping", "exit.repeat", "stopping"),
|
|
2336
|
+
t("repeat", "stopping", "exit.repeat", "stopping", { guard: "a process trigger or shutdown() repeats, or a signal repeats and on_repeat_signal is not exit" }),
|
|
2337
|
+
t("repeat-exit", "stopping", "exit.repeat", "exited", {
|
|
2338
|
+
guard: "a signal repeats and on_repeat_signal is exit",
|
|
2339
|
+
note: "the process exits at once with the code 1"
|
|
2340
|
+
}),
|
|
1757
2341
|
t("startup-fail-stopping", "stopping", "startup.fail", "stopping"),
|
|
1758
2342
|
t("error", "stopping", "error", "stopping"),
|
|
1759
|
-
t("exit", "stopping", "shutdown.done", "exited", { note: "the exit code is 1 after an error, else 0" })
|
|
2343
|
+
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
2344
|
]
|
|
1761
2345
|
};
|
|
1762
2346
|
/** A watchdog measures how late its own tick comes, and a late tick shows a blocked event loop. */
|
|
@@ -1807,7 +2391,8 @@ const capKnob = (phase) => `${phase.replace(/[^A-Za-z0-9]+/g, "_")}_cap`;
|
|
|
1807
2391
|
/**
|
|
1808
2392
|
* This function gives the drain of a declaration as a machine. It has one state for each declared
|
|
1809
2393
|
* 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
|
|
2394
|
+
* cap of the drain less the cooperative window. With the floor `half`, it is half that cap at the
|
|
2395
|
+
* least. With the floor `none`, it is 0 at the least.
|
|
1811
2396
|
*/
|
|
1812
2397
|
const drainMachine = (description) => {
|
|
1813
2398
|
const phases = description.shutdown.phases;
|
|
@@ -1835,9 +2420,10 @@ const drainMachine = (description) => {
|
|
|
1835
2420
|
cancels: ["phase.done"],
|
|
1836
2421
|
fires: "phase.cap"
|
|
1837
2422
|
};
|
|
2423
|
+
const floor = description.shutdown.cooperativeFloor === "none" ? "0" : "drain_cap / 2";
|
|
1838
2424
|
if (phases.some((phase) => phase.name === "drain")) timers["cooperative_abort"] = {
|
|
1839
2425
|
duration: {
|
|
1840
|
-
expr:
|
|
2426
|
+
expr: `max(drain_cap - cooperative_window, ${floor})`,
|
|
1841
2427
|
unit: "s"
|
|
1842
2428
|
},
|
|
1843
2429
|
arms: ["enter:drain"],
|
|
@@ -1885,6 +2471,6 @@ const MACHINES = {
|
|
|
1885
2471
|
watchdog: WATCHDOG_MACHINE
|
|
1886
2472
|
};
|
|
1887
2473
|
//#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 };
|
|
2474
|
+
export { DECLARATION_RULE, EXIT_MACHINE, EXIT_SIGNALS, FORMAT, GATE_MACHINE, LATCH_MACHINE, MACHINES, POLLER_MACHINE, PROCESS_TRIGGERS, SERVICE_RULES, STAGES, WATCHDOG_MACHINE, WINDOW_MACHINE, asDebounce, asWindow, backoffBand, backoffDelay, backoffProblems, capKnob, cooperativeAbortAt, daemon, detectionTime, drainMachine, ephemeral, evaluate, failSpan, heartbeat, heartbeatDetection, inequalities, isExitCode, isExitSignal, kubernetes, lint, localDev, mayReissue, nodeProcess, platformClock, processPortOf, realClock, recoverSpan, replay, service, serviceBindings, stageRank, symbolsOf, verdictOf, watchdog };
|
|
1889
2475
|
|
|
1890
2476
|
//# sourceMappingURL=index.js.map
|