adaptive-concurrency 0.10.1 → 0.12.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 (44) hide show
  1. package/dist/Limiter.d.ts +7 -0
  2. package/dist/Limiter.d.ts.map +1 -1
  3. package/dist/Limiter.js +19 -13
  4. package/dist/MetricRegistry.d.ts +6 -2
  5. package/dist/MetricRegistry.d.ts.map +1 -1
  6. package/dist/MetricRegistry.js +5 -1
  7. package/dist/index.d.ts +3 -2
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +3 -2
  10. package/dist/limit/AIMDLimit.d.ts +1 -1
  11. package/dist/limit/AIMDLimit.d.ts.map +1 -1
  12. package/dist/limit/AIMDLimit.js +1 -1
  13. package/dist/limit/FixedLimit.d.ts +1 -1
  14. package/dist/limit/FixedLimit.d.ts.map +1 -1
  15. package/dist/limit/FixedLimit.js +1 -1
  16. package/dist/limit/GradientLimit.d.ts +1 -1
  17. package/dist/limit/GradientLimit.d.ts.map +1 -1
  18. package/dist/limit/GradientLimit.js +1 -1
  19. package/dist/limit/GroupAwareLimit.d.ts +78 -0
  20. package/dist/limit/GroupAwareLimit.d.ts.map +1 -0
  21. package/dist/limit/GroupAwareLimit.js +208 -0
  22. package/dist/limit/SettableLimit.d.ts +1 -1
  23. package/dist/limit/SettableLimit.d.ts.map +1 -1
  24. package/dist/limit/SettableLimit.js +1 -1
  25. package/dist/limit/StreamingLimit.d.ts +3 -1
  26. package/dist/limit/StreamingLimit.d.ts.map +1 -1
  27. package/dist/limit/TracingLimitDecorator.d.ts +1 -1
  28. package/dist/limit/TracingLimitDecorator.d.ts.map +1 -1
  29. package/dist/limit/TracingLimitDecorator.js +2 -2
  30. package/dist/limit/VegasLimit.d.ts +1 -1
  31. package/dist/limit/VegasLimit.d.ts.map +1 -1
  32. package/dist/limit/VegasLimit.js +2 -10
  33. package/dist/limit/WindowedLimit.d.ts +8 -1
  34. package/dist/limit/WindowedLimit.d.ts.map +1 -1
  35. package/dist/limit/WindowedLimit.js +16 -4
  36. package/dist/limit/window/PercentileSampleWindow.d.ts.map +1 -1
  37. package/dist/limit/window/PercentileSampleWindow.js +40 -4
  38. package/dist/statistics/DecayingHistogram.d.ts +63 -0
  39. package/dist/statistics/DecayingHistogram.d.ts.map +1 -0
  40. package/dist/statistics/DecayingHistogram.js +144 -0
  41. package/dist/utils/index.d.ts +6 -0
  42. package/dist/utils/index.d.ts.map +1 -1
  43. package/dist/utils/index.js +13 -0
  44. package/package.json +4 -1
package/dist/Limiter.d.ts CHANGED
@@ -98,6 +98,12 @@ export interface LimiterOptions<ContextT> {
98
98
  * request. When omitted, rejected requests immediately receive `undefined`.
99
99
  */
100
100
  allotmentUnavailableStrategy?: AllotmentUnavailableStrategy<ContextT>;
101
+ /**
102
+ * Derives an operation name from the request context, passed to the limit
103
+ * algorithm's `addSample` so group-aware limits can distinguish
104
+ * heterogeneous workloads. When omitted, no operation name is provided.
105
+ */
106
+ operationNameFor?: (context: ContextT) => string | undefined;
101
107
  }
102
108
  /**
103
109
  * Concurrency limiter with pluggable strategies for gating decisions and
@@ -113,6 +119,7 @@ export declare class Limiter<Context = void> {
113
119
  private readonly acquireStrategy;
114
120
  private readonly rejectionStrategy;
115
121
  private readonly bypassResolver;
122
+ private readonly operationNameFor;
116
123
  private readonly acquireBypassedAllotment;
117
124
  private readonly successCounter;
118
125
  private readonly droppedCounter;
@@ -1 +1 @@
1
- {"version":3,"file":"Limiter.d.ts","sourceRoot":"","sources":["../src/Limiter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Limiter.d.ts","sourceRoot":"","sources":["../src/Limiter.ts"],"names":[],"mappings":"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"}
package/dist/Limiter.js CHANGED
@@ -20,6 +20,7 @@ export class Limiter {
20
20
  acquireStrategy;
21
21
  rejectionStrategy;
22
22
  bypassResolver;
23
+ operationNameFor;
23
24
  acquireBypassedAllotment;
24
25
  successCounter;
25
26
  droppedCounter;
@@ -38,6 +39,7 @@ export class Limiter {
38
39
  this.limitAlgorithm = options.limit ?? Limiter.makeDefaultLimit();
39
40
  this._limit = this.limitAlgorithm.currentLimit;
40
41
  this.bypassResolver = options.bypassResolver;
42
+ this.operationNameFor = options.operationNameFor;
41
43
  this.acquireStrategy =
42
44
  options.acquireStrategy ?? new SemaphoreStrategy(this._limit);
43
45
  this.rejectionStrategy = options.allotmentUnavailableStrategy;
@@ -72,13 +74,13 @@ export class Limiter {
72
74
  this.acquireTimeOnUnavailableDistribution = registry.distribution(MetricIds.ACQUIRE_TIME_NAME, { id: limiterName, status: "unavailable" });
73
75
  this.acquireBypassedAllotment = {
74
76
  releaseAndRecordSuccess: async () => {
75
- this.successCounter.increment();
77
+ this.successCounter.add(1);
76
78
  },
77
79
  releaseAndIgnore: async () => {
78
- this.ignoredCounter.increment();
80
+ this.ignoredCounter.add(1);
79
81
  },
80
82
  releaseAndRecordDropped: async () => {
81
- this.droppedCounter.increment();
83
+ this.droppedCounter.add(1);
82
84
  },
83
85
  };
84
86
  // Emit metric for initial limit.
@@ -89,7 +91,7 @@ export class Limiter {
89
91
  return undefined;
90
92
  const ctx = (options?.context ?? undefined);
91
93
  if (this.bypassResolver?.(ctx)) {
92
- this.acquireBypassedCounter.increment();
94
+ this.acquireBypassedCounter.add(1);
93
95
  return this.acquireBypassedAllotment;
94
96
  }
95
97
  const acquireStart = this.clock();
@@ -98,7 +100,7 @@ export class Limiter {
98
100
  inflight: this._inflight,
99
101
  };
100
102
  if (!(await this.acquireStrategy.tryAcquireAllotment(ctx, state))) {
101
- this.acquireFailedCounter.increment();
103
+ this.acquireFailedCounter.add(1);
102
104
  if (!this.rejectionStrategy) {
103
105
  this.acquireTimeOnUnavailableDistribution.addSample(this.clock() - acquireStart);
104
106
  return undefined;
@@ -121,7 +123,7 @@ export class Limiter {
121
123
  distribution.addSample(this.clock() - acquireStart);
122
124
  return result;
123
125
  }
124
- this.acquireSucceededCounter.increment();
126
+ this.acquireSucceededCounter.add(1);
125
127
  const allotment = this.createAllotment(ctx);
126
128
  // Record the acquire time as a success, since we did actually succeed even
127
129
  // if we abort below.
@@ -139,15 +141,19 @@ export class Limiter {
139
141
  inflight: this._inflight,
140
142
  };
141
143
  if (!(await this.acquireStrategy.tryAcquireAllotment(ctx, state))) {
142
- this.acquireFailedCounter.increment();
144
+ this.acquireFailedCounter.add(1);
143
145
  return undefined;
144
146
  }
145
- this.acquireSucceededCounter.increment();
147
+ this.acquireSucceededCounter.add(1);
146
148
  return this.createAllotment(ctx);
147
149
  }
148
150
  createAllotment(ctx) {
149
151
  const startTime = this.clock();
150
152
  const currentInflight = ++this._inflight;
153
+ const operationName = this.operationNameFor?.(ctx);
154
+ const incrementTags = operationName
155
+ ? { [MetricIds.OPERATION_NAME_TAG]: operationName }
156
+ : {};
151
157
  // Make sure an allotment can only be released once; future calls become a
152
158
  // no-op. This simplifies a lot of cleanup handling etc that'd otherwise be
153
159
  // much racier/more complicated. It could hide subtle correctness issue, but
@@ -161,7 +167,7 @@ export class Limiter {
161
167
  const endTime = this.clock();
162
168
  const rtt = endTime - startTime;
163
169
  this._inflight--;
164
- this.successCounter.increment();
170
+ this.successCounter.add(1, incrementTags);
165
171
  // If one onAllotmentReleased call fails, hard to know what to do here.
166
172
  // We're in some kind of inconsistent state, but we probably have to
167
173
  // soldier on.
@@ -170,7 +176,7 @@ export class Limiter {
170
176
  }
171
177
  catch { }
172
178
  try {
173
- this.limitAlgorithm.addSample(startTime, rtt, currentInflight, false);
179
+ this.limitAlgorithm.addSample(startTime, rtt, currentInflight, false, operationName);
174
180
  }
175
181
  catch { }
176
182
  try {
@@ -183,7 +189,7 @@ export class Limiter {
183
189
  return;
184
190
  releaseStarted = true;
185
191
  this._inflight--;
186
- this.ignoredCounter.increment();
192
+ this.ignoredCounter.add(1, incrementTags);
187
193
  // If one onAllotmentReleased call fails, hard to know what to do here.
188
194
  // We're in some kind of inconsistent state, but we probably have to
189
195
  // soldier on.
@@ -203,7 +209,7 @@ export class Limiter {
203
209
  const endTime = this.clock();
204
210
  const rtt = endTime - startTime;
205
211
  this._inflight--;
206
- this.droppedCounter.increment();
212
+ this.droppedCounter.add(1, incrementTags);
207
213
  // If one onAllotmentReleased call fails, hard to know what to do here.
208
214
  // We're in some kind of inconsistent state, but we probably have to
209
215
  // soldier on.
@@ -212,7 +218,7 @@ export class Limiter {
212
218
  }
213
219
  catch { }
214
220
  try {
215
- this.limitAlgorithm.addSample(startTime, rtt, currentInflight, true);
221
+ this.limitAlgorithm.addSample(startTime, rtt, currentInflight, true, operationName);
216
222
  }
217
223
  catch { }
218
224
  try {
@@ -7,6 +7,7 @@
7
7
  * failures from retry attempts.
8
8
  */
