@crawlee/core 4.0.0-beta.99 → 4.0.0-rc.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 (109) hide show
  1. package/autoscaling/autoscaled_pool.d.ts +3 -21
  2. package/autoscaling/autoscaled_pool.js +105 -97
  3. package/autoscaling/concurrency_system.d.ts +7 -22
  4. package/autoscaling/concurrency_system.js +112 -100
  5. package/autoscaling/cpu_load_signal.d.ts +1 -2
  6. package/autoscaling/cpu_load_signal.js +11 -10
  7. package/autoscaling/event_loop_load_signal.d.ts +1 -4
  8. package/autoscaling/event_loop_load_signal.js +18 -18
  9. package/autoscaling/index.d.ts +1 -1
  10. package/autoscaling/index.js +1 -1
  11. package/autoscaling/load_signal.d.ts +8 -7
  12. package/autoscaling/load_signal.js +14 -12
  13. package/autoscaling/memory_load_signal.d.ts +3 -12
  14. package/autoscaling/memory_load_signal.js +41 -41
  15. package/autoscaling/snapshotter.d.ts +7 -10
  16. package/autoscaling/snapshotter.js +18 -18
  17. package/autoscaling/{client_load_signal.d.ts → storage_backend_load_signal.d.ts} +14 -17
  18. package/autoscaling/{client_load_signal.js → storage_backend_load_signal.js} +27 -27
  19. package/autoscaling/system_status.d.ts +9 -11
  20. package/autoscaling/system_status.js +13 -13
  21. package/configuration.d.ts +16 -16
  22. package/configuration.js +6 -6
  23. package/crawlers/context_pipeline.js +6 -6
  24. package/crawlers/crawler_commons.d.ts +14 -112
  25. package/crawlers/crawler_commons.js +1 -107
  26. package/crawlers/error_snapshotter.d.ts +2 -4
  27. package/crawlers/error_snapshotter.js +5 -5
  28. package/crawlers/index.d.ts +1 -1
  29. package/crawlers/index.js +0 -1
  30. package/crawlers/statistics.d.ts +164 -45
  31. package/crawlers/statistics.js +339 -153
  32. package/debug.js +4 -4
  33. package/enqueue_links/enqueue_links.d.ts +51 -159
  34. package/enqueue_links/enqueue_links.js +38 -231
  35. package/enqueue_links/shared.d.ts +34 -24
  36. package/enqueue_links/shared.js +54 -36
  37. package/errors.d.ts +43 -12
  38. package/errors.js +48 -9
  39. package/events/event_manager.d.ts +2 -2
  40. package/events/event_manager.js +7 -6
  41. package/events/local_event_manager.d.ts +1 -1
  42. package/events/local_event_manager.js +5 -5
  43. package/http.d.ts +9 -0
  44. package/http.js +28 -0
  45. package/index.d.ts +1 -1
  46. package/index.js +1 -1
  47. package/log.js +5 -1
  48. package/memory-storage/memory-storage.d.ts +3 -11
  49. package/memory-storage/memory-storage.js +42 -48
  50. package/memory-storage/resource-clients/dataset.d.ts +1 -1
  51. package/memory-storage/resource-clients/dataset.js +8 -13
  52. package/memory-storage/resource-clients/key-value-store.d.ts +1 -1
  53. package/memory-storage/resource-clients/key-value-store.js +36 -38
  54. package/memory-storage/resource-clients/request-queue.d.ts +4 -23
  55. package/memory-storage/resource-clients/request-queue.js +68 -80
  56. package/owned_or_injected.d.ts +1 -3
  57. package/owned_or_injected.js +17 -17
  58. package/package.json +8 -9
  59. package/proxy_configuration.d.ts +1 -3
  60. package/proxy_configuration.js +18 -14
  61. package/recoverable_state.d.ts +80 -45
  62. package/recoverable_state.js +163 -75
  63. package/request.d.ts +3 -4
  64. package/request.js +57 -44
  65. package/router.d.ts +6 -9
  66. package/router.js +23 -23
  67. package/serialization.js +14 -13
  68. package/service_locator.d.ts +19 -10
  69. package/service_locator.js +57 -48
  70. package/session_pool/session.d.ts +3 -14
  71. package/session_pool/session.js +76 -74
  72. package/session_pool/session_pool.d.ts +6 -12
  73. package/session_pool/session_pool.js +85 -77
  74. package/storages/batched_adds.d.ts +37 -0
  75. package/storages/batched_adds.js +73 -0
  76. package/storages/dataset.d.ts +12 -1
  77. package/storages/dataset.js +133 -32
  78. package/storages/index.d.ts +2 -1
  79. package/storages/index.js +2 -1
  80. package/storages/key_value_store.d.ts +19 -4
  81. package/storages/key_value_store.js +204 -75
  82. package/storages/request_dedup_cache.d.ts +1 -2
  83. package/storages/request_dedup_cache.js +9 -9
  84. package/storages/request_list.d.ts +4 -23
  85. package/storages/request_list.js +103 -96
  86. package/storages/request_loader.d.ts +5 -0
  87. package/storages/request_manager_tandem.d.ts +1 -10
  88. package/storages/request_manager_tandem.js +27 -27
  89. package/storages/request_queue.d.ts +21 -18
  90. package/storages/request_queue.js +318 -177
  91. package/storages/sitemap_request_loader.d.ts +22 -70
  92. package/storages/sitemap_request_loader.js +122 -116
  93. package/storages/storage_instance_manager.d.ts +1 -2
  94. package/storages/storage_instance_manager.js +17 -17
  95. package/storages/storage_stats.d.ts +1 -1
  96. package/storages/storage_stats.js +4 -4
  97. package/storages/throttling_request_manager.d.ts +239 -0
  98. package/storages/throttling_request_manager.js +646 -0
  99. package/storages/transaction.d.ts +252 -0
  100. package/storages/transaction.js +251 -0
  101. package/storages/utils.d.ts +6 -3
  102. package/storages/utils.js +11 -2
  103. package/system-info/runtime.js +7 -7
  104. package/url.d.ts +9 -0
  105. package/url.js +11 -0
  106. package/validators.d.ts +22 -25
  107. package/validators.js +13 -25
  108. package/storages/access_checking.d.ts +0 -12
  109. package/storages/access_checking.js +0 -17
