adaptive-concurrency 0.1.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.
Files changed (157) hide show
  1. package/dist/Limit.d.ts +29 -0
  2. package/dist/Limit.d.ts.map +1 -0
  3. package/dist/Limit.js +1 -0
  4. package/dist/LimitAllotment.d.ts +23 -0
  5. package/dist/LimitAllotment.d.ts.map +1 -0
  6. package/dist/LimitAllotment.js +1 -0
  7. package/dist/Limiter.d.ts +175 -0
  8. package/dist/Limiter.d.ts.map +1 -0
  9. package/dist/Limiter.js +240 -0
  10. package/dist/Listener.d.ts +23 -0
  11. package/dist/Listener.d.ts.map +1 -0
  12. package/dist/Listener.js +1 -0
  13. package/dist/ListenerSet.d.ts +12 -0
  14. package/dist/ListenerSet.d.ts.map +1 -0
  15. package/dist/ListenerSet.js +35 -0
  16. package/dist/MetricIds.d.ts +13 -0
  17. package/dist/MetricIds.d.ts.map +1 -0
  18. package/dist/MetricIds.js +12 -0
  19. package/dist/MetricRegistry.d.ts +66 -0
  20. package/dist/MetricRegistry.d.ts.map +1 -0
  21. package/dist/MetricRegistry.js +30 -0
  22. package/dist/RunResult.d.ts +33 -0
  23. package/dist/RunResult.d.ts.map +1 -0
  24. package/dist/RunResult.js +35 -0
  25. package/dist/StreamingLimit.d.ts +26 -0
  26. package/dist/StreamingLimit.d.ts.map +1 -0
  27. package/dist/StreamingLimit.js +1 -0
  28. package/dist/executors/AdaptiveExecutor.d.ts +50 -0
  29. package/dist/executors/AdaptiveExecutor.d.ts.map +1 -0
  30. package/dist/executors/AdaptiveExecutor.js +80 -0
  31. package/dist/index.d.ts +27 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +28 -0
  34. package/dist/limit/AIMDLimit.d.ts +37 -0
  35. package/dist/limit/AIMDLimit.d.ts.map +1 -0
  36. package/dist/limit/AIMDLimit.js +49 -0
  37. package/dist/limit/FixedLimit.d.ts +15 -0
  38. package/dist/limit/FixedLimit.d.ts.map +1 -0
  39. package/dist/limit/FixedLimit.js +23 -0
  40. package/dist/limit/Gradient2Limit.d.ts +122 -0
  41. package/dist/limit/Gradient2Limit.d.ts.map +1 -0
  42. package/dist/limit/Gradient2Limit.js +107 -0
  43. package/dist/limit/GradientLimit.d.ts +122 -0
  44. package/dist/limit/GradientLimit.d.ts.map +1 -0
  45. package/dist/limit/GradientLimit.js +108 -0
  46. package/dist/limit/SettableLimit.d.ts +18 -0
  47. package/dist/limit/SettableLimit.d.ts.map +1 -0
  48. package/dist/limit/SettableLimit.js +30 -0
  49. package/dist/limit/StreamingLimit.d.ts +26 -0
  50. package/dist/limit/StreamingLimit.d.ts.map +1 -0
  51. package/dist/limit/StreamingLimit.js +1 -0
  52. package/dist/limit/TracingLimitDecorator.d.ts +16 -0
  53. package/dist/limit/TracingLimitDecorator.d.ts.map +1 -0
  54. package/dist/limit/TracingLimitDecorator.js +23 -0
  55. package/dist/limit/VegasLimit.d.ts +85 -0
  56. package/dist/limit/VegasLimit.d.ts.map +1 -0
  57. package/dist/limit/VegasLimit.js +127 -0
  58. package/dist/limit/WindowedLimit.d.ts +48 -0
  59. package/dist/limit/WindowedLimit.d.ts.map +1 -0
  60. package/dist/limit/WindowedLimit.js +67 -0
  61. package/dist/limit/statistics/ExpMovingAverage.d.ts +21 -0
  62. package/dist/limit/statistics/ExpMovingAverage.d.ts.map +1 -0
  63. package/dist/limit/statistics/ExpMovingAverage.js +43 -0
  64. package/dist/limit/statistics/Minimum.d.ts +12 -0
  65. package/dist/limit/statistics/Minimum.d.ts.map +1 -0
  66. package/dist/limit/statistics/Minimum.js +22 -0
  67. package/dist/limit/statistics/MinimumValue.d.ts +12 -0
  68. package/dist/limit/statistics/MinimumValue.d.ts.map +1 -0
  69. package/dist/limit/statistics/MinimumValue.js +22 -0
  70. package/dist/limit/statistics/SingleMeasurement.d.ts +12 -0
  71. package/dist/limit/statistics/SingleMeasurement.d.ts.map +1 -0
  72. package/dist/limit/statistics/SingleMeasurement.js +21 -0
  73. package/dist/limit/statistics/StreamingStatistic.d.ts +29 -0
  74. package/dist/limit/statistics/StreamingStatistic.d.ts.map +1 -0
  75. package/dist/limit/statistics/StreamingStatistic.js +1 -0
  76. package/dist/limit/utils/index.d.ts +10 -0
  77. package/dist/limit/utils/index.d.ts.map +1 -0
  78. package/dist/limit/utils/index.js +19 -0
  79. package/dist/limit/window/AverageSampleWindow.d.ts +4 -0
  80. package/dist/limit/window/AverageSampleWindow.d.ts.map +1 -0
  81. package/dist/limit/window/AverageSampleWindow.js +46 -0
  82. package/dist/limit/window/PercentileSampleWindow.d.ts +38 -0
  83. package/dist/limit/window/PercentileSampleWindow.d.ts.map +1 -0
  84. package/dist/limit/window/PercentileSampleWindow.js +81 -0
  85. package/dist/limit/window/SampleWindow.d.ts +30 -0
  86. package/dist/limit/window/SampleWindow.d.ts.map +1 -0
  87. package/dist/limit/window/SampleWindow.js +1 -0
  88. package/dist/limiter/AbstractLimiter.d.ts +48 -0
  89. package/dist/limiter/AbstractLimiter.d.ts.map +1 -0
  90. package/dist/limiter/AbstractLimiter.js +78 -0
  91. package/dist/limiter/AbstractPartitionedLimiter.d.ts +66 -0
  92. package/dist/limiter/AbstractPartitionedLimiter.d.ts.map +1 -0
  93. package/dist/limiter/AbstractPartitionedLimiter.js +209 -0
  94. package/dist/limiter/BlockingLimiter.d.ts +55 -0
  95. package/dist/limiter/BlockingLimiter.d.ts.map +1 -0
  96. package/dist/limiter/BlockingLimiter.js +111 -0
  97. package/dist/limiter/DelayedRejectStrategy.d.ts +32 -0
  98. package/dist/limiter/DelayedRejectStrategy.d.ts.map +1 -0
  99. package/dist/limiter/DelayedRejectStrategy.js +60 -0
  100. package/dist/limiter/DelayedThenBlockingRejection.d.ts +19 -0
  101. package/dist/limiter/DelayedThenBlockingRejection.d.ts.map +1 -0
  102. package/dist/limiter/DelayedThenBlockingRejection.js +26 -0
  103. package/dist/limiter/FifoBlockingRejection.d.ts +26 -0
  104. package/dist/limiter/FifoBlockingRejection.d.ts.map +1 -0
  105. package/dist/limiter/FifoBlockingRejection.js +77 -0
  106. package/dist/limiter/LifoBlockingLimiter.d.ts +53 -0
  107. package/dist/limiter/LifoBlockingLimiter.d.ts.map +1 -0
  108. package/dist/limiter/LifoBlockingLimiter.js +108 -0
  109. package/dist/limiter/LifoBlockingRejection.d.ts +31 -0
  110. package/dist/limiter/LifoBlockingRejection.d.ts.map +1 -0
  111. package/dist/limiter/LifoBlockingRejection.js +63 -0
  112. package/dist/limiter/PartitionedStrategy.d.ts +90 -0
  113. package/dist/limiter/PartitionedStrategy.d.ts.map +1 -0
  114. package/dist/limiter/PartitionedStrategy.js +183 -0
  115. package/dist/limiter/SimpleLimiter.d.ts +31 -0
  116. package/dist/limiter/SimpleLimiter.d.ts.map +1 -0
  117. package/dist/limiter/SimpleLimiter.js +119 -0
  118. package/dist/limiter/factories/index.d.ts +7 -0
  119. package/dist/limiter/factories/index.d.ts.map +1 -0
  120. package/dist/limiter/factories/index.js +6 -0
  121. package/dist/limiter/factories/makeBlockingLimiter.d.ts +6 -0
  122. package/dist/limiter/factories/makeBlockingLimiter.d.ts.map +1 -0
  123. package/dist/limiter/factories/makeBlockingLimiter.js +8 -0
  124. package/dist/limiter/factories/makeLifoBlockingLimiter.d.ts +8 -0
  125. package/dist/limiter/factories/makeLifoBlockingLimiter.d.ts.map +1 -0
  126. package/dist/limiter/factories/makeLifoBlockingLimiter.js +15 -0
  127. package/dist/limiter/factories/makePartitionedBlockingLimiter.d.ts +12 -0
  128. package/dist/limiter/factories/makePartitionedBlockingLimiter.d.ts.map +1 -0
  129. package/dist/limiter/factories/makePartitionedBlockingLimiter.js +35 -0
  130. package/dist/limiter/factories/makePartitionedLifoBlockingLimiter.d.ts +14 -0
  131. package/dist/limiter/factories/makePartitionedLifoBlockingLimiter.d.ts.map +1 -0
  132. package/dist/limiter/factories/makePartitionedLifoBlockingLimiter.js +38 -0
  133. package/dist/limiter/factories/makePartitionedLimiter.d.ts +11 -0
  134. package/dist/limiter/factories/makePartitionedLimiter.d.ts.map +1 -0
  135. package/dist/limiter/factories/makePartitionedLimiter.js +30 -0
  136. package/dist/limiter/factories/makeSimpleLimiter.d.ts +3 -0
  137. package/dist/limiter/factories/makeSimpleLimiter.d.ts.map +1 -0
  138. package/dist/limiter/factories/makeSimpleLimiter.js +9 -0
  139. package/dist/limiter/factories.d.ts +31 -0
  140. package/dist/limiter/factories.d.ts.map +1 -0
  141. package/dist/limiter/factories.js +74 -0
  142. package/dist/statistics/ExpMovingAverage.d.ts +21 -0
  143. package/dist/statistics/ExpMovingAverage.d.ts.map +1 -0
  144. package/dist/statistics/ExpMovingAverage.js +43 -0
  145. package/dist/statistics/MinimumValue.d.ts +12 -0
  146. package/dist/statistics/MinimumValue.d.ts.map +1 -0
  147. package/dist/statistics/MinimumValue.js +22 -0
  148. package/dist/statistics/MostRecentValue.d.ts +12 -0
  149. package/dist/statistics/MostRecentValue.d.ts.map +1 -0
  150. package/dist/statistics/MostRecentValue.js +21 -0
  151. package/dist/statistics/StreamingStatistic.d.ts +29 -0
  152. package/dist/statistics/StreamingStatistic.d.ts.map +1 -0
  153. package/dist/statistics/StreamingStatistic.js +1 -0
  154. package/dist/utils/index.d.ts +10 -0
  155. package/dist/utils/index.d.ts.map +1 -0
  156. package/dist/utils/index.js +19 -0
  157. package/package.json +31 -0