9
9
  export declare const MetricIds: {
10
+ readonly OPERATION_NAME_TAG: "operation";
10
11
  readonly LIMIT_NAME: "limit";
11
12
  readonly CALL_NAME: "call";
12
13
  readonly ACQUIRE_ATTEMPT_NAME: "acquire_attempt";
@@ -16,6 +17,9 @@ export declare const MetricIds: {
16
17
  readonly WINDOW_MIN_RTT_NAME: "min_window_rtt";
17
18
  readonly WINDOW_QUEUE_SIZE_NAME: "queue_size";
18
19
  readonly ACQUIRE_TIME_NAME: "acquire_time";
20
+ readonly CONGESTION_SIGNAL_NAME: "congestion_signal";
21
+ readonly WARMED_GROUPS_COUNT_NAME: "warmed_groups_count";
22
+ readonly GROUP_RTT_RATIO_NAME: "group_rtt_ratio";
19
23
  };
20
24
  /**
21
25
  * Listener to receive samples for a distribution.
@@ -28,11 +32,11 @@ export interface DistributionMetric {
28
32
  * translate into an actions-per-second metric.
29
33
  */
30
34
  export interface Counter {
31
- increment(): void;
35
+ add(value: number, attributes?: Record<string, string>): void;
32
36
  }
33
37
  /** Opaque handle for a registered gauge (supplier is polled by the registry on flush). */
34
38
  export interface Gauge {
35
- record(value: number): void;
39
+ record(value: number, attributes?: Record<string, string>): void;
36
40
  }
37
41
  /**
38
42
  * Simple abstraction for tracking metrics in the limiters.
@@ -1 +1 @@
1
- {"version":3,"file":"MetricRegistry.d.ts","sourceRoot":"","sources":["../src/MetricRegistry.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;CAUZ,CAAC;AAEX;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED;;;GAGG;AACH,MAAM,WAAW,OAAO;IACtB,SAAS,IAAI,IAAI,CAAC;CACnB;AAED,0FAA0F;AAC1F,MAAM,WAAW,KAAK;IACpB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B;;;;;;;;OAQG;IACH,YAAY,CACV,EAAE,EAAE,MAAM,EACV,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAClC,kBAAkB,CAAC;IAEtB;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,KAAK,CAAC;IAE9D;;;;;OAKG;IACH,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC;CACnE;AAMD;;;GAGG;AACH,eAAO,MAAM,kBAAkB,EAAE,cAUhC,CAAC"}
1
+ {"version":3,"file":"MetricRegistry.d.ts","sourceRoot":"","sources":["../src/MetricRegistry.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;CAcZ,CAAC;AAEX;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED;;;GAGG;AACH,MAAM,WAAW,OAAO;IACtB,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;CAC/D;AAED,0FAA0F;AAC1F,MAAM,WAAW,KAAK;IACpB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;CAClE;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B;;;;;;;;OAQG;IACH,YAAY,CACV,EAAE,EAAE,MAAM,EACV,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAClC,kBAAkB,CAAC;IAEtB;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,KAAK,CAAC;IAE9D;;;;;OAKG;IACH,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC;CACnE;AAMD;;;GAGG;AACH,eAAO,MAAM,kBAAkB,EAAE,cAUhC,CAAC"}
@@ -7,6 +7,7 @@
7
7
  * failures from retry attempts.
8
8
  */
9
9
  export const MetricIds = {
10
+ OPERATION_NAME_TAG: "operation",
10
11
  LIMIT_NAME: "limit",
11
12
  CALL_NAME: "call",
12
13
  ACQUIRE_ATTEMPT_NAME: "acquire_attempt",
@@ -16,9 +17,12 @@ export const MetricIds = {
16
17
  WINDOW_MIN_RTT_NAME: "min_window_rtt",
17
18
  WINDOW_QUEUE_SIZE_NAME: "queue_size",
18
19
  ACQUIRE_TIME_NAME: "acquire_time",
20
+ CONGESTION_SIGNAL_NAME: "congestion_signal",
21
+ WARMED_GROUPS_COUNT_NAME: "warmed_groups_count",
22
+ GROUP_RTT_RATIO_NAME: "group_rtt_ratio",
19
23
  };
20
24
  const NOOP_SAMPLE_LISTENER = { addSample() { } };
21
- const NOOP_COUNTER = { increment() { } };
25
+ const NOOP_COUNTER = { add() { } };
22
26
  const NOOP_GAUGE = { record() { } };
23
27
  /**
24
28
  * No-op MetricRegistry that discards all metrics. Used as the default when
package/dist/index.d.ts CHANGED
@@ -7,17 +7,18 @@ export { AdaptiveTimeoutError, dropped, ignore, isAdaptiveTimeoutError, isRunRes
7
7
  export { AIMDLimit, type AIMDLimitOptions } from "./limit/AIMDLimit.js";
8
8
  export { FixedLimit } from "./limit/FixedLimit.js";
9
9
  export { GradientLimit, type Gradient2LimitOptions, } from "./limit/GradientLimit.js";
10
+ export { GroupAwareLimit } from "./limit/GroupAwareLimit.js";
10
11
  export { SettableLimit } from "./limit/SettableLimit.js";
11
- export { TracingLimitDecorator } from "./limit/TracingLimitDecorator.js";
12
12
  export { VegasLimit, type VegasLimitOptions, type VegasLimitPolicy, } from "./limit/VegasLimit.js";
13
13
  export { WindowedLimit, type WindowedLimitOptions, } from "./limit/WindowedLimit.js";
14
+ export { DecayingHistogram } from "./statistics/DecayingHistogram.js";
14
15
  export { ExpMovingAverage } from "./statistics/ExpMovingAverage.js";
15
16
  export { MinimumValue } from "./statistics/MinimumValue.js";
16
17
  export type { StreamingStatistic } from "./statistics/StreamingStatistic.js";
17
18
  export { makeAverageSampleWindow } from "./limit/window/AverageSampleWindow.js";
18
19
  export { createPercentileSampleWindow } from "./limit/window/PercentileSampleWindow.js";
19
20
  export type { SampleWindow } from "./limit/window/SampleWindow.js";
20
- export { LinkedWaiterQueue, squareRoot, squareRootWithBaseline, } from "./utils/index.js";
21
+ export { LinkedWaiterQueue, log10Scale, squareRoot, squareRootWithBaseline, } from "./utils/index.js";
21
22
  export * from "./limiter/factories/index.js";
22
23
  export { PartitionedStrategy, type PartitionConfig, } from "./limiter/acquire-strategies/PartitionedStrategy.js";
23
24
  export { SemaphoreStrategy } from "./limiter/acquire-strategies/SemaphoreStrategy.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC/D,YAAY,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EACL,OAAO,EACP,WAAW,EACX,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,4BAA4B,EACjC,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,eAAe,GACrB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EACL,SAAS,EACT,kBAAkB,EAClB,KAAK,OAAO,EACZ,KAAK,kBAAkB,EACvB,KAAK,KAAK,EACV,KAAK,cAAc,GACpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,oBAAoB,EACpB,OAAO,EACP,MAAM,EACN,sBAAsB,EACtB,WAAW,EACX,iBAAiB,EACjB,OAAO,EACP,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,UAAU,GAChB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxE,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EACL,aAAa,EACb,KAAK,qBAAqB,GAC3B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAAE,qBAAqB,EAAE,MAAM,kCAAkC,CAAC;AACzE,OAAO,EACL,UAAU,EACV,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,GACtB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,aAAa,EACb,KAAK,oBAAoB,GAC1B,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAC5D,YAAY,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AAG7E,OAAO,EAAE,uBAAuB,EAAE,MAAM,uCAAuC,CAAC;AAChF,OAAO,EAAE,4BAA4B,EAAE,MAAM,0CAA0C,CAAC;AACxF,YAAY,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAEnE,OAAO,EACL,iBAAiB,EACjB,UAAU,EACV,sBAAsB,GACvB,MAAM,kBAAkB,CAAC;AAE1B,cAAc,8BAA8B,CAAC;AAG7C,OAAO,EACL,mBAAmB,EACnB,KAAK,eAAe,GACrB,MAAM,qDAAqD,CAAC;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,mDAAmD,CAAC;AAGtF,OAAO,EACL,wBAAwB,EACxB,WAAW,EACX,KAAK,+BAA+B,EACpC,KAAK,WAAW,GACjB,MAAM,yEAAyE,CAAC;AACjF,OAAO,EACL,qBAAqB,EACrB,KAAK,4BAA4B,GAClC,MAAM,sEAAsE,CAAC;AAC9E,OAAO,EAAE,4BAA4B,EAAE,MAAM,6EAA6E,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC/D,YAAY,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EACL,OAAO,EACP,WAAW,EACX,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,4BAA4B,EACjC,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,eAAe,GACrB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EACL,SAAS,EACT,kBAAkB,EAClB,KAAK,OAAO,EACZ,KAAK,kBAAkB,EACvB,KAAK,KAAK,EACV,KAAK,cAAc,GACpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,oBAAoB,EACpB,OAAO,EACP,MAAM,EACN,sBAAsB,EACtB,WAAW,EACX,iBAAiB,EACjB,OAAO,EACP,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,UAAU,GAChB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxE,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EACL,aAAa,EACb,KAAK,qBAAqB,GAC3B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EACL,UAAU,EACV,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,GACtB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,aAAa,EACb,KAAK,oBAAoB,GAC1B,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AACtE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAC5D,YAAY,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AAG7E,OAAO,EAAE,uBAAuB,EAAE,MAAM,uCAAuC,CAAC;AAChF,OAAO,EAAE,4BAA4B,EAAE,MAAM,0CAA0C,CAAC;AACxF,YAAY,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAEnE,OAAO,EACL,iBAAiB,EACjB,UAAU,EACV,UAAU,EACV,sBAAsB,GACvB,MAAM,kBAAkB,CAAC;AAE1B,cAAc,8BAA8B,CAAC;AAG7C,OAAO,EACL,mBAAmB,EACnB,KAAK,eAAe,GACrB,MAAM,qDAAqD,CAAC;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,mDAAmD,CAAC;AAGtF,OAAO,EACL,wBAAwB,EACxB,WAAW,EACX,KAAK,+BAA+B,EACpC,KAAK,WAAW,GACjB,MAAM,yEAAyE,CAAC;AACjF,OAAO,EACL,qBAAqB,EACrB,KAAK,4BAA4B,GAClC,MAAM,sEAAsE,CAAC;AAC9E,OAAO,EAAE,4BAA4B,EAAE,MAAM,6EAA6E,CAAC"}
package/dist/index.js CHANGED
@@ -6,17 +6,18 @@ export { AdaptiveTimeoutError, dropped, ignore, isAdaptiveTimeoutError, isRunRes
6
6
  export { AIMDLimit } from "./limit/AIMDLimit.js";
7
7
  export { FixedLimit } from "./limit/FixedLimit.js";
8
8
  export { GradientLimit, } from "./limit/GradientLimit.js";
9
+ export { GroupAwareLimit } from "./limit/GroupAwareLimit.js";
9
10
  export { SettableLimit } from "./limit/SettableLimit.js";
10
- export { TracingLimitDecorator } from "./limit/TracingLimitDecorator.js";
11
11
  export { VegasLimit, } from "./limit/VegasLimit.js";
12
12
  export { WindowedLimit, } from "./limit/WindowedLimit.js";
13
13
  // Streaming statistics
14
+ export { DecayingHistogram } from "./statistics/DecayingHistogram.js";
14
15
  export { ExpMovingAverage } from "./statistics/ExpMovingAverage.js";
15
16
  export { MinimumValue } from "./statistics/MinimumValue.js";
16
17
  // Sample window types
17
18
  export { makeAverageSampleWindow } from "./limit/window/AverageSampleWindow.js";
18
19
  export { createPercentileSampleWindow } from "./limit/window/PercentileSampleWindow.js";
19
- export { LinkedWaiterQueue, squareRoot, squareRootWithBaseline, } from "./utils/index.js";
20
+ export { LinkedWaiterQueue, log10Scale, squareRoot, squareRootWithBaseline, } from "./utils/index.js";
20
21
  export * from "./limiter/factories/index.js";
21
22
  // Acquire strategies
22
23
  export { PartitionedStrategy, } from "./limiter/acquire-strategies/PartitionedStrategy.js";
@@ -37,7 +37,7 @@ export declare class AIMDLimit implements AdaptiveLimit {
37
37
  private readonly maxLimit;
38
38
  private readonly backoffJitter;
39
39
  constructor(options?: AIMDLimitOptions);
40
- addSample(_startTime: number, rtt: number, inflight: number, didDrop: boolean): void;
40
+ addSample(_startTime: number, rtt: number, inflight: number, didDrop: boolean, _operationName?: string): void;
41
41
  get currentLimit(): number;
42
42
  private applyNewLimit;
43
43
  subscribe(consumer: (newLimit: number) => void, options?: {
@@ -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;CACxB;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;gBAE3B,OAAO,GAAE,gBAAqB;IA0B1C,SAAS,CACP,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,OAAO,GACf,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"}
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;CACxB;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;gBAE3B,OAAO,GAAE,gBAAqB;IA0B1C,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"}
@@ -28,7 +28,7 @@ export class AIMDLimit {
28
28
  throw new Error("backoffRatio + backoffJitter must be < 1.0 to guarantee the limit decreases on drop");
29
29
  }
30
30
  }
31
- addSample(_startTime, rtt, inflight, didDrop) {
31
+ addSample(_startTime, rtt, inflight, didDrop, _operationName) {
32
32
  let currentLimit = this._limit;
33
33
  if (didDrop || rtt > this.timeout) {
34
34
  const jitteredRatio = Math.max(0.5, Math.min(1 - Number.EPSILON, this.backoffRatio + (Math.random() * 2 - 1) * this.backoffJitter));
@@ -5,7 +5,7 @@ import type { AdaptiveLimit } from "./StreamingLimit.js";
5
5
  export declare class FixedLimit implements AdaptiveLimit {
6
6
  private _limit;
7
7
  constructor(limit: number);
8
- addSample(_startTime: number, _rtt: number, _inflight: number, _didDrop: boolean): void;
8
+ addSample(_startTime: number, _rtt: number, _inflight: number, _didDrop: boolean, _operationName?: string): void;
9
9
  get currentLimit(): number;
10
10
  subscribe(_consumer: (newLimit: number) => void, _options?: {
11
11
  signal?: AbortSignal;
@@ -1 +1 @@
1
- {"version":3,"file":"FixedLimit.d.ts","sourceRoot":"","sources":["../../src/limit/FixedLimit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAIzD;;GAEG;AACH,qBAAa,UAAW,YAAW,aAAa;IAC9C,OAAO,CAAC,MAAM,CAAS;gBAEX,KAAK,EAAE,MAAM;IAIzB,SAAS,CACP,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,OAAO,GAChB,IAAI;IAGP,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,SAAS,CACP,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,EACrC,QAAQ,GAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAO,GACtC,MAAM,IAAI;IAMb,QAAQ,IAAI,MAAM;CAGnB"}
1
+ {"version":3,"file":"FixedLimit.d.ts","sourceRoot":"","sources":["../../src/limit/FixedLimit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAIzD;;GAEG;AACH,qBAAa,UAAW,YAAW,aAAa;IAC9C,OAAO,CAAC,MAAM,CAAS;gBAEX,KAAK,EAAE,MAAM;IAIzB,SAAS,CACP,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,OAAO,EACjB,cAAc,CAAC,EAAE,MAAM,GACtB,IAAI;IAGP,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,SAAS,CACP,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,EACrC,QAAQ,GAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAO,GACtC,MAAM,IAAI;IAMb,QAAQ,IAAI,MAAM;CAGnB"}
@@ -7,7 +7,7 @@ export class FixedLimit {
7
7
  constructor(limit) {
8
8
  this._limit = limit;
9
9
  }
10
- addSample(_startTime, _rtt, _inflight, _didDrop) {
10
+ addSample(_startTime, _rtt, _inflight, _didDrop, _operationName) {
11
11
  }
12
12
  get currentLimit() {
13
13
  return this._limit;
@@ -108,7 +108,7 @@ export declare class GradientLimit implements AdaptiveLimit {
108
108
  private readonly shortRttSampleListener;
109
109
  private readonly queueSizeSampleListener;
110
110
  constructor(options?: Gradient2LimitOptions);
111
- addSample(_startTime: number, rtt: number, inflight: number, _didDrop: boolean): void;
111
+ addSample(_startTime: number, rtt: number, inflight: number, _didDrop: boolean, _operationName?: string): void;
112
112
  get currentLimit(): number;
113
113
  private applyNewLimit;
114
114
  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;IAEnC,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAqB;IAC3D,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAqB;IAC5D,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAAqB;gBAEjD,OAAO,GAAE,qBAA0B;IA4B/C,SAAS,CACP,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,OAAO,GAChB,IAAI;IASP,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"}
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;IAEnC,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAqB;IAC3D,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAqB;IAC5D,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAAqB;gBAEjD,OAAO,GAAE,qBAA0B;IA4B/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,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"}
@@ -46,7 +46,7 @@ export class GradientLimit {
46
46
  this.shortRttSampleListener = registry.distribution(MetricIds.WINDOW_MIN_RTT_NAME);
47
47
  this.queueSizeSampleListener = registry.distribution(MetricIds.WINDOW_QUEUE_SIZE_NAME);
48
48
  }
49
- addSample(_startTime, rtt, inflight, _didDrop) {
49
+ addSample(_startTime, rtt, inflight, _didDrop, _operationName) {
50
50
  const newLimitNoFloor = this.computeNextLimitUnrounded(rtt, inflight);
51
51
  this.estimatedLimit = newLimitNoFloor;
52
52
  const newLimit = Math.floor(newLimitNoFloor);
@@ -0,0 +1,78 @@
1
+ import type { MetricRegistry } from "../MetricRegistry.js";
2
+ import type { AdaptiveLimit } from "./StreamingLimit.js";
3
+ /**
4
+ * Mix-agnostic adaptive concurrency limit that detects congestion using
5
+ * per-group RTT ratios weighted by sample density.
6
+ *
7
+ * Each sample is associated with an operation group via `operationName`. Per-group
8
+ * state tracks a long-lived p10 RTT baseline (decaying histogram) and a short
9
+ * EMA of recent RTT. The congestion signal is the weighted average of
10
+ * `recentRtt / p10` across warmed-up groups, where weight is
11
+ * `sqrt(decayedSampleCount)`.
12
+ *
13
+ * The limit increases additively when the congestion signal is low and inflight
14
+ * is near the limit, and decreases via a configurable `decrease` function when
15
+ * the signal is high or a drop occurs. Before any groups are warmed up, the
16
+ * limit only responds to drops.
17
+ *
18
+ * Samples without an `operationName` do not contribute to any group's state
19
+ * (no histogram, EMA, or activity counter update). However, they still
20
+ * participate in limit decisions: drops always trigger a decrease, and
21
+ * non-drop samples can trigger an increase or decrease based on the current
22
+ * congestion signal from warmed-up groups. This means unnamed operations
23
+ * free-ride on the congestion detection provided by named groups without
24
+ * polluting group baselines.
25
+ *
26
+ * This design is immune to operation mix shifts: a transition from fast to slow
27
+ * operations does not cause a spurious RTT spike because each group is measured
28
+ * against its own baseline.
29
+ */
30
+ export declare class GroupAwareLimit implements AdaptiveLimit {
31
+ private _limit;
32
+ private readonly limitListeners;
33
+ private readonly groups;
34
+ private readonly minLimit;
35
+ private readonly maxLimit;
36
+ private readonly alpha;
37
+ private readonly beta;
38
+ private readonly decrease;
39
+ private readonly baselineHalfLife;
40
+ private readonly activityHalfLife;
41
+ private readonly recentRttWindow;
42
+ private readonly minGroupSamples;
43
+ private readonly clock;
44
+ private readonly registry;
45
+ private readonly congestionSignalGauge;
46
+ private readonly warmedGroupsCountGauge;
47
+ private readonly groupRttRatioGauge;
48
+ constructor(options?: {
49
+ initialLimit?: number;
50
+ minLimit?: number;
51
+ maxLimit?: number;
52
+ maxGroups?: number;
53
+ alpha?: number;
54
+ beta?: number;
55
+ decrease?: (limit: number, didDrop: boolean) => number;
56
+ baselineHalfLife?: number;
57
+ activityHalfLife?: number;
58
+ recentRttWindow?: number;
59
+ minGroupSamples?: number;
60
+ clock?: () => number;
61
+ metricRegistry?: MetricRegistry;
62
+ });
63
+ addSample(_startTime: number, rtt: number, inflight: number, didDrop: boolean, operationName?: string): void;
64
+ get currentLimit(): number;
65
+ subscribe(consumer: (newLimit: number) => void, options?: {
66
+ signal?: AbortSignal;
67
+ }): () => void;
68
+ private computeCongestionSignal;
69
+ private clamp;
70
+ private applyNewLimit;
71
+ toString(): string;
72
+ }
73
+ declare global {
74
+ interface Math {
75
+ sumPrecise?: (numbers: Iterable<number>) => number;
76
+ }
77
+ }
78
+ //# sourceMappingURL=GroupAwareLimit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GroupAwareLimit.d.ts","sourceRoot":"","sources":["../../src/limit/GroupAwareLimit.ts"],"names":[],"mappings":"AAIA,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;IAErC,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;KACjC;IA0BD,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,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;IA8C/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"}
@@ -0,0 +1,208 @@
1
+ import lruPkg from "lru_map";
2
+ const { LRUMap } = lruPkg;
3
+ import { ListenerSet } from "../ListenerSet.js";
4
+ import { MetricIds, NoopMetricRegistry } from "../MetricRegistry.js";
5
+ import { DecayingHistogram } from "../statistics/DecayingHistogram.js";
6
+ import { ExpMovingAverage } from "../statistics/ExpMovingAverage.js";
7
+ import { log10Scale } from "../utils/index.js";
8
+ function defaultDecrease(limit, didDrop) {
9
+ if (didDrop) {
10
+ return Math.floor(limit * 0.9);
11
+ }
12
+ return limit - log10Scale(Math.floor(limit));
13
+ }
14
+ /**
15
+ * Per-group tracking state. Fixed memory regardless of sample volume.
16
+ */
17
+ class GroupState {
18
+ /** Long-lived decaying histogram for the p10 baseline. */
19
+ histogram;
20
+ /** Short EMA tracking the group's current RTT. */
21
+ recentRtt;
22
+ /**
23
+ * Decaying counter of recent samples, used for weight and warmup.
24
+ * Decays with its own (shorter) half-life.
25
+ */
26
+ _activityCount = 0;
27
+ _lastActivityDecayTime;
28
+ activityLambda;
29
+ constructor(baselineHalfLife, activityHalfLife, recentRttWindow) {
30
+ this.histogram = new DecayingHistogram({ halfLife: baselineHalfLife });
31
+ this.recentRtt = new ExpMovingAverage(recentRttWindow, 5);
32
+ this.activityLambda = Math.LN2 / activityHalfLife;
33
+ }
34
+ recordSample(rtt, now) {
35
+ this.histogram.addSample(rtt, now);
36
+ this.recentRtt.addSample(rtt);
37
+ this.applyActivityDecay(now);
38
+ this._activityCount += 1;
39
+ }
40
+ activityCount(now) {
41
+ this.applyActivityDecay(now);
42
+ return this._activityCount;
43
+ }
44
+ applyActivityDecay(now) {
45
+ if (this._lastActivityDecayTime === undefined) {
46
+ this._lastActivityDecayTime = now;
47
+ return;
48
+ }
49
+ const elapsed = now - this._lastActivityDecayTime;
50
+ if (elapsed <= 0)
51
+ return;
52
+ this._activityCount *= Math.exp(-this.activityLambda * elapsed);
53
+ this._lastActivityDecayTime = now;
54
+ }
55
+ }
56
+ /**
57
+ * Mix-agnostic adaptive concurrency limit that detects congestion using
58
+ * per-group RTT ratios weighted by sample density.
59
+ *
60
+ * Each sample is associated with an operation group via `operationName`. Per-group
61
+ * state tracks a long-lived p10 RTT baseline (decaying histogram) and a short
62
+ * EMA of recent RTT. The congestion signal is the weighted average of
63
+ * `recentRtt / p10` across warmed-up groups, where weight is
64
+ * `sqrt(decayedSampleCount)`.
65
+ *
66
+ * The limit increases additively when the congestion signal is low and inflight
67
+ * is near the limit, and decreases via a configurable `decrease` function when
68
+ * the signal is high or a drop occurs. Before any groups are warmed up, the
69
+ * limit only responds to drops.
70
+ *
71
+ * Samples without an `operationName` do not contribute to any group's state
72
+ * (no histogram, EMA, or activity counter update). However, they still
73
+ * participate in limit decisions: drops always trigger a decrease, and
74
+ * non-drop samples can trigger an increase or decrease based on the current
75
+ * congestion signal from warmed-up groups. This means unnamed operations
76
+ * free-ride on the congestion detection provided by named groups without
77
+ * polluting group baselines.
78
+ *
79
+ * This design is immune to operation mix shifts: a transition from fast to slow
80
+ * operations does not cause a spurious RTT spike because each group is measured
81
+ * against its own baseline.
82
+ */
83
+ export class GroupAwareLimit {
84
+ _limit;
85
+ limitListeners = new ListenerSet();
86
+ groups;
87
+ minLimit;
88
+ maxLimit;
89
+ alpha;
90
+ beta;
91
+ decrease;
92
+ baselineHalfLife;
93
+ activityHalfLife;
94
+ recentRttWindow;
95
+ minGroupSamples;
96
+ clock;
97
+ registry;
98
+ congestionSignalGauge;
99
+ warmedGroupsCountGauge;
100
+ groupRttRatioGauge;
101
+ constructor(options) {
102
+ this.minLimit = options?.minLimit ?? 10;
103
+ this.maxLimit = options?.maxLimit ?? 200;
104
+ this._limit = this.clamp(options?.initialLimit ?? 20);
105
+ this.groups = new LRUMap(options?.maxGroups ?? 50);
106
+ this.alpha = options?.alpha ?? 1.1;
107
+ this.beta = options?.beta ?? 1.5;
108
+ this.decrease = options?.decrease ?? defaultDecrease;
109
+ this.baselineHalfLife = options?.baselineHalfLife ?? 600_000;
110
+ this.activityHalfLife = options?.activityHalfLife ?? 30_000;
111
+ this.recentRttWindow = options?.recentRttWindow ?? 100;
112
+ this.minGroupSamples = options?.minGroupSamples ?? 20;
113
+ this.clock = options?.clock ?? (() => performance.now());
114
+ this.registry = options?.metricRegistry ?? NoopMetricRegistry;
115
+ this.congestionSignalGauge = this.registry.gauge(MetricIds.CONGESTION_SIGNAL_NAME);
116
+ this.warmedGroupsCountGauge = this.registry.gauge(MetricIds.WARMED_GROUPS_COUNT_NAME);
117
+ this.groupRttRatioGauge = this.registry.gauge(MetricIds.GROUP_RTT_RATIO_NAME);
118
+ }
119
+ addSample(_startTime, rtt, inflight, didDrop, operationName) {
120
+ const now = this.clock();
121
+ // Drops are handled before group state updates so that dropped RTTs never
122
+ // pollute baselines or recent-RTT tracking. A dropped request's timing
123
+ // reflects when the drop was detected (e.g. a timeout firing), not how
124
+ // long the operation actually takes to complete.
125
+ if (didDrop) {
126
+ this.applyNewLimit(this.clamp(this.decrease(this._limit, true)));
127
+ return;
128
+ }
129
+ if (operationName !== undefined) {
130
+ let group = this.groups.get(operationName);
131
+ if (!group) {
132
+ group = new GroupState(this.baselineHalfLife, this.activityHalfLife, this.recentRttWindow);
133
+ this.groups.set(operationName, group);
134
+ }
135
+ group.recordSample(rtt, now);
136
+ }
137
+ const result = this.computeCongestionSignal(now);
138
+ if (result === undefined) {
139
+ // No warmed-up groups; hold (drop-only mode).
140
+ this.warmedGroupsCountGauge.record(0);
141
+ return;
142
+ }
143
+ const { warmedGroupInfos, signal } = result;
144
+ this.warmedGroupsCountGauge.record(warmedGroupInfos.length);
145
+ this.congestionSignalGauge.record(signal);
146
+ for (const { groupName: group, ratio } of warmedGroupInfos) {
147
+ this.groupRttRatioGauge.record(ratio, { group });
148
+ }
149
+ if (signal > this.beta) {
150
+ this.applyNewLimit(this.clamp(this.decrease(this._limit, false)));
151
+ }
152
+ else if (signal < this.alpha && inflight >= this._limit / 2) {
153
+ this.applyNewLimit(this.clamp(this._limit + 1));
154
+ }
155
+ }
156
+ get currentLimit() {
157
+ return this._limit;
158
+ }
159
+ subscribe(consumer, options = {}) {
160
+ return this.limitListeners.subscribe(consumer, options);
161
+ }
162
+ computeCongestionSignal(now) {
163
+ // See https://github.com/rsms/js-lru/pull/42/changes
164
+ const entries = [
165
+ ...this.groups.entries(),
166
+ ];
167
+ const warmedGroupInfos = entries
168
+ .map(([groupName, groupState]) => {
169
+ const activity = groupState.activityCount(now);
170
+ if (activity < this.minGroupSamples)
171
+ return undefined;
172
+ const p10 = groupState.histogram.percentile(0.1, now);
173
+ if (!Number.isFinite(p10) || p10 <= 0)
174
+ return undefined;
175
+ const recentRtt = groupState.recentRtt.currentValue;
176
+ if (recentRtt <= 0)
177
+ return undefined;
178
+ const ratio = recentRtt / p10;
179
+ const weight = Math.sqrt(activity);
180
+ return { groupName, ratio, weight };
181
+ })
182
+ // Remove non-warmed or otherwise-invalid groups
183
+ .filter((it) => it !== undefined);
184
+ const totalWeight = sum(warmedGroupInfos.map(({ weight }) => weight));
185
+ if (totalWeight <= 0)
186
+ return undefined;
187
+ const weightedRatioSum = sum(warmedGroupInfos.map(({ weight, ratio }) => weight * ratio));
188
+ return {
189
+ signal: weightedRatioSum / totalWeight,
190
+ warmedGroupInfos,
191
+ };
192
+ }
193
+ clamp(limit) {
194
+ return Math.min(this.maxLimit, Math.max(this.minLimit, Math.floor(limit)));
195
+ }
196
+ applyNewLimit(newLimit) {
197
+ if (newLimit !== this._limit) {
198
+ this._limit = newLimit;
199
+ this.limitListeners.notify(newLimit);
200
+ }
201
+ }
202
+ toString() {
203
+ return `GroupAwareLimit [limit=${this._limit}, groups=${this.groups.size}]`;
204
+ }
205
+ }
206
+ const sum = typeof Math.sumPrecise === "function"
207
+ ? Math.sumPrecise.bind(Math)
208
+ : (numbers) => numbers.reduce((acc, curr) => acc + curr, 0);
@@ -8,7 +8,7 @@ export declare class SettableLimit implements AdaptiveLimit {
8
8
  private readonly limitListeners;
9
9
  constructor(limit: number);
10
10
  get currentLimit(): number;
11
- addSample(_startTime: number, _rtt: number, _inflight: number, _didDrop: boolean): void;
11
+ addSample(_startTime: number, _rtt: number, _inflight: number, _didDrop: boolean, _operationName?: string): void;
12
12
  setLimit(limit: number): void;
13
13
  subscribe(consumer: (newLimit: number) => void, options?: {
14
14
  signal?: AbortSignal;
@@ -1 +1 @@
1
- {"version":3,"file":"SettableLimit.d.ts","sourceRoot":"","sources":["../../src/limit/SettableLimit.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD;;;GAGG;AACH,qBAAa,aAAc,YAAW,aAAa;IACjD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAiD;gBAEpE,KAAK,EAAE,MAAM;IAIzB,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,SAAS,CACP,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,OAAO,GAChB,IAAI;IAIP,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAO7B,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"}
1
+ {"version":3,"file":"SettableLimit.d.ts","sourceRoot":"","sources":["../../src/limit/SettableLimit.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD;;;GAGG;AACH,qBAAa,aAAc,YAAW,aAAa;IACjD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAiD;gBAEpE,KAAK,EAAE,MAAM;IAIzB,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,SAAS,CACP,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,OAAO,EACjB,cAAc,CAAC,EAAE,MAAM,GACtB,IAAI;IAIP,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAO7B,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"}
@@ -12,7 +12,7 @@ export class SettableLimit {
12
12
  get currentLimit() {
13
13
  return this._limit;
14
14
  }
15
- addSample(_startTime, _rtt, _inflight, _didDrop) {
15
+ addSample(_startTime, _rtt, _inflight, _didDrop, _operationName) {
16
16
  // No-op
17
17
  }
18
18
  setLimit(limit) {
@@ -20,7 +20,9 @@ export interface AdaptiveLimit {
20
20
  * @param rtt Round trip time in fractional milliseconds
21
21
  * @param inflight Number of inflight requests at the time the request started
22
22
  * @param didDrop Whether the request was dropped (timeout or rejection)
23
+ * @param operationName Optional name identifying the type of operation,
24
+ * used by group-aware limits to distinguish heterogeneous workloads.
23
25
  */
24
- addSample(startTime: number, rtt: number, inflight: number, didDrop: boolean): void;
26
+ addSample(startTime: number, rtt: number, inflight: number, didDrop: boolean, operationName?: string): void;
25
27
  }
26
28
  //# 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;;;;;;OAMG;IACH,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;CACrF"}
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,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7G"}
@@ -8,7 +8,7 @@ export declare class TracingLimitDecorator implements AdaptiveLimit {
8
8
  static wrap(delegate: AdaptiveLimit): TracingLimitDecorator;
9
9
  constructor(delegate: AdaptiveLimit);
10
10
  get currentLimit(): number;
11
- addSample(startTime: number, rtt: number, inflight: number, didDrop: boolean): void;
11
+ addSample(startTime: number, rtt: number, inflight: number, didDrop: boolean, operationName?: string): void;
12
12
  subscribe(consumer: (newLimit: number) => void, options?: {
13
13
  signal?: AbortSignal;
14
14
  }): () => void;
@@ -1 +1 @@
1
- {"version":3,"file":"TracingLimitDecorator.d.ts","sourceRoot":"","sources":["../../src/limit/TracingLimitDecorator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD;;;GAGG;AACH,qBAAa,qBAAsB,YAAW,aAAa;IACzD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgB;IAEzC,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,aAAa,GAAG,qBAAqB;gBAI/C,QAAQ,EAAE,aAAa;IAInC,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;IAKnF,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"}
1
+ {"version":3,"file":"TracingLimitDecorator.d.ts","sourceRoot":"","sources":["../../src/limit/TracingLimitDecorator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD;;;GAGG;AACH,qBAAa,qBAAsB,YAAW,aAAa;IACzD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgB;IAEzC,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,aAAa,GAAG,qBAAqB;gBAI/C,QAAQ,EAAE,aAAa;IAInC,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI;IAK3G,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"}
@@ -13,9 +13,9 @@ export class TracingLimitDecorator {
13
13
  get currentLimit() {
14
14
  return this.delegate.currentLimit;
15
15
  }
16
- addSample(startTime, rtt, inflight, didDrop) {
16
+ addSample(startTime, rtt, inflight, didDrop, operationName) {
17
17
  console.debug(`maxInFlight=${inflight} rtt=${rtt.toFixed(3)} ms`);
18
- this.delegate.addSample(startTime, rtt, inflight, didDrop);
18
+ this.delegate.addSample(startTime, rtt, inflight, didDrop, operationName);
19
19
  }
20
20
  subscribe(consumer, options) {
21
21
  return this.delegate.subscribe(consumer, options);
@@ -73,7 +73,7 @@ export declare class VegasLimit implements AdaptiveLimit {
73
73
  constructor(options?: VegasLimitOptions);
74
74
  private resetProbeJitter;
75
75
  private shouldProbe;
76
- addSample(startTime: number, rtt: number, inflight: number, didDrop: boolean): void;
76
+ addSample(startTime: number, rtt: number, inflight: number, didDrop: boolean, _operationName?: string): void;
77
77
  get currentLimit(): number;
78
78
  private applyNewLimit;
79
79
  subscribe(consumer: (newLimit: number) => void, options?: {
@@ -1 +1 @@
1
- {"version":3,"file":"VegasLimit.d.ts","sourceRoot":"","sources":["../../src/limit/VegasLimit.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAsB,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE/E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAezD;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,iBAAiB;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,MAAM,CAAC,EAAE;QACP;;;;WAIG;QACH,KAAK,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QAE9B;;;;WAIG;QACH,IAAI,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QAE7B;;WAEG;QACH,SAAS,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QAElC;;WAEG;QACH,QAAQ,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QAEjC;;;WAGG;QACH,QAAQ,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC;KACpD,CAAC;IAEF;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,cAAc,CAAC,EAAE,cAAc,CAAC;CACjC;AAED,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CACrC,WAAW,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CACzC,CAAC;AAEF,qBAAa,UAAW,YAAW,aAAa;IAC9C,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAqB;IAEpD,yDAAyD;IACzD,OAAO,CAAC,cAAc,CAAS;IAE/B,OAAO,CAAC,SAAS,CAAK;IAEtB,8DAA8D;IAC9D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAElC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmB;IAC1C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAqB;IAEvD,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;gBAE7B,OAAO,GAAE,iBAAsB;IAyB3C,OAAO,CAAC,gBAAgB;IAKxB,OAAO,CAAC,WAAW;IAOnB,SAAS,CACP,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,OAAO,GACf,IAAI;IAMP,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,gBAAgB;IA+BxB,OAAO,CAAC,oBAAoB;IA2C5B,QAAQ,IAAI,MAAM;CAGnB"}
1
+ {"version":3,"file":"VegasLimit.d.ts","sourceRoot":"","sources":["../../src/limit/VegasLimit.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAsB,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE/E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAIzD;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,iBAAiB;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,MAAM,CAAC,EAAE;QACP;;;;WAIG;QACH,KAAK,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QAE9B;;;;WAIG;QACH,IAAI,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QAE7B;;WAEG;QACH,SAAS,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QAElC;;WAEG;QACH,QAAQ,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QAEjC;;;WAGG;QACH,QAAQ,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC;KACpD,CAAC;IAEF;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,cAAc,CAAC,EAAE,cAAc,CAAC;CACjC;AAED,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CACrC,WAAW,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CACzC,CAAC;AAEF,qBAAa,UAAW,YAAW,aAAa;IAC9C,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAqB;IAEpD,yDAAyD;IACzD,OAAO,CAAC,cAAc,CAAS;IAE/B,OAAO,CAAC,SAAS,CAAK;IAEtB,8DAA8D;IAC9D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAElC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmB;IAC1C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAqB;IAEvD,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;gBAE7B,OAAO,GAAE,iBAAsB;IAyB3C,OAAO,CAAC,gBAAgB;IAKxB,OAAO,CAAC,WAAW;IAOnB,SAAS,CACP,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,OAAO,EAChB,cAAc,CAAC,EAAE,MAAM,GACtB,IAAI;IAMP,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,gBAAgB;IA+BxB,OAAO,CAAC,oBAAoB;IA2C5B,QAAQ,IAAI,MAAM;CAGnB"}
@@ -1,14 +1,6 @@
1
1
  import { ListenerSet } from "../ListenerSet.js";
2
2
  import { MetricIds, NoopMetricRegistry } from "../MetricRegistry.js";
3
- /**
4
- * Sublinear scale of the concurrency limit (floor of log10, lower-bounded for
5
- * small n). Same idea as Netflix's Log10RootIntFunction. Used only for
6
- * {@link VegasLimit} default policy.
7
- */
8
- const log10ScaleLookup = Array.from({ length: 1000 }, (_, i) => Math.max(1, Math.floor(Math.log10(i))));
9
- function log10Scale(n) {
10
- return n < 1000 ? log10ScaleLookup[n] : Math.floor(Math.log10(n));
11
- }
3
+ import { log10Scale } from "../utils/index.js";
12
4
  export class VegasLimit {
13
5
  _limit;
14
6
  limitListeners = new ListenerSet();
@@ -49,7 +41,7 @@ export class VegasLimit {
49
41
  return (this.probeJitter * this.probeMultiplier * this.estimatedLimit <=
50
42
  this.probeCount);
51
43
  }
52
- addSample(startTime, rtt, inflight, didDrop) {
44
+ addSample(startTime, rtt, inflight, didDrop, _operationName) {
53
45
  this.applyNewLimit(this.computeNextLimit(startTime, rtt, inflight, didDrop));
54
46
  }
55
47
  get currentLimit() {
@@ -26,6 +26,13 @@ export interface WindowedLimitOptions {
26
26
  * forwarding aggregated results to a delegate StreamingLimit. This reduces
27
27
  * noise from individual samples and ensures the delegate only sees
28
28
  * representative data.
29
+ *
30
+ * **Important:** Because a window aggregates samples across many requests,
31
+ * the `operationName` received in each `addSample` call is intentionally
32
+ * **not** forwarded to the delegate. This means `WindowedLimit` is
33
+ * incompatible with delegates that rely on per-sample operation names
34
+ * (e.g. `GroupAwareLimit`). Wrapping a `GroupAwareLimit` in a
35
+ * `WindowedLimit` will silently disable all group-aware behavior.
29
36
  */
30
37
  export declare class WindowedLimit implements AdaptiveLimit {
31
38
  private readonly delegate;
@@ -39,7 +46,7 @@ export declare class WindowedLimit implements AdaptiveLimit {
39
46
  /** Object tracking stats for the current sample window */
40
47
  private sample;
41
48
  constructor(delegate: AdaptiveLimit, options?: WindowedLimitOptions);
42
- addSample(startTime: number, rtt: number, inflight: number, didDrop: boolean): void;
49
+ addSample(startTime: number, rtt: number, inflight: number, didDrop: boolean, operationName?: string): void;
43
50
  get currentLimit(): number;
44
51
  subscribe(consumer: (newLimit: number) => void, options?: {
45
52
  signal?: AbortSignal;
@@ -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;;;;;GAKG;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;gBAEjB,QAAQ,EAAE,aAAa,EAAE,OAAO,GAAE,oBAAyB;IAsBvE,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;IAkCnF,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"}
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;gBAEjB,QAAQ,EAAE,aAAa,EAAE,OAAO,GAAE,oBAAyB;IAwBvE,SAAS,CACP,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,OAAO,EAChB,aAAa,CAAC,EAAE,MAAM,GACrB,IAAI;IAsCP,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"}
@@ -9,6 +9,13 @@ const DEFAULT_MIN_RTT_THRESHOLD = 0.1; // 100 microseconds in ms
9
9
  * forwarding aggregated results to a delegate StreamingLimit. This reduces
10
10
  * noise from individual samples and ensures the delegate only sees
11
11
  * representative data.
12
+ *
13
+ * **Important:** Because a window aggregates samples across many requests,
14
+ * the `operationName` received in each `addSample` call is intentionally
15
+ * **not** forwarded to the delegate. This means `WindowedLimit` is
16
+ * incompatible with delegates that rely on per-sample operation names
17
+ * (e.g. `GroupAwareLimit`). Wrapping a `GroupAwareLimit` in a
18
+ * `WindowedLimit` will silently disable all group-aware behavior.
12
19
  */
13
20
  export class WindowedLimit {
14
21
  delegate;
@@ -26,8 +33,10 @@ export class WindowedLimit {
26
33
  this.minWindowTime = options.minWindowTimeMs ?? DEFAULT_MIN_WINDOW_TIME;
27
34
  this.maxWindowTime = options.maxWindowTimeMs ?? DEFAULT_MAX_WINDOW_TIME;
28
35
  this.windowSize = options.windowSize ?? DEFAULT_WINDOW_SIZE;
29
- this.minRttThreshold = options.minRttThresholdMs ?? DEFAULT_MIN_RTT_THRESHOLD;
30
- this.sampleWindowFactory = options.sampleWindowFactory ?? makeAverageSampleWindow;
36
+ this.minRttThreshold =
37
+ options.minRttThresholdMs ?? DEFAULT_MIN_RTT_THRESHOLD;
38
+ this.sampleWindowFactory =
39
+ options.sampleWindowFactory ?? makeAverageSampleWindow;
31
40
  if (this.minWindowTime < 100) {
32
41
  throw new Error("minWindowTime must be >= 100 ms");
33
42
  }
@@ -39,7 +48,7 @@ export class WindowedLimit {
39
48
  }
40
49
  this.sample = this.sampleWindowFactory();
41
50
  }
42
- addSample(startTime, rtt, inflight, didDrop) {
51
+ addSample(startTime, rtt, inflight, didDrop, operationName) {
43
52
  const endTime = startTime + rtt;
44
53
  if (rtt < this.minRttThreshold) {
45
54
  return;
@@ -53,7 +62,10 @@ export class WindowedLimit {
53
62
  this.nextUpdateTime =
54
63
  endTime +
55
64
  Math.min(Math.max(current.candidateRttMs * 2, this.minWindowTime), this.maxWindowTime);
56
- const isWindowReady = current.candidateRttMs < Infinity && current.sampleCount >= this.windowSize;
65
+ const isWindowReady = current.candidateRttMs < Infinity &&
66
+ current.sampleCount >= this.windowSize;
67
+ // The window has a mix of operations, so we can't provide a single
68
+ // operation name to the delegate.
57
69
  if (isWindowReady) {
58
70
  this.delegate.addSample(startTime, current.trackedRttMs, current.maxInFlight, current.dropped);
59
71
  }
@@ -1 +1 @@
1
- {"version":3,"file":"PercentileSampleWindow.d.ts","sourceRoot":"","sources":["../../../src/limit/window/PercentileSampleWindow.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD;;;;GAIG;AACH,cAAM,+BAAgC,YAAW,YAAY;IAC3D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAU;IACtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAW;IACxC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;gBAExB,IAAI,EAAE;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,UAAU,EAAE,MAAM,CAAC;QAGnB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;KACzB;IAWD,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;IAuBzD,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,IAAI,YAAY,IAAI,MAAM,CAQzB;IAED,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,QAAQ,IAAI,MAAM;CAUnB;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAC1C,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,+BAA+B,CAMjC"}
1
+ {"version":3,"file":"PercentileSampleWindow.d.ts","sourceRoot":"","sources":["../../../src/limit/window/PercentileSampleWindow.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD;;;;GAIG;AACH,cAAM,+BAAgC,YAAW,YAAY;IAC3D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAU;IACtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAW;IACxC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;gBAExB,IAAI,EAAE;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,UAAU,EAAE,MAAM,CAAC;QAGnB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;KACzB;IAWD,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;IAuBzD,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,IAAI,YAAY,IAAI,MAAM,CASzB;IAED,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,QAAQ,IAAI,MAAM;CAUnB;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAC1C,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,+BAA+B,CAMjC"}
@@ -46,10 +46,11 @@ class ImmutablePercentileSampleWindow {
46
46
  if (this._sampleCount === 0) {
47
47
  return 0;
48
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];
49
+ const k = Math.round(this._sampleCount * this.percentile) - 1;
50
+ // Slice first so quickselect's in-place mutations don't affect
51
+ // other window instances that share the same backing array.
52
+ const copy = this.observedRtts.slice(0, this._sampleCount);
53
+ return quickselect(copy, k, 0, this._sampleCount - 1);
53
54
  }
54
55
  get maxInFlight() {
55
56
  return this._maxInflight;
@@ -79,3 +80,38 @@ export function createPercentileSampleWindow(percentile, windowSize) {
79
80
  }
80
81
  return new ImmutablePercentileSampleWindow({ percentile, windowSize });
81
82
  }
83
+ /**
84
+ * In-place Hoare-partition quickselect. Returns the k-th smallest element
85
+ * (0-indexed) within arr[lo..hi]. Mutates the array, but only within
86
+ * the [lo, hi] range.
87
+ */
88
+ function quickselect(arr, k, lo, hi) {
89
+ while (lo < hi) {
90
+ const pivot = arr[lo + ((hi - lo) >> 1)];
91
+ let i = lo;
92
+ let j = hi;
93
+ while (i <= j) {
94
+ while (arr[i] < pivot)
95
+ i++;
96
+ while (arr[j] > pivot)
97
+ j--;
98
+ if (i <= j) {
99
+ const tmp = arr[i];
100
+ arr[i] = arr[j];
101
+ arr[j] = tmp;
102
+ i++;
103
+ j--;
104
+ }
105
+ }
106
+ if (k <= j) {
107
+ hi = j;
108
+ }
109
+ else if (k >= i) {
110
+ lo = i;
111
+ }
112
+ else {
113
+ break;
114
+ }
115
+ }
116
+ return arr[k];
117
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * A histogram with log-spaced bins and continuous exponential time decay.
3
+ *
4
+ * Each bin stores a decayed count that is incremented when a sample falls
5
+ * within its range. All counts decay toward zero over time using an
6
+ * exponential half-life, so older observations naturally lose influence.
7
+ *
8
+ * Provides approximate percentile queries in O(numBins) time and fixed memory
9
+ * (~25 floats), independent of sample volume.
10
+ */
11
+ export declare class DecayingHistogram {
12
+ /** Lower boundary (inclusive) of each bin. The last bin extends to +Infinity. */
13
+ private readonly binEdges;
14
+ private readonly binCounts;
15
+ private readonly numBins;
16
+ private readonly minValue;
17
+ private readonly maxValue;
18
+ private readonly logWarning;
19
+ /** Precomputed decay constant: ln(2) / halfLife */
20
+ private readonly lambda;
21
+ /** Timestamp of the last decay application, or undefined before the first sample. */
22
+ private lastDecayTime;
23
+ /** Accumulated decayed total (sum of all bin counts after last decay). */
24
+ private _totalCount;
25
+ constructor(options: {
26
+ /** Half-life for exponential decay, in milliseconds. */
27
+ halfLife: number;
28
+ minValue?: number;
29
+ maxValue?: number;
30
+ binsPerDecade?: number;
31
+ logWarning?: (message: string) => void;
32
+ });
33
+ /**
34
+ * Record a sample value at the given time.
35
+ */
36
+ addSample(value: number, now: number): void;
37
+ /**
38
+ * Query an approximate percentile from the decayed histogram.
39
+ *
40
+ * Returns the geometric midpoint of the bin containing the target
41
+ * percentile, which reduces quantization bias from log-spaced bin edges.
42
+ *
43
+ * @param p Percentile in (0, 1), e.g. 0.1 for p10.
44
+ * @param now Current time for decay application.
45
+ * @returns The approximate value at the requested percentile, or NaN if
46
+ * the histogram is empty.
47
+ */
48
+ percentile(p: number, now: number): number;
49
+ /**
50
+ * The total decayed sample count across all bins.
51
+ */
52
+ get totalCount(): number;
53
+ /**
54
+ * Apply time-based decay to all bins. Idempotent if called multiple times
55
+ * at the same timestamp.
56
+ */
57
+ private applyDecay;
58
+ /**
59
+ * Find the bin index for a value using binary search.
60
+ */
61
+ private findBin;
62
+ }
63
+ //# sourceMappingURL=DecayingHistogram.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DecayingHistogram.d.ts","sourceRoot":"","sources":["../../src/statistics/DecayingHistogram.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,qBAAa,iBAAiB;IAC5B,iFAAiF;IACjF,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAe;IACxC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;IACzC,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,UAAU,CAA0C;IAErE,mDAAmD;IACnD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAEhC,qFAAqF;IACrF,OAAO,CAAC,aAAa,CAAqB;IAE1C,0EAA0E;IAC1E,OAAO,CAAC,WAAW,CAAK;gBAEZ,OAAO,EAAE;QACnB,wDAAwD;QACxD,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;KACxC;IAiCD;;OAEG;IACH,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;IAsB3C;;;;;;;;;;OAUG;IACH,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM;IA2B1C;;OAEG;IACH,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED;;;OAGG;IACH,OAAO,CAAC,UAAU;IAsBlB;;OAEG;IACH,OAAO,CAAC,OAAO;CAehB"}
@@ -0,0 +1,144 @@
1
+ /**
2
+ * A histogram with log-spaced bins and continuous exponential time decay.
3
+ *
4
+ * Each bin stores a decayed count that is incremented when a sample falls
5
+ * within its range. All counts decay toward zero over time using an
6
+ * exponential half-life, so older observations naturally lose influence.
7
+ *
8
+ * Provides approximate percentile queries in O(numBins) time and fixed memory
9
+ * (~25 floats), independent of sample volume.
10
+ */
11
+ export class DecayingHistogram {
12
+ /** Lower boundary (inclusive) of each bin. The last bin extends to +Infinity. */
13
+ binEdges;
14
+ binCounts;
15
+ numBins;
16
+ minValue;
17
+ maxValue;
18
+ logWarning;
19
+ /** Precomputed decay constant: ln(2) / halfLife */
20
+ lambda;
21
+ /** Timestamp of the last decay application, or undefined before the first sample. */
22
+ lastDecayTime;
23
+ /** Accumulated decayed total (sum of all bin counts after last decay). */
24
+ _totalCount = 0;
25
+ constructor(options) {
26
+ const { halfLife, minValue = 0.1, maxValue = 100_000, binsPerDecade = 5, logWarning, } = options;
27
+ if (halfLife <= 0) {
28
+ throw new Error("halfLife must be positive");
29
+ }
30
+ if (minValue <= 0 || maxValue <= minValue) {
31
+ throw new Error("Must have 0 < minValue < maxValue");
32
+ }
33
+ this.lambda = Math.LN2 / halfLife;
34
+ this.minValue = minValue;
35
+ this.maxValue = maxValue;
36
+ this.logWarning = logWarning;
37
+ const logMin = Math.log10(minValue);
38
+ const logMax = Math.log10(maxValue);
39
+ const decades = logMax - logMin;
40
+ this.numBins = Math.max(2, Math.ceil(decades * binsPerDecade) + 1);
41
+ this.binEdges = new Float64Array(this.numBins);
42
+ for (let i = 0; i < this.numBins; i++) {
43
+ this.binEdges[i] = Math.pow(10, logMin + (i * decades) / (this.numBins - 1));
44
+ }
45
+ this.binCounts = new Float64Array(this.numBins);
46
+ }
47
+ /**
48
+ * Record a sample value at the given time.
49
+ */
50
+ addSample(value, now) {
51
+ if (this.logWarning) {
52
+ if (value < this.minValue) {
53
+ this.logWarning(`DecayingHistogram: sample ${value} is below minValue ${this.minValue}; ` +
54
+ `it will be clamped to the lowest bin. Consider lowering minValue.`);
55
+ }
56
+ else if (value > this.maxValue) {
57
+ this.logWarning(`DecayingHistogram: sample ${value} is above maxValue ${this.maxValue}; ` +
58
+ `it will be clamped to the highest bin. Consider raising maxValue.`);
59
+ }
60
+ }
61
+ this.applyDecay(now);
62
+ const bin = this.findBin(value);
63
+ this.binCounts[bin]++;
64
+ this._totalCount += 1;
65
+ }
66
+ /**
67
+ * Query an approximate percentile from the decayed histogram.
68
+ *
69
+ * Returns the geometric midpoint of the bin containing the target
70
+ * percentile, which reduces quantization bias from log-spaced bin edges.
71
+ *
72
+ * @param p Percentile in (0, 1), e.g. 0.1 for p10.
73
+ * @param now Current time for decay application.
74
+ * @returns The approximate value at the requested percentile, or NaN if
75
+ * the histogram is empty.
76
+ */
77
+ percentile(p, now) {
78
+ if (p <= 0 || p >= 1) {
79
+ throw new RangeError(`p must be in (0, 1), got ${p}`);
80
+ }
81
+ this.applyDecay(now);
82
+ if (this._totalCount <= 0) {
83
+ return NaN;
84
+ }
85
+ const target = p * this._totalCount;
86
+ let cumulative = 0;
87
+ for (let i = 0; i < this.numBins; i++) {
88
+ cumulative += this.binCounts[i];
89
+ if (cumulative >= target) {
90
+ const lo = this.binEdges[i];
91
+ const hi = i + 1 < this.numBins ? this.binEdges[i + 1] : lo * 2;
92
+ return Math.sqrt(lo * hi);
93
+ }
94
+ }
95
+ const lastEdge = this.binEdges[this.numBins - 1];
96
+ return Math.sqrt(lastEdge * lastEdge * 2);
97
+ }
98
+ /**
99
+ * The total decayed sample count across all bins.
100
+ */
101
+ get totalCount() {
102
+ return this._totalCount;
103
+ }
104
+ /**
105
+ * Apply time-based decay to all bins. Idempotent if called multiple times
106
+ * at the same timestamp.
107
+ */
108
+ applyDecay(now) {
109
+ if (this.lastDecayTime === undefined) {
110
+ this.lastDecayTime = now;
111
+ return;
112
+ }
113
+ const elapsed = now - this.lastDecayTime;
114
+ if (elapsed <= 0) {
115
+ return;
116
+ }
117
+ const factor = Math.exp(-this.lambda * elapsed);
118
+ let total = 0;
119
+ for (let i = 0; i < this.numBins; i++) {
120
+ const decayed = this.binCounts[i] * factor;
121
+ this.binCounts[i] = decayed;
122
+ total += decayed;
123
+ }
124
+ this._totalCount = total;
125
+ this.lastDecayTime = now;
126
+ }
127
+ /**
128
+ * Find the bin index for a value using binary search.
129
+ */
130
+ findBin(value) {
131
+ let lo = 0;
132
+ let hi = this.numBins - 1;
133
+ while (lo < hi) {
134
+ const mid = (lo + hi + 1) >>> 1;
135
+ if (value >= this.binEdges[mid]) {
136
+ lo = mid;
137
+ }
138
+ else {
139
+ hi = mid - 1;
140
+ }
141
+ }
142
+ return lo;
143
+ }
144
+ }
@@ -7,5 +7,11 @@ export declare function squareRoot(n: number): number;
7
7
  * Create a function that returns: max(baseline, squareRoot(n))
8
8
  */
9
9
  export declare function squareRootWithBaseline(baseline: number): (n: number) => number;
10
+ /**
11
+ * Only expects positive integer inputs (behavior is undefined for
12
+ * non-finite, negative, or non-integer values). Floors the result to an int.
13
+ * Returns >= 1 for all valid inputs. So, log10Scale(0) = 1.
14
+ */
15
+ export declare function log10Scale(n: number): number;
10
16
  export { LinkedWaiterQueue } from "./LinkedWaiterQueue.js";
11
17
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AASA;;;GAGG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE5C;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,GACf,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAEvB;AAED,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AASA;;;GAGG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE5C;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,GACf,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAEvB;AAUD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE5C;AAED,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC"}
@@ -17,4 +17,17 @@ export function squareRoot(n) {
17
17
  export function squareRootWithBaseline(baseline) {
18
18
  return (n) => Math.max(baseline, squareRoot(n));
19
19
  }
20
+ /**
21
+ * Sublinear scale of the concurrency limit (floor of log10, lower-bounded for
22
+ * small n). Same idea as Netflix's Log10RootIntFunction.
23
+ */
24
+ const log10ScaleLookup = Array.from({ length: 1000 }, (_, i) => Math.max(1, Math.floor(Math.log10(i))));
25
+ /**
26
+ * Only expects positive integer inputs (behavior is undefined for
27
+ * non-finite, negative, or non-integer values). Floors the result to an int.
28
+ * Returns >= 1 for all valid inputs. So, log10Scale(0) = 1.
29
+ */
30
+ export function log10Scale(n) {
31
+ return n < 1000 ? log10ScaleLookup[n] : Math.floor(Math.log10(n));
32
+ }
20
33
  export { LinkedWaiterQueue } from "./LinkedWaiterQueue.js";
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.10.1",
12
+ "version": "0.12.0",
13
13
  "type": "module",
14
14
  "exports": {
15
15
  ".": {
@@ -31,5 +31,8 @@
31
31
  "type-fest": "^5.5.0",
32
32
  "type-party": "^0.7.3",
33
33
  "typescript": "^6.0.2"
34
+ },
35
+ "dependencies": {
36
+ "lru_map": "^0.4.1"
34
37
  }
35
38
  }