@@ -1,8 +1,34 @@
1
- import ow from 'ow';
1
+ import { z } from 'zod';
2
2
  import { betterClearInterval, betterSetInterval } from '@apify/utilities';
3
3
  import { serviceLocator } from '../service_locator.js';
4
+ import { parseArgument, schemas, validators } from '../validators.js';
4
5
  import { Snapshotter } from './snapshotter.js';
5
6
  import { SystemStatus } from './system_status.js';
7
+ const concurrencySchema = z.number().int().gte(1).optional();
8
+ // `schemas.anyObject` passes values through by reference, so the load signal instances inside
9
+ // `loadSignals` and class instances like loggers keep their prototype.
10
+ const concurrencySystemOptionsSchema = z.strictObject({
11
+ maxConcurrency: z.number().int().gte(1).default(200),
12
+ minConcurrency: z.number().int().gte(1).default(1),
13
+ desiredConcurrency: z.number().int().gte(1).optional(),
14
+ desiredConcurrencyRatio: z.number().gt(0).lt(1).default(0.9),
15
+ scaleUpStepRatio: z.number().gt(0).lt(1).default(0.05),
16
+ scaleDownStepRatio: z.number().gt(0).lt(1).default(0.05),
17
+ loggingIntervalSecs: schemas.anyNumber
18
+ .refine((value) => value > 0, 'Expected a number greater than 0')
19
+ .nullish()
20
+ .default(60),
21
+ autoscaleIntervalSecs: schemas.anyNumber
22
+ .refine((value) => value > 0, 'Expected a number greater than 0')
23
+ .default(10),
24
+ loadSignals: schemas.anyObject.default(() => ({})),
25
+ snapshotHistorySecs: schemas.anyNumber.refine((value) => value > 0, 'Expected a number greater than 0').optional(),
26
+ currentHistorySecs: schemas.anyNumber.refine((value) => value > 0, 'Expected a number greater than 0').optional(),
27
+ log: validators.logger.default(() => serviceLocator.getLogger()),
28
+ maxTasksPerMinute: z
29
+ .union([z.number().int().gte(1), z.literal(Number.POSITIVE_INFINITY)])
30
+ .default(Number.POSITIVE_INFINITY),
31
+ });
6
32
  /**
7
33
  * The shareable "governor" behind an {@link AutoscaledPool}: it decides whether there is free compute for one more
8
34
  * task by combining live system load (via an internal {@link Snapshotter}) with a concurrency budget it autoscales
@@ -22,60 +48,46 @@ export class ConcurrencySystem {
22
48
  desiredConcurrencyRatio;
23
49
  scaleUpStepRatio;
24
50
  scaleDownStepRatio;
25
- loggingIntervalMillis;
26
- autoscaleIntervalMillis;
51
+ #loggingIntervalMillis;
52
+ #autoscaleIntervalMillis;
27
53
  maxTasksPerMinute;
28
- _minConcurrency;
29
- _maxConcurrency;
30
- _desiredConcurrency;
54
+ #minConcurrency;
55
+ #maxConcurrency;
56
+ #desiredConcurrency;
57
+ // kept as TS-private _-prefixed: autoscaled_pool tests write this backing field directly
31
58
  _currentConcurrency = 0;
32
- lastLoggingTime;
33
- _tasksPerMinute = Array.from({ length: 60 }, () => 0);
59
+ #lastLoggingTime;
60
+ #tasksPerMinute = Array.from({ length: 60 }, () => 0);
34
61
  snapshotter;
35
- loadSignals;
62
+ #loadSignals;
36
63
  systemStatus;
37
- autoscaleInterval;
38
- tasksDonePerSecondInterval;
64
+ #autoscaleInterval;
65
+ #tasksDonePerSecondInterval;
39
66
  /** Whether the snapshotter and autoscaling intervals are currently running. */
40
- running = false;
67
+ #running = false;
41
68
  /** The in-flight (or completed) startup, memoized so concurrent `start()` calls await one boot. */
42
- startPromise;
69
+ #startPromise;
43
70
  /** Set once per session, so a pool outliving `stop()` is reported once rather than every half second. */
44
- warnedAboutQueryWhileStopped = false;
71
+ #warnedAboutQueryWhileStopped = false;
45
72
  constructor(options = {}) {
46
- ow(options, ow.object.exactShape({
47
- maxConcurrency: ow.optional.number.integer.greaterThanOrEqual(1),
48
- minConcurrency: ow.optional.number.integer.greaterThanOrEqual(1),
49
- desiredConcurrency: ow.optional.number.integer.greaterThanOrEqual(1),
50
- desiredConcurrencyRatio: ow.optional.number.greaterThan(0).lessThan(1),
51
- scaleUpStepRatio: ow.optional.number.greaterThan(0).lessThan(1),
52
- scaleDownStepRatio: ow.optional.number.greaterThan(0).lessThan(1),
53
- loggingIntervalSecs: ow.any(ow.number.greaterThan(0), ow.nullOrUndefined),
54
- autoscaleIntervalSecs: ow.optional.number.greaterThan(0),
55
- loadSignals: ow.optional.object,
56
- snapshotHistorySecs: ow.optional.number.greaterThan(0),
57
- currentHistorySecs: ow.optional.number.greaterThan(0),
58
- log: ow.optional.object,
59
- maxTasksPerMinute: ow.optional.number.integerOrInfinite.greaterThanOrEqual(1),
60
- }));
61
- const { maxConcurrency = 200, minConcurrency = 1, desiredConcurrency, desiredConcurrencyRatio = 0.9, scaleUpStepRatio = 0.05, scaleDownStepRatio = 0.05, loggingIntervalSecs = 60, autoscaleIntervalSecs = 10, loadSignals = {}, snapshotHistorySecs, currentHistorySecs, log = serviceLocator.getLogger(), maxTasksPerMinute = Infinity, } = options;
73
+ const { maxConcurrency, minConcurrency, desiredConcurrency, desiredConcurrencyRatio, scaleUpStepRatio, scaleDownStepRatio, loggingIntervalSecs, autoscaleIntervalSecs, loadSignals, snapshotHistorySecs, currentHistorySecs, log, maxTasksPerMinute, } = parseArgument(options, concurrencySystemOptionsSchema, 'ConcurrencySystemOptions');
62
74
  this.log = log.child({ prefix: 'ConcurrencySystem' });
63
75
  this.desiredConcurrencyRatio = desiredConcurrencyRatio;
64
76
  this.scaleUpStepRatio = scaleUpStepRatio;
65
77
  this.scaleDownStepRatio = scaleDownStepRatio;
66
- this.loggingIntervalMillis = (loggingIntervalSecs ?? 0) * 1000;
67
- this.autoscaleIntervalMillis = autoscaleIntervalSecs * 1000;
78
+ this.#loggingIntervalMillis = (loggingIntervalSecs ?? 0) * 1000;
79
+ this.#autoscaleIntervalMillis = autoscaleIntervalSecs * 1000;
68
80
  this.maxTasksPerMinute = maxTasksPerMinute;
69
- this._minConcurrency = minConcurrency;
70
- this._maxConcurrency = maxConcurrency;
71
- this._desiredConcurrency = desiredConcurrency ?? minConcurrency;
81
+ this.#minConcurrency = minConcurrency;
82
+ this.#maxConcurrency = maxConcurrency;
83
+ this.#desiredConcurrency = desiredConcurrency ?? minConcurrency;
72
84
  this.clampDesiredConcurrency();
73
- this._autoscale = this._autoscale.bind(this);
74
- this._incrementTasksDonePerSecond = this._incrementTasksDonePerSecond.bind(this);
85
+ this.autoscale = this.autoscale.bind(this);
86
+ this.incrementTasksDonePerSecond = this.incrementTasksDonePerSecond.bind(this);
75
87
  // The built-in signals are collected by the snapshotter; custom ones are simply evaluated alongside them.
76
88
  const { custom: customLoadSignals = [], ...builtinSignalOptions } = loadSignals;
77
89
  this.snapshotter = new Snapshotter(builtinSignalOptions);
78
- this.loadSignals = customLoadSignals;
90
+ this.#loadSignals = customLoadSignals;
79
91
  this.systemStatus = new SystemStatus({
80
92
  snapshotter: this.snapshotter,
81
93
  loadSignals: customLoadSignals,
@@ -89,7 +101,7 @@ export class ConcurrencySystem {
89
101
  * Gets the minimum number of tasks running in parallel.
90
102
  */
91
103
  get minConcurrency() {
92
- return this._minConcurrency;
104
+ return this.#minConcurrency;
93
105
  }
94
106
  /**
95
107
  * Sets the minimum number of tasks running in parallel.
@@ -98,15 +110,15 @@ export class ConcurrencySystem {
98
110
  * If you're not sure, just keep the default value and the concurrency will scale up automatically.
99
111
  */
100
112
  set minConcurrency(value) {
101
- ow(value, ow.optional.number.integer.greaterThanOrEqual(1));
102
- this._minConcurrency = value;
113
+ parseArgument(value, concurrencySchema);
114
+ this.#minConcurrency = value;
103
115
  this.clampDesiredConcurrency();
104
116
  }
105
117
  /**
106
118
  * Gets the maximum number of tasks running in parallel.
107
119
  */
108
120
  get maxConcurrency() {
109
- return this._maxConcurrency;
121
+ return this.#maxConcurrency;
110
122
  }
111
123
  /**
112
124
  * Sets the maximum number of tasks running in parallel. Lowering it below the current
@@ -115,8 +127,8 @@ export class ConcurrencySystem {
115
127
  * limit as they settle).
116
128
  */
117
129
  set maxConcurrency(value) {
118
- ow(value, ow.optional.number.integer.greaterThanOrEqual(1));
119
- this._maxConcurrency = value;
130
+ parseArgument(value, concurrencySchema);
131
+ this.#maxConcurrency = value;
120
132
  this.clampDesiredConcurrency();
121
133
  }
122
134
  /**
@@ -124,15 +136,15 @@ export class ConcurrencySystem {
124
136
  * which is an estimated number of parallel tasks that the system can currently support.
125
137
  */
126
138
  get desiredConcurrency() {
127
- return this._desiredConcurrency;
139
+ return this.#desiredConcurrency;
128
140
  }
129
141
  /**
130
142
  * Sets the desired concurrency for the system, i.e. the number of tasks that should be running
131
143
  * in parallel if there's large enough supply of tasks.
132
144
  */
133
145
  set desiredConcurrency(value) {
134
- ow(value, ow.optional.number.integer.greaterThanOrEqual(1));
135
- this._desiredConcurrency = value;
146
+ parseArgument(value, concurrencySchema);
147
+ this.#desiredConcurrency = value;
136
148
  this.clampDesiredConcurrency();
137
149
  }
138
150
  /**
@@ -142,15 +154,15 @@ export class ConcurrencySystem {
142
154
  * that is the limit callers set in order to protect something.
143
155
  */
144
156
  clampDesiredConcurrency() {
145
- const atLeastMin = Math.max(this._desiredConcurrency, this._minConcurrency);
146
- this._desiredConcurrency = Math.min(atLeastMin, this._maxConcurrency);
157
+ const atLeastMin = Math.max(this.#desiredConcurrency, this.#minConcurrency);
158
+ this.#desiredConcurrency = Math.min(atLeastMin, this.#maxConcurrency);
147
159
  }
148
160
  get currentConcurrency() {
149
161
  return this._currentConcurrency;
150
162
  }
151
163
  /** Whether the system is currently monitoring load and autoscaling the budget. */
152
164
  get isRunning() {
153
- return this.running;
165
+ return this.#running;
154
166
  }
155
167
  /**
156
168
  * Boots the underlying snapshotter and the autoscaling interval. Idempotent, so a shared system isn't restarted
@@ -160,52 +172,52 @@ export class ConcurrencySystem {
160
172
  async start() {
161
173
  // Unwound and dropped again on failure, so a later `start()` retries instead of resolving instantly against
162
174
  // a system that is down.
163
- this.startPromise ??= this.boot().catch(async (error) => {
164
- this.startPromise = undefined;
175
+ this.#startPromise ??= this.boot().catch(async (error) => {
176
+ this.#startPromise = undefined;
165
177
  await this.shutDown();
166
178
  throw error;
167
179
  });
168
- await this.startPromise;
180
+ await this.#startPromise;
169
181
  }
170
182
  async boot() {
171
183
  // Per-session measurement state, reset so a restarted system isn't judged on the previous session. The
172
184
  // per-minute window matters most: its ageing interval is cleared while we are down, so starts from before an
173
185
  // arbitrarily long stop would otherwise still count against "this minute" and trip the cap immediately.
174
- this._tasksPerMinute = Array.from({ length: 60 }, () => 0);
175
- this.lastLoggingTime = undefined;
176
- this.warnedAboutQueryWhileStopped = false;
186
+ this.#tasksPerMinute = Array.from({ length: 60 }, () => 0);
187
+ this.#lastLoggingTime = undefined;
188
+ this.#warnedAboutQueryWhileStopped = false;
177
189
  // Signals are told how much history to keep when they start: exactly the longest window they will be sampled
178
190
  // over, so nobody has to guess a retention value that matches this system's configuration.
179
191
  const startContext = { maxSampleWindowMillis: this.systemStatus.maxSampleWindowMillis };
180
192
  await this.snapshotter.start(startContext);
181
- await Promise.all(this.loadSignals.map(async (s) => s.start(startContext)));
182
- this.autoscaleInterval = betterSetInterval(this._autoscale, this.autoscaleIntervalMillis);
193
+ await Promise.all(this.#loadSignals.map(async (s) => s.start(startContext)));
194
+ this.#autoscaleInterval = betterSetInterval(this.autoscale, this.#autoscaleIntervalMillis);
183
195
  if (this.maxTasksPerMinute !== Infinity) {
184
- this.tasksDonePerSecondInterval = betterSetInterval(this._incrementTasksDonePerSecond, 1000);
196
+ this.#tasksDonePerSecondInterval = betterSetInterval(this.incrementTasksDonePerSecond, 1000);
185
197
  }
186
198
  // Last, so `isRunning` never claims a system whose signals aren't collecting yet.
187
- this.running = true;
199
+ this.#running = true;
188
200
  }
189
201
  /**
190
202
  * Stops the snapshotter and intervals. Idempotent and safe to call even if the system was never started.
191
203
  */
192
204
  async stop() {
193
- if (this.startPromise === undefined) {
205
+ if (this.#startPromise === undefined) {
194
206
  return;
195
207
  }
196
208
  // Waited out rather than interrupted, or the intervals a starting signal is about to register outlive us.
197
- await this.startPromise.catch(() => { });
198
- this.startPromise = undefined;
199
- this.running = false;
209
+ await this.#startPromise.catch(() => { });
210
+ this.#startPromise = undefined;
211
+ this.#running = false;
200
212
  await this.shutDown();
201
213
  }
202
214
  async shutDown() {
203
- if (this.autoscaleInterval)
204
- betterClearInterval(this.autoscaleInterval);
205
- if (this.tasksDonePerSecondInterval)
206
- betterClearInterval(this.tasksDonePerSecondInterval);
215
+ if (this.#autoscaleInterval)
216
+ betterClearInterval(this.#autoscaleInterval);
217
+ if (this.#tasksDonePerSecondInterval)
218
+ betterClearInterval(this.#tasksDonePerSecondInterval);
207
219
  await this.snapshotter.stop();
208
- await Promise.all(this.loadSignals.map(async (s) => s.stop()));
220
+ await Promise.all(this.#loadSignals.map(async (s) => s.stop()));
209
221
  }
210
222
  /**
211
223
  * Reports, once per session, that capacity is being queried on a system that isn't running — a mistake nothing
@@ -214,10 +226,10 @@ export class ConcurrencySystem {
214
226
  * `desiredConcurrency` are frozen at that point, so the borrowing pool would otherwise just quietly mis-scale.
215
227
  */
216
228
  warnIfNotRunning() {
217
- if (this.running || this.warnedAboutQueryWhileStopped) {
229
+ if (this.#running || this.#warnedAboutQueryWhileStopped) {
218
230
  return;
219
231
  }
220
- this.warnedAboutQueryWhileStopped = true;
232
+ this.#warnedAboutQueryWhileStopped = true;
221
233
  this.log.warning('Capacity is being queried on a ConcurrencySystem that is not running, so system load is no longer being ' +
222
234
  'monitored and the concurrency will no longer be adjusted. Whoever creates a ConcurrencySystem owns ' +
223
235
  'its lifecycle: call `await concurrencySystem.stop()` only once every pool and crawler borrowing it ' +
@@ -232,13 +244,13 @@ export class ConcurrencySystem {
232
244
  */
233
245
  hasCapacityForTask(_consumer) {
234
246
  this.warnIfNotRunning();
235
- if (this._currentConcurrency >= this._desiredConcurrency) {
247
+ if (this._currentConcurrency >= this.#desiredConcurrency) {
236
248
  this.log.perf('Task will not run. Desired concurrency achieved.');
237
249
  return false;
238
250
  }
239
251
  const currentStatus = this.systemStatus.getCurrentStatus();
240
252
  const { isSystemIdle } = currentStatus;
241
- if (!isSystemIdle && this._currentConcurrency >= this._minConcurrency) {
253
+ if (!isSystemIdle && this._currentConcurrency >= this.#minConcurrency) {
242
254
  this.log.perf('Task will not be run. System is overloaded.', currentStatus);
243
255
  return false;
244
256
  }
@@ -249,7 +261,7 @@ export class ConcurrencySystem {
249
261
  if (this.maxTasksPerMinute === Infinity) {
250
262
  return false;
251
263
  }
252
- return this._tasksPerMinute.reduce((acc, curr) => acc + curr, 0) >= this.maxTasksPerMinute;
264
+ return this.#tasksPerMinute.reduce((acc, curr) => acc + curr, 0) >= this.maxTasksPerMinute;
253
265
  }
254
266
  /**
255
267
  * Atomically books a task against the shared budget: re-checks
@@ -269,7 +281,7 @@ export class ConcurrencySystem {
269
281
  return false;
270
282
  }
271
283
  this._currentConcurrency++;
272
- this._tasksPerMinute[0]++;
284
+ this.#tasksPerMinute[0]++;
273
285
  return true;
274
286
  }
275
287
  /** Returns a slot to the shared budget, whoever booked it. */
@@ -288,30 +300,30 @@ export class ConcurrencySystem {
288
300
  * Evaluates the historical system status and scales the shared desired concurrency up or down accordingly. Driven
289
301
  * by the autoscaling interval started in {@link ConcurrencySystem.start|`start()`}.
290
302
  */
291
- _autoscale(intervalCallback) {
303
+ autoscale(intervalCallback) {
292
304
  if (this.isOverMaxRequestLimit)
293
305
  return intervalCallback();
294
306
  const systemStatus = this.systemStatus.getHistoricalStatus();
295
307
  const { isSystemIdle } = systemStatus;
296
- const weAreNotAtMax = this._desiredConcurrency < this._maxConcurrency;
297
- const minCurrentConcurrency = Math.floor(this._desiredConcurrency * this.desiredConcurrencyRatio);
308
+ const weAreNotAtMax = this.#desiredConcurrency < this.#maxConcurrency;
309
+ const minCurrentConcurrency = Math.floor(this.#desiredConcurrency * this.desiredConcurrencyRatio);
298
310
  const weAreReachingDesiredConcurrency = this._currentConcurrency >= minCurrentConcurrency;
299
311
  if (isSystemIdle && weAreNotAtMax && weAreReachingDesiredConcurrency)
300
- this._scaleUp(systemStatus);
312
+ this.scaleUp(systemStatus);
301
313
  const isSystemOverloaded = !isSystemIdle;
302
- const weAreNotAtMin = this._desiredConcurrency > this._minConcurrency;
314
+ const weAreNotAtMin = this.#desiredConcurrency > this.#minConcurrency;
303
315
  if (isSystemOverloaded && weAreNotAtMin)
304
- this._scaleDown(systemStatus);
305
- if (this.loggingIntervalMillis > 0) {
316
+ this.scaleDown(systemStatus);
317
+ if (this.#loggingIntervalMillis > 0) {
306
318
  const now = Date.now();
307
- if (this.lastLoggingTime == null) {
308
- this.lastLoggingTime = now;
319
+ if (this.#lastLoggingTime == null) {
320
+ this.#lastLoggingTime = now;
309
321
  }
310
- else if (now > this.lastLoggingTime + this.loggingIntervalMillis) {
311
- this.lastLoggingTime = now;
322
+ else if (now > this.#lastLoggingTime + this.#loggingIntervalMillis) {
323
+ this.#lastLoggingTime = now;
312
324
  this.log.info('state', {
313
325
  currentConcurrency: this._currentConcurrency,
314
- desiredConcurrency: this._desiredConcurrency,
326
+ desiredConcurrency: this.#desiredConcurrency,
315
327
  systemStatus,
316
328
  });
317
329
  }
@@ -321,30 +333,30 @@ export class ConcurrencySystem {
321
333
  /**
322
334
  * Scales the system up by increasing the desired concurrency by the scaleUpStepRatio.
323
335
  */
324
- _scaleUp(systemStatus) {
325
- const step = Math.ceil(this._desiredConcurrency * this.scaleUpStepRatio);
326
- this._desiredConcurrency = Math.min(this._maxConcurrency, this._desiredConcurrency + step);
336
+ scaleUp(systemStatus) {
337
+ const step = Math.ceil(this.#desiredConcurrency * this.scaleUpStepRatio);
338
+ this.#desiredConcurrency = Math.min(this.#maxConcurrency, this.#desiredConcurrency + step);
327
339
  this.log.debug('scaling up', {
328
- oldConcurrency: this._desiredConcurrency - step,
329
- newConcurrency: this._desiredConcurrency,
340
+ oldConcurrency: this.#desiredConcurrency - step,
341
+ newConcurrency: this.#desiredConcurrency,
330
342
  systemStatus,
331
343
  });
332
344
  }
333
345
  /**
334
346
  * Scales the system down by decreasing the desired concurrency by the scaleDownStepRatio.
335
347
  */
336
- _scaleDown(systemStatus) {
337
- const step = Math.ceil(this._desiredConcurrency * this.scaleDownStepRatio);
338
- this._desiredConcurrency = Math.max(this._minConcurrency, this._desiredConcurrency - step);
348
+ scaleDown(systemStatus) {
349
+ const step = Math.ceil(this.#desiredConcurrency * this.scaleDownStepRatio);
350
+ this.#desiredConcurrency = Math.max(this.#minConcurrency, this.#desiredConcurrency - step);
339
351
  this.log.debug('scaling down', {
340
- oldConcurrency: this._desiredConcurrency + step,
341
- newConcurrency: this._desiredConcurrency,
352
+ oldConcurrency: this.#desiredConcurrency + step,
353
+ newConcurrency: this.#desiredConcurrency,
342
354
  systemStatus,
343
355
  });
344
356
  }
345
- _incrementTasksDonePerSecond(intervalCallback) {
346
- this._tasksPerMinute.unshift(0);
347
- this._tasksPerMinute.pop();
357
+ incrementTasksDonePerSecond(intervalCallback) {
358
+ this.#tasksPerMinute.unshift(0);
359
+ this.#tasksPerMinute.pop();
348
360
  return intervalCallback();
349
361
  }
350
362
  }
@@ -31,10 +31,9 @@ export interface CpuLoadSignalOptions {
31
31
  * @category Scaling
32
32
  */
33
33
  export declare class CpuLoadSignal implements LoadSignal {
34
+ #private;
34
35
  readonly name = "cpuInfo";
35
36
  readonly overloadedRatio: number;
36
- private readonly store;
37
- private events?;
38
37
  constructor(options?: CpuLoadSignalOptions);
39
38
  start(context: LoadSignalStartContext): Promise<void>;
40
39
  stop(): Promise<void>;
@@ -1,3 +1,4 @@
1
+ import { EventType } from '../events/event_manager.js';
1
2
  import { serviceLocator } from '../service_locator.js';
2
3
  import { SnapshotStore } from './load_signal.js';
3
4
  /**
@@ -11,33 +12,33 @@ import { SnapshotStore } from './load_signal.js';
11
12
  export class CpuLoadSignal {
12
13
  name = 'cpuInfo';
13
14
  overloadedRatio;
14
- store = new SnapshotStore();
15
- events;
15
+ #store = new SnapshotStore();
16
+ #events;
16
17
  constructor(options = {}) {
17
18
  this.overloadedRatio = options.overloadedRatio ?? 0.4;
18
19
  this.handle = this.handle.bind(this);
19
20
  }
20
21
  async start(context) {
21
- this.store.useSampleWindow(context.maxSampleWindowMillis);
22
+ this.#store.useSampleWindow(context.maxSampleWindowMillis);
22
23
  // A new session starts from a clean slate, so it is not judged on measurements from before the downtime.
23
- this.store.clear();
24
+ this.#store.clear();
24
25
  // Resolved here rather than in the constructor, so an instance built ahead of time (to be wrapped, or shared
25
26
  // between systems) cannot capture whichever event manager happened to be registered at that moment.
26
- this.events = serviceLocator.getEventManager();
27
- this.events.on("systemInfo" /* EventType.SYSTEM_INFO */, this.handle);
27
+ this.#events = serviceLocator.getEventManager();
28
+ this.#events.on(EventType.SYSTEM_INFO, this.handle);
28
29
  }
29
30
  async stop() {
30
- this.events?.off("systemInfo" /* EventType.SYSTEM_INFO */, this.handle);
31
- this.events = undefined;
31
+ this.#events?.off(EventType.SYSTEM_INFO, this.handle);
32
+ this.#events = undefined;
32
33
  }
33
34
  getSample(sampleDurationMillis) {
34
- return this.store.getSample(sampleDurationMillis);
35
+ return this.#store.getSample(sampleDurationMillis);
35
36
  }
36
37
  /** @internal Records a snapshot from a `SYSTEM_INFO` payload. Exposed for tests. */
37
38
  handle(systemInfo) {
38
39
  const { cpuCurrentUsage, isCpuOverloaded } = systemInfo;
39
40
  const createdAt = systemInfo.createdAt ? new Date(systemInfo.createdAt) : new Date();
40
- this.store.push({
41
+ this.#store.push({
41
42
  createdAt,
42
43
  isOverloaded: isCpuOverloaded,
43
44
  usedRatio: Math.ceil(cpuCurrentUsage / 100),
@@ -36,12 +36,9 @@ export interface EventLoopLoadSignalOptions {
36
36
  * @category Scaling
37
37
  */
38
38
  export declare class EventLoopLoadSignal implements LoadSignal {
39
+ #private;
39
40
  readonly name = "eventLoopInfo";
40
41
  readonly overloadedRatio: number;
41
- private readonly store;
42
- private readonly intervalMillis;
43
- private readonly maxBlockedMillis;
44
- private interval?;
45
42
  constructor(options?: EventLoopLoadSignalOptions);
46
43
  start(context: LoadSignalStartContext): Promise<void>;
47
44
  stop(): Promise<void>;
@@ -10,30 +10,30 @@ import { SnapshotStore } from './load_signal.js';
10
10
  export class EventLoopLoadSignal {
11
11
  name = 'eventLoopInfo';
12
12
  overloadedRatio;
13
- store = new SnapshotStore();
14
- intervalMillis;
15
- maxBlockedMillis;
16
- interval;
13
+ #store = new SnapshotStore();
14
+ #intervalMillis;
15
+ #maxBlockedMillis;
16
+ #interval;
17
17
  constructor(options = {}) {
18
18
  this.overloadedRatio = options.overloadedRatio ?? 0.6;
19
- this.intervalMillis = (options.snapshotIntervalSecs ?? 0.5) * 1000;
20
- this.maxBlockedMillis = options.maxBlockedMillis ?? 50;
19
+ this.#intervalMillis = (options.snapshotIntervalSecs ?? 0.5) * 1000;
20
+ this.#maxBlockedMillis = options.maxBlockedMillis ?? 50;
21
21
  this.handle = this.handle.bind(this);
22
22
  }
23
23
  async start(context) {
24
- this.store.useSampleWindow(context.maxSampleWindowMillis);
24
+ this.#store.useSampleWindow(context.maxSampleWindowMillis);
25
25
  // A new session starts from a clean slate, or the downtime gets charged to the event loop: `handle()` measures
26
26
  // the gap since the previous snapshot, which across a restart is however long the system was stopped.
27
- this.store.clear();
28
- this.interval = betterSetInterval(this.handle, this.intervalMillis);
27
+ this.#store.clear();
28
+ this.#interval = betterSetInterval(this.handle, this.#intervalMillis);
29
29
  }
30
30
  async stop() {
31
- if (this.interval)
32
- betterClearInterval(this.interval);
33
- this.interval = undefined;
31
+ if (this.#interval)
32
+ betterClearInterval(this.#interval);
33
+ this.#interval = undefined;
34
34
  }
35
35
  getSample(sampleDurationMillis) {
36
- return this.store.getSample(sampleDurationMillis);
36
+ return this.#store.getSample(sampleDurationMillis);
37
37
  }
38
38
  /**
39
39
  * Records one snapshot: how much later than scheduled this tick ran is how long the loop was blocked.
@@ -46,15 +46,15 @@ export class EventLoopLoadSignal {
46
46
  isOverloaded: false,
47
47
  exceededMillis: 0,
48
48
  };
49
- const all = this.store.getAll();
49
+ const all = this.#store.getAll();
50
50
  const previousSnapshot = all[all.length - 1];
51
51
  if (previousSnapshot) {
52
- const delta = now.getTime() - +previousSnapshot.createdAt - this.intervalMillis;
53
- if (delta > this.maxBlockedMillis)
52
+ const delta = now.getTime() - +previousSnapshot.createdAt - this.#intervalMillis;
53
+ if (delta > this.#maxBlockedMillis)
54
54
  snapshot.isOverloaded = true;
55
- snapshot.exceededMillis = Math.max(delta - this.maxBlockedMillis, 0);
55
+ snapshot.exceededMillis = Math.max(delta - this.#maxBlockedMillis, 0);
56
56
  }
57
- this.store.push(snapshot, now);
57
+ this.#store.push(snapshot, now);
58
58
  intervalCallback();
59
59
  }
60
60
  }
@@ -1,9 +1,9 @@
1
1
  export * from './autoscaled_pool.js';
2
2
  export * from './concurrency_system.js';
3
- export * from './client_load_signal.js';
4
3
  export * from './cpu_load_signal.js';
5
4
  export * from './event_loop_load_signal.js';
6
5
  export * from './load_signal.js';
7
6
  export * from './memory_load_signal.js';
8
7
  export * from './snapshotter.js';
8
+ export * from './storage_backend_load_signal.js';
9
9
  export * from './system_status.js';
@@ -1,9 +1,9 @@
1
1
  export * from './autoscaled_pool.js';
2
2
  export * from './concurrency_system.js';
3
- export * from './client_load_signal.js';
4
3
  export * from './cpu_load_signal.js';
5
4
  export * from './event_loop_load_signal.js';
6
5
  export * from './load_signal.js';
7
6
  export * from './memory_load_signal.js';
8
7
  export * from './snapshotter.js';
8
+ export * from './storage_backend_load_signal.js';
9
9
  export * from './system_status.js';
@@ -1,4 +1,4 @@
1
- import type { ClientInfo } from './system_status.js';
1
+ import type { LoadSignalInfo } from './system_status.js';
2
2
  /**
3
3
  * A snapshot of a resource's overload state at a point in time.
4
4
  */
@@ -22,7 +22,7 @@ export interface LoadSignalStartContext {
22
22
  * A signal that reports whether a particular resource is overloaded. The {@link ConcurrencySystem} aggregates
23
23
  * several of them — if any one reports overload, the system is overloaded.
24
24
  *
25
- * The built-in signals cover memory, CPU, event loop and storage-client rate limits. Implement this interface to add
25
+ * The built-in signals cover memory, CPU, event loop and storage backend rate limits. Implement this interface to add
26
26
  * your own (navigation timeouts, proxy health, …) and pass them via
27
27
  * {@link LoadSignalsOptions.custom|`loadSignals.custom`}; {@link SnapshotStore} does the time-windowed
28
28
  * bookkeeping if you want it. Each built-in is also a public class, so one can be *wrapped* rather than reimplemented
@@ -32,8 +32,8 @@ export interface LoadSignal {
32
32
  /**
33
33
  * This signal's key in the reported {@link SystemInfo}, also used in logging — so it must be unique among the
34
34
  * signals of one {@link ConcurrencySystem}, which throws on a duplicate. The four built-in names (`memInfo`,
35
- * `eventLoopInfo`, `cpuInfo`, `clientInfo`) land in the correspondingly named `SystemInfo` fields rather than the
36
- * `loadSignalInfo` bag; taking one over means switching that built-in off.
35
+ * `eventLoopInfo`, `cpuInfo`, `storageBackendInfo`) land in the correspondingly named `SystemInfo` fields rather
36
+ * than the `loadSignalInfo` bag; taking one over means switching that built-in off.
37
37
  */
38
38
  readonly name: string;
39
39
  /**
@@ -60,7 +60,7 @@ export interface LoadSignal {
60
60
  * and so can yours — it is the only part of their machinery worth reusing.
61
61
  */
62
62
  export declare class SnapshotStore<T extends LoadSnapshot = LoadSnapshot> {
63
- private snapshots;
63
+ #private;
64
64
  /** Retention window in milliseconds. Unbounded until {@link SnapshotStore.useSampleWindow|`useSampleWindow()`}. */
65
65
  private historyMillis;
66
66
  /**
@@ -79,7 +79,8 @@ export declare class SnapshotStore<T extends LoadSnapshot = LoadSnapshot> {
79
79
  getSample(sampleDurationMillis?: number): T[];
80
80
  /**
81
81
  * Direct, unwindowed access to the underlying array — used by signals whose handler needs the previous snapshot
82
- * to compute a delta (e.g. the event loop and client signals read the last entry to measure change since it).
82
+ * to compute a delta (e.g. the event loop and storage backend signals read the last entry to measure change since
83
+ * it).
83
84
  */
84
85
  getAll(): T[];
85
86
  /**
@@ -96,4 +97,4 @@ export declare class SnapshotStore<T extends LoadSnapshot = LoadSnapshot> {
96
97
  * evaluation logic used by `SystemStatus` for all signal types.
97
98
  * @internal
98
99
  */
99
- export declare function evaluateLoadSignalSample(sample: LoadSnapshot[], overloadedRatio: number): ClientInfo;
100
+ export declare function evaluateLoadSignalSample(sample: LoadSnapshot[], overloadedRatio: number): LoadSignalInfo;