@crawlee/core 4.0.0-beta.98 → 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 -116
- 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 -10
- 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 -21
- 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 -4
- package/storages/storage_instance_manager.js +17 -19
- 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
|
@@ -4,8 +4,9 @@ import { weightedAvg } from './weighted_avg.js';
|
|
|
4
4
|
* and so can yours — it is the only part of their machinery worth reusing.
|
|
5
5
|
*/
|
|
6
6
|
export class SnapshotStore {
|
|
7
|
-
snapshots = [];
|
|
7
|
+
#snapshots = [];
|
|
8
8
|
/** Retention window in milliseconds. Unbounded until {@link SnapshotStore.useSampleWindow|`useSampleWindow()`}. */
|
|
9
|
+
// kept as TS-private: concurrency_system tests read this retention window directly
|
|
9
10
|
historyMillis = Infinity;
|
|
10
11
|
/**
|
|
11
12
|
* Sizes retention to the window the signal will be sampled over, as handed to it in
|
|
@@ -21,30 +22,30 @@ export class SnapshotStore {
|
|
|
21
22
|
push(snapshot, now = snapshot.createdAt) {
|
|
22
23
|
// Inline pruning to avoid private-method transpilation issues
|
|
23
24
|
let oldCount = 0;
|
|
24
|
-
for (let i = 0; i < this
|
|
25
|
-
const { createdAt } = this
|
|
25
|
+
for (let i = 0; i < this.#snapshots.length; i++) {
|
|
26
|
+
const { createdAt } = this.#snapshots[i];
|
|
26
27
|
if (now.getTime() - new Date(createdAt).getTime() > this.historyMillis)
|
|
27
28
|
oldCount++;
|
|
28
29
|
else
|
|
29
30
|
break;
|
|
30
31
|
}
|
|
31
32
|
if (oldCount)
|
|
32
|
-
this
|
|
33
|
-
this
|
|
33
|
+
this.#snapshots.splice(0, oldCount);
|
|
34
|
+
this.#snapshots.push(snapshot);
|
|
34
35
|
}
|
|
35
36
|
/**
|
|
36
37
|
* Return all snapshots, or only those within the given time window.
|
|
37
38
|
*/
|
|
38
39
|
getSample(sampleDurationMillis) {
|
|
39
40
|
if (!sampleDurationMillis)
|
|
40
|
-
return this
|
|
41
|
+
return this.#snapshots;
|
|
41
42
|
const sample = [];
|
|
42
|
-
let idx = this
|
|
43
|
+
let idx = this.#snapshots.length;
|
|
43
44
|
if (!idx)
|
|
44
45
|
return sample;
|
|
45
|
-
const latestTime = this
|
|
46
|
+
const latestTime = this.#snapshots[idx - 1].createdAt;
|
|
46
47
|
while (idx--) {
|
|
47
|
-
const snapshot = this
|
|
48
|
+
const snapshot = this.#snapshots[idx];
|
|
48
49
|
if (+latestTime - +snapshot.createdAt <= sampleDurationMillis) {
|
|
49
50
|
sample.unshift(snapshot);
|
|
50
51
|
}
|
|
@@ -56,10 +57,11 @@ export class SnapshotStore {
|
|
|
56
57
|
}
|
|
57
58
|
/**
|
|
58
59
|
* Direct, unwindowed access to the underlying array — used by signals whose handler needs the previous snapshot
|
|
59
|
-
* to compute a delta (e.g. the event loop and
|
|
60
|
+
* to compute a delta (e.g. the event loop and storage backend signals read the last entry to measure change since
|
|
61
|
+
* it).
|
|
60
62
|
*/
|
|
61
63
|
getAll() {
|
|
62
|
-
return this
|
|
64
|
+
return this.#snapshots;
|
|
63
65
|
}
|
|
64
66
|
/**
|
|
65
67
|
* Discards every retained snapshot. The built-in signals do this when they *start*, so that a session neither
|
|
@@ -68,7 +70,7 @@ export class SnapshotStore {
|
|
|
68
70
|
* rather than on stop leaves a finished session readable.
|
|
69
71
|
*/
|
|
70
72
|
clear() {
|
|
71
|
-
this
|
|
73
|
+
this.#snapshots = [];
|
|
72
74
|
}
|
|
73
75
|
}
|
|
74
76
|
/**
|
|
@@ -33,24 +33,15 @@ export interface MemoryLoadSignalOptions {
|
|
|
33
33
|
* @category Scaling
|
|
34
34
|
*/
|
|
35
35
|
export declare class MemoryLoadSignal implements LoadSignal {
|
|
36
|
+
#private;
|
|
36
37
|
readonly name = "memInfo";
|
|
37
38
|
readonly overloadedRatio: number;
|
|
38
|
-
private readonly store;
|
|
39
|
-
private readonly maxUsedRatio;
|
|
40
|
-
/** All resolved in `start()`, before anything that reads them can fire. */
|
|
41
|
-
private config;
|
|
42
|
-
private log;
|
|
43
|
-
private maxMemoryBytes;
|
|
44
|
-
private events?;
|
|
45
|
-
private maxMemoryRatio;
|
|
46
|
-
private lastLoggedCriticalMemoryOverloadAt;
|
|
47
39
|
constructor(options?: MemoryLoadSignalOptions);
|
|
48
40
|
start(context: LoadSignalStartContext): Promise<void>;
|
|
49
41
|
stop(): Promise<void>;
|
|
50
42
|
getSample(sampleDurationMillis?: number): LoadSnapshot[];
|
|
51
43
|
/** @internal Records a snapshot from a `SYSTEM_INFO` payload. Exposed for tests. */
|
|
52
44
|
handle(systemInfo: SystemInfo): void;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
private _getTotalMemoryBytes;
|
|
45
|
+
private memoryOverloadWarning;
|
|
46
|
+
private getTotalMemoryBytes;
|
|
56
47
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EventType } from '../events/event_manager.js';
|
|
1
2
|
import { serviceLocator } from '../service_locator.js';
|
|
2
3
|
import { getMemoryInfo } from '../system-info/memory-info.js';
|
|
3
4
|
import { isContainerized } from '../system-info/runtime.js';
|
|
@@ -15,92 +16,91 @@ const CRITICAL_OVERLOAD_RATE_LIMIT_MILLIS = 10_000;
|
|
|
15
16
|
export class MemoryLoadSignal {
|
|
16
17
|
name = 'memInfo';
|
|
17
18
|
overloadedRatio;
|
|
18
|
-
store = new SnapshotStore();
|
|
19
|
-
maxUsedRatio;
|
|
19
|
+
#store = new SnapshotStore();
|
|
20
|
+
#maxUsedRatio;
|
|
20
21
|
/** All resolved in `start()`, before anything that reads them can fire. */
|
|
21
|
-
config;
|
|
22
|
-
log;
|
|
23
|
-
maxMemoryBytes;
|
|
24
|
-
events;
|
|
25
|
-
maxMemoryRatio;
|
|
26
|
-
lastLoggedCriticalMemoryOverloadAt = null;
|
|
22
|
+
#config;
|
|
23
|
+
#log;
|
|
24
|
+
#maxMemoryBytes;
|
|
25
|
+
#events;
|
|
26
|
+
#maxMemoryRatio;
|
|
27
|
+
#lastLoggedCriticalMemoryOverloadAt = null;
|
|
27
28
|
constructor(options = {}) {
|
|
28
|
-
this
|
|
29
|
+
this.#maxUsedRatio = options.maxUsedRatio ?? 0.9;
|
|
29
30
|
this.overloadedRatio = options.overloadedRatio ?? 0.2;
|
|
30
31
|
this.handle = this.handle.bind(this);
|
|
31
32
|
}
|
|
32
33
|
async start(context) {
|
|
33
|
-
this
|
|
34
|
+
this.#store.useSampleWindow(context.maxSampleWindowMillis);
|
|
34
35
|
// A new session starts from a clean slate, so it is not judged on measurements from before the downtime.
|
|
35
|
-
this
|
|
36
|
+
this.#store.clear();
|
|
36
37
|
// Resolved here rather than in the constructor: an instance built ahead of time (to be wrapped, or shared
|
|
37
38
|
// between systems) must not capture whichever services happened to be registered at that moment.
|
|
38
|
-
this
|
|
39
|
-
this
|
|
40
|
-
this
|
|
41
|
-
const memoryMbytes = this
|
|
39
|
+
this.#config = serviceLocator.getConfiguration();
|
|
40
|
+
this.#events = serviceLocator.getEventManager();
|
|
41
|
+
this.#log = serviceLocator.getLogger().child({ prefix: 'MemoryLoadSignal' });
|
|
42
|
+
const memoryMbytes = this.#config.memoryMbytes ?? 0;
|
|
42
43
|
if (memoryMbytes > 0) {
|
|
43
|
-
this
|
|
44
|
+
this.#maxMemoryBytes = memoryMbytes * 1024 * 1024;
|
|
44
45
|
}
|
|
45
46
|
else {
|
|
46
|
-
this
|
|
47
|
-
if (!this
|
|
47
|
+
this.#maxMemoryRatio = this.#config.availableMemoryRatio;
|
|
48
|
+
if (!this.#maxMemoryRatio) {
|
|
48
49
|
throw new Error('availableMemoryRatio is not set in configuration.');
|
|
49
50
|
}
|
|
50
51
|
else {
|
|
51
|
-
this
|
|
52
|
+
this.#log.debug(`Setting max memory of this run to ${this.#maxMemoryRatio * 100} % of available memory. ` +
|
|
52
53
|
'Use the CRAWLEE_MEMORY_MBYTES or CRAWLEE_AVAILABLE_MEMORY_RATIO environment variable to override it.');
|
|
53
54
|
}
|
|
54
55
|
// Fallback memory measurement in case memTotalBytes is missing from SystemInfo.
|
|
55
|
-
this
|
|
56
|
+
this.#maxMemoryBytes = await this.getTotalMemoryBytes();
|
|
56
57
|
}
|
|
57
|
-
this
|
|
58
|
+
this.#events.on(EventType.SYSTEM_INFO, this.handle);
|
|
58
59
|
}
|
|
59
60
|
async stop() {
|
|
60
|
-
this
|
|
61
|
-
this
|
|
61
|
+
this.#events?.off(EventType.SYSTEM_INFO, this.handle);
|
|
62
|
+
this.#events = undefined;
|
|
62
63
|
}
|
|
63
64
|
getSample(sampleDurationMillis) {
|
|
64
|
-
return this
|
|
65
|
+
return this.#store.getSample(sampleDurationMillis);
|
|
65
66
|
}
|
|
66
67
|
/** @internal Records a snapshot from a `SYSTEM_INFO` payload. Exposed for tests. */
|
|
67
68
|
handle(systemInfo) {
|
|
68
69
|
const createdAt = systemInfo.createdAt ? new Date(systemInfo.createdAt) : new Date();
|
|
69
70
|
const { memCurrentBytes, memTotalBytes } = systemInfo;
|
|
70
|
-
let maxMemoryBytes = this
|
|
71
|
-
if (this
|
|
72
|
-
maxMemoryBytes = this
|
|
71
|
+
let maxMemoryBytes = this.#maxMemoryBytes;
|
|
72
|
+
if (this.#maxMemoryRatio !== undefined && this.#maxMemoryRatio > 0) {
|
|
73
|
+
maxMemoryBytes = this.#maxMemoryRatio * (memTotalBytes ?? this.#maxMemoryBytes);
|
|
73
74
|
}
|
|
74
75
|
const snapshot = {
|
|
75
76
|
createdAt,
|
|
76
|
-
isOverloaded: memCurrentBytes / maxMemoryBytes > this
|
|
77
|
+
isOverloaded: memCurrentBytes / maxMemoryBytes > this.#maxUsedRatio,
|
|
77
78
|
usedBytes: memCurrentBytes,
|
|
78
79
|
};
|
|
79
|
-
this
|
|
80
|
-
this.
|
|
80
|
+
this.#store.push(snapshot, createdAt);
|
|
81
|
+
this.memoryOverloadWarning(systemInfo, maxMemoryBytes);
|
|
81
82
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const effectiveMax = maxMemoryBytes ?? this.maxMemoryBytes;
|
|
83
|
+
memoryOverloadWarning(systemInfo, maxMemoryBytes) {
|
|
84
|
+
const effectiveMax = maxMemoryBytes ?? this.#maxMemoryBytes;
|
|
85
85
|
const { memCurrentBytes } = systemInfo;
|
|
86
86
|
const createdAt = systemInfo.createdAt ? new Date(systemInfo.createdAt) : new Date();
|
|
87
|
-
if (this
|
|
88
|
-
+createdAt < +this
|
|
87
|
+
if (this.#lastLoggedCriticalMemoryOverloadAt &&
|
|
88
|
+
+createdAt < +this.#lastLoggedCriticalMemoryOverloadAt + CRITICAL_OVERLOAD_RATE_LIMIT_MILLIS)
|
|
89
89
|
return;
|
|
90
|
-
const maxDesiredMemoryBytes = this
|
|
91
|
-
const reserveMemory = effectiveMax * (1 - this
|
|
90
|
+
const maxDesiredMemoryBytes = this.#maxUsedRatio * effectiveMax;
|
|
91
|
+
const reserveMemory = effectiveMax * (1 - this.#maxUsedRatio) * RESERVE_MEMORY_RATIO;
|
|
92
92
|
const criticalOverloadBytes = maxDesiredMemoryBytes + reserveMemory;
|
|
93
93
|
const isCriticalOverload = memCurrentBytes > criticalOverloadBytes;
|
|
94
94
|
if (isCriticalOverload) {
|
|
95
95
|
const usedPercentage = Math.round((memCurrentBytes / effectiveMax) * 100);
|
|
96
96
|
const toMb = (bytes) => Math.round(bytes / 1024 ** 2);
|
|
97
|
-
this
|
|
97
|
+
this.#log.warning('Memory is critically overloaded. ' +
|
|
98
98
|
`Using ${toMb(memCurrentBytes)} MB of ${toMb(effectiveMax)} MB (${usedPercentage}%). Consider increasing available memory.`);
|
|
99
|
-
this
|
|
99
|
+
this.#lastLoggedCriticalMemoryOverloadAt = createdAt;
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
|
-
async
|
|
103
|
-
const containerized = this
|
|
102
|
+
async getTotalMemoryBytes() {
|
|
103
|
+
const containerized = this.#config.containerized ?? (await isContainerized());
|
|
104
104
|
return (await getMemoryInfo({ containerized, logger: serviceLocator.getLogger() })).totalBytes;
|
|
105
105
|
}
|
|
106
106
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { ClientLoadSignalOptions } from './client_load_signal.js';
|
|
2
1
|
import type { CpuLoadSignalOptions } from './cpu_load_signal.js';
|
|
3
2
|
import type { EventLoopLoadSignalOptions } from './event_loop_load_signal.js';
|
|
4
3
|
import type { LoadSignal, LoadSignalStartContext } from './load_signal.js';
|
|
5
4
|
import type { MemoryLoadSignalOptions } from './memory_load_signal.js';
|
|
5
|
+
import type { StorageBackendLoadSignalOptions } from './storage_backend_load_signal.js';
|
|
6
6
|
/**
|
|
7
7
|
* The load signals a {@link ConcurrencySystem} watches to decide whether the machine is overloaded.
|
|
8
8
|
*
|
|
@@ -31,11 +31,11 @@ export interface LoadSignalsOptions {
|
|
|
31
31
|
*/
|
|
32
32
|
cpu?: CpuLoadSignalOptions | false;
|
|
33
33
|
/**
|
|
34
|
-
* Tuning for the built-in {@link
|
|
35
|
-
* `false` to switch it off — worth doing when the storage backend reports no rate-limit statistics, since the
|
|
34
|
+
* Tuning for the built-in {@link StorageBackendLoadSignal} (snapshot interval + error limit + overload ratio),
|
|
35
|
+
* or `false` to switch it off — worth doing when the storage backend reports no rate-limit statistics, since the
|
|
36
36
|
* signal otherwise polls it every second to no purpose.
|
|
37
37
|
*/
|
|
38
|
-
|
|
38
|
+
storageBackend?: StorageBackendLoadSignalOptions | false;
|
|
39
39
|
/**
|
|
40
40
|
* Additional {@link LoadSignal} implementations — e.g. navigation timeouts or proxy health — evaluated
|
|
41
41
|
* alongside the built-in four. If any signal reports overload, the system counts as overloaded. Their lifecycle
|
|
@@ -53,18 +53,15 @@ export interface LoadSignalsOptions {
|
|
|
53
53
|
export type SnapshotterOptions = Omit<LoadSignalsOptions, 'custom'>;
|
|
54
54
|
/**
|
|
55
55
|
* Owns the four built-in {@link LoadSignal} instances — {@link MemoryLoadSignal},
|
|
56
|
-
* {@link EventLoopLoadSignal}, {@link CpuLoadSignal} and {@link
|
|
57
|
-
* that were not switched off and driving their shared lifecycle.
|
|
56
|
+
* {@link EventLoopLoadSignal}, {@link CpuLoadSignal} and {@link StorageBackendLoadSignal} — constructing
|
|
57
|
+
* the ones that were not switched off and driving their shared lifecycle.
|
|
58
58
|
*
|
|
59
59
|
* Configured indirectly through {@link ConcurrencySystemOptions.loadSignals|`loadSignals`}, whose per-signal bags
|
|
60
60
|
* are simply forwarded to the corresponding constructor.
|
|
61
61
|
* @internal
|
|
62
62
|
*/
|
|
63
63
|
export declare class Snapshotter {
|
|
64
|
-
private
|
|
65
|
-
private readonly eventLoopSignal?;
|
|
66
|
-
private readonly cpuSignal?;
|
|
67
|
-
private readonly clientSignal?;
|
|
64
|
+
#private;
|
|
68
65
|
/**
|
|
69
66
|
* Returns the enabled built-in signals, so `SystemStatus` can iterate them alongside any custom `LoadSignal`
|
|
70
67
|
* instances. Signals switched off through the options are simply absent — the system status reports them as
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import { ClientLoadSignal } from './client_load_signal.js';
|
|
2
1
|
import { CpuLoadSignal } from './cpu_load_signal.js';
|
|
3
2
|
import { EventLoopLoadSignal } from './event_loop_load_signal.js';
|
|
4
3
|
import { MemoryLoadSignal } from './memory_load_signal.js';
|
|
4
|
+
import { StorageBackendLoadSignal } from './storage_backend_load_signal.js';
|
|
5
5
|
/**
|
|
6
6
|
* Owns the four built-in {@link LoadSignal} instances — {@link MemoryLoadSignal},
|
|
7
|
-
* {@link EventLoopLoadSignal}, {@link CpuLoadSignal} and {@link
|
|
8
|
-
* that were not switched off and driving their shared lifecycle.
|
|
7
|
+
* {@link EventLoopLoadSignal}, {@link CpuLoadSignal} and {@link StorageBackendLoadSignal} — constructing
|
|
8
|
+
* the ones that were not switched off and driving their shared lifecycle.
|
|
9
9
|
*
|
|
10
10
|
* Configured indirectly through {@link ConcurrencySystemOptions.loadSignals|`loadSignals`}, whose per-signal bags
|
|
11
11
|
* are simply forwarded to the corresponding constructor.
|
|
12
12
|
* @internal
|
|
13
13
|
*/
|
|
14
14
|
export class Snapshotter {
|
|
15
|
-
// Absent when switched off through the corresponding option (e.g. `
|
|
16
|
-
memorySignal;
|
|
17
|
-
eventLoopSignal;
|
|
18
|
-
cpuSignal;
|
|
19
|
-
|
|
15
|
+
// Absent when switched off through the corresponding option (e.g. `storageBackend: false`).
|
|
16
|
+
#memorySignal;
|
|
17
|
+
#eventLoopSignal;
|
|
18
|
+
#cpuSignal;
|
|
19
|
+
#storageBackendSignal;
|
|
20
20
|
/**
|
|
21
21
|
* Returns the enabled built-in signals, so `SystemStatus` can iterate them alongside any custom `LoadSignal`
|
|
22
22
|
* instances. Signals switched off through the options are simply absent — the system status reports them as
|
|
@@ -24,10 +24,10 @@ export class Snapshotter {
|
|
|
24
24
|
*/
|
|
25
25
|
getLoadSignals() {
|
|
26
26
|
const builtin = [
|
|
27
|
-
this
|
|
28
|
-
this
|
|
29
|
-
this
|
|
30
|
-
this
|
|
27
|
+
this.#memorySignal,
|
|
28
|
+
this.#eventLoopSignal,
|
|
29
|
+
this.#cpuSignal,
|
|
30
|
+
this.#storageBackendSignal,
|
|
31
31
|
];
|
|
32
32
|
return builtin.filter((signal) => signal !== undefined);
|
|
33
33
|
}
|
|
@@ -35,17 +35,17 @@ export class Snapshotter {
|
|
|
35
35
|
* @param [options] All `Snapshotter` configuration options.
|
|
36
36
|
*/
|
|
37
37
|
constructor(options = {}) {
|
|
38
|
-
const { memory = {}, eventLoop = {}, cpu = {},
|
|
38
|
+
const { memory = {}, eventLoop = {}, cpu = {}, storageBackend = {} } = options;
|
|
39
39
|
// Each signal resolves its own ambient dependencies when started, and is told the window it will be sampled
|
|
40
40
|
// over then too - so there is nothing to thread in here beyond the caller's tuning.
|
|
41
41
|
if (memory !== false)
|
|
42
|
-
this
|
|
42
|
+
this.#memorySignal = new MemoryLoadSignal(memory);
|
|
43
43
|
if (eventLoop !== false)
|
|
44
|
-
this
|
|
44
|
+
this.#eventLoopSignal = new EventLoopLoadSignal(eventLoop);
|
|
45
45
|
if (cpu !== false)
|
|
46
|
-
this
|
|
47
|
-
if (
|
|
48
|
-
this
|
|
46
|
+
this.#cpuSignal = new CpuLoadSignal(cpu);
|
|
47
|
+
if (storageBackend !== false)
|
|
48
|
+
this.#storageBackendSignal = new StorageBackendLoadSignal(storageBackend);
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
51
|
* Starts capturing snapshots at configured intervals. The `context` carries the sample window the signals will
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import type { LoadSignal, LoadSignalStartContext, LoadSnapshot } from './load_signal.js';
|
|
2
2
|
/**
|
|
3
|
-
* A snapshot produced by the built-in
|
|
3
|
+
* A snapshot produced by the built-in storage backend (rate-limit) signal.
|
|
4
4
|
* @internal
|
|
5
5
|
*/
|
|
6
|
-
export interface
|
|
6
|
+
export interface StorageBackendSnapshot extends LoadSnapshot {
|
|
7
7
|
rateLimitErrorCount: number;
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* Tuning for the built-in **
|
|
11
|
-
*
|
|
10
|
+
* Tuning for the built-in **storage backend** (rate-limit) load signal, as accepted both by
|
|
11
|
+
* {@link StorageBackendLoadSignal} and by the
|
|
12
|
+
* {@link LoadSignalsOptions.storageBackend|`storageBackend`} shorthand on {@link LoadSignalsOptions}.
|
|
12
13
|
*/
|
|
13
|
-
export interface
|
|
14
|
+
export interface StorageBackendLoadSignalOptions {
|
|
14
15
|
/**
|
|
15
|
-
* Defines the interval of checking the current state of the
|
|
16
|
+
* Defines the interval of checking the current state of the storage backend, in seconds.
|
|
16
17
|
* @default 1
|
|
17
18
|
*/
|
|
18
19
|
snapshotIntervalSecs?: number;
|
|
@@ -22,7 +23,7 @@ export interface ClientLoadSignalOptions {
|
|
|
22
23
|
*/
|
|
23
24
|
maxErrors?: number;
|
|
24
25
|
/**
|
|
25
|
-
* Maximum ratio of overloaded snapshots in a sample before the
|
|
26
|
+
* Maximum ratio of overloaded snapshots in a sample before the storage backend counts as overloaded.
|
|
26
27
|
* @default 0.3
|
|
27
28
|
*/
|
|
28
29
|
overloadedRatio?: number;
|
|
@@ -33,20 +34,16 @@ export interface ClientLoadSignalOptions {
|
|
|
33
34
|
*
|
|
34
35
|
* Built by default; construct one yourself only to wrap or adapt it — see {@link LoadSignal}.
|
|
35
36
|
*
|
|
36
|
-
* Switch it off entirely ({@link LoadSignalsOptions.
|
|
37
|
-
* rate-limit statistics, since it otherwise polls it every second to no purpose.
|
|
37
|
+
* Switch it off entirely ({@link LoadSignalsOptions.storageBackend|`storageBackend: false`}) if the storage backend
|
|
38
|
+
* reports no rate-limit statistics, since it otherwise polls it every second to no purpose.
|
|
38
39
|
*
|
|
39
40
|
* @category Scaling
|
|
40
41
|
*/
|
|
41
|
-
export declare class
|
|
42
|
-
|
|
42
|
+
export declare class StorageBackendLoadSignal implements LoadSignal {
|
|
43
|
+
#private;
|
|
44
|
+
readonly name = "storageBackendInfo";
|
|
43
45
|
readonly overloadedRatio: number;
|
|
44
|
-
|
|
45
|
-
private readonly intervalMillis;
|
|
46
|
-
private readonly maxErrors;
|
|
47
|
-
private interval?;
|
|
48
|
-
private client?;
|
|
49
|
-
constructor(options?: ClientLoadSignalOptions);
|
|
46
|
+
constructor(options?: StorageBackendLoadSignalOptions);
|
|
50
47
|
start(context: LoadSignalStartContext): Promise<void>;
|
|
51
48
|
stop(): Promise<void>;
|
|
52
49
|
getSample(sampleDurationMillis?: number): LoadSnapshot[];
|
|
@@ -1,50 +1,50 @@
|
|
|
1
1
|
import { betterClearInterval, betterSetInterval } from '@apify/utilities';
|
|
2
2
|
import { serviceLocator } from '../service_locator.js';
|
|
3
3
|
import { SnapshotStore } from './load_signal.js';
|
|
4
|
-
const
|
|
4
|
+
const RATE_LIMIT_ERROR_RETRY_COUNT = 2;
|
|
5
5
|
/**
|
|
6
6
|
* Periodically checks the storage backend for rate-limit errors (HTTP 429) and reports overload when the error delta
|
|
7
7
|
* exceeds a threshold.
|
|
8
8
|
*
|
|
9
9
|
* Built by default; construct one yourself only to wrap or adapt it — see {@link LoadSignal}.
|
|
10
10
|
*
|
|
11
|
-
* Switch it off entirely ({@link LoadSignalsOptions.
|
|
12
|
-
* rate-limit statistics, since it otherwise polls it every second to no purpose.
|
|
11
|
+
* Switch it off entirely ({@link LoadSignalsOptions.storageBackend|`storageBackend: false`}) if the storage backend
|
|
12
|
+
* reports no rate-limit statistics, since it otherwise polls it every second to no purpose.
|
|
13
13
|
*
|
|
14
14
|
* @category Scaling
|
|
15
15
|
*/
|
|
16
|
-
export class
|
|
17
|
-
name = '
|
|
16
|
+
export class StorageBackendLoadSignal {
|
|
17
|
+
name = 'storageBackendInfo';
|
|
18
18
|
overloadedRatio;
|
|
19
|
-
store = new SnapshotStore();
|
|
20
|
-
intervalMillis;
|
|
21
|
-
maxErrors;
|
|
22
|
-
interval;
|
|
23
|
-
|
|
19
|
+
#store = new SnapshotStore();
|
|
20
|
+
#intervalMillis;
|
|
21
|
+
#maxErrors;
|
|
22
|
+
#interval;
|
|
23
|
+
#storageBackend;
|
|
24
24
|
constructor(options = {}) {
|
|
25
25
|
this.overloadedRatio = options.overloadedRatio ?? 0.3;
|
|
26
|
-
this
|
|
27
|
-
this
|
|
26
|
+
this.#intervalMillis = (options.snapshotIntervalSecs ?? 1) * 1000;
|
|
27
|
+
this.#maxErrors = options.maxErrors ?? 3;
|
|
28
28
|
this.handle = this.handle.bind(this);
|
|
29
29
|
}
|
|
30
30
|
async start(context) {
|
|
31
|
-
this
|
|
31
|
+
this.#store.useSampleWindow(context.maxSampleWindowMillis);
|
|
32
32
|
// A new session starts from a clean slate, or its first measurement diffs the error count against the previous
|
|
33
|
-
// session's — possibly against a different backend, since
|
|
34
|
-
this
|
|
33
|
+
// session's — possibly against a different backend, since it is resolved afresh just below.
|
|
34
|
+
this.#store.clear();
|
|
35
35
|
// Resolved here rather than in the constructor, where asking for the backend would instantiate a default one
|
|
36
36
|
// as a side effect - long before the crawler that owns the run has had a chance to register its own.
|
|
37
|
-
this
|
|
38
|
-
this
|
|
37
|
+
this.#storageBackend = serviceLocator.getStorageBackend();
|
|
38
|
+
this.#interval = betterSetInterval(this.handle, this.#intervalMillis);
|
|
39
39
|
}
|
|
40
40
|
async stop() {
|
|
41
|
-
if (this
|
|
42
|
-
betterClearInterval(this
|
|
43
|
-
this
|
|
44
|
-
this
|
|
41
|
+
if (this.#interval)
|
|
42
|
+
betterClearInterval(this.#interval);
|
|
43
|
+
this.#interval = undefined;
|
|
44
|
+
this.#storageBackend = undefined;
|
|
45
45
|
}
|
|
46
46
|
getSample(sampleDurationMillis) {
|
|
47
|
-
return this
|
|
47
|
+
return this.#store.getSample(sampleDurationMillis);
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
50
|
* Records one snapshot, overloaded when rate-limit errors grew by more than the configured limit since the
|
|
@@ -53,21 +53,21 @@ export class ClientLoadSignal {
|
|
|
53
53
|
*/
|
|
54
54
|
handle(intervalCallback) {
|
|
55
55
|
const now = new Date();
|
|
56
|
-
const allErrorCounts = this
|
|
57
|
-
const currentErrCount = allErrorCounts[
|
|
56
|
+
const allErrorCounts = this.#storageBackend?.stats?.rateLimitErrors ?? [];
|
|
57
|
+
const currentErrCount = allErrorCounts[RATE_LIMIT_ERROR_RETRY_COUNT] || 0;
|
|
58
58
|
const snapshot = {
|
|
59
59
|
createdAt: now,
|
|
60
60
|
isOverloaded: false,
|
|
61
61
|
rateLimitErrorCount: currentErrCount,
|
|
62
62
|
};
|
|
63
|
-
const all = this
|
|
63
|
+
const all = this.#store.getAll();
|
|
64
64
|
const previousSnapshot = all[all.length - 1];
|
|
65
65
|
if (previousSnapshot) {
|
|
66
66
|
const delta = currentErrCount - previousSnapshot.rateLimitErrorCount;
|
|
67
|
-
if (delta > this
|
|
67
|
+
if (delta > this.#maxErrors)
|
|
68
68
|
snapshot.isOverloaded = true;
|
|
69
69
|
}
|
|
70
|
-
this
|
|
70
|
+
this.#store.push(snapshot, now);
|
|
71
71
|
intervalCallback();
|
|
72
72
|
}
|
|
73
73
|
}
|
|
@@ -6,10 +6,10 @@ import type { Snapshotter } from './snapshotter.js';
|
|
|
6
6
|
export interface SystemInfo {
|
|
7
7
|
/** If false, system is being overloaded. */
|
|
8
8
|
isSystemIdle: boolean;
|
|
9
|
-
memInfo:
|
|
10
|
-
eventLoopInfo:
|
|
11
|
-
cpuInfo:
|
|
12
|
-
|
|
9
|
+
memInfo: LoadSignalInfo;
|
|
10
|
+
eventLoopInfo: LoadSignalInfo;
|
|
11
|
+
cpuInfo: LoadSignalInfo;
|
|
12
|
+
storageBackendInfo: LoadSignalInfo;
|
|
13
13
|
memTotalBytes?: number;
|
|
14
14
|
memCurrentBytes?: number;
|
|
15
15
|
/**
|
|
@@ -31,7 +31,7 @@ export interface SystemInfo {
|
|
|
31
31
|
* Status of additional load signals beyond the built-in four.
|
|
32
32
|
* Keys are `LoadSignal.name` values, values are overload info.
|
|
33
33
|
*/
|
|
34
|
-
loadSignalInfo?: Record<string,
|
|
34
|
+
loadSignalInfo?: Record<string, LoadSignalInfo>;
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
37
|
* How far back the *current* system status looks by default — the window that gates task dispatch.
|
|
@@ -69,12 +69,12 @@ export interface SystemStatusOptions {
|
|
|
69
69
|
/**
|
|
70
70
|
* Additional load signals to include in the system status evaluation.
|
|
71
71
|
* These are evaluated alongside the built-in memory, CPU, event loop,
|
|
72
|
-
* and
|
|
73
|
-
* considered overloaded. Each signal carries its own overload ratio.
|
|
72
|
+
* and storage backend signals. If any signal reports overload, the system
|
|
73
|
+
* is considered overloaded. Each signal carries its own overload ratio.
|
|
74
74
|
*/
|
|
75
75
|
loadSignals?: LoadSignal[];
|
|
76
76
|
}
|
|
77
|
-
export interface
|
|
77
|
+
export interface LoadSignalInfo {
|
|
78
78
|
isOverloaded: boolean;
|
|
79
79
|
limitRatio: number;
|
|
80
80
|
actualRatio: number;
|
|
@@ -105,9 +105,7 @@ export interface FinalStatistics {
|
|
|
105
105
|
* @internal
|
|
106
106
|
*/
|
|
107
107
|
export declare class SystemStatus {
|
|
108
|
-
private
|
|
109
|
-
private readonly historyMillis;
|
|
110
|
-
private readonly signals;
|
|
108
|
+
#private;
|
|
111
109
|
constructor(options: SystemStatusOptions);
|
|
112
110
|
/**
|
|
113
111
|
* The widest window any signal will be queried with, and therefore exactly how much history the signals are asked
|
|
@@ -15,7 +15,7 @@ const BUILTIN_SIGNAL_OPTION_KEYS = {
|
|
|
15
15
|
memInfo: 'memory',
|
|
16
16
|
eventLoopInfo: 'eventLoop',
|
|
17
17
|
cpuInfo: 'cpu',
|
|
18
|
-
|
|
18
|
+
storageBackendInfo: 'storageBackend',
|
|
19
19
|
};
|
|
20
20
|
const BUILTIN_SIGNAL_NAMES = new Set(Object.keys(BUILTIN_SIGNAL_OPTION_KEYS));
|
|
21
21
|
/**
|
|
@@ -32,14 +32,14 @@ const BUILTIN_SIGNAL_NAMES = new Set(Object.keys(BUILTIN_SIGNAL_OPTION_KEYS));
|
|
|
32
32
|
* @internal
|
|
33
33
|
*/
|
|
34
34
|
export class SystemStatus {
|
|
35
|
-
currentHistoryMillis;
|
|
36
|
-
historyMillis;
|
|
37
|
-
signals;
|
|
35
|
+
#currentHistoryMillis;
|
|
36
|
+
#historyMillis;
|
|
37
|
+
#signals;
|
|
38
38
|
constructor(options) {
|
|
39
39
|
const { currentHistorySecs = DEFAULT_CURRENT_HISTORY_SECS, historySecs = DEFAULT_SNAPSHOT_HISTORY_SECS, snapshotter, loadSignals = [], } = options;
|
|
40
|
-
this
|
|
41
|
-
this
|
|
42
|
-
this
|
|
40
|
+
this.#currentHistoryMillis = currentHistorySecs * 1000;
|
|
41
|
+
this.#historyMillis = historySecs * 1000;
|
|
42
|
+
this.#signals = [...snapshotter.getLoadSignals(), ...loadSignals];
|
|
43
43
|
this.assertUniqueSignalNames();
|
|
44
44
|
}
|
|
45
45
|
/**
|
|
@@ -48,7 +48,7 @@ export class SystemStatus {
|
|
|
48
48
|
* defaults.
|
|
49
49
|
*/
|
|
50
50
|
get maxSampleWindowMillis() {
|
|
51
|
-
return Math.max(this
|
|
51
|
+
return Math.max(this.#currentHistoryMillis, this.#historyMillis);
|
|
52
52
|
}
|
|
53
53
|
/**
|
|
54
54
|
* Signal names are the keys of the reported {@link SystemInfo}, so a duplicate would leave a status object that
|
|
@@ -57,7 +57,7 @@ export class SystemStatus {
|
|
|
57
57
|
*/
|
|
58
58
|
assertUniqueSignalNames() {
|
|
59
59
|
const seen = new Set();
|
|
60
|
-
for (const { name } of this
|
|
60
|
+
for (const { name } of this.#signals) {
|
|
61
61
|
if (!seen.has(name)) {
|
|
62
62
|
seen.add(name);
|
|
63
63
|
continue;
|
|
@@ -85,7 +85,7 @@ export class SystemStatus {
|
|
|
85
85
|
* and `true` otherwise.
|
|
86
86
|
*/
|
|
87
87
|
getCurrentStatus() {
|
|
88
|
-
return this.isSystemIdle(this
|
|
88
|
+
return this.isSystemIdle(this.#currentHistoryMillis);
|
|
89
89
|
}
|
|
90
90
|
/**
|
|
91
91
|
* Returns an {@link SystemInfo} object with the following structure:
|
|
@@ -103,7 +103,7 @@ export class SystemStatus {
|
|
|
103
103
|
* `historySecs` seconds and `true` otherwise.
|
|
104
104
|
*/
|
|
105
105
|
getHistoricalStatus() {
|
|
106
|
-
return this.isSystemIdle(this
|
|
106
|
+
return this.isSystemIdle(this.#historyMillis);
|
|
107
107
|
}
|
|
108
108
|
/**
|
|
109
109
|
* Returns a system status object.
|
|
@@ -114,10 +114,10 @@ export class SystemStatus {
|
|
|
114
114
|
memInfo: { isOverloaded: false, limitRatio: 0, actualRatio: 0 },
|
|
115
115
|
eventLoopInfo: { isOverloaded: false, limitRatio: 0, actualRatio: 0 },
|
|
116
116
|
cpuInfo: { isOverloaded: false, limitRatio: 0, actualRatio: 0 },
|
|
117
|
-
|
|
117
|
+
storageBackendInfo: { isOverloaded: false, limitRatio: 0, actualRatio: 0 },
|
|
118
118
|
};
|
|
119
119
|
let loadSignalInfo;
|
|
120
|
-
for (const signal of this
|
|
120
|
+
for (const signal of this.#signals) {
|
|
121
121
|
const sample = signal.getSample(sampleDurationMillis);
|
|
122
122
|
const info = evaluateLoadSignalSample(sample, signal.overloadedRatio);
|
|
123
123
|
if (info.isOverloaded) {
|