@@ -0,0 +1,81 @@
1
+ /**
2
+ * SampleWindow that uses a configurable percentile of observed RTTs as the
3
+ * tracked RTT and the minimum RTT as the candidate RTT. Samples beyond the
4
+ * window size are discarded.
5
+ */
6
+ class ImmutablePercentileSampleWindow {
7
+ minRtt;
8
+ _maxInflight;
9
+ hasSeenDrop;
10
+ observedRtts;
11
+ _sampleCount;
12
+ percentile;
13
+ windowSize;
14
+ constructor(opts) {
15
+ const { minRtt, maxInflight, dropped, observedRtts, sampleCount, percentile, windowSize } = opts;
16
+ this.minRtt = minRtt ?? Infinity;
17
+ this._maxInflight = maxInflight ?? 0;
18
+ this.hasSeenDrop = dropped ?? false;
19
+ this.observedRtts = observedRtts ?? new Array(windowSize).fill(0);
20
+ this._sampleCount = sampleCount ?? 0;
21
+ this.percentile = percentile;
22
+ this.windowSize = windowSize;
23
+ }
24
+ addSample(rtt, inflight, didDrop) {
25
+ if (this._sampleCount >= this.windowSize) {
26
+ return this;
27
+ }
28
+ // Share the backing array between windows (safe because we only ever
29
+ // write to indices that haven't been written to yet in a given window
30
+ // lifecycle, and the array is never modified after the window is closed).
31
+ this.observedRtts[this._sampleCount] = rtt;
32
+ return new ImmutablePercentileSampleWindow({
33
+ minRtt: Math.min(this.minRtt, rtt),
34
+ maxInflight: Math.max(inflight, this._maxInflight),
35
+ dropped: this.hasSeenDrop || didDrop,
36
+ observedRtts: this.observedRtts,
37
+ sampleCount: this._sampleCount + 1,
38
+ percentile: this.percentile,
39
+ windowSize: this.windowSize,
40
+ });
41
+ }
42
+ get candidateRttMs() {
43
+ return this.minRtt;
44
+ }
45
+ get trackedRttMs() {
46
+ if (this._sampleCount === 0) {
47
+ return 0;
48
+ }
49
+ const sorted = this.observedRtts.slice(0, this._sampleCount).sort((a, b) => a - b);
50
+ const rttIndex = Math.round(this._sampleCount * this.percentile);
51
+ const zeroBasedIndex = rttIndex - 1;
52
+ return sorted[zeroBasedIndex];
53
+ }
54
+ get maxInFlight() {
55
+ return this._maxInflight;
56
+ }
57
+ get sampleCount() {
58
+ return this._sampleCount;
59
+ }
60
+ get dropped() {
61
+ return this.hasSeenDrop;
62
+ }
63
+ toString() {
64
+ return (`ImmutablePercentileSampleWindow [` +
65
+ `minRtt=${this.minRtt.toFixed(3)}` +
66
+ `, p${this.percentile} rtt=${this.trackedRttMs.toFixed(3)}` +
67
+ `, maxInFlight=${this._maxInflight}` +
68
+ `, sampleCount=${this._sampleCount}` +
69
+ `, didDrop=${this.hasSeenDrop}]`);
70
+ }
71
+ }
72
+ /**
73
+ * @param percentile Percentile to track, in the range (0, 1)
74
+ * @param windowSize Maximum number of samples per window
75
+ */
76
+ export function createPercentileSampleWindow(percentile, windowSize) {
77
+ if (percentile <= 0 || percentile >= 1) {
78
+ throw new Error("Percentile should belong to (0, 1.0)");
79
+ }
80
+ return new ImmutablePercentileSampleWindow({ percentile, windowSize });
81
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Immutable sample window used to track request samples over a time window.
3
+ * Each call to addSample returns a new instance with the updated state.
4
+ *
5
+ * @see WindowedLimit
6
+ */
7
+ export interface SampleWindow {
8
+ addSample(rtt: number, inflight: number, dropped: boolean): SampleWindow;
9
+ /**
10
+ * RTT used as a candidate for the minimum (e.g. the minimum observed RTT).
11
+ */
12
+ readonly candidateRttMs: number;
13
+ /**
14
+ * RTT used for limit decisions (e.g. average or percentile).
15
+ */
16
+ readonly trackedRttMs: number;
17
+ /**
18
+ * Maximum number of inflight requests seen in this window.
19
+ */
20
+ readonly maxInFlight: number;
21
+ /**
22
+ * Number of samples collected in this window.
23
+ */
24
+ readonly sampleCount: number;
25
+ /**
26
+ * Whether any sample in this window was a drop.
27
+ */
28
+ readonly dropped: boolean;
29
+ }
30
+ //# sourceMappingURL=SampleWindow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SampleWindow.d.ts","sourceRoot":"","sources":["../../../src/limit/window/SampleWindow.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,YAAY,CAAC;IAEzE;;OAEG;IACH,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAEhC;;OAEG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAE9B;;OAEG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAE7B;;OAEG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAE7B;;OAEG;IACH,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,48 @@
1
+ import type { Limiter } from "../Limiter.js";
2
+ import type { LimitAllotment } from "../LimitAllotment.js";
3
+ import type { MetricRegistry } from "../MetricRegistry.js";
4
+ import type { StreamingLimit } from "../limit/StreamingLimit.js";
5
+ export interface AbstractLimiterOptions<ContextT> {
6
+ limit?: StreamingLimit;
7
+ /**
8
+ * Clock function returning the current time in fractional milliseconds
9
+ * (like performance.now()). Default: performance.now
10
+ */
11
+ clock?: () => number;
12
+ name?: string;
13
+ metricRegistry?: MetricRegistry;
14
+ /**
15
+ * Predicate that, when returning true for a context, causes the request to
16
+ * bypass the limiter entirely. The request won't affect inflight count or
17
+ * the limit algorithm.
18
+ *
19
+ * Predicates should not rely strictly on the state of the limiter (such as
20
+ * inflight count) when evaluating whether to bypass. There is no guarantee
21
+ * that the state will be synchronized or consistent with respect to the
22
+ * bypass predicate.
23
+ */
24
+ bypassResolver?: (context: ContextT) => boolean;
25
+ }
26
+ export declare abstract class AbstractLimiter<ContextT> implements Limiter<ContextT> {
27
+ private inflight;
28
+ private readonly clock;
29
+ private readonly limitAlgorithm;
30
+ private readonly successCounter;
31
+ private readonly droppedCounter;
32
+ private readonly ignoredCounter;
33
+ private readonly rejectedCounter;
34
+ private readonly bypassCounter;
35
+ private readonly bypassResolver;
36
+ private _limit;
37
+ protected constructor(options?: AbstractLimiterOptions<ContextT>);
38
+ abstract acquire(this: Limiter<void>): LimitAllotment | undefined;
39
+ abstract acquire(context: ContextT): LimitAllotment | undefined;
40
+ protected shouldBypass(context: ContextT): boolean;
41
+ protected createRejectedAllotment(): undefined;
42
+ protected createBypassAllotment(): LimitAllotment;
43
+ protected createAllotment(): LimitAllotment;
44
+ getLimit(): number;
45
+ getInflight(): number;
46
+ protected onNewLimit(newLimit: number): void;
47
+ }
48
+ //# sourceMappingURL=AbstractLimiter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AbstractLimiter.d.ts","sourceRoot":"","sources":["../../src/limiter/AbstractLimiter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,OAAO,KAAK,EAAW,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEpE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAcjE,MAAM,WAAW,sBAAsB,CAAC,QAAQ;IAC9C,KAAK,CAAC,EAAE,cAAc,CAAC;IAEvB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC;IAErB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,cAAc,CAAC,EAAE,cAAc,CAAC;IAEhC;;;;;;;;;OASG;IACH,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,OAAO,CAAC;CACjD;AAED,8BAAsB,eAAe,CAAC,QAAQ,CAAE,YAAW,OAAO,CAAC,QAAQ,CAAC;IAC1E,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAiB;IAChD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAU;IACzC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAU;IACzC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAU;IACzC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAU;IAC1C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAU;IACxC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA+C;IAE9E,OAAO,CAAC,MAAM,CAAS;IAEvB,SAAS,aAAa,OAAO,GAAE,sBAAsB,CAAC,QAAQ,CAAM;IAkBpE,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,cAAc,GAAG,SAAS;IACjE,QAAQ,CAAC,OAAO,CAAC,OAAO,EAAE,QAAQ,GAAG,cAAc,GAAG,SAAS;IAE/D,SAAS,CAAC,YAAY,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO;IAIlD,SAAS,CAAC,uBAAuB,IAAI,SAAS;IAK9C,SAAS,CAAC,qBAAqB,IAAI,cAAc;IAKjD,SAAS,CAAC,eAAe,IAAI,cAAc;IAgC3C,QAAQ,IAAI,MAAM;IAIlB,WAAW,IAAI,MAAM;IAIrB,SAAS,CAAC,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;CAG7C"}
@@ -0,0 +1,78 @@
1
+ import { MetricIds } from "../MetricIds.js";
2
+ import { NoopMetricRegistry } from "../MetricRegistry.js";
3
+ import { VegasLimit } from "../limit/VegasLimit.js";
4
+ const ID_TAG = "id";
5
+ const STATUS_TAG = "status";
6
+ const NOOP_ALLOTMENT = {
7
+ reportSuccess() { },
8
+ reportIgnore() { },
9
+ reportDropped() { },
10
+ };
11
+ let idCounter = 0;
12
+ export class AbstractLimiter {
13
+ inflight = 0;
14
+ clock;
15
+ limitAlgorithm;
16
+ successCounter;
17
+ droppedCounter;
18
+ ignoredCounter;
19
+ rejectedCounter;
20
+ bypassCounter;
21
+ bypassResolver;
22
+ _limit;
23
+ constructor(options = {}) {
24
+ this.clock = options.clock ?? (() => performance.now());
25
+ this.limitAlgorithm = options.limit ?? new VegasLimit();
26
+ this._limit = this.limitAlgorithm.currentLimit;
27
+ this.limitAlgorithm.subscribe((newLimit) => this.onNewLimit(newLimit));
28
+ this.bypassResolver = options.bypassResolver;
29
+ const registry = options.metricRegistry ?? NoopMetricRegistry;
30
+ const name = options.name ?? `unnamed-${++idCounter}`;
31
+ registry.gauge(MetricIds.LIMIT_NAME, () => this.getLimit());
32
+ this.successCounter = registry.counter(MetricIds.CALL_NAME, { [ID_TAG]: name, [STATUS_TAG]: "success" });
33
+ this.droppedCounter = registry.counter(MetricIds.CALL_NAME, { [ID_TAG]: name, [STATUS_TAG]: "dropped" });
34
+ this.ignoredCounter = registry.counter(MetricIds.CALL_NAME, { [ID_TAG]: name, [STATUS_TAG]: "ignored" });
35
+ this.rejectedCounter = registry.counter(MetricIds.CALL_NAME, { [ID_TAG]: name, [STATUS_TAG]: "rejected" });
36
+ this.bypassCounter = registry.counter(MetricIds.CALL_NAME, { [ID_TAG]: name, [STATUS_TAG]: "bypassed" });
37
+ }
38
+ shouldBypass(context) {
39
+ return this.bypassResolver?.(context) ?? false;
40
+ }
41
+ createRejectedAllotment() {
42
+ this.rejectedCounter.increment();
43
+ return undefined;
44
+ }
45
+ createBypassAllotment() {
46
+ this.bypassCounter.increment();
47
+ return NOOP_ALLOTMENT;
48
+ }
49
+ createAllotment() {
50
+ const startTime = this.clock();
51
+ const currentInflight = ++this.inflight;
52
+ return {
53
+ reportSuccess: () => {
54
+ this.inflight--;
55
+ this.successCounter.increment();
56
+ this.limitAlgorithm.accumulateSample(startTime, this.clock() - startTime, currentInflight, false);
57
+ },
58
+ reportIgnore: () => {
59
+ this.inflight--;
60
+ this.ignoredCounter.increment();
61
+ },
62
+ reportDropped: () => {
63
+ this.inflight--;
64
+ this.droppedCounter.increment();
65
+ this.limitAlgorithm.accumulateSample(startTime, this.clock() - startTime, currentInflight, true);
66
+ },
67
+ };
68
+ }
69
+ getLimit() {
70
+ return this._limit;
71
+ }
72
+ getInflight() {
73
+ return this.inflight;
74
+ }
75
+ onNewLimit(newLimit) {
76
+ this._limit = newLimit;
77
+ }
78
+ }
@@ -0,0 +1,66 @@
1
+ import type { Limiter } from "../Limiter.js";
2
+ import type { LimitAllotment } from "../LimitAllotment.js";
3
+ import type { AbstractLimiterOptions } from "./AbstractLimiter.js";
4
+ import { AbstractLimiter } from "./AbstractLimiter.js";
5
+ export interface PartitionConfig {
6
+ /** Percentage of the total limit guaranteed to this partition. Must be in [0.0, 1.0]. */
7
+ percent: number;
8
+ /**
9
+ * Delay in milliseconds introduced as a sleep to slow down the caller when
10
+ * this partition is rejected. Not recommended when using an event loop.
11
+ */
12
+ rejectDelay?: number;
13
+ }
14
+ export interface AbstractPartitionedLimiterOptions<ContextT> extends AbstractLimiterOptions<ContextT> {
15
+ /**
16
+ * Function(s) to resolve a context to a partition name. Multiple resolvers
17
+ * are processed in order; the first non-null/non-undefined value is used.
18
+ * If all resolvers return null/undefined, the "unknown" partition is used.
19
+ */
20
+ partitionResolver?: ((context: ContextT) => string | undefined) | Array<(context: ContextT) => string | undefined>;
21
+ /**
22
+ * Partitions keyed by name.
23
+ */
24
+ partitions?: Record<string, PartitionConfig>;
25
+ /**
26
+ * Maximum number of in-flight requests that can be delayed when rejecting
27
+ * excessive traffic for a partition. Default: 100
28
+ */
29
+ maxDelayedRequests?: number;
30
+ }
31
+ export interface PartitionState {
32
+ name: string;
33
+ percent: number;
34
+ limit: number;
35
+ inflight: number;
36
+ backoffMillis: number;
37
+ limitExceeded: boolean;
38
+ }
39
+ /**
40
+ * Build a limiter from partitioned options. If partitions and resolvers are
41
+ * both provided, creates a partitioned limiter; otherwise falls back to a
42
+ * SimpleLimiter.
43
+ */
44
+ export declare function buildPartitionedLimiter<ContextT>(options: AbstractPartitionedLimiterOptions<ContextT>): Limiter<ContextT>;
45
+ /**
46
+ * Limiter that supports partitioning requests into groups, each with a
47
+ * guaranteed percentage of the total concurrency limit. Partitions are not
48
+ * hard limits: when the system is below the global limit, any partition can
49
+ * burst beyond its guaranteed share. Only when the global limit is reached
50
+ * are per-partition limits enforced.
51
+ */
52
+ export declare class PartitionedLimiter<ContextT> extends AbstractLimiter<ContextT> {
53
+ private readonly partitions;
54
+ private readonly unknownPartition;
55
+ private readonly partitionResolvers;
56
+ private readonly maxDelayedRequests;
57
+ private delayedRequests;
58
+ constructor(options: AbstractPartitionedLimiterOptions<ContextT>);
59
+ private resolvePartition;
60
+ acquire(this: PartitionedLimiter<void>): LimitAllotment | undefined;
61
+ acquire(context: ContextT): LimitAllotment | undefined;
62
+ protected onNewLimit(newLimit: number): void;
63
+ /** Returns partition state for observability and tests. */
64
+ getPartition(name: string): PartitionState | undefined;
65
+ }
66
+ //# sourceMappingURL=AbstractPartitionedLimiter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AbstractPartitionedLimiter.d.ts","sourceRoot":"","sources":["../../src/limiter/AbstractPartitionedLimiter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAI3D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAgGvD,MAAM,WAAW,eAAe;IAC9B,yFAAyF;IACzF,OAAO,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,iCAAiC,CAAC,QAAQ,CAAE,SAAQ,sBAAsB,CAAC,QAAQ,CAAC;IACnG;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,QAAQ,KAAK,MAAM,GAAG,SAAS,CAAC,GAAG,KAAK,CAAC,CAAC,OAAO,EAAE,QAAQ,KAAK,MAAM,GAAG,SAAS,CAAC,CAAC;IAEnH;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAE7C;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAC9C,OAAO,EAAE,iCAAiC,CAAC,QAAQ,CAAC,GACnD,OAAO,CAAC,QAAQ,CAAC,CAQnB;AAED;;;;;;GAMG;AACH,qBAAa,kBAAkB,CAAC,QAAQ,CAAE,SAAQ,eAAe,CAAC,QAAQ,CAAC;IACzE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAyB;IACpD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAY;IAC7C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAmD;IACtF,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAS;IAC5C,OAAO,CAAC,eAAe,CAAK;gBAEhB,OAAO,EAAE,iCAAiC,CAAC,QAAQ,CAAC;IAuChE,OAAO,CAAC,gBAAgB;IAaxB,OAAO,CAAC,IAAI,EAAE,kBAAkB,CAAC,IAAI,CAAC,GAAG,cAAc,GAAG,SAAS;IACnE,OAAO,CAAC,OAAO,EAAE,QAAQ,GAAG,cAAc,GAAG,SAAS;cAmDnC,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAOrD,2DAA2D;IAC3D,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS;CAcvD"}
@@ -0,0 +1,209 @@
1
+ import { MetricIds } from "../MetricIds.js";
2
+ import { NoopMetricRegistry } from "../MetricRegistry.js";
3
+ import { AbstractLimiter } from "./AbstractLimiter.js";
4
+ import { SimpleLimiter } from "./SimpleLimiter.js";
5
+ const PARTITION_TAG_NAME = "partition";
6
+ class Partition {
7
+ name;
8
+ busy = 0;
9
+ _percent = 0;
10
+ _limit = 0;
11
+ _backoffMillis = 0;
12
+ inflightDistribution;
13
+ constructor(name) {
14
+ this.name = name;
15
+ }
16
+ get percent() {
17
+ return this._percent;
18
+ }
19
+ setPercent(percent) {
20
+ this._percent = percent;
21
+ return this;
22
+ }
23
+ setBackoffMillis(backoffMillis) {
24
+ this._backoffMillis = backoffMillis;
25
+ return this;
26
+ }
27
+ get backoffMillis() {
28
+ return this._backoffMillis;
29
+ }
30
+ /**
31
+ * Calculate this partition's limit while rounding up and ensuring the value
32
+ * is at least 1. With this technique the sum of partition limits may end up
33
+ * being higher than the concurrency limit.
34
+ */
35
+ updateLimit(totalLimit) {
36
+ this._limit = Math.max(1, Math.ceil(totalLimit * this._percent));
37
+ }
38
+ get limit() {
39
+ return this._limit;
40
+ }
41
+ isLimitExceeded() {
42
+ return this.busy >= this._limit;
43
+ }
44
+ acquire() {
45
+ this.busy++;
46
+ this.inflightDistribution.addSample(this.busy);
47
+ }
48
+ /**
49
+ * Try to acquire a slot, returning false if the partition limit is exceeded.
50
+ */
51
+ tryAcquire() {
52
+ if (this.busy < this._limit) {
53
+ this.busy++;
54
+ this.inflightDistribution.addSample(this.busy);
55
+ return true;
56
+ }
57
+ return false;
58
+ }
59
+ release() {
60
+ this.busy--;
61
+ }
62
+ getInflight() {
63
+ return this.busy;
64
+ }
65
+ createMetrics(registry) {
66
+ this.inflightDistribution = registry.distribution(MetricIds.INFLIGHT_NAME, PARTITION_TAG_NAME, this.name);
67
+ registry.gauge(MetricIds.PARTITION_LIMIT_NAME, () => this._limit, PARTITION_TAG_NAME, this.name);
68
+ }
69
+ toString() {
70
+ return `Partition [pct=${this._percent}, limit=${this._limit}, busy=${this.busy}]`;
71
+ }
72
+ }
73
+ /**
74
+ * Build a limiter from partitioned options. If partitions and resolvers are
75
+ * both provided, creates a partitioned limiter; otherwise falls back to a
76
+ * SimpleLimiter.
77
+ */
78
+ export function buildPartitionedLimiter(options) {
79
+ const hasPartitions = options.partitions && Object.keys(options.partitions).length > 0;
80
+ const hasResolvers = options.partitionResolver !== undefined;
81
+ if (hasPartitions && hasResolvers) {
82
+ return new PartitionedLimiter(options);
83
+ }
84
+ return new SimpleLimiter(options);
85
+ }
86
+ /**
87
+ * Limiter that supports partitioning requests into groups, each with a
88
+ * guaranteed percentage of the total concurrency limit. Partitions are not
89
+ * hard limits: when the system is below the global limit, any partition can
90
+ * burst beyond its guaranteed share. Only when the global limit is reached
91
+ * are per-partition limits enforced.
92
+ */
93
+ export class PartitionedLimiter extends AbstractLimiter {
94
+ partitions;
95
+ unknownPartition;
96
+ partitionResolvers;
97
+ maxDelayedRequests;
98
+ delayedRequests = 0;
99
+ constructor(options) {
100
+ super(options);
101
+ const partitionEntries = Object.entries(options.partitions ?? {});
102
+ if (partitionEntries.length === 0) {
103
+ throw new Error("No partitions specified");
104
+ }
105
+ const totalPercent = partitionEntries.reduce((sum, [, cfg]) => sum + cfg.percent, 0);
106
+ if (totalPercent > 1.0) {
107
+ throw new Error("Sum of partition percentages must be <= 1.0");
108
+ }
109
+ const registry = options.metricRegistry ?? NoopMetricRegistry;
110
+ this.partitions = new Map();
111
+ for (const [name, cfg] of partitionEntries) {
112
+ if (cfg.percent < 0 || cfg.percent > 1) {
113
+ throw new Error("Partition percentage must be in the range [0.0, 1.0]");
114
+ }
115
+ const partition = new Partition(name);
116
+ partition.setPercent(cfg.percent);
117
+ if (cfg.rejectDelay !== undefined) {
118
+ partition.setBackoffMillis(cfg.rejectDelay);
119
+ }
120
+ partition.createMetrics(registry);
121
+ this.partitions.set(name, partition);
122
+ }
123
+ this.unknownPartition = new Partition("unknown");
124
+ this.unknownPartition.createMetrics(registry);
125
+ const resolver = options.partitionResolver;
126
+ this.partitionResolvers = Array.isArray(resolver) ? resolver : resolver ? [resolver] : [];
127
+ this.maxDelayedRequests = options.maxDelayedRequests ?? 100;
128
+ this.onNewLimit(this.getLimit());
129
+ }
130
+ resolvePartition(context) {
131
+ for (const resolver of this.partitionResolvers) {
132
+ const name = resolver(context);
133
+ if (name != null) {
134
+ const partition = this.partitions.get(name);
135
+ if (partition) {
136
+ return partition;
137
+ }
138
+ }
139
+ }
140
+ return this.unknownPartition;
141
+ }
142
+ acquire(context) {
143
+ const requestContext = context;
144
+ if (this.shouldBypass(requestContext)) {
145
+ return this.createBypassAllotment();
146
+ }
147
+ const partition = this.resolvePartition(requestContext);
148
+ // The partition limit is not a hard limit. It is only applied if the
149
+ // global limit is exceeded. This allows for excess capacity in each
150
+ // partition to allow for bursting over the limit, but only if there is
151
+ // spare global capacity.
152
+ let overLimit;
153
+ if (this.getInflight() >= this.getLimit()) {
154
+ // Over global limit, so respect partition limit
155
+ overLimit = !partition.tryAcquire();
156
+ }
157
+ else {
158
+ // Below global limit, so no need to respect partition limit
159
+ partition.acquire();
160
+ overLimit = false;
161
+ }
162
+ if (overLimit) {
163
+ if (partition.backoffMillis > 0 && this.delayedRequests < this.maxDelayedRequests) {
164
+ // In the Java version this sleeps the thread. In JS we don't block,
165
+ // but we track the delayed count for consistency. A future async
166
+ // variant could use setTimeout-based delay.
167
+ this.delayedRequests++;
168
+ this.delayedRequests--;
169
+ }
170
+ return this.createRejectedAllotment();
171
+ }
172
+ const inner = this.createAllotment();
173
+ return {
174
+ reportSuccess: () => {
175
+ inner.reportSuccess();
176
+ partition.release();
177
+ },
178
+ reportIgnore: () => {
179
+ inner.reportIgnore();
180
+ partition.release();
181
+ },
182
+ reportDropped: () => {
183
+ inner.reportDropped();
184
+ partition.release();
185
+ },
186
+ };
187
+ }
188
+ onNewLimit(newLimit) {
189
+ super.onNewLimit(newLimit);
190
+ for (const [, partition] of this.partitions) {
191
+ partition.updateLimit(newLimit);
192
+ }
193
+ }
194
+ /** Returns partition state for observability and tests. */
195
+ getPartition(name) {
196
+ const partition = this.partitions.get(name);
197
+ if (!partition) {
198
+ return undefined;
199
+ }
200
+ return {
201
+ name: partition.name,
202
+ percent: partition.percent,
203
+ limit: partition.limit,
204
+ inflight: partition.getInflight(),
205
+ backoffMillis: partition.backoffMillis,
206
+ limitExceeded: partition.isLimitExceeded(),
207
+ };
208
+ }
209
+ }
@@ -0,0 +1,55 @@
1
+ import type { Limiter } from "../Limiter.js";
2
+ import type { LimitAllotment } from "../LimitAllotment.js";
3
+ /**
4
+ * A limiter whose acquire method returns a promise, for use with blocking
5
+ * limiter wrappers that may need to wait for a token to become available.
6
+ */
7
+ export interface AcquireOptions {
8
+ signal?: AbortSignal;
9
+ }
10
+ export interface AsyncLimiter<ContextT = void> {
11
+ acquire(this: AsyncLimiter<void>): Promise<LimitAllotment | undefined>;
12
+ acquire(context: ContextT, options?: AcquireOptions): Promise<LimitAllotment | undefined>;
13
+ }
14
+ /**
15
+ * Limiter that blocks the caller when the limit has been reached. The caller
16
+ * is blocked (via a promise) until the limiter has been released, or a timeout
17
+ * is reached. This limiter is commonly used in batch clients that use the
18
+ * limiter as a back-pressure mechanism.
19
+ *
20
+ * Because JavaScript is single-threaded, "blocking" here means awaiting a
21
+ * promise that resolves when a token becomes available.
22
+ */
23
+ export declare class BlockingLimiter<ContextT> implements AsyncLimiter<ContextT> {
24
+ static readonly MAX_TIMEOUT: number;
25
+ /**
26
+ * Wrap a limiter such that acquire will block up to MAX_TIMEOUT if the
27
+ * limit was reached instead of returning undefined immediately.
28
+ */
29
+ static wrap<ContextT>(delegate: Limiter<ContextT>): BlockingLimiter<ContextT>;
30
+ /**
31
+ * Wrap a limiter such that acquire will block up to the provided timeout
32
+ * (in ms) if the limit was reached instead of returning undefined
33
+ * immediately.
34
+ */
35
+ static wrap<ContextT>(delegate: Limiter<ContextT>, timeout: number): BlockingLimiter<ContextT>;
36
+ private readonly delegate;
37
+ private readonly timeout;
38
+ /** Queue of waiters blocked until a token is released */
39
+ private readonly waiters;
40
+ private constructor();
41
+ /** Returns the timeout used when blocking to acquire a permit, in ms. */
42
+ getTimeout(): number;
43
+ /**
44
+ * Acquire a token, waiting up to the configured timeout if the limit has
45
+ * been reached. Returns a promise that resolves to a LimitAllotment if acquired,
46
+ * or undefined if the timeout elapsed or the operation was aborted.
47
+ */
48
+ acquire(this: BlockingLimiter<void>): Promise<LimitAllotment | undefined>;
49
+ acquire(context: ContextT, options?: AcquireOptions): Promise<LimitAllotment | undefined>;
50
+ private waitForRelease;
51
+ private wrapAllotment;
52
+ private unblock;
53
+ toString(): string;
54
+ }
55
+ //# sourceMappingURL=BlockingLimiter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BlockingLimiter.d.ts","sourceRoot":"","sources":["../../src/limiter/BlockingLimiter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAMD,MAAM,WAAW,YAAY,CAAC,QAAQ,GAAG,IAAI;IAC3C,OAAO,CAAC,IAAI,EAAE,YAAY,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;IACvE,OAAO,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;CAC3F;AAED;;;;;;;;GAQG;AACH,qBAAa,eAAe,CAAC,QAAQ,CAAE,YAAW,YAAY,CAAC,QAAQ,CAAC;IACtE,MAAM,CAAC,QAAQ,CAAC,WAAW,SAAkB;IAE7C;;;OAGG;IACH,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,eAAe,CAAC,QAAQ,CAAC;IAE7E;;;;OAIG;IACH,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,eAAe,CAAC,QAAQ,CAAC;IAa9F,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoB;IAC7C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IAEjC,yDAAyD;IACzD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAE7C,OAAO;IAKP,yEAAyE;IACzE,UAAU,IAAI,MAAM;IAIpB;;;;OAIG;IACH,OAAO,CAAC,IAAI,EAAE,eAAe,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IACzE,OAAO,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IA8BzF,OAAO,CAAC,cAAc;IAsCtB,OAAO,CAAC,aAAa;IAiBrB,OAAO,CAAC,OAAO;IAQf,QAAQ,IAAI,MAAM;CAGnB"}