@mark1russell7/service 0.5.1 → 0.5.3
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 +62 -0
- package/build/{builder-QVxu1WcL.d.ts → builder-DyDB7zM9.d.ts} +9 -2
- package/build/http.d.ts +9 -1
- package/build/http.js +25 -1
- package/build/http.js.map +1 -1
- package/build/index.d.ts +30 -8
- package/build/index.js +144 -5
- package/build/index.js.map +1 -1
- package/build/{lifecycle-DtJZW09J.d.ts → lifecycle-Chu9Pri2.d.ts} +23 -2
- package/build/limiter-DzTyosou.d.ts +72 -0
- package/build/limiter-wv0uXDk9.js +126 -0
- package/build/limiter-wv0uXDk9.js.map +1 -0
- package/build/limiter.d.ts +3 -0
- package/build/limiter.js +3 -0
- package/build/policy-CgS4y94U.js +187 -0
- package/build/policy-CgS4y94U.js.map +1 -0
- package/build/policy-G4QJ1ZZq.d.ts +176 -0
- package/build/redis.d.ts +2 -2
- package/build/testing.d.ts +2 -2
- package/package.json +9 -4
package/build/index.js
CHANGED
|
@@ -2,7 +2,9 @@ import { platformClock, realClock } from "./clock.js";
|
|
|
2
2
|
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
|
+
import { a as limiterProblems, i as limitRule, n as freeAt, o as longestRetryAfter, r as idleSpan, s as retryAfterSeconds, t as decide } from "./policy-CgS4y94U.js";
|
|
5
6
|
import { backoffBand, backoffDelay, backoffProblems } from "./backoff.js";
|
|
7
|
+
import { n as memoryStore, t as keyedLimiter } from "./limiter-wv0uXDk9.js";
|
|
6
8
|
import { n as countOf, r as millisecondsOf, t as boundedMillisecondsOf } from "./knobs-BxG-c7Zb.js";
|
|
7
9
|
import { convertExact, convertible, quantity, rationals, valueIn } from "@mark1russell7/quantity";
|
|
8
10
|
import { replay } from "@mark1russell7/algebra/replay";
|
|
@@ -616,6 +618,33 @@ const processPortOf = (find) => {
|
|
|
616
618
|
};
|
|
617
619
|
/** This port is the process of Node. */
|
|
618
620
|
const nodeProcess = processPortOf(() => globalThis.process);
|
|
621
|
+
/**
|
|
622
|
+
* This function gives a process port for a lifecycle in a process that another part owns. That
|
|
623
|
+
* part is a script, a test of an application, or a library entry. The port adds no handler of a signal or of
|
|
624
|
+
* a trigger, it does not hold the process, and its `exit` only records the code. The caller stops
|
|
625
|
+
* the lifecycle with `life.shutdown()`. A process with a service of its own gives its parts to
|
|
626
|
+
* that service, and does not use this port.
|
|
627
|
+
*/
|
|
628
|
+
const embeddedProcess = () => {
|
|
629
|
+
let code = null;
|
|
630
|
+
let resolve = () => {};
|
|
631
|
+
return {
|
|
632
|
+
on() {
|
|
633
|
+
return () => {};
|
|
634
|
+
},
|
|
635
|
+
exit(requested) {
|
|
636
|
+
if (code !== null) return;
|
|
637
|
+
code = requested;
|
|
638
|
+
resolve(requested);
|
|
639
|
+
},
|
|
640
|
+
get exitCode() {
|
|
641
|
+
return code;
|
|
642
|
+
},
|
|
643
|
+
exited: new Promise((done) => {
|
|
644
|
+
resolve = done;
|
|
645
|
+
})
|
|
646
|
+
};
|
|
647
|
+
};
|
|
619
648
|
//#endregion
|
|
620
649
|
//#region src/routes.ts
|
|
621
650
|
const NAME = /^[A-Za-z_][A-Za-z0-9_]*/;
|
|
@@ -1400,6 +1429,7 @@ const service = (name) => {
|
|
|
1400
1429
|
const choices = /* @__PURE__ */ new Map();
|
|
1401
1430
|
const checks = [];
|
|
1402
1431
|
const runs = /* @__PURE__ */ new Map();
|
|
1432
|
+
const limiters = [];
|
|
1403
1433
|
const couplings = [];
|
|
1404
1434
|
const phases = [];
|
|
1405
1435
|
let readiness;
|
|
@@ -1560,6 +1590,19 @@ const service = (name) => {
|
|
|
1560
1590
|
}
|
|
1561
1591
|
return builder;
|
|
1562
1592
|
},
|
|
1593
|
+
limiter(entry, spec) {
|
|
1594
|
+
composing(`the limiter ${entry}`);
|
|
1595
|
+
once(limiters, entry, "limiter");
|
|
1596
|
+
const given = spec ?? {};
|
|
1597
|
+
limiters.push({
|
|
1598
|
+
name: entry,
|
|
1599
|
+
...given,
|
|
1600
|
+
...given.policy === void 0 ? {} : { policy: { ...given.policy } },
|
|
1601
|
+
...given.store === void 0 ? {} : { store: { ...given.store } },
|
|
1602
|
+
...given.evict === void 0 ? {} : { evict: { ...given.evict } }
|
|
1603
|
+
});
|
|
1604
|
+
return builder;
|
|
1605
|
+
},
|
|
1563
1606
|
healthServer(server) {
|
|
1564
1607
|
composing("the health server");
|
|
1565
1608
|
if (probes !== null) throw new Error(`${name} already has a health server`);
|
|
@@ -1689,6 +1732,11 @@ const service = (name) => {
|
|
|
1689
1732
|
for (const [route, operation] of Object.entries(controller.routes)) problems.push(...operationProblems(`controller ${controller.name}`, route, operation));
|
|
1690
1733
|
for (const declared of controller.patterns) problems.push(...routePatternProblems(`controller ${controller.name}`, declared));
|
|
1691
1734
|
}
|
|
1735
|
+
for (const limiter of limiters) {
|
|
1736
|
+
const { name: entry, ...spec } = limiter;
|
|
1737
|
+
problems.push(...limiterProblems(entry, spec));
|
|
1738
|
+
if (limiter.store?.kind === "shared" && typeof limiter.store.dependency === "string" && !known.has(limiter.store.dependency)) problems.push(`limiter ${entry} keeps its counts in ${limiter.store.dependency}, which is not a dependency`);
|
|
1739
|
+
}
|
|
1692
1740
|
for (const dependency of dependencies) for (const [name, operation] of Object.entries(dependency.operations)) problems.push(...operationProblems(`dependency ${dependency.name}`, name, operation));
|
|
1693
1741
|
for (const phase of phases) if (!isTime$1(phase.cap)) problems.push(`phase ${phase.name}'s cap is not a time`);
|
|
1694
1742
|
problems.push(...shutdownProblems(floor, repeat, triggers, exitCodes, messages));
|
|
@@ -1722,6 +1770,12 @@ const service = (name) => {
|
|
|
1722
1770
|
...entry,
|
|
1723
1771
|
poll: { ...entry.poll }
|
|
1724
1772
|
})),
|
|
1773
|
+
...limiters.length === 0 ? {} : { limiters: limiters.map((entry) => ({
|
|
1774
|
+
...entry,
|
|
1775
|
+
policy: { ...entry.policy },
|
|
1776
|
+
store: { ...entry.store },
|
|
1777
|
+
evict: { maxKeys: { ...entry.evict.maxKeys } }
|
|
1778
|
+
})) },
|
|
1725
1779
|
policies: {
|
|
1726
1780
|
readiness,
|
|
1727
1781
|
liveness,
|
|
@@ -1883,8 +1937,74 @@ const SERVICE_RULES = [
|
|
|
1883
1937
|
"O21",
|
|
1884
1938
|
"O22",
|
|
1885
1939
|
"O23",
|
|
1886
|
-
"O27"
|
|
1940
|
+
"O27",
|
|
1941
|
+
"O29",
|
|
1942
|
+
"O30"
|
|
1887
1943
|
];
|
|
1944
|
+
/**
|
|
1945
|
+
* These are the two rules of the law for a keyed limiter, as data. The caller of `lint()` gives
|
|
1946
|
+
* them, as it gives each other rule. Each one applies one time for each limiter.
|
|
1947
|
+
* - O29: the longest `Retry-After` of a limiter is at most the retry window of its caller. Its
|
|
1948
|
+
* second inequality applies the same bound to a new key at the cap of keys.
|
|
1949
|
+
* Otherwise the caller stops before the limit lets its call in, and a refusal becomes a failure.
|
|
1950
|
+
* - O30: the least delay of a caller before it sends a refused call again is at least the longest
|
|
1951
|
+
* `Retry-After` of the policy. Otherwise a caller that does not read the header sends again too
|
|
1952
|
+
* early, and the limiter refuses each early call again.
|
|
1953
|
+
*
|
|
1954
|
+
* The identifiers O29 and O30 are provisional. The corpus of the law in jarvis (R3, track B) has 28
|
|
1955
|
+
* rules, O01 to O28. It does not have these two rules yet, thus they take the next two identifiers.
|
|
1956
|
+
*/
|
|
1957
|
+
const LIMITER_RULES = [{
|
|
1958
|
+
id: "O29",
|
|
1959
|
+
rule: "the longest Retry-After of a limiter, at its cap of keys too, is at most the retry window of its caller",
|
|
1960
|
+
window: "the caller stops before the limit lets its call in, thus a refusal becomes a failure",
|
|
1961
|
+
expr: {
|
|
1962
|
+
lhs: { terms: [{
|
|
1963
|
+
knob: "retry_after",
|
|
1964
|
+
coef: 1,
|
|
1965
|
+
at: "limiter"
|
|
1966
|
+
}] },
|
|
1967
|
+
op: "<=",
|
|
1968
|
+
rhs: { terms: [{
|
|
1969
|
+
knob: "client_retry_window",
|
|
1970
|
+
coef: 1,
|
|
1971
|
+
at: "caller"
|
|
1972
|
+
}] },
|
|
1973
|
+
margin: 0
|
|
1974
|
+
},
|
|
1975
|
+
expr_more: [{
|
|
1976
|
+
lhs: { terms: [{
|
|
1977
|
+
knob: "retry_after_at_cap",
|
|
1978
|
+
coef: 1,
|
|
1979
|
+
at: "limiter"
|
|
1980
|
+
}] },
|
|
1981
|
+
op: "<=",
|
|
1982
|
+
rhs: { terms: [{
|
|
1983
|
+
knob: "client_retry_window",
|
|
1984
|
+
coef: 1,
|
|
1985
|
+
at: "caller"
|
|
1986
|
+
}] },
|
|
1987
|
+
margin: 0
|
|
1988
|
+
}]
|
|
1989
|
+
}, {
|
|
1990
|
+
id: "O30",
|
|
1991
|
+
rule: "the least delay of a caller before it sends a refused call again is at least the longest Retry-After of the limiter",
|
|
1992
|
+
window: "a caller that does not read Retry-After sends again too early, and the limiter refuses each early call again",
|
|
1993
|
+
expr: {
|
|
1994
|
+
lhs: { terms: [{
|
|
1995
|
+
knob: "retry_delay",
|
|
1996
|
+
coef: 1,
|
|
1997
|
+
at: "caller"
|
|
1998
|
+
}] },
|
|
1999
|
+
op: ">=",
|
|
2000
|
+
rhs: { terms: [{
|
|
2001
|
+
knob: "retry_after",
|
|
2002
|
+
coef: 1,
|
|
2003
|
+
at: "limiter"
|
|
2004
|
+
}] },
|
|
2005
|
+
margin: 0
|
|
2006
|
+
}
|
|
2007
|
+
}];
|
|
1888
2008
|
/** This function gives a finite amount in seconds. */
|
|
1889
2009
|
const secondsOf = (amount) => rationals.toNumber(convertExact(rationals.fromNumber(amount.value), amount.unit, "s"));
|
|
1890
2010
|
/** This function gives an amount, or no bound, as the law binds it. */
|
|
@@ -1960,8 +2080,24 @@ const transportBindings = (transport, hop, scenario) => {
|
|
|
1960
2080
|
for (const [symbol, amount] of Object.entries(scenario.transports?.[transport.name] ?? {})) bind[symbol] = boundOf(amount);
|
|
1961
2081
|
return bind;
|
|
1962
2082
|
};
|
|
2083
|
+
/**
|
|
2084
|
+
* This function gives the symbols of a limiter, and the values of the scenario for its callers.
|
|
2085
|
+
* - `retry_after@limiter` is the longest `Retry-After` of a refusal by the policy, in whole seconds.
|
|
2086
|
+
* It is one interval of the bucket, or one window, as the HTTP edge sends it.
|
|
2087
|
+
* - `retry_after_at_cap@limiter` is the longest `Retry-After` of a new key at the cap of keys. It is
|
|
2088
|
+
* the idle span, `burst / rate` or one window.
|
|
2089
|
+
*/
|
|
2090
|
+
const limiterBindings = (limiter, scenario) => {
|
|
2091
|
+
const bind = {
|
|
2092
|
+
"retry_after@limiter": retryAfterSeconds(longestRetryAfter(limiter.policy)),
|
|
2093
|
+
"retry_after_at_cap@limiter": retryAfterSeconds(idleSpan(limiter.policy))
|
|
2094
|
+
};
|
|
2095
|
+
for (const [symbol, amount] of Object.entries(scenario.limiters?.[limiter.name] ?? {})) bind[symbol.includes("@") ? symbol : `${symbol}@caller`] = boundOf(amount);
|
|
2096
|
+
return bind;
|
|
2097
|
+
};
|
|
1963
2098
|
const SCOPED = /@(client|dependency|server)$/;
|
|
1964
2099
|
const TRANSPORT_SCOPED = /@consumer$/;
|
|
2100
|
+
const LIMITER_SCOPED = /@(limiter|caller)$/;
|
|
1965
2101
|
/** This identifier is the rule of the findings about a field that the declaration does not give. */
|
|
1966
2102
|
const DECLARATION_RULE = "declaration";
|
|
1967
2103
|
/**
|
|
@@ -1980,8 +2116,8 @@ const declarationFindings = (svc) => svc.shutdown.onRepeatSignal === void 0 ? [{
|
|
|
1980
2116
|
}] : [];
|
|
1981
2117
|
/**
|
|
1982
2118
|
* This function applies each service rule to a declaration and a scenario. A rule applies one time
|
|
1983
|
-
* for the service, for each client,
|
|
1984
|
-
* finding for each necessary field that the description does not give (`DECLARATION_RULE`).
|
|
2119
|
+
* for the service, for each client, for each consumer and worker, or for each limiter. The function
|
|
2120
|
+
* also gives a finding for each necessary field that the description does not give (`DECLARATION_RULE`).
|
|
1985
2121
|
*/
|
|
1986
2122
|
const lint = (svc, scenario, rules) => {
|
|
1987
2123
|
const service = serviceBindings(svc, scenario).bind;
|
|
@@ -1989,7 +2125,10 @@ const lint = (svc, scenario, rules) => {
|
|
|
1989
2125
|
for (const rule of rules) {
|
|
1990
2126
|
if (!SERVICE_RULES.includes(rule.id)) continue;
|
|
1991
2127
|
const symbols = symbolsOf(rule);
|
|
1992
|
-
const scopes = symbols.some((symbol) =>
|
|
2128
|
+
const scopes = symbols.some((symbol) => LIMITER_SCOPED.test(symbol)) ? (svc.limiters ?? []).map((limiter) => [limiter.name, {
|
|
2129
|
+
...service,
|
|
2130
|
+
...limiterBindings(limiter, scenario)
|
|
2131
|
+
}]) : symbols.some((symbol) => TRANSPORT_SCOPED.test(symbol)) ? svc.transports.flatMap((transport) => {
|
|
1993
2132
|
const hop = TRANSPORT_HOPS[transport.kind];
|
|
1994
2133
|
return hop === null ? [] : [[transport.name, {
|
|
1995
2134
|
...service,
|
|
@@ -3225,6 +3364,6 @@ const MACHINES = {
|
|
|
3225
3364
|
watchdog: WATCHDOG_MACHINE
|
|
3226
3365
|
};
|
|
3227
3366
|
//#endregion
|
|
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 };
|
|
3367
|
+
export { DECLARATION_RULE, EXIT_MACHINE, EXIT_SIGNALS, FORMAT, GATE_MACHINE, LATCH_MACHINE, LIMITER_RULES, MACHINES, POLLER_MACHINE, PROCESS_TRIGGERS, SERVICE_RULES, STAGES, WATCHDOG_MACHINE, WINDOW_MACHINE, asDebounce, asWindow, backoffBand, backoffDelay, backoffProblems, capKnob, cooperativeAbortAt, daemon, decide, detectionTime, drainMachine, embeddedProcess, ephemeral, evaluate, failSpan, freeAt, heartbeat, heartbeatDetection, idleSpan, inequalities, isExitCode, isExitSignal, isMessageShape, keyedLimiter, kubernetes, limitRule, limiterProblems, lint, localDev, longestRetryAfter, matchesMessage, mayReissue, memoryStore, nodeProcess, platformClock, processPortOf, queueDrain, realClock, recoverSpan, redeliverOnce, replay, retryAfterSeconds, service, serviceBindings, stageRank, symbolsOf, verdictOf, watchdog, workerLoop };
|
|
3229
3368
|
|
|
3230
3369
|
//# sourceMappingURL=index.js.map
|