adaptive-concurrency 0.12.0 → 0.12.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/dist/Limiter.d.ts +130 -4
- package/dist/Limiter.d.ts.map +1 -1
- package/dist/Limiter.js +369 -38
- package/dist/limit/AIMDLimit.d.ts +15 -0
- package/dist/limit/AIMDLimit.d.ts.map +1 -1
- package/dist/limit/AIMDLimit.js +15 -4
- package/dist/limit/GradientLimit.d.ts +16 -0
- package/dist/limit/GradientLimit.d.ts.map +1 -1
- package/dist/limit/GradientLimit.js +15 -1
- package/dist/limit/GroupAwareLimit.d.ts +16 -0
- package/dist/limit/GroupAwareLimit.d.ts.map +1 -1
- package/dist/limit/GroupAwareLimit.js +30 -1
- package/dist/limit/StreamingLimit.d.ts +25 -0
- package/dist/limit/StreamingLimit.d.ts.map +1 -1
- package/dist/limit/WindowedLimit.d.ts +10 -0
- package/dist/limit/WindowedLimit.d.ts.map +1 -1
- package/dist/limit/WindowedLimit.js +30 -3
- package/dist/limit/window/PercentileSampleWindow.js +1 -1
- package/dist/limiter/acquire-strategies/PartitionedStrategy.js +3 -3
- package/dist/limiter/allocation-unavailable-strategies/DelayedRejectStrategy.js +1 -1
- package/dist/statistics/DecayingHistogram.js +2 -2
- package/package.json +2 -1
package/dist/Limiter.d.ts
CHANGED
|
@@ -73,7 +73,8 @@ export interface AllotmentUnavailableStrategy<ContextT> {
|
|
|
73
73
|
*/
|
|
74
74
|
onLimitChanged?(oldLimit: number, newLimit: number): MaybePromise<void>;
|
|
75
75
|
}
|
|
76
|
-
|
|
76
|
+
type AnyTimerHandle = NodeJS.Timeout | number;
|
|
77
|
+
export interface LimiterOptions<ContextT, TimerHandle extends AnyTimerHandle = AnyTimerHandle> {
|
|
77
78
|
limit?: AdaptiveLimit;
|
|
78
79
|
/**
|
|
79
80
|
* Clock function returning the current time in fractional milliseconds
|
|
@@ -104,6 +105,44 @@ export interface LimiterOptions<ContextT> {
|
|
|
104
105
|
* heterogeneous workloads. When omitted, no operation name is provided.
|
|
105
106
|
*/
|
|
106
107
|
operationNameFor?: (context: ContextT) => string | undefined;
|
|
108
|
+
/**
|
|
109
|
+
* Configuration for the recovery probe used to nudge the limit back above
|
|
110
|
+
* 0 when the underlying strategy has fallen there and there are no
|
|
111
|
+
* inflight requests left to produce samples.
|
|
112
|
+
*
|
|
113
|
+
* The limiter relies on the strategy itself to declare a base interval
|
|
114
|
+
* (via {@link AdaptiveLimit.probeFromZeroInterval}) which is grown
|
|
115
|
+
* exponentially across consecutive failed probes; the options here cap
|
|
116
|
+
* and jitter that interval. Strategies that do not implement the probe
|
|
117
|
+
* methods (e.g. {@link FixedLimit}, {@link VegasLimit}) get no recovery
|
|
118
|
+
* probing regardless of these options.
|
|
119
|
+
*/
|
|
120
|
+
recoveryProbe?: {
|
|
121
|
+
/**
|
|
122
|
+
* Soft ceiling on a single probe's unjittered wait time, in
|
|
123
|
+
* milliseconds. The strategy-derived base interval is clamped to this
|
|
124
|
+
* value, then symmetric jitter is applied — so realized waits stay
|
|
125
|
+
* symmetric around `min(base, maxMs)` rather than being biased downward
|
|
126
|
+
* near the cap. Realized waits can therefore exceed `maxMs` by up to
|
|
127
|
+
* `jitter` (e.g. 20% with the default jitter). Default: 30_000.
|
|
128
|
+
*/
|
|
129
|
+
maxMs?: number;
|
|
130
|
+
/**
|
|
131
|
+
* Symmetric jitter fraction applied to each probe interval (e.g. 0.2
|
|
132
|
+
* means `interval * uniform(0.8, 1.2)`). Must be in [0, 0.5).
|
|
133
|
+
* Default: 0.2.
|
|
134
|
+
*/
|
|
135
|
+
jitter?: number;
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Timer functions used to schedule recovery probes. Defaults to the
|
|
139
|
+
* environment's global `setTimeout` / `clearTimeout`. Tests can supply a
|
|
140
|
+
* fake to drive the probe state machine deterministically.
|
|
141
|
+
*/
|
|
142
|
+
timer?: {
|
|
143
|
+
setTimeout(fn: () => void, ms: number): TimerHandle;
|
|
144
|
+
clearTimeout(handle: TimerHandle): void;
|
|
145
|
+
};
|
|
107
146
|
}
|
|
108
147
|
/**
|
|
109
148
|
* Concurrency limiter with pluggable strategies for gating decisions and
|
|
@@ -111,7 +150,7 @@ export interface LimiterOptions<ContextT> {
|
|
|
111
150
|
*
|
|
112
151
|
* @typeParam ContextT Request context type (e.g. partition key).
|
|
113
152
|
*/
|
|
114
|
-
export declare class Limiter<Context = void> {
|
|
153
|
+
export declare class Limiter<Context = void, TimerHandle extends AnyTimerHandle = AnyTimerHandle> implements Disposable {
|
|
115
154
|
private _inflight;
|
|
116
155
|
private _limit;
|
|
117
156
|
private readonly clock;
|
|
@@ -120,7 +159,38 @@ export declare class Limiter<Context = void> {
|
|
|
120
159
|
private readonly rejectionStrategy;
|
|
121
160
|
private readonly bypassResolver;
|
|
122
161
|
private readonly operationNameFor;
|
|
123
|
-
private readonly
|
|
162
|
+
private readonly recoveryProbeMaxMs;
|
|
163
|
+
private readonly recoveryProbeJitter;
|
|
164
|
+
private readonly timer;
|
|
165
|
+
/**
|
|
166
|
+
* Number of consecutive recovery-probe *fires* since the last confirming
|
|
167
|
+
* success sample. Read by {@link maybeArmRecoveryProbe} to compute the
|
|
168
|
+
* exponentially-growing wait between probes:
|
|
169
|
+
*
|
|
170
|
+
* `interval(n) = limitAlgorithm.probeFromZeroInterval(n)`
|
|
171
|
+
*
|
|
172
|
+
* Incremented inside the probe-timer callback (right after
|
|
173
|
+
* `applyProbeFromZero` runs), and reset only on a successful (non-drop,
|
|
174
|
+
* non-ignored) sample in `releaseAndRecordSuccess`.
|
|
175
|
+
*
|
|
176
|
+
* Note: this is "consecutive probe fires without a confirming success",
|
|
177
|
+
* **not** "consecutive failed probes". A probe firing successfully raises
|
|
178
|
+
* the limit to 1, but until traffic actually flows through and produces
|
|
179
|
+
* a non-drop sample, we have no evidence the system is healthy. Resetting
|
|
180
|
+
* on the limit-rise alone would cause the failure ladder to collapse to
|
|
181
|
+
* the base interval forever in a flapping-downstream scenario (probe →
|
|
182
|
+
* raise → drop → 0 → probe → ...), defeating the purpose of the
|
|
183
|
+
* exponential backoff.
|
|
184
|
+
*
|
|
185
|
+
* The downside is a benign quirk: after a probe successfully raises the
|
|
186
|
+
* limit, if no traffic arrives for a long time and then the limit later
|
|
187
|
+
* drops to 0 again, the next probe waits at `interval(N+1)` instead of
|
|
188
|
+
* `interval(0)`. This errs on the side of caution and is acceptable.
|
|
189
|
+
*/
|
|
190
|
+
private probeFailures;
|
|
191
|
+
private probeTimerHandle;
|
|
192
|
+
private disposed;
|
|
193
|
+
private readonly unsubscribeFromLimit;
|
|
124
194
|
private readonly successCounter;
|
|
125
195
|
private readonly droppedCounter;
|
|
126
196
|
private readonly ignoredCounter;
|
|
@@ -131,12 +201,67 @@ export declare class Limiter<Context = void> {
|
|
|
131
201
|
private readonly acquireTimeOnSuccessDistribution;
|
|
132
202
|
private readonly acquireTimeOnUnavailableDistribution;
|
|
133
203
|
static makeDefaultLimit(): AdaptiveLimit;
|
|
134
|
-
constructor(options?: LimiterOptions<Context>);
|
|
204
|
+
constructor(options?: LimiterOptions<Context, TimerHandle>);
|
|
135
205
|
acquire(options?: AcquireOptions<Context>): AcquireResult;
|
|
136
206
|
private tryAcquireCore;
|
|
137
207
|
private createAllotment;
|
|
208
|
+
private safeReadClockWithFallback;
|
|
209
|
+
/**
|
|
210
|
+
* Builds a per-acquire allotment for a bypassed request. Mirrors the
|
|
211
|
+
* one-shot `releaseStarted` semantics of {@link createAllotment} so that
|
|
212
|
+
* bypassed and non-bypassed allotments behave consistently when callers
|
|
213
|
+
* accidentally invoke a release method more than once.
|
|
214
|
+
*
|
|
215
|
+
* Bypassed acquires deliberately do not affect `_inflight` or the limit
|
|
216
|
+
* algorithm — `addSample` is never called — but they still emit the
|
|
217
|
+
* standard CALL_NAME counter, tagged with `operationNameFor` so per-
|
|
218
|
+
* operation dashboards include bypassed traffic.
|
|
219
|
+
*/
|
|
220
|
+
private createBypassedAllotment;
|
|
138
221
|
getLimit(): number;
|
|
139
222
|
getInflight(): number;
|
|
223
|
+
/**
|
|
224
|
+
* Cancels any pending recovery-probe timer and unsubscribes from the
|
|
225
|
+
* underlying limit algorithm. Useful for clean shutdown in long-lived
|
|
226
|
+
* processes, especially when the underlying `AdaptiveLimit` outlives
|
|
227
|
+
* (or is shared across) limiters. Safe to call multiple times.
|
|
228
|
+
*
|
|
229
|
+
* Also exposed via `Symbol.dispose`, so a `Limiter` can be used with the
|
|
230
|
+
* `using` declaration:
|
|
231
|
+
* ```ts
|
|
232
|
+
* using limiter = new Limiter({ ... });
|
|
233
|
+
* ```
|
|
234
|
+
*/
|
|
235
|
+
dispose(): void;
|
|
236
|
+
[Symbol.dispose](): void;
|
|
237
|
+
/**
|
|
238
|
+
* True when the limit has collapsed to 0 and there are no inflight
|
|
239
|
+
* requests left to produce samples. In this state, no future
|
|
240
|
+
* `addSample` call will ever arrive on its own, so the limiter has no
|
|
241
|
+
* way to climb back without an external nudge — i.e. a recovery probe.
|
|
242
|
+
*/
|
|
243
|
+
private isStarvedForSamples;
|
|
244
|
+
private shouldArmRecoveryProbe;
|
|
245
|
+
/**
|
|
246
|
+
* Schedules a recovery probe if (and only if) the limiter is starved for
|
|
247
|
+
* samples (see {@link isStarvedForSamples}), the strategy supports
|
|
248
|
+
* probing, and no probe is already armed.
|
|
249
|
+
*
|
|
250
|
+
* Never throws. Errors from the limit algorithm's
|
|
251
|
+
* {@link AdaptiveLimit.probeFromZeroInterval}, the timer, or
|
|
252
|
+
* {@link AdaptiveLimit.applyProbeFromZero} are swallowed so callers in
|
|
253
|
+
* the release path / subscribe callback / constructor don't have to
|
|
254
|
+
* wrap each invocation. A failed probe arming simply means recovery
|
|
255
|
+
* may take longer; the next limit transition to 0 will re-attempt
|
|
256
|
+
* through the normal hooks.
|
|
257
|
+
*/
|
|
258
|
+
private maybeArmRecoveryProbe;
|
|
259
|
+
/**
|
|
260
|
+
* Cancels the currently-armed recovery probe, if any. Idempotent and
|
|
261
|
+
* never throws (errors from the timer's `clearTimeout` are swallowed so
|
|
262
|
+
* callers don't have to wrap each invocation).
|
|
263
|
+
*/
|
|
264
|
+
private cancelProbe;
|
|
140
265
|
}
|
|
141
266
|
export type RunCallbackArgs<ContextT> = {
|
|
142
267
|
context: ContextT | undefined;
|
|
@@ -161,4 +286,5 @@ export interface LimitedFunction<ContextT> {
|
|
|
161
286
|
* - Callback receives `{ context, signal }` from acquire options.
|
|
162
287
|
*/
|
|
163
288
|
export declare function withLimiter<ContextT>(limiter: Limiter<ContextT>): LimitedFunction<ContextT>;
|
|
289
|
+
export {};
|
|
164
290
|
//# sourceMappingURL=Limiter.d.ts.map
|
package/dist/Limiter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Limiter.d.ts","sourceRoot":"","sources":["../src/Limiter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,OAAO,KAAK,EAIV,cAAc,EACf,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAGL,iBAAiB,EACjB,KAAK,SAAS,EACf,MAAM,gBAAgB,CAAC;AAExB,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AAE7C;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;AAEhE,MAAM,WAAW,cAAc,CAAC,QAAQ,GAAG,IAAI;IAC7C,OAAO,CAAC,EAAE,QAAQ,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;CAClC;AAMD;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe,CAAC,QAAQ;IACvC;;;;OAIG;IACH,mBAAmB,CACjB,OAAO,EAAE,QAAQ,EACjB,KAAK,EAAE,YAAY,GAClB,YAAY,CAAC,OAAO,CAAC,CAAC;IAEzB;;;OAGG;IACH,mBAAmB,CAAC,OAAO,EAAE,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAE3D;;;OAGG;IACH,cAAc,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3D;AAED;;;;;GAKG;AACH,MAAM,WAAW,4BAA4B,CAAC,QAAQ;IACpD;;;;;;;;;;OAUG;IACH,sBAAsB,CACpB,OAAO,EAAE,QAAQ,EACjB,KAAK,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,aAAa,EAC3C,MAAM,CAAC,EAAE,WAAW,GACnB,aAAa,CAAC;IAEjB;;;OAGG;IACH,mBAAmB,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC;IAE1C;;;;OAIG;IACH,cAAc,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;CACzE;AAOD,MAAM,WAAW,cAAc,
|
|
1
|
+
{"version":3,"file":"Limiter.d.ts","sourceRoot":"","sources":["../src/Limiter.ts"],"names":[],"mappings":"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"}
|
package/dist/Limiter.js
CHANGED
|
@@ -21,7 +21,38 @@ export class Limiter {
|
|
|
21
21
|
rejectionStrategy;
|
|
22
22
|
bypassResolver;
|
|
23
23
|
operationNameFor;
|
|
24
|
-
|
|
24
|
+
recoveryProbeMaxMs;
|
|
25
|
+
recoveryProbeJitter;
|
|
26
|
+
timer;
|
|
27
|
+
/**
|
|
28
|
+
* Number of consecutive recovery-probe *fires* since the last confirming
|
|
29
|
+
* success sample. Read by {@link maybeArmRecoveryProbe} to compute the
|
|
30
|
+
* exponentially-growing wait between probes:
|
|
31
|
+
*
|
|
32
|
+
* `interval(n) = limitAlgorithm.probeFromZeroInterval(n)`
|
|
33
|
+
*
|
|
34
|
+
* Incremented inside the probe-timer callback (right after
|
|
35
|
+
* `applyProbeFromZero` runs), and reset only on a successful (non-drop,
|
|
36
|
+
* non-ignored) sample in `releaseAndRecordSuccess`.
|
|
37
|
+
*
|
|
38
|
+
* Note: this is "consecutive probe fires without a confirming success",
|
|
39
|
+
* **not** "consecutive failed probes". A probe firing successfully raises
|
|
40
|
+
* the limit to 1, but until traffic actually flows through and produces
|
|
41
|
+
* a non-drop sample, we have no evidence the system is healthy. Resetting
|
|
42
|
+
* on the limit-rise alone would cause the failure ladder to collapse to
|
|
43
|
+
* the base interval forever in a flapping-downstream scenario (probe →
|
|
44
|
+
* raise → drop → 0 → probe → ...), defeating the purpose of the
|
|
45
|
+
* exponential backoff.
|
|
46
|
+
*
|
|
47
|
+
* The downside is a benign quirk: after a probe successfully raises the
|
|
48
|
+
* limit, if no traffic arrives for a long time and then the limit later
|
|
49
|
+
* drops to 0 again, the next probe waits at `interval(N+1)` instead of
|
|
50
|
+
* `interval(0)`. This errs on the side of caution and is acceptable.
|
|
51
|
+
*/
|
|
52
|
+
probeFailures = 0;
|
|
53
|
+
probeTimerHandle;
|
|
54
|
+
disposed = false;
|
|
55
|
+
unsubscribeFromLimit;
|
|
25
56
|
successCounter;
|
|
26
57
|
droppedCounter;
|
|
27
58
|
ignoredCounter;
|
|
@@ -43,12 +74,57 @@ export class Limiter {
|
|
|
43
74
|
this.acquireStrategy =
|
|
44
75
|
options.acquireStrategy ?? new SemaphoreStrategy(this._limit);
|
|
45
76
|
this.rejectionStrategy = options.allotmentUnavailableStrategy;
|
|
46
|
-
this.
|
|
77
|
+
this.recoveryProbeMaxMs = options.recoveryProbe?.maxMs ?? 30_000;
|
|
78
|
+
this.recoveryProbeJitter = options.recoveryProbe?.jitter ?? 0.2;
|
|
79
|
+
if (!Number.isFinite(this.recoveryProbeMaxMs) ||
|
|
80
|
+
this.recoveryProbeMaxMs <= 0) {
|
|
81
|
+
// Reject NaN / ±Infinity along with non-positive values so a misconfigured
|
|
82
|
+
// probe interval can't silently degrade into a near-busy-loop (Node coerces
|
|
83
|
+
// NaN to ~1ms in `setTimeout(fn, NaN)`) or a non-finite wait.
|
|
84
|
+
throw new RangeError("recoveryProbe.maxMs must be a finite number > 0");
|
|
85
|
+
}
|
|
86
|
+
if (!Number.isFinite(this.recoveryProbeJitter) ||
|
|
87
|
+
this.recoveryProbeJitter < 0 ||
|
|
88
|
+
this.recoveryProbeJitter >= 0.5) {
|
|
89
|
+
throw new RangeError("recoveryProbe.jitter must be a finite number in [0, 0.5)");
|
|
90
|
+
}
|
|
91
|
+
this.timer = options.timer ?? {
|
|
92
|
+
setTimeout: (fn, ms) => globalThis.setTimeout(fn, ms),
|
|
93
|
+
clearTimeout: (handle) => globalThis.clearTimeout(handle),
|
|
94
|
+
};
|
|
95
|
+
this.unsubscribeFromLimit = this.limitAlgorithm.subscribe((newLimit) => {
|
|
47
96
|
const oldLimit = this._limit;
|
|
48
|
-
|
|
49
|
-
|
|
97
|
+
// Notify the acquire strategy FIRST. If it throws atomically (i.e.
|
|
98
|
+
// refuses without mutating its own state), we want `_limit`, the
|
|
99
|
+
// gauge, the rejection strategy, and the recovery probe to all stay
|
|
100
|
+
// pinned to the old value — there's no partial-commit to clean up.
|
|
101
|
+
//
|
|
102
|
+
// If the strategy throws after partially mutating its state, the
|
|
103
|
+
// limiter and strategy are inconsistent either way; we still don't
|
|
104
|
+
// commit `_limit` here, since we'd just be guessing at the strategy's
|
|
105
|
+
// post-throw state. The error propagates to whatever drove the limit
|
|
106
|
+
// change (caller-visible from `setLimit`, swallowed by the release-
|
|
107
|
+
// path `try { addSample }` wrapper from `releaseAndRecord*`).
|
|
50
108
|
this.acquireStrategy.onLimitChanged?.(oldLimit, newLimit);
|
|
51
|
-
|
|
109
|
+
this._limit = newLimit;
|
|
110
|
+
try {
|
|
111
|
+
this.limitGauge.record(newLimit);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Best-effort metric recording; a misbehaving registry must not
|
|
115
|
+
// prevent recovery probe arming below.
|
|
116
|
+
}
|
|
117
|
+
// `onLimitChanged` may return a rejecting Promise; using `void` would
|
|
118
|
+
// surface it as an unhandled rejection (process-terminating in modern
|
|
119
|
+
// Node by default). Wrap in `fireAndForget` so any rejection is
|
|
120
|
+
// swallowed consistently with the release-path try/catch idiom.
|
|
121
|
+
void fireAndForget(() => this.rejectionStrategy?.onLimitChanged?.(oldLimit, newLimit));
|
|
122
|
+
if (newLimit === 0) {
|
|
123
|
+
this.maybeArmRecoveryProbe();
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
this.cancelProbe();
|
|
127
|
+
}
|
|
52
128
|
});
|
|
53
129
|
const registry = options.metricRegistry ?? NoopMetricRegistry;
|
|
54
130
|
const limiterName = options.name ?? `unnamed-${++idCounter}`;
|
|
@@ -72,27 +148,25 @@ export class Limiter {
|
|
|
72
148
|
this.acquireBypassedCounter = registry.counter(MetricIds.ACQUIRE_ATTEMPT_NAME, { id: limiterName, status: "bypassed" });
|
|
73
149
|
this.acquireTimeOnSuccessDistribution = registry.distribution(MetricIds.ACQUIRE_TIME_NAME, { id: limiterName, status: "success" });
|
|
74
150
|
this.acquireTimeOnUnavailableDistribution = registry.distribution(MetricIds.ACQUIRE_TIME_NAME, { id: limiterName, status: "unavailable" });
|
|
75
|
-
this.acquireBypassedAllotment = {
|
|
76
|
-
releaseAndRecordSuccess: async () => {
|
|
77
|
-
this.successCounter.add(1);
|
|
78
|
-
},
|
|
79
|
-
releaseAndIgnore: async () => {
|
|
80
|
-
this.ignoredCounter.add(1);
|
|
81
|
-
},
|
|
82
|
-
releaseAndRecordDropped: async () => {
|
|
83
|
-
this.droppedCounter.add(1);
|
|
84
|
-
},
|
|
85
|
-
};
|
|
86
151
|
// Emit metric for initial limit.
|
|
87
152
|
this.limitGauge.record(this._limit);
|
|
153
|
+
// If the underlying strategy starts at 0 (e.g.
|
|
154
|
+
// `new AIMDLimit({ initialLimit: 0, minLimit: 0 })` or a deserialized
|
|
155
|
+
// snapshot), the limiter is born starved-for-samples. The subscription
|
|
156
|
+
// and release-path hooks only fire on transitions, so without this we'd
|
|
157
|
+
// be stuck at 0 forever — no acquires can land while limit is 0, and no
|
|
158
|
+
// samples can arrive without acquires.
|
|
159
|
+
this.maybeArmRecoveryProbe();
|
|
88
160
|
}
|
|
89
161
|
async acquire(options) {
|
|
90
162
|
if (options?.signal?.aborted)
|
|
91
163
|
return undefined;
|
|
92
164
|
const ctx = (options?.context ?? undefined);
|
|
93
165
|
if (this.bypassResolver?.(ctx)) {
|
|
94
|
-
|
|
95
|
-
|
|
166
|
+
void fireAndForget(() => {
|
|
167
|
+
this.acquireBypassedCounter.add(1);
|
|
168
|
+
});
|
|
169
|
+
return this.createBypassedAllotment(ctx);
|
|
96
170
|
}
|
|
97
171
|
const acquireStart = this.clock();
|
|
98
172
|
const state = {
|
|
@@ -117,17 +191,35 @@ export class Limiter {
|
|
|
117
191
|
}
|
|
118
192
|
return this.tryAcquireCore(retryCtx);
|
|
119
193
|
}, options?.signal);
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
194
|
+
try {
|
|
195
|
+
const distribution = result
|
|
196
|
+
? this.acquireTimeOnSuccessDistribution
|
|
197
|
+
: this.acquireTimeOnUnavailableDistribution;
|
|
198
|
+
distribution.addSample(this.clock() - acquireStart);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// Best-effort metric update: do not fail the acquire path after
|
|
202
|
+
// capacity has already been acquired via the rejection strategy.
|
|
203
|
+
}
|
|
204
|
+
if (result && options?.signal?.aborted) {
|
|
205
|
+
// The rejection strategy may have acquired via retry just before the
|
|
206
|
+
// signal aborted. Return the caller's capacity to the limiter.
|
|
207
|
+
await result.releaseAndIgnore();
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
124
210
|
return result;
|
|
125
211
|
}
|
|
126
|
-
this.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
212
|
+
const allotment = await this.createAllotment(ctx);
|
|
213
|
+
try {
|
|
214
|
+
this.acquireSucceededCounter.add(1);
|
|
215
|
+
// Record the acquire time as a success, since we did actually succeed even
|
|
216
|
+
// if we abort below.
|
|
217
|
+
this.acquireTimeOnSuccessDistribution.addSample(this.clock() - acquireStart);
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
// Best-effort metric update: do not fail the acquire path after
|
|
221
|
+
// capacity has already been reserved and an allotment created.
|
|
222
|
+
}
|
|
131
223
|
if (options?.signal?.aborted) {
|
|
132
224
|
// here, we did acquire, so we need to try to cleanup.
|
|
133
225
|
await allotment.releaseAndIgnore();
|
|
@@ -144,13 +236,37 @@ export class Limiter {
|
|
|
144
236
|
this.acquireFailedCounter.add(1);
|
|
145
237
|
return undefined;
|
|
146
238
|
}
|
|
147
|
-
|
|
148
|
-
|
|
239
|
+
// increment counter only after we've successfully acquired the allotment
|
|
240
|
+
const allotment = await this.createAllotment(ctx);
|
|
241
|
+
try {
|
|
242
|
+
this.acquireSucceededCounter.add(1);
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
// Best-effort metric update: do not fail the acquire path after
|
|
246
|
+
// capacity has already been reserved and an allotment created.
|
|
247
|
+
}
|
|
248
|
+
return allotment;
|
|
149
249
|
}
|
|
150
|
-
createAllotment(ctx) {
|
|
151
|
-
|
|
250
|
+
async createAllotment(ctx) {
|
|
251
|
+
// Capture quantities derived from user-supplied callbacks BEFORE
|
|
252
|
+
// touching `_inflight`. If `clock()` or `operationNameFor()` throws,
|
|
253
|
+
// the strategy has already reserved a slot in `tryAcquireAllotment`
|
|
254
|
+
// and we must roll that reservation back — otherwise the permit is
|
|
255
|
+
// permanently leaked and concurrent admission silently degrades.
|
|
256
|
+
let startTime;
|
|
257
|
+
let operationName;
|
|
258
|
+
try {
|
|
259
|
+
startTime = this.clock();
|
|
260
|
+
operationName = this.operationNameFor?.(ctx);
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
// Best-effort release of the strategy reservation. We're about to
|
|
264
|
+
// throw to the caller of `acquire()`, who will not get an allotment
|
|
265
|
+
// to release manually.
|
|
266
|
+
await fireAndForget(() => this.acquireStrategy.onAllotmentReleased(ctx));
|
|
267
|
+
throw err;
|
|
268
|
+
}
|
|
152
269
|
const currentInflight = ++this._inflight;
|
|
153
|
-
const operationName = this.operationNameFor?.(ctx);
|
|
154
270
|
const incrementTags = operationName
|
|
155
271
|
? { [MetricIds.OPERATION_NAME_TAG]: operationName }
|
|
156
272
|
: {};
|
|
@@ -164,10 +280,24 @@ export class Limiter {
|
|
|
164
280
|
if (releaseStarted)
|
|
165
281
|
return;
|
|
166
282
|
releaseStarted = true;
|
|
167
|
-
|
|
283
|
+
// Every interaction with user-supplied code (metrics, clock, the
|
|
284
|
+
// limit algorithm, the strategies) is wrapped in try/catch so that
|
|
285
|
+
// a single misbehaving collaborator can't leave the strategy with
|
|
286
|
+
// a leaked permit or stall queued waiters in a blocking rejection
|
|
287
|
+
// strategy. This release method must never throw or reject (per
|
|
288
|
+
// `LimitAllotment` contract).
|
|
289
|
+
const endTime = this.safeReadClockWithFallback(startTime);
|
|
168
290
|
const rtt = endTime - startTime;
|
|
169
291
|
this._inflight--;
|
|
170
|
-
|
|
292
|
+
void fireAndForget(() => {
|
|
293
|
+
this.successCounter.add(1, incrementTags);
|
|
294
|
+
});
|
|
295
|
+
// A successful sample is our signal that requests are flowing
|
|
296
|
+
// again: clear any pending probe and reset the failure ladder.
|
|
297
|
+
// (See the docstring on `probeFailures` for why neither is reset
|
|
298
|
+
// on the limit-rise from `applyProbeFromZero` itself.)
|
|
299
|
+
this.probeFailures = 0;
|
|
300
|
+
this.cancelProbe();
|
|
171
301
|
// If one onAllotmentReleased call fails, hard to know what to do here.
|
|
172
302
|
// We're in some kind of inconsistent state, but we probably have to
|
|
173
303
|
// soldier on.
|
|
@@ -183,13 +313,23 @@ export class Limiter {
|
|
|
183
313
|
await this.rejectionStrategy?.onAllotmentReleased();
|
|
184
314
|
}
|
|
185
315
|
catch { }
|
|
316
|
+
// Needed because some limit implementations could keep the limit at 0
|
|
317
|
+
// even after a successful sample (e.g., a GradientLimit where the
|
|
318
|
+
// successful sample's RTT was slow), so we need to re-arm the probe in
|
|
319
|
+
// that case (and won't get a limit subscription callback).
|
|
320
|
+
this.maybeArmRecoveryProbe();
|
|
186
321
|
},
|
|
187
322
|
releaseAndIgnore: async () => {
|
|
188
323
|
if (releaseStarted)
|
|
189
324
|
return;
|
|
190
325
|
releaseStarted = true;
|
|
191
326
|
this._inflight--;
|
|
192
|
-
|
|
327
|
+
void fireAndForget(() => {
|
|
328
|
+
this.ignoredCounter.add(1, incrementTags);
|
|
329
|
+
});
|
|
330
|
+
// Decrementing inflight above, possibly to 0, means we might need to
|
|
331
|
+
// arm the probe now.
|
|
332
|
+
this.maybeArmRecoveryProbe();
|
|
193
333
|
// If one onAllotmentReleased call fails, hard to know what to do here.
|
|
194
334
|
// We're in some kind of inconsistent state, but we probably have to
|
|
195
335
|
// soldier on.
|
|
@@ -206,10 +346,14 @@ export class Limiter {
|
|
|
206
346
|
if (releaseStarted)
|
|
207
347
|
return;
|
|
208
348
|
releaseStarted = true;
|
|
209
|
-
const endTime = this.
|
|
349
|
+
const endTime = this.safeReadClockWithFallback(startTime);
|
|
210
350
|
const rtt = endTime - startTime;
|
|
211
351
|
this._inflight--;
|
|
212
|
-
|
|
352
|
+
void fireAndForget(() => {
|
|
353
|
+
this.droppedCounter.add(1, incrementTags);
|
|
354
|
+
});
|
|
355
|
+
// Needed after the inflight decrement like in releaseAndIgnore.
|
|
356
|
+
this.maybeArmRecoveryProbe();
|
|
213
357
|
// If one onAllotmentReleased call fails, hard to know what to do here.
|
|
214
358
|
// We're in some kind of inconsistent state, but we probably have to
|
|
215
359
|
// soldier on.
|
|
@@ -228,12 +372,191 @@ export class Limiter {
|
|
|
228
372
|
},
|
|
229
373
|
};
|
|
230
374
|
}
|
|
375
|
+
safeReadClockWithFallback(fallback) {
|
|
376
|
+
try {
|
|
377
|
+
return this.clock();
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
return fallback;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Builds a per-acquire allotment for a bypassed request. Mirrors the
|
|
385
|
+
* one-shot `releaseStarted` semantics of {@link createAllotment} so that
|
|
386
|
+
* bypassed and non-bypassed allotments behave consistently when callers
|
|
387
|
+
* accidentally invoke a release method more than once.
|
|
388
|
+
*
|
|
389
|
+
* Bypassed acquires deliberately do not affect `_inflight` or the limit
|
|
390
|
+
* algorithm — `addSample` is never called — but they still emit the
|
|
391
|
+
* standard CALL_NAME counter, tagged with `operationNameFor` so per-
|
|
392
|
+
* operation dashboards include bypassed traffic.
|
|
393
|
+
*/
|
|
394
|
+
createBypassedAllotment(ctx) {
|
|
395
|
+
let operationName;
|
|
396
|
+
try {
|
|
397
|
+
operationName = this.operationNameFor?.(ctx);
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
// A misbehaving `operationNameFor` shouldn't break the bypass path;
|
|
401
|
+
// just emit untagged metrics in that case.
|
|
402
|
+
}
|
|
403
|
+
const incrementTags = operationName
|
|
404
|
+
? { [MetricIds.OPERATION_NAME_TAG]: operationName }
|
|
405
|
+
: {};
|
|
406
|
+
let releaseStarted = false;
|
|
407
|
+
return {
|
|
408
|
+
releaseAndRecordSuccess: async () => {
|
|
409
|
+
if (releaseStarted)
|
|
410
|
+
return;
|
|
411
|
+
releaseStarted = true;
|
|
412
|
+
void fireAndForget(() => {
|
|
413
|
+
this.successCounter.add(1, incrementTags);
|
|
414
|
+
});
|
|
415
|
+
},
|
|
416
|
+
releaseAndIgnore: async () => {
|
|
417
|
+
if (releaseStarted)
|
|
418
|
+
return;
|
|
419
|
+
releaseStarted = true;
|
|
420
|
+
void fireAndForget(() => {
|
|
421
|
+
this.ignoredCounter.add(1, incrementTags);
|
|
422
|
+
});
|
|
423
|
+
},
|
|
424
|
+
releaseAndRecordDropped: async () => {
|
|
425
|
+
if (releaseStarted)
|
|
426
|
+
return;
|
|
427
|
+
releaseStarted = true;
|
|
428
|
+
void fireAndForget(() => {
|
|
429
|
+
this.droppedCounter.add(1, incrementTags);
|
|
430
|
+
});
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
}
|
|
231
434
|
getLimit() {
|
|
232
435
|
return this._limit;
|
|
233
436
|
}
|
|
234
437
|
getInflight() {
|
|
235
438
|
return this._inflight;
|
|
236
439
|
}
|
|
440
|
+
/**
|
|
441
|
+
* Cancels any pending recovery-probe timer and unsubscribes from the
|
|
442
|
+
* underlying limit algorithm. Useful for clean shutdown in long-lived
|
|
443
|
+
* processes, especially when the underlying `AdaptiveLimit` outlives
|
|
444
|
+
* (or is shared across) limiters. Safe to call multiple times.
|
|
445
|
+
*
|
|
446
|
+
* Also exposed via `Symbol.dispose`, so a `Limiter` can be used with the
|
|
447
|
+
* `using` declaration:
|
|
448
|
+
* ```ts
|
|
449
|
+
* using limiter = new Limiter({ ... });
|
|
450
|
+
* ```
|
|
451
|
+
*/
|
|
452
|
+
dispose() {
|
|
453
|
+
if (this.disposed)
|
|
454
|
+
return;
|
|
455
|
+
this.disposed = true;
|
|
456
|
+
this.cancelProbe();
|
|
457
|
+
this.unsubscribeFromLimit();
|
|
458
|
+
}
|
|
459
|
+
[Symbol.dispose]() {
|
|
460
|
+
this.dispose();
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* True when the limit has collapsed to 0 and there are no inflight
|
|
464
|
+
* requests left to produce samples. In this state, no future
|
|
465
|
+
* `addSample` call will ever arrive on its own, so the limiter has no
|
|
466
|
+
* way to climb back without an external nudge — i.e. a recovery probe.
|
|
467
|
+
*/
|
|
468
|
+
isStarvedForSamples() {
|
|
469
|
+
return this._limit === 0 && this._inflight === 0;
|
|
470
|
+
}
|
|
471
|
+
shouldArmRecoveryProbe() {
|
|
472
|
+
return (!this.disposed &&
|
|
473
|
+
this.probeTimerHandle === undefined &&
|
|
474
|
+
this.isStarvedForSamples() &&
|
|
475
|
+
typeof this.limitAlgorithm.probeFromZeroInterval === "function" &&
|
|
476
|
+
typeof this.limitAlgorithm.applyProbeFromZero === "function");
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Schedules a recovery probe if (and only if) the limiter is starved for
|
|
480
|
+
* samples (see {@link isStarvedForSamples}), the strategy supports
|
|
481
|
+
* probing, and no probe is already armed.
|
|
482
|
+
*
|
|
483
|
+
* Never throws. Errors from the limit algorithm's
|
|
484
|
+
* {@link AdaptiveLimit.probeFromZeroInterval}, the timer, or
|
|
485
|
+
* {@link AdaptiveLimit.applyProbeFromZero} are swallowed so callers in
|
|
486
|
+
* the release path / subscribe callback / constructor don't have to
|
|
487
|
+
* wrap each invocation. A failed probe arming simply means recovery
|
|
488
|
+
* may take longer; the next limit transition to 0 will re-attempt
|
|
489
|
+
* through the normal hooks.
|
|
490
|
+
*/
|
|
491
|
+
maybeArmRecoveryProbe() {
|
|
492
|
+
try {
|
|
493
|
+
if (!this.shouldArmRecoveryProbe()) {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
// not-null assertion is safe because of the shouldArmRecoveryProbe()
|
|
497
|
+
// guard, but hard to prove to TS.
|
|
498
|
+
const raw = this.limitAlgorithm.probeFromZeroInterval(this.probeFailures);
|
|
499
|
+
// Cap before the finite check so that an exponential backoff that
|
|
500
|
+
// overflows to Infinity is clamped to recoveryProbeMaxMs instead of
|
|
501
|
+
// silently disabling recovery probing.
|
|
502
|
+
const capped = Math.min(this.recoveryProbeMaxMs, raw);
|
|
503
|
+
if (!Number.isFinite(capped) || capped <= 0)
|
|
504
|
+
return;
|
|
505
|
+
const jitterFactor = 1 + (Math.random() * 2 - 1) * this.recoveryProbeJitter;
|
|
506
|
+
const ms = capped * jitterFactor;
|
|
507
|
+
this.probeTimerHandle = this.timer.setTimeout(() => {
|
|
508
|
+
this.probeTimerHandle = undefined;
|
|
509
|
+
if (this.disposed)
|
|
510
|
+
return;
|
|
511
|
+
if (this._limit !== 0) {
|
|
512
|
+
// For the strategies in this package the limit should be unable to
|
|
513
|
+
// climb back from 0 without a sample, and samples can't arrive while
|
|
514
|
+
// we're starved, so this branch *seems* unreachable. It isn't,
|
|
515
|
+
// though: a custom `AdaptiveLimit` (or `WindowedLimit` wrapping one)
|
|
516
|
+
// could mutate its limit from a timer of its own or via an external
|
|
517
|
+
// `setLimit`-style hook; tests / harnesses can call `addSample`
|
|
518
|
+
// directly; and a maintainer could add a non-completing path that
|
|
519
|
+
// injects samples. None of those should crash the process, so we
|
|
520
|
+
// simply abandon the probe — the next limit transition to 0 will
|
|
521
|
+
// re-arm it through the normal hooks.
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
try {
|
|
525
|
+
// not-null assertion is safe because of the shouldArmRecoveryProbe()
|
|
526
|
+
// guard, but hard to prove to TS.
|
|
527
|
+
this.limitAlgorithm.applyProbeFromZero();
|
|
528
|
+
// Increment *after* the apply (and the synchronous subscription it
|
|
529
|
+
// triggers): see the docstring on `probeFailures`. Notably we do NOT
|
|
530
|
+
// reset on the limit-rise caused by `applyProbeFromZero`, because
|
|
531
|
+
// the rise alone doesn't confirm any traffic actually flowed
|
|
532
|
+
// through. A confirming success sample in `releaseAndRecordSuccess`
|
|
533
|
+
// is what eventually clears this.
|
|
534
|
+
this.probeFailures++;
|
|
535
|
+
}
|
|
536
|
+
catch {
|
|
537
|
+
// Ignore — see the method docstring. Future samples will re-trigger
|
|
538
|
+
// probe arming through the normal hooks.
|
|
539
|
+
}
|
|
540
|
+
}, ms);
|
|
541
|
+
}
|
|
542
|
+
catch {
|
|
543
|
+
// Ignore — see the method docstring.
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Cancels the currently-armed recovery probe, if any. Idempotent and
|
|
548
|
+
* never throws (errors from the timer's `clearTimeout` are swallowed so
|
|
549
|
+
* callers don't have to wrap each invocation).
|
|
550
|
+
*/
|
|
551
|
+
cancelProbe() {
|
|
552
|
+
if (this.probeTimerHandle === undefined)
|
|
553
|
+
return;
|
|
554
|
+
const handle = this.probeTimerHandle;
|
|
555
|
+
this.probeTimerHandle = undefined;
|
|
556
|
+
void fireAndForget(() => {
|
|
557
|
+
this.timer.clearTimeout(handle);
|
|
558
|
+
});
|
|
559
|
+
}
|
|
237
560
|
}
|
|
238
561
|
/**
|
|
239
562
|
* Creates a helper that runs callbacks under acquired limiter allotments.
|
|
@@ -262,7 +585,7 @@ export function withLimiter(limiter) {
|
|
|
262
585
|
return QuotaNotAvailable;
|
|
263
586
|
}
|
|
264
587
|
const [result] = await Promise.allSettled([
|
|
265
|
-
fn({ context: options?.context, signal: options?.signal }),
|
|
588
|
+
Promise.resolve().then(() => fn({ context: options?.context, signal: options?.signal })),
|
|
266
589
|
]);
|
|
267
590
|
if (result.status === "rejected") {
|
|
268
591
|
if (isAdaptiveTimeoutError(result.reason)) {
|
|
@@ -293,3 +616,11 @@ export function withLimiter(limiter) {
|
|
|
293
616
|
}
|
|
294
617
|
return limited;
|
|
295
618
|
}
|
|
619
|
+
async function fireAndForget(fn) {
|
|
620
|
+
try {
|
|
621
|
+
await fn();
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
// Ignore errors
|
|
625
|
+
}
|
|
626
|
+
}
|
|
@@ -27,6 +27,18 @@ export interface AIMDLimitOptions {
|
|
|
27
27
|
* Must be in [0, 0.05]. Default: 0.02.
|
|
28
28
|
*/
|
|
29
29
|
backoffJitter?: number;
|
|
30
|
+
/**
|
|
31
|
+
* Configuration for the limiter's recovery probe when the limit reaches 0.
|
|
32
|
+
* See {@link AdaptiveLimit.probeFromZeroInterval}.
|
|
33
|
+
*/
|
|
34
|
+
recoveryProbe?: {
|
|
35
|
+
/**
|
|
36
|
+
* Base interval in milliseconds between probes. The probe interval grows
|
|
37
|
+
* as `baseMs * 2^failedProbes`, then is jittered and capped by the
|
|
38
|
+
* limiter. Default: the configured `timeout`.
|
|
39
|
+
*/
|
|
40
|
+
baseMs?: number;
|
|
41
|
+
};
|
|
30
42
|
}
|
|
31
43
|
export declare class AIMDLimit implements AdaptiveLimit {
|
|
32
44
|
private _limit;
|
|
@@ -36,7 +48,10 @@ export declare class AIMDLimit implements AdaptiveLimit {
|
|
|
36
48
|
private readonly minLimit;
|
|
37
49
|
private readonly maxLimit;
|
|
38
50
|
private readonly backoffJitter;
|
|
51
|
+
private readonly recoveryProbeBaseMs;
|
|
39
52
|
constructor(options?: AIMDLimitOptions);
|
|
53
|
+
probeFromZeroInterval(failedProbes: number): number;
|
|
54
|
+
applyProbeFromZero(): void;
|
|
40
55
|
addSample(_startTime: number, rtt: number, inflight: number, didDrop: boolean, _operationName?: string): void;
|
|
41
56
|
get currentLimit(): number;
|
|
42
57
|
private applyNewLimit;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AIMDLimit.d.ts","sourceRoot":"","sources":["../../src/limit/AIMDLimit.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"AIMDLimit.d.ts","sourceRoot":"","sources":["../../src/limit/AIMDLimit.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;OAGG;IACH,aAAa,CAAC,EAAE;QACd;;;;WAIG;QACH,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;CACH;AAED,qBAAa,SAAU,YAAW,aAAa;IAC7C,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAqB;IAEpD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAS;gBAEjC,OAAO,GAAE,gBAAqB;IA8B1C,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM;IAInD,kBAAkB,IAAI,IAAI;IAI1B,SAAS,CACP,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,OAAO,EAChB,cAAc,CAAC,EAAE,MAAM,GACtB,IAAI;IAuBP,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,OAAO,CAAC,aAAa;IAOrB,SAAS,CACP,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,EACpC,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAO,GACrC,MAAM,IAAI;IAIb,QAAQ,IAAI,MAAM;CAGnB"}
|
package/dist/limit/AIMDLimit.js
CHANGED
|
@@ -7,6 +7,7 @@ export class AIMDLimit {
|
|
|
7
7
|
minLimit;
|
|
8
8
|
maxLimit;
|
|
9
9
|
backoffJitter;
|
|
10
|
+
recoveryProbeBaseMs;
|
|
10
11
|
constructor(options = {}) {
|
|
11
12
|
const initialLimit = options.initialLimit ?? 20;
|
|
12
13
|
this._limit = initialLimit;
|
|
@@ -15,18 +16,28 @@ export class AIMDLimit {
|
|
|
15
16
|
this.minLimit = options.minLimit ?? 20;
|
|
16
17
|
this.maxLimit = options.maxLimit ?? 200;
|
|
17
18
|
this.backoffJitter = options.backoffJitter ?? 0.02;
|
|
19
|
+
this.recoveryProbeBaseMs = options.recoveryProbe?.baseMs ?? this.timeout;
|
|
18
20
|
if (this.backoffRatio >= 1.0 || this.backoffRatio < 0.5) {
|
|
19
|
-
throw new
|
|
21
|
+
throw new RangeError("Backoff ratio must be in the range [0.5, 1.0)");
|
|
20
22
|
}
|
|
21
23
|
if (this.timeout <= 0) {
|
|
22
|
-
throw new
|
|
24
|
+
throw new RangeError("Timeout must be positive");
|
|
23
25
|
}
|
|
24
26
|
if (this.backoffJitter < 0 || this.backoffJitter > 0.05) {
|
|
25
|
-
throw new
|
|
27
|
+
throw new RangeError("backoffJitter must be in the range [0, 0.05]");
|
|
26
28
|
}
|
|
27
29
|
if (this.backoffRatio + this.backoffJitter >= 1.0) {
|
|
28
|
-
throw new
|
|
30
|
+
throw new RangeError("backoffRatio + backoffJitter must be < 1.0 to guarantee the limit decreases on drop");
|
|
29
31
|
}
|
|
32
|
+
if (!(this.recoveryProbeBaseMs > 0)) {
|
|
33
|
+
throw new RangeError("recoveryProbe.baseMs must be > 0");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
probeFromZeroInterval(failedProbes) {
|
|
37
|
+
return this.recoveryProbeBaseMs * Math.pow(2, failedProbes);
|
|
38
|
+
}
|
|
39
|
+
applyProbeFromZero() {
|
|
40
|
+
this.applyNewLimit(1);
|
|
30
41
|
}
|
|
31
42
|
addSample(_startTime, rtt, inflight, didDrop, _operationName) {
|
|
32
43
|
let currentLimit = this._limit;
|
|
@@ -80,6 +80,19 @@ export interface Gradient2LimitOptions {
|
|
|
80
80
|
* Default: 600
|
|
81
81
|
*/
|
|
82
82
|
longWindow?: number;
|
|
83
|
+
/**
|
|
84
|
+
* Configuration for the limiter's recovery probe when the limit reaches 0.
|
|
85
|
+
* See {@link AdaptiveLimit.probeFromZeroInterval}.
|
|
86
|
+
*/
|
|
87
|
+
recoveryProbe?: {
|
|
88
|
+
/**
|
|
89
|
+
* Fallback base interval in milliseconds between probes, used when the
|
|
90
|
+
* algorithm has no usable RTT estimate (e.g. before any sample has been
|
|
91
|
+
* recorded). When an RTT estimate is available the probe interval is
|
|
92
|
+
* derived from `longRtt * 5`. Default: 1000.
|
|
93
|
+
*/
|
|
94
|
+
baseMs?: number;
|
|
95
|
+
};
|
|
83
96
|
metricRegistry?: MetricRegistry;
|
|
84
97
|
}
|
|
85
98
|
export declare class GradientLimit implements AdaptiveLimit {
|
|
@@ -104,11 +117,14 @@ export declare class GradientLimit implements AdaptiveLimit {
|
|
|
104
117
|
private readonly queueSize;
|
|
105
118
|
private readonly smoothing;
|
|
106
119
|
private readonly tolerance;
|
|
120
|
+
private readonly recoveryProbeBaseMs;
|
|
107
121
|
private readonly longRttSampleListener;
|
|
108
122
|
private readonly shortRttSampleListener;
|
|
109
123
|
private readonly queueSizeSampleListener;
|
|
110
124
|
constructor(options?: Gradient2LimitOptions);
|
|
111
125
|
addSample(_startTime: number, rtt: number, inflight: number, _didDrop: boolean, _operationName?: string): void;
|
|
126
|
+
probeFromZeroInterval(failedProbes: number): number;
|
|
127
|
+
applyProbeFromZero(): void;
|
|
112
128
|
get currentLimit(): number;
|
|
113
129
|
private applyNewLimit;
|
|
114
130
|
subscribe(consumer: (newLimit: number) => void, options?: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GradientLimit.d.ts","sourceRoot":"","sources":["../../src/limit/GradientLimit.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAsB,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAI/E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,MAAM,WAAW,qBAAqB;IACpC,qDAAqD;IACrD,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,WAAW,EAAE,MAAM,KAAK,MAAM,CAAC,CAAC;IAEvD;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,cAAc,CAAC,EAAE,cAAc,CAAC;CACjC;AAED,qBAAa,aAAc,YAAW,aAAa;IACjD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAqB;IAEpD,yDAAyD;IACzD,OAAO,CAAC,cAAc,CAAS;IAE/B;;;OAGG;IACH,OAAO,CAAC,OAAO,CAAK;IAEpB;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAE7C,8DAA8D;IAC9D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAElC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAkC;IAC5D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;
|
|
1
|
+
{"version":3,"file":"GradientLimit.d.ts","sourceRoot":"","sources":["../../src/limit/GradientLimit.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAsB,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAI/E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,MAAM,WAAW,qBAAqB;IACpC,qDAAqD;IACrD,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,WAAW,EAAE,MAAM,KAAK,MAAM,CAAC,CAAC;IAEvD;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,aAAa,CAAC,EAAE;QACd;;;;;WAKG;QACH,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;IAEF,cAAc,CAAC,EAAE,cAAc,CAAC;CACjC;AAED,qBAAa,aAAc,YAAW,aAAa;IACjD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAqB;IAEpD,yDAAyD;IACzD,OAAO,CAAC,cAAc,CAAS;IAE/B;;;OAGG;IACH,OAAO,CAAC,OAAO,CAAK;IAEpB;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAE7C,8DAA8D;IAC9D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAElC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAkC;IAC5D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAS;IAE7C,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAqB;IAC3D,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAqB;IAC5D,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAAqB;gBAEjD,OAAO,GAAE,qBAA0B;IAgC/C,SAAS,CACP,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,OAAO,EACjB,cAAc,CAAC,EAAE,MAAM,GACtB,IAAI;IASP,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM;IAMnD,kBAAkB,IAAI,IAAI;IAK1B,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,OAAO,CAAC,aAAa;IAOrB,SAAS,CACP,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,EACpC,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAO,GACrC,MAAM,IAAI;IAIb,OAAO,CAAC,yBAAyB;IA0CjC,UAAU,IAAI,MAAM;IAIpB,YAAY,IAAI,MAAM;IAItB,QAAQ,IAAI,MAAM;CAGnB"}
|
|
@@ -24,6 +24,7 @@ export class GradientLimit {
|
|
|
24
24
|
queueSize;
|
|
25
25
|
smoothing;
|
|
26
26
|
tolerance;
|
|
27
|
+
recoveryProbeBaseMs;
|
|
27
28
|
longRttSampleListener;
|
|
28
29
|
shortRttSampleListener;
|
|
29
30
|
queueSizeSampleListener;
|
|
@@ -36,8 +37,12 @@ export class GradientLimit {
|
|
|
36
37
|
this.smoothing = options.smoothing ?? 0.2;
|
|
37
38
|
this.tolerance = options.rttTolerance ?? 1.5;
|
|
38
39
|
this.longRtt = new ExpMovingAverage(options.longWindow ?? 600, 10);
|
|
40
|
+
this.recoveryProbeBaseMs = options.recoveryProbe?.baseMs ?? 1000;
|
|
39
41
|
if (options.rttTolerance !== undefined && options.rttTolerance < 1.0) {
|
|
40
|
-
throw new
|
|
42
|
+
throw new RangeError("Tolerance must be >= 1.0");
|
|
43
|
+
}
|
|
44
|
+
if (!(this.recoveryProbeBaseMs > 0)) {
|
|
45
|
+
throw new RangeError("recoveryProbe.baseMs must be > 0");
|
|
41
46
|
}
|
|
42
47
|
const qs = options.queueSize ?? 4;
|
|
43
48
|
this.queueSize = typeof qs === "number" ? () => qs : qs;
|
|
@@ -52,6 +57,15 @@ export class GradientLimit {
|
|
|
52
57
|
const newLimit = Math.floor(newLimitNoFloor);
|
|
53
58
|
this.applyNewLimit(newLimit);
|
|
54
59
|
}
|
|
60
|
+
probeFromZeroInterval(failedProbes) {
|
|
61
|
+
const longRtt = this.longRtt.currentValue;
|
|
62
|
+
const base = longRtt > 0 ? longRtt * 5 : this.recoveryProbeBaseMs;
|
|
63
|
+
return base * Math.pow(2, failedProbes);
|
|
64
|
+
}
|
|
65
|
+
applyProbeFromZero() {
|
|
66
|
+
this.estimatedLimit = 1;
|
|
67
|
+
this.applyNewLimit(1);
|
|
68
|
+
}
|
|
55
69
|
get currentLimit() {
|
|
56
70
|
return this._limit;
|
|
57
71
|
}
|
|
@@ -41,6 +41,7 @@ export declare class GroupAwareLimit implements AdaptiveLimit {
|
|
|
41
41
|
private readonly recentRttWindow;
|
|
42
42
|
private readonly minGroupSamples;
|
|
43
43
|
private readonly clock;
|
|
44
|
+
private readonly recoveryProbeBaseMs;
|
|
44
45
|
private readonly registry;
|
|
45
46
|
private readonly congestionSignalGauge;
|
|
46
47
|
private readonly warmedGroupsCountGauge;
|
|
@@ -59,9 +60,24 @@ export declare class GroupAwareLimit implements AdaptiveLimit {
|
|
|
59
60
|
minGroupSamples?: number;
|
|
60
61
|
clock?: () => number;
|
|
61
62
|
metricRegistry?: MetricRegistry;
|
|
63
|
+
/**
|
|
64
|
+
* Configuration for the limiter's recovery probe when the limit reaches
|
|
65
|
+
* 0. See {@link AdaptiveLimit.probeFromZeroInterval}.
|
|
66
|
+
*/
|
|
67
|
+
recoveryProbe?: {
|
|
68
|
+
/**
|
|
69
|
+
* Fallback base interval in milliseconds between probes, used when no
|
|
70
|
+
* group is warmed up enough to provide a recent RTT estimate. When
|
|
71
|
+
* warmed groups are available, the probe interval is derived from
|
|
72
|
+
* their weighted-mean recent RTT × 5. Default: 1000.
|
|
73
|
+
*/
|
|
74
|
+
baseMs?: number;
|
|
75
|
+
};
|
|
62
76
|
});
|
|
63
77
|
addSample(_startTime: number, rtt: number, inflight: number, didDrop: boolean, operationName?: string): void;
|
|
64
78
|
get currentLimit(): number;
|
|
79
|
+
probeFromZeroInterval(failedProbes: number): number;
|
|
80
|
+
applyProbeFromZero(): void;
|
|
65
81
|
subscribe(consumer: (newLimit: number) => void, options?: {
|
|
66
82
|
signal?: AbortSignal;
|
|
67
83
|
}): () => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GroupAwareLimit.d.ts","sourceRoot":"","sources":["../../src/limit/GroupAwareLimit.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"GroupAwareLimit.d.ts","sourceRoot":"","sources":["../../src/limit/GroupAwareLimit.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAS,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAKlE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AA6DzD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBAAa,eAAgB,YAAW,aAAa;IACnD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA0C;IACzE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkD;IAEzE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA8C;IACvE,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAS;IAE7C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiB;IAC1C,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAQ;IAC9C,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAQ;IAC/C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAQ;gBAE/B,OAAO,CAAC,EAAE;QACpB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC;QACvD,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC;QACrB,cAAc,CAAC,EAAE,cAAc,CAAC;QAChC;;;WAGG;QACH,aAAa,CAAC,EAAE;YACd;;;;;eAKG;YACH,MAAM,CAAC,EAAE,MAAM,CAAC;SACjB,CAAC;KACH;IA8BD,SAAS,CACP,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,OAAO,EAChB,aAAa,CAAC,EAAE,MAAM,GACrB,IAAI;IAgDP,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM;IAiCnD,kBAAkB,IAAI,IAAI;IAI1B,SAAS,CACP,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,EACpC,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAO,GACrC,MAAM,IAAI;IAIb,OAAO,CAAC,uBAAuB;IA+C/B,OAAO,CAAC,KAAK;IAIb,OAAO,CAAC,aAAa;IAOrB,QAAQ,IAAI,MAAM;CAGnB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,IAAI;QACZ,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,KAAK,MAAM,CAAC;KACpD;CACF"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sumBy } from "es-toolkit";
|
|
1
2
|
import lruPkg from "lru_map";
|
|
2
3
|
const { LRUMap } = lruPkg;
|
|
3
4
|
import { ListenerSet } from "../ListenerSet.js";
|
|
@@ -94,6 +95,7 @@ export class GroupAwareLimit {
|
|
|
94
95
|
recentRttWindow;
|
|
95
96
|
minGroupSamples;
|
|
96
97
|
clock;
|
|
98
|
+
recoveryProbeBaseMs;
|
|
97
99
|
registry;
|
|
98
100
|
congestionSignalGauge;
|
|
99
101
|
warmedGroupsCountGauge;
|
|
@@ -111,6 +113,10 @@ export class GroupAwareLimit {
|
|
|
111
113
|
this.recentRttWindow = options?.recentRttWindow ?? 100;
|
|
112
114
|
this.minGroupSamples = options?.minGroupSamples ?? 20;
|
|
113
115
|
this.clock = options?.clock ?? (() => performance.now());
|
|
116
|
+
this.recoveryProbeBaseMs = options?.recoveryProbe?.baseMs ?? 1000;
|
|
117
|
+
if (!(this.recoveryProbeBaseMs > 0)) {
|
|
118
|
+
throw new RangeError("recoveryProbe.baseMs must be > 0");
|
|
119
|
+
}
|
|
114
120
|
this.registry = options?.metricRegistry ?? NoopMetricRegistry;
|
|
115
121
|
this.congestionSignalGauge = this.registry.gauge(MetricIds.CONGESTION_SIGNAL_NAME);
|
|
116
122
|
this.warmedGroupsCountGauge = this.registry.gauge(MetricIds.WARMED_GROUPS_COUNT_NAME);
|
|
@@ -156,6 +162,29 @@ export class GroupAwareLimit {
|
|
|
156
162
|
get currentLimit() {
|
|
157
163
|
return this._limit;
|
|
158
164
|
}
|
|
165
|
+
probeFromZeroInterval(failedProbes) {
|
|
166
|
+
const now = this.clock();
|
|
167
|
+
const entries = [
|
|
168
|
+
...this.groups.entries(),
|
|
169
|
+
];
|
|
170
|
+
const warmedGroupInfos = entries.flatMap(([, groupState]) => {
|
|
171
|
+
const activity = groupState.activityCount(now);
|
|
172
|
+
const recentRtt = groupState.recentRtt.currentValue;
|
|
173
|
+
const validRecentRtt = Number.isFinite(recentRtt) && recentRtt > 0;
|
|
174
|
+
return validRecentRtt && activity >= this.minGroupSamples
|
|
175
|
+
? [{ recentRtt, weight: Math.sqrt(activity) }]
|
|
176
|
+
: [];
|
|
177
|
+
});
|
|
178
|
+
const totalWeight = sumBy(warmedGroupInfos, ({ weight }) => weight);
|
|
179
|
+
const weightedRatioSum = sumBy(warmedGroupInfos, ({ weight, recentRtt }) => weight * recentRtt);
|
|
180
|
+
const base = totalWeight > 0
|
|
181
|
+
? (weightedRatioSum / totalWeight) * 5
|
|
182
|
+
: this.recoveryProbeBaseMs;
|
|
183
|
+
return base * Math.pow(2, failedProbes);
|
|
184
|
+
}
|
|
185
|
+
applyProbeFromZero() {
|
|
186
|
+
this.applyNewLimit(1);
|
|
187
|
+
}
|
|
159
188
|
subscribe(consumer, options = {}) {
|
|
160
189
|
return this.limitListeners.subscribe(consumer, options);
|
|
161
190
|
}
|
|
@@ -184,7 +213,7 @@ export class GroupAwareLimit {
|
|
|
184
213
|
const totalWeight = sum(warmedGroupInfos.map(({ weight }) => weight));
|
|
185
214
|
if (totalWeight <= 0)
|
|
186
215
|
return undefined;
|
|
187
|
-
const weightedRatioSum =
|
|
216
|
+
const weightedRatioSum = sumBy(warmedGroupInfos, ({ weight, ratio }) => weight * ratio);
|
|
188
217
|
return {
|
|
189
218
|
signal: weightedRatioSum / totalWeight,
|
|
190
219
|
warmedGroupInfos,
|
|
@@ -24,5 +24,30 @@ export interface AdaptiveLimit {
|
|
|
24
24
|
* used by group-aware limits to distinguish heterogeneous workloads.
|
|
25
25
|
*/
|
|
26
26
|
addSample(startTime: number, rtt: number, inflight: number, didDrop: boolean, operationName?: string): void;
|
|
27
|
+
/**
|
|
28
|
+
* Returns the recommended interval, in milliseconds, between recovery
|
|
29
|
+
* probes when the limit has fallen to 0 and there are no inflight requests
|
|
30
|
+
* left to produce samples. The {@link Limiter} uses this to schedule a
|
|
31
|
+
* probe that bumps the limit back to 1 so a single request can flow and
|
|
32
|
+
* generate a fresh sample.
|
|
33
|
+
*
|
|
34
|
+
* @param failedProbes The number of consecutive probes that have already
|
|
35
|
+
* fired without producing a non-drop sample since the limit last
|
|
36
|
+
* recovered. Implementations typically use this to grow the interval
|
|
37
|
+
* exponentially (e.g. `base * 2^failedProbes`).
|
|
38
|
+
*
|
|
39
|
+
* Strategies that cannot reach a limit of 0 (e.g. {@link VegasLimit}) or
|
|
40
|
+
* are intentionally inert (e.g. {@link FixedLimit}) may omit this method;
|
|
41
|
+
* the limiter will then skip recovery probing for them.
|
|
42
|
+
*/
|
|
43
|
+
probeFromZeroInterval?(failedProbes: number): number;
|
|
44
|
+
/**
|
|
45
|
+
* Atomically raises the limit to (at least) 1 so a single request can get
|
|
46
|
+
* through, allowing the sample to be used to probe recovery. Called by the
|
|
47
|
+
* limiter when its probe timer fires while the limit is at 0.
|
|
48
|
+
*
|
|
49
|
+
* Must be implemented if and only if {@link probeFromZeroInterval} is.
|
|
50
|
+
*/
|
|
51
|
+
applyProbeFromZero?(): void;
|
|
27
52
|
}
|
|
28
53
|
//# sourceMappingURL=StreamingLimit.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StreamingLimit.d.ts","sourceRoot":"","sources":["../../src/limit/StreamingLimit.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,2CAA2C;IAC3C,IAAI,YAAY,IAAI,MAAM,CAAC;IAE3B;;;;;OAKG;IACH,SAAS,CACP,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,EACpC,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GACjC,MAAM,IAAI,CAAC;IAEd;;;;;;;;OAQG;IACH,SAAS,
|
|
1
|
+
{"version":3,"file":"StreamingLimit.d.ts","sourceRoot":"","sources":["../../src/limit/StreamingLimit.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,2CAA2C;IAC3C,IAAI,YAAY,IAAI,MAAM,CAAC;IAE3B;;;;;OAKG;IACH,SAAS,CACP,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,EACpC,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GACjC,MAAM,IAAI,CAAC;IAEd;;;;;;;;OAQG;IACH,SAAS,CACP,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,OAAO,EAChB,aAAa,CAAC,EAAE,MAAM,GACrB,IAAI,CAAC;IAER;;;;;;;;;;;;;;;OAeG;IACH,qBAAqB,CAAC,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC;IAErD;;;;;;OAMG;IACH,kBAAkB,CAAC,IAAI,IAAI,CAAC;CAC7B"}
|
|
@@ -45,6 +45,16 @@ export declare class WindowedLimit implements AdaptiveLimit {
|
|
|
45
45
|
private readonly sampleWindowFactory;
|
|
46
46
|
/** Object tracking stats for the current sample window */
|
|
47
47
|
private sample;
|
|
48
|
+
/**
|
|
49
|
+
* Set when `applyProbeFromZero` fires so the very next `addSample` bypasses
|
|
50
|
+
* windowing and is forwarded directly to the delegate. Without this, a
|
|
51
|
+
* single dropped probe sample never reaches the delegate (the window
|
|
52
|
+
* requires `windowSize` samples to become ready), so the delegate stays at
|
|
53
|
+
* limit 1 and the exponential backoff never re-arms.
|
|
54
|
+
*/
|
|
55
|
+
private probeActive;
|
|
56
|
+
readonly probeFromZeroInterval?: (failedProbes: number) => number;
|
|
57
|
+
readonly applyProbeFromZero?: () => void;
|
|
48
58
|
constructor(delegate: AdaptiveLimit, options?: WindowedLimitOptions);
|
|
49
59
|
addSample(startTime: number, rtt: number, inflight: number, didDrop: boolean, operationName?: string): void;
|
|
50
60
|
get currentLimit(): number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WindowedLimit.d.ts","sourceRoot":"","sources":["../../src/limit/WindowedLimit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAS7D,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,6EAA6E;IAC7E,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B,+EAA+E;IAC/E,mBAAmB,CAAC,EAAE,MAAM,YAAY,CAAC;CAC1C;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,aAAc,YAAW,aAAa;IACjD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgB;IAEzC,kFAAkF;IAClF,OAAO,CAAC,cAAc,CAAK;IAE3B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IAEzD,0DAA0D;IAC1D,OAAO,CAAC,MAAM,CAAe;
|
|
1
|
+
{"version":3,"file":"WindowedLimit.d.ts","sourceRoot":"","sources":["../../src/limit/WindowedLimit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAS7D,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,6EAA6E;IAC7E,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B,+EAA+E;IAC/E,mBAAmB,CAAC,EAAE,MAAM,YAAY,CAAC;CAC1C;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,aAAc,YAAW,aAAa;IACjD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgB;IAEzC,kFAAkF;IAClF,OAAO,CAAC,cAAc,CAAK;IAE3B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IAEzD,0DAA0D;IAC1D,OAAO,CAAC,MAAM,CAAe;IAE7B;;;;;;OAMG;IACH,OAAO,CAAC,WAAW,CAAS;IAK5B,QAAQ,CAAC,qBAAqB,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,MAAM,CAAC;IAClE,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;gBAE7B,QAAQ,EAAE,aAAa,EAAE,OAAO,GAAE,oBAAyB;IAoCvE,SAAS,CACP,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,OAAO,EAChB,aAAa,CAAC,EAAE,MAAM,GACrB,IAAI;IA4CP,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,SAAS,CACP,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,EACpC,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GACjC,MAAM,IAAI;CAGd"}
|
|
@@ -28,8 +28,30 @@ export class WindowedLimit {
|
|
|
28
28
|
sampleWindowFactory;
|
|
29
29
|
/** Object tracking stats for the current sample window */
|
|
30
30
|
sample;
|
|
31
|
+
/**
|
|
32
|
+
* Set when `applyProbeFromZero` fires so the very next `addSample` bypasses
|
|
33
|
+
* windowing and is forwarded directly to the delegate. Without this, a
|
|
34
|
+
* single dropped probe sample never reaches the delegate (the window
|
|
35
|
+
* requires `windowSize` samples to become ready), so the delegate stays at
|
|
36
|
+
* limit 1 and the exponential backoff never re-arms.
|
|
37
|
+
*/
|
|
38
|
+
probeActive = false;
|
|
39
|
+
// Forward recovery-probe support to the delegate when supported. Both
|
|
40
|
+
// methods are provided iff the delegate provides them, so the limiter's
|
|
41
|
+
// "both methods present" check works correctly.
|
|
42
|
+
probeFromZeroInterval;
|
|
43
|
+
applyProbeFromZero;
|
|
31
44
|
constructor(delegate, options = {}) {
|
|
32
45
|
this.delegate = delegate;
|
|
46
|
+
const { probeFromZeroInterval, applyProbeFromZero } = delegate;
|
|
47
|
+
if (typeof probeFromZeroInterval === "function" &&
|
|
48
|
+
typeof applyProbeFromZero === "function") {
|
|
49
|
+
this.probeFromZeroInterval = probeFromZeroInterval.bind(delegate);
|
|
50
|
+
this.applyProbeFromZero = () => {
|
|
51
|
+
this.probeActive = true;
|
|
52
|
+
applyProbeFromZero.call(delegate);
|
|
53
|
+
};
|
|
54
|
+
}
|
|
33
55
|
this.minWindowTime = options.minWindowTimeMs ?? DEFAULT_MIN_WINDOW_TIME;
|
|
34
56
|
this.maxWindowTime = options.maxWindowTimeMs ?? DEFAULT_MAX_WINDOW_TIME;
|
|
35
57
|
this.windowSize = options.windowSize ?? DEFAULT_WINDOW_SIZE;
|
|
@@ -38,17 +60,22 @@ export class WindowedLimit {
|
|
|
38
60
|
this.sampleWindowFactory =
|
|
39
61
|
options.sampleWindowFactory ?? makeAverageSampleWindow;
|
|
40
62
|
if (this.minWindowTime < 100) {
|
|
41
|
-
throw new
|
|
63
|
+
throw new RangeError("minWindowTime must be >= 100 ms");
|
|
42
64
|
}
|
|
43
65
|
if (this.maxWindowTime < 100) {
|
|
44
|
-
throw new
|
|
66
|
+
throw new RangeError("maxWindowTime must be >= 100 ms");
|
|
45
67
|
}
|
|
46
68
|
if (this.windowSize < 10) {
|
|
47
|
-
throw new
|
|
69
|
+
throw new RangeError("Window size must be >= 10");
|
|
48
70
|
}
|
|
49
71
|
this.sample = this.sampleWindowFactory();
|
|
50
72
|
}
|
|
51
73
|
addSample(startTime, rtt, inflight, didDrop, operationName) {
|
|
74
|
+
if (this.probeActive) {
|
|
75
|
+
this.probeActive = false;
|
|
76
|
+
this.delegate.addSample(startTime, rtt, inflight, didDrop);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
52
79
|
const endTime = startTime + rtt;
|
|
53
80
|
if (rtt < this.minRttThreshold) {
|
|
54
81
|
return;
|
|
@@ -76,7 +76,7 @@ class ImmutablePercentileSampleWindow {
|
|
|
76
76
|
*/
|
|
77
77
|
export function createPercentileSampleWindow(percentile, windowSize) {
|
|
78
78
|
if (percentile <= 0 || percentile >= 1) {
|
|
79
|
-
throw new
|
|
79
|
+
throw new RangeError("Percentile should belong to (0, 1.0)");
|
|
80
80
|
}
|
|
81
81
|
return new ImmutablePercentileSampleWindow({ percentile, windowSize });
|
|
82
82
|
}
|
|
@@ -59,7 +59,7 @@ export class PartitionedStrategy {
|
|
|
59
59
|
throw new Error("No partitions specified");
|
|
60
60
|
}
|
|
61
61
|
if (totalPercent > 1) {
|
|
62
|
-
throw new
|
|
62
|
+
throw new RangeError("Sum of partition percentages must be <= 1.0");
|
|
63
63
|
}
|
|
64
64
|
this.partitionResolver = options.partitionResolver;
|
|
65
65
|
this.knownPartitions = new Map(partitionEntries.map(([name, cfg]) => [
|
|
@@ -139,7 +139,7 @@ class Partition {
|
|
|
139
139
|
_limitAtGlobalSaturation;
|
|
140
140
|
constructor(init) {
|
|
141
141
|
if (init.percent < 0 || init.percent > 1) {
|
|
142
|
-
throw new
|
|
142
|
+
throw new RangeError("Partition percentage must be in the range [0.0, 1.0]");
|
|
143
143
|
}
|
|
144
144
|
this.name = init.name;
|
|
145
145
|
this.percent = init.percent;
|
|
@@ -147,7 +147,7 @@ class Partition {
|
|
|
147
147
|
this._limitAtGlobalSaturation = init.initialLimitAtGlobalSaturation;
|
|
148
148
|
if (this.burstMode.kind === "capped" &&
|
|
149
149
|
this.burstMode.maxBurstMultiplier < 1.0) {
|
|
150
|
-
throw new
|
|
150
|
+
throw new RangeError("maxBurstMultiplier must be >= 1.0");
|
|
151
151
|
}
|
|
152
152
|
const registry = init.registry;
|
|
153
153
|
this.inflightDistribution = registry.distribution(MetricIds.INFLIGHT_NAME, {
|
|
@@ -13,7 +13,7 @@ export class DelayedRejectStrategy {
|
|
|
13
13
|
this.delayMsForContext = options.delayMsForContext;
|
|
14
14
|
const max = options.maxConcurrentDelays ?? 100;
|
|
15
15
|
if (max < 1 || !Number.isFinite(max)) {
|
|
16
|
-
throw new
|
|
16
|
+
throw new RangeError("maxConcurrentDelays must be a finite number >= 1");
|
|
17
17
|
}
|
|
18
18
|
this.maxConcurrentDelays = max;
|
|
19
19
|
}
|
|
@@ -25,10 +25,10 @@ export class DecayingHistogram {
|
|
|
25
25
|
constructor(options) {
|
|
26
26
|
const { halfLife, minValue = 0.1, maxValue = 100_000, binsPerDecade = 5, logWarning, } = options;
|
|
27
27
|
if (halfLife <= 0) {
|
|
28
|
-
throw new
|
|
28
|
+
throw new RangeError("halfLife must be positive");
|
|
29
29
|
}
|
|
30
30
|
if (minValue <= 0 || maxValue <= minValue) {
|
|
31
|
-
throw new
|
|
31
|
+
throw new RangeError("Must have 0 < minValue < maxValue");
|
|
32
32
|
}
|
|
33
33
|
this.lambda = Math.LN2 / halfLife;
|
|
34
34
|
this.minValue = minValue;
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"dynamic rate limiting"
|
|
10
10
|
],
|
|
11
11
|
"author": "Ethan Resnick <ethan.resnick@gmail.com>",
|
|
12
|
-
"version": "0.12.
|
|
12
|
+
"version": "0.12.1",
|
|
13
13
|
"type": "module",
|
|
14
14
|
"exports": {
|
|
15
15
|
".": {
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"typescript": "^6.0.2"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
+
"es-toolkit": "^1.46.1",
|
|
36
37
|
"lru_map": "^0.4.1"
|
|
37
38
|
}
|
|
38
39
|
}
|