@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.
- package/autoscaling/autoscaled_pool.d.ts +3 -21
- package/autoscaling/autoscaled_pool.js +105 -97
- package/autoscaling/concurrency_system.d.ts +7 -22
- package/autoscaling/concurrency_system.js +112 -100
- package/autoscaling/cpu_load_signal.d.ts +1 -2
- package/autoscaling/cpu_load_signal.js +11 -10
- package/autoscaling/event_loop_load_signal.d.ts +1 -4
- package/autoscaling/event_loop_load_signal.js +18 -18
- package/autoscaling/index.d.ts +1 -1
- package/autoscaling/index.js +1 -1
- package/autoscaling/load_signal.d.ts +8 -7
- package/autoscaling/load_signal.js +14 -12
- package/autoscaling/memory_load_signal.d.ts +3 -12
- package/autoscaling/memory_load_signal.js +41 -41
- package/autoscaling/snapshotter.d.ts +7 -10
- package/autoscaling/snapshotter.js +18 -18
- package/autoscaling/{client_load_signal.d.ts → storage_backend_load_signal.d.ts} +14 -17
- package/autoscaling/{client_load_signal.js → storage_backend_load_signal.js} +27 -27
- package/autoscaling/system_status.d.ts +9 -11
- package/autoscaling/system_status.js +13 -13
- package/configuration.d.ts +16 -16
- package/configuration.js +6 -6
- package/crawlers/context_pipeline.js +6 -6
- package/crawlers/crawler_commons.d.ts +14 -112
- package/crawlers/crawler_commons.js +1 -107
- package/crawlers/error_snapshotter.d.ts +2 -4
- package/crawlers/error_snapshotter.js +5 -5
- package/crawlers/index.d.ts +1 -1
- package/crawlers/index.js +0 -1
- package/crawlers/statistics.d.ts +164 -45
- package/crawlers/statistics.js +339 -153
- package/debug.js +4 -4
- package/enqueue_links/enqueue_links.d.ts +51 -159
- package/enqueue_links/enqueue_links.js +38 -231
- package/enqueue_links/shared.d.ts +34 -24
- package/enqueue_links/shared.js +54 -36
- package/errors.d.ts +43 -12
- package/errors.js +48 -9
- package/events/event_manager.d.ts +2 -2
- package/events/event_manager.js +7 -6
- package/events/local_event_manager.d.ts +1 -1
- package/events/local_event_manager.js +5 -5
- package/http.d.ts +9 -0
- package/http.js +28 -0
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/log.js +5 -1
- package/memory-storage/memory-storage.d.ts +3 -11
- package/memory-storage/memory-storage.js +42 -48
- package/memory-storage/resource-clients/dataset.d.ts +1 -1
- package/memory-storage/resource-clients/dataset.js +8 -13
- package/memory-storage/resource-clients/key-value-store.d.ts +1 -1
- package/memory-storage/resource-clients/key-value-store.js +36 -38
- package/memory-storage/resource-clients/request-queue.d.ts +4 -23
- package/memory-storage/resource-clients/request-queue.js +68 -80
- package/owned_or_injected.d.ts +1 -3
- package/owned_or_injected.js +17 -17
- package/package.json +8 -9
- package/proxy_configuration.d.ts +1 -3
- package/proxy_configuration.js +18 -14
- package/recoverable_state.d.ts +80 -45
- package/recoverable_state.js +163 -75
- package/request.d.ts +3 -4
- package/request.js +57 -44
- package/router.d.ts +6 -9
- package/router.js +23 -23
- package/serialization.js +14 -13
- package/service_locator.d.ts +19 -10
- package/service_locator.js +57 -48
- package/session_pool/session.d.ts +3 -14
- package/session_pool/session.js +76 -74
- package/session_pool/session_pool.d.ts +6 -12
- package/session_pool/session_pool.js +85 -77
- package/storages/batched_adds.d.ts +37 -0
- package/storages/batched_adds.js +73 -0
- package/storages/dataset.d.ts +12 -1
- package/storages/dataset.js +133 -32
- package/storages/index.d.ts +2 -1
- package/storages/index.js +2 -1
- package/storages/key_value_store.d.ts +19 -4
- package/storages/key_value_store.js +204 -75
- package/storages/request_dedup_cache.d.ts +1 -2
- package/storages/request_dedup_cache.js +9 -9
- package/storages/request_list.d.ts +4 -23
- package/storages/request_list.js +103 -96
- package/storages/request_loader.d.ts +5 -0
- package/storages/request_manager_tandem.d.ts +1 -10
- package/storages/request_manager_tandem.js +27 -27
- package/storages/request_queue.d.ts +21 -18
- package/storages/request_queue.js +318 -177
- package/storages/sitemap_request_loader.d.ts +22 -70
- package/storages/sitemap_request_loader.js +122 -116
- package/storages/storage_instance_manager.d.ts +1 -2
- package/storages/storage_instance_manager.js +17 -17
- package/storages/storage_stats.d.ts +1 -1
- package/storages/storage_stats.js +4 -4
- package/storages/throttling_request_manager.d.ts +239 -0
- package/storages/throttling_request_manager.js +646 -0
- package/storages/transaction.d.ts +252 -0
- package/storages/transaction.js +251 -0
- package/storages/utils.d.ts +6 -3
- package/storages/utils.js +11 -2
- package/system-info/runtime.js +7 -7
- package/url.d.ts +9 -0
- package/url.js +11 -0
- package/validators.d.ts +22 -25
- package/validators.js +13 -25
- package/storages/access_checking.d.ts +0 -12
- package/storages/access_checking.js +0 -17
|
@@ -1,8 +1,34 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
67
|
-
this
|
|
78
|
+
this.#loggingIntervalMillis = (loggingIntervalSecs ?? 0) * 1000;
|
|
79
|
+
this.#autoscaleIntervalMillis = autoscaleIntervalSecs * 1000;
|
|
68
80
|
this.maxTasksPerMinute = maxTasksPerMinute;
|
|
69
|
-
this
|
|
70
|
-
this
|
|
71
|
-
this
|
|
81
|
+
this.#minConcurrency = minConcurrency;
|
|
82
|
+
this.#maxConcurrency = maxConcurrency;
|
|
83
|
+
this.#desiredConcurrency = desiredConcurrency ?? minConcurrency;
|
|
72
84
|
this.clampDesiredConcurrency();
|
|
73
|
-
this.
|
|
74
|
-
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
|
|
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
|
|
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
|
-
|
|
102
|
-
this
|
|
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
|
|
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
|
-
|
|
119
|
-
this
|
|
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
|
|
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
|
-
|
|
135
|
-
this
|
|
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
|
|
146
|
-
this
|
|
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
|
|
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
|
|
164
|
-
this
|
|
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
|
|
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
|
|
175
|
-
this
|
|
176
|
-
this
|
|
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
|
|
182
|
-
this
|
|
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
|
|
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
|
|
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
|
|
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
|
|
198
|
-
this
|
|
199
|
-
this
|
|
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
|
|
204
|
-
betterClearInterval(this
|
|
205
|
-
if (this
|
|
206
|
-
betterClearInterval(this
|
|
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
|
|
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
|
|
229
|
+
if (this.#running || this.#warnedAboutQueryWhileStopped) {
|
|
218
230
|
return;
|
|
219
231
|
}
|
|
220
|
-
this
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
297
|
-
const minCurrentConcurrency = Math.floor(this
|
|
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.
|
|
312
|
+
this.scaleUp(systemStatus);
|
|
301
313
|
const isSystemOverloaded = !isSystemIdle;
|
|
302
|
-
const weAreNotAtMin = this
|
|
314
|
+
const weAreNotAtMin = this.#desiredConcurrency > this.#minConcurrency;
|
|
303
315
|
if (isSystemOverloaded && weAreNotAtMin)
|
|
304
|
-
this.
|
|
305
|
-
if (this
|
|
316
|
+
this.scaleDown(systemStatus);
|
|
317
|
+
if (this.#loggingIntervalMillis > 0) {
|
|
306
318
|
const now = Date.now();
|
|
307
|
-
if (this
|
|
308
|
-
this
|
|
319
|
+
if (this.#lastLoggingTime == null) {
|
|
320
|
+
this.#lastLoggingTime = now;
|
|
309
321
|
}
|
|
310
|
-
else if (now > this
|
|
311
|
-
this
|
|
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
|
|
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
|
-
|
|
325
|
-
const step = Math.ceil(this
|
|
326
|
-
this
|
|
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
|
|
329
|
-
newConcurrency: this
|
|
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
|
-
|
|
337
|
-
const step = Math.ceil(this
|
|
338
|
-
this
|
|
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
|
|
341
|
-
newConcurrency: this
|
|
352
|
+
oldConcurrency: this.#desiredConcurrency + step,
|
|
353
|
+
newConcurrency: this.#desiredConcurrency,
|
|
342
354
|
systemStatus,
|
|
343
355
|
});
|
|
344
356
|
}
|
|
345
|
-
|
|
346
|
-
this.
|
|
347
|
-
this.
|
|
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
|
|
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
|
|
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
|
|
27
|
-
this
|
|
27
|
+
this.#events = serviceLocator.getEventManager();
|
|
28
|
+
this.#events.on(EventType.SYSTEM_INFO, this.handle);
|
|
28
29
|
}
|
|
29
30
|
async stop() {
|
|
30
|
-
this
|
|
31
|
-
this
|
|
31
|
+
this.#events?.off(EventType.SYSTEM_INFO, this.handle);
|
|
32
|
+
this.#events = undefined;
|
|
32
33
|
}
|
|
33
34
|
getSample(sampleDurationMillis) {
|
|
34
|
-
return this
|
|
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
|
|
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
|
|
20
|
-
this
|
|
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
|
|
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
|
|
28
|
-
this
|
|
27
|
+
this.#store.clear();
|
|
28
|
+
this.#interval = betterSetInterval(this.handle, this.#intervalMillis);
|
|
29
29
|
}
|
|
30
30
|
async stop() {
|
|
31
|
-
if (this
|
|
32
|
-
betterClearInterval(this
|
|
33
|
-
this
|
|
31
|
+
if (this.#interval)
|
|
32
|
+
betterClearInterval(this.#interval);
|
|
33
|
+
this.#interval = undefined;
|
|
34
34
|
}
|
|
35
35
|
getSample(sampleDurationMillis) {
|
|
36
|
-
return this
|
|
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
|
|
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
|
|
53
|
-
if (delta > this
|
|
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
|
|
55
|
+
snapshot.exceededMillis = Math.max(delta - this.#maxBlockedMillis, 0);
|
|
56
56
|
}
|
|
57
|
-
this
|
|
57
|
+
this.#store.push(snapshot, now);
|
|
58
58
|
intervalCallback();
|
|
59
59
|
}
|
|
60
60
|
}
|
package/autoscaling/index.d.ts
CHANGED
|
@@ -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';
|
package/autoscaling/index.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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`, `
|
|
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
|
|
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
|
|
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):
|
|
100
|
+
export declare function evaluateLoadSignalSample(sample: LoadSnapshot[], overloadedRatio: number): LoadSignalInfo;
|