@crawlee/core 4.0.0-beta.121 → 4.0.0-beta.123
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.js +20 -12
- package/autoscaling/concurrency_system.d.ts +2 -2
- package/autoscaling/concurrency_system.js +31 -20
- package/autoscaling/index.d.ts +1 -1
- package/autoscaling/index.js +1 -1
- package/autoscaling/load_signal.d.ts +7 -6
- package/autoscaling/load_signal.js +2 -1
- package/autoscaling/snapshotter.d.ts +6 -6
- package/autoscaling/snapshotter.js +9 -9
- package/autoscaling/{client_load_signal.d.ts → storage_backend_load_signal.d.ts} +13 -12
- package/autoscaling/{client_load_signal.js → storage_backend_load_signal.js} +11 -11
- package/autoscaling/system_status.d.ts +8 -8
- package/autoscaling/system_status.js +2 -2
- package/configuration.d.ts +15 -15
- package/configuration.js +3 -3
- package/crawlers/crawler_commons.d.ts +8 -54
- package/crawlers/statistics.d.ts +1 -1
- package/crawlers/statistics.js +14 -14
- package/debug.js +4 -4
- package/enqueue_links/enqueue_links.d.ts +33 -61
- package/enqueue_links/enqueue_links.js +35 -152
- package/enqueue_links/shared.d.ts +17 -4
- package/enqueue_links/shared.js +28 -1
- package/memory-storage/resource-clients/dataset.js +2 -8
- package/memory-storage/resource-clients/key-value-store.js +23 -26
- package/memory-storage/resource-clients/request-queue.js +9 -22
- package/package.json +7 -8
- package/proxy_configuration.js +10 -6
- package/request.d.ts +2 -2
- package/request.js +44 -31
- package/router.d.ts +5 -5
- package/serialization.js +6 -4
- package/session_pool/session.js +22 -20
- package/session_pool/session_pool.js +20 -17
- package/storages/dataset.js +11 -9
- package/storages/key_value_store.js +30 -27
- package/storages/request_list.d.ts +2 -1
- package/storages/request_list.js +26 -21
- package/storages/request_queue.js +64 -59
- package/storages/sitemap_request_loader.d.ts +1 -1
- package/storages/sitemap_request_loader.js +22 -22
- package/storages/throttling_request_manager.js +11 -9
- package/storages/utils.d.ts +2 -1
- package/validators.d.ts +22 -25
- package/validators.js +13 -25
|
@@ -1,8 +1,26 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { z } from 'zod';
|
|
2
2
|
import { addTimeoutToPromise } from '@apify/timeout';
|
|
3
3
|
import { betterClearInterval, betterSetInterval } from '@apify/utilities';
|
|
4
4
|
import { CriticalError } from '../errors.js';
|
|
5
5
|
import { serviceLocator } from '../service_locator.js';
|
|
6
|
+
import { parseArgument, schemas, validators } from '../validators.js';
|
|
7
|
+
// `schemas.anyObject` and `objectWithKeys`-based validators pass values through by reference
|
|
8
|
+
// (object schemas return a pruned plain copy), so class instances like loggers and the
|
|
9
|
+
// concurrency system keep their prototype.
|
|
10
|
+
const autoscaledPoolOptionsSchema = z.strictObject({
|
|
11
|
+
runTaskFunction: schemas.anyFunction,
|
|
12
|
+
isFinishedFunction: schemas.anyFunction,
|
|
13
|
+
isTaskReadyFunction: schemas.anyFunction,
|
|
14
|
+
maybeRunIntervalSecs: schemas.anyNumber
|
|
15
|
+
.refine((value) => value > 0, 'Expected a number greater than 0')
|
|
16
|
+
.default(0.5),
|
|
17
|
+
taskTimeoutSecs: schemas.anyNumber
|
|
18
|
+
.refine((value) => value >= 0, 'Expected a number greater than or equal to 0')
|
|
19
|
+
.default(0),
|
|
20
|
+
log: validators.logger.default(() => serviceLocator.getLogger()),
|
|
21
|
+
concurrencySystem: schemas.anyObject,
|
|
22
|
+
consumer: schemas.anyObject.refine((value) => typeof value.id === 'string' && value.id.length > 0, "Expected an object with a non-empty string 'id'"),
|
|
23
|
+
});
|
|
6
24
|
/**
|
|
7
25
|
* Manages a pool of asynchronous resource-intensive tasks that are executed in parallel.
|
|
8
26
|
* The pool only starts new tasks while its {@link IConcurrencySystem|concurrency system} reports free capacity —
|
|
@@ -78,17 +96,7 @@ export class AutoscaledPool {
|
|
|
78
96
|
*/
|
|
79
97
|
#ownConcurrency = 0;
|
|
80
98
|
constructor(options) {
|
|
81
|
-
|
|
82
|
-
runTaskFunction: ow.function,
|
|
83
|
-
isFinishedFunction: ow.function,
|
|
84
|
-
isTaskReadyFunction: ow.function,
|
|
85
|
-
maybeRunIntervalSecs: ow.optional.number.greaterThan(0),
|
|
86
|
-
taskTimeoutSecs: ow.optional.number.greaterThanOrEqual(0),
|
|
87
|
-
log: ow.optional.object,
|
|
88
|
-
concurrencySystem: ow.object,
|
|
89
|
-
consumer: ow.object.partialShape({ id: ow.string.nonEmpty }),
|
|
90
|
-
}));
|
|
91
|
-
const { runTaskFunction, isFinishedFunction, isTaskReadyFunction, maybeRunIntervalSecs = 0.5, taskTimeoutSecs = 0, log = serviceLocator.getLogger(), concurrencySystem, consumer, } = options;
|
|
99
|
+
const { runTaskFunction, isFinishedFunction, isTaskReadyFunction, maybeRunIntervalSecs, taskTimeoutSecs, log, concurrencySystem, consumer, } = parseArgument(options, autoscaledPoolOptionsSchema, 'AutoscaledPoolOptions');
|
|
92
100
|
this.#log = log.child({ prefix: 'AutoscaledPool' });
|
|
93
101
|
// Configurable properties.
|
|
94
102
|
this.#maybeRunIntervalMillis = maybeRunIntervalSecs * 1000;
|
|
@@ -53,8 +53,8 @@ export interface ConcurrencySystemOptions {
|
|
|
53
53
|
autoscaleIntervalSecs?: number;
|
|
54
54
|
/**
|
|
55
55
|
* The signals that tell the system whether the machine is overloaded: per-resource tuning for the built-in four
|
|
56
|
-
* (memory, event loop, CPU,
|
|
57
|
-
* your own. See {@link LoadSignalsOptions}.
|
|
56
|
+
* (memory, event loop, CPU, storage backend) plus any {@link LoadSignalsOptions.custom|`custom`}
|
|
57
|
+
* implementations of your own. See {@link LoadSignalsOptions}.
|
|
58
58
|
*/
|
|
59
59
|
loadSignals?: LoadSignalsOptions;
|
|
60
60
|
/**
|
|
@@ -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
|
|
@@ -44,22 +70,7 @@ export class ConcurrencySystem {
|
|
|
44
70
|
/** Set once per session, so a pool outliving `stop()` is reported once rather than every half second. */
|
|
45
71
|
#warnedAboutQueryWhileStopped = false;
|
|
46
72
|
constructor(options = {}) {
|
|
47
|
-
|
|
48
|
-
maxConcurrency: ow.optional.number.integer.greaterThanOrEqual(1),
|
|
49
|
-
minConcurrency: ow.optional.number.integer.greaterThanOrEqual(1),
|
|
50
|
-
desiredConcurrency: ow.optional.number.integer.greaterThanOrEqual(1),
|
|
51
|
-
desiredConcurrencyRatio: ow.optional.number.greaterThan(0).lessThan(1),
|
|
52
|
-
scaleUpStepRatio: ow.optional.number.greaterThan(0).lessThan(1),
|
|
53
|
-
scaleDownStepRatio: ow.optional.number.greaterThan(0).lessThan(1),
|
|
54
|
-
loggingIntervalSecs: ow.any(ow.number.greaterThan(0), ow.nullOrUndefined),
|
|
55
|
-
autoscaleIntervalSecs: ow.optional.number.greaterThan(0),
|
|
56
|
-
loadSignals: ow.optional.object,
|
|
57
|
-
snapshotHistorySecs: ow.optional.number.greaterThan(0),
|
|
58
|
-
currentHistorySecs: ow.optional.number.greaterThan(0),
|
|
59
|
-
log: ow.optional.object,
|
|
60
|
-
maxTasksPerMinute: ow.optional.number.integerOrInfinite.greaterThanOrEqual(1),
|
|
61
|
-
}));
|
|
62
|
-
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');
|
|
63
74
|
this.log = log.child({ prefix: 'ConcurrencySystem' });
|
|
64
75
|
this.desiredConcurrencyRatio = desiredConcurrencyRatio;
|
|
65
76
|
this.scaleUpStepRatio = scaleUpStepRatio;
|
|
@@ -99,7 +110,7 @@ export class ConcurrencySystem {
|
|
|
99
110
|
* If you're not sure, just keep the default value and the concurrency will scale up automatically.
|
|
100
111
|
*/
|
|
101
112
|
set minConcurrency(value) {
|
|
102
|
-
|
|
113
|
+
parseArgument(value, concurrencySchema);
|
|
103
114
|
this.#minConcurrency = value;
|
|
104
115
|
this.clampDesiredConcurrency();
|
|
105
116
|
}
|
|
@@ -116,7 +127,7 @@ export class ConcurrencySystem {
|
|
|
116
127
|
* limit as they settle).
|
|
117
128
|
*/
|
|
118
129
|
set maxConcurrency(value) {
|
|
119
|
-
|
|
130
|
+
parseArgument(value, concurrencySchema);
|
|
120
131
|
this.#maxConcurrency = value;
|
|
121
132
|
this.clampDesiredConcurrency();
|
|
122
133
|
}
|
|
@@ -132,7 +143,7 @@ export class ConcurrencySystem {
|
|
|
132
143
|
* in parallel if there's large enough supply of tasks.
|
|
133
144
|
*/
|
|
134
145
|
set desiredConcurrency(value) {
|
|
135
|
-
|
|
146
|
+
parseArgument(value, concurrencySchema);
|
|
136
147
|
this.#desiredConcurrency = value;
|
|
137
148
|
this.clampDesiredConcurrency();
|
|
138
149
|
}
|
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
|
/**
|
|
@@ -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;
|
|
@@ -57,7 +57,8 @@ export class SnapshotStore {
|
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
59
|
* Direct, unwindowed access to the underlying array — used by signals whose handler needs the previous snapshot
|
|
60
|
-
* 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).
|
|
61
62
|
*/
|
|
62
63
|
getAll() {
|
|
63
64
|
return this.#snapshots;
|
|
@@ -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,8 +53,8 @@ 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.
|
|
@@ -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. `
|
|
15
|
+
// Absent when switched off through the corresponding option (e.g. `storageBackend: false`).
|
|
16
16
|
#memorySignal;
|
|
17
17
|
#eventLoopSignal;
|
|
18
18
|
#cpuSignal;
|
|
19
|
-
#
|
|
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
|
|
@@ -27,7 +27,7 @@ export class Snapshotter {
|
|
|
27
27
|
this.#memorySignal,
|
|
28
28
|
this.#eventLoopSignal,
|
|
29
29
|
this.#cpuSignal,
|
|
30
|
-
this.#
|
|
30
|
+
this.#storageBackendSignal,
|
|
31
31
|
];
|
|
32
32
|
return builtin.filter((signal) => signal !== undefined);
|
|
33
33
|
}
|
|
@@ -35,7 +35,7 @@ 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)
|
|
@@ -44,8 +44,8 @@ export class Snapshotter {
|
|
|
44
44
|
this.#eventLoopSignal = new EventLoopLoadSignal(eventLoop);
|
|
45
45
|
if (cpu !== false)
|
|
46
46
|
this.#cpuSignal = new CpuLoadSignal(cpu);
|
|
47
|
-
if (
|
|
48
|
-
this.#
|
|
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,16 +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
|
+
export declare class StorageBackendLoadSignal implements LoadSignal {
|
|
42
43
|
#private;
|
|
43
|
-
readonly name = "
|
|
44
|
+
readonly name = "storageBackendInfo";
|
|
44
45
|
readonly overloadedRatio: number;
|
|
45
|
-
constructor(options?:
|
|
46
|
+
constructor(options?: StorageBackendLoadSignalOptions);
|
|
46
47
|
start(context: LoadSignalStartContext): Promise<void>;
|
|
47
48
|
stop(): Promise<void>;
|
|
48
49
|
getSample(sampleDurationMillis?: number): LoadSnapshot[];
|
|
@@ -1,26 +1,26 @@
|
|
|
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
19
|
#store = new SnapshotStore();
|
|
20
20
|
#intervalMillis;
|
|
21
21
|
#maxErrors;
|
|
22
22
|
#interval;
|
|
23
|
-
#
|
|
23
|
+
#storageBackend;
|
|
24
24
|
constructor(options = {}) {
|
|
25
25
|
this.overloadedRatio = options.overloadedRatio ?? 0.3;
|
|
26
26
|
this.#intervalMillis = (options.snapshotIntervalSecs ?? 1) * 1000;
|
|
@@ -30,18 +30,18 @@ export class ClientLoadSignal {
|
|
|
30
30
|
async start(context) {
|
|
31
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
|
|
33
|
+
// session's — possibly against a different backend, since it is resolved afresh just below.
|
|
34
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.#
|
|
37
|
+
this.#storageBackend = serviceLocator.getStorageBackend();
|
|
38
38
|
this.#interval = betterSetInterval(this.handle, this.#intervalMillis);
|
|
39
39
|
}
|
|
40
40
|
async stop() {
|
|
41
41
|
if (this.#interval)
|
|
42
42
|
betterClearInterval(this.#interval);
|
|
43
43
|
this.#interval = undefined;
|
|
44
|
-
this.#
|
|
44
|
+
this.#storageBackend = undefined;
|
|
45
45
|
}
|
|
46
46
|
getSample(sampleDurationMillis) {
|
|
47
47
|
return this.#store.getSample(sampleDurationMillis);
|
|
@@ -53,8 +53,8 @@ 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,
|
|
@@ -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;
|
|
@@ -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
|
/**
|
|
@@ -114,7 +114,7 @@ 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
120
|
for (const signal of this.#signals) {
|
package/configuration.d.ts
CHANGED
|
@@ -6,47 +6,47 @@ export interface ConfigField<T extends z.ZodType = z.ZodType> {
|
|
|
6
6
|
}
|
|
7
7
|
export declare function field<T extends z.ZodType>(schema: T, envVar?: string | string[]): ConfigField<T>;
|
|
8
8
|
/** Zod preprocessor treating `'0'` and `'false'` as falsy. */
|
|
9
|
-
export declare const coerceBoolean: z.
|
|
10
|
-
export declare const coerceNumber: z.
|
|
9
|
+
export declare const coerceBoolean: z.ZodPreprocess<z.ZodBoolean>;
|
|
10
|
+
export declare const coerceNumber: z.ZodPreprocess<z.ZodNumber>;
|
|
11
11
|
export declare const crawleeConfigFields: {
|
|
12
12
|
/** @default 'default' */
|
|
13
13
|
defaultDatasetId: ConfigField<z.ZodDefault<z.ZodString>>;
|
|
14
14
|
/** @default true */
|
|
15
|
-
purgeOnStart: ConfigField<z.ZodDefault<z.
|
|
15
|
+
purgeOnStart: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodBoolean>>>;
|
|
16
16
|
/** @default 'default' */
|
|
17
17
|
defaultKeyValueStoreId: ConfigField<z.ZodDefault<z.ZodString>>;
|
|
18
18
|
/** @default 'default' */
|
|
19
19
|
defaultRequestQueueId: ConfigField<z.ZodDefault<z.ZodString>>;
|
|
20
20
|
/** @default 0.95 */
|
|
21
|
-
maxUsedCpuRatio: ConfigField<z.ZodDefault<z.
|
|
21
|
+
maxUsedCpuRatio: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodNumber>>>;
|
|
22
22
|
/** @default 0.25 */
|
|
23
|
-
availableMemoryRatio: ConfigField<z.ZodDefault<z.
|
|
24
|
-
memoryMbytes: ConfigField<z.ZodOptional<z.
|
|
23
|
+
availableMemoryRatio: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodNumber>>>;
|
|
24
|
+
memoryMbytes: ConfigField<z.ZodOptional<z.ZodPreprocess<z.ZodNumber>>>;
|
|
25
25
|
/** @default 60_000 */
|
|
26
|
-
persistStateIntervalMillis: ConfigField<z.ZodDefault<z.
|
|
26
|
+
persistStateIntervalMillis: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodNumber>>>;
|
|
27
27
|
/**
|
|
28
28
|
* Internal safety-net timeout for a single request, in milliseconds. When unset the crawler derives it from
|
|
29
29
|
* the request handler timeout (twice it, and never below 5 minutes).
|
|
30
30
|
*/
|
|
31
|
-
internalTimeoutMillis: ConfigField<z.ZodOptional<z.
|
|
31
|
+
internalTimeoutMillis: ConfigField<z.ZodOptional<z.ZodPreprocess<z.ZodNumber>>>;
|
|
32
32
|
/** @default 1_000 */
|
|
33
|
-
systemInfoIntervalMillis: ConfigField<z.ZodDefault<z.
|
|
33
|
+
systemInfoIntervalMillis: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodNumber>>>;
|
|
34
34
|
/** @default 'INPUT' */
|
|
35
35
|
inputKey: ConfigField<z.ZodDefault<z.ZodString>>;
|
|
36
36
|
/** @default true */
|
|
37
|
-
headless: ConfigField<z.ZodDefault<z.
|
|
37
|
+
headless: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodBoolean>>>;
|
|
38
38
|
/** @default false */
|
|
39
|
-
xvfb: ConfigField<z.ZodDefault<z.
|
|
39
|
+
xvfb: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodBoolean>>>;
|
|
40
40
|
chromeExecutablePath: ConfigField<z.ZodOptional<z.ZodString>>;
|
|
41
41
|
defaultBrowserPath: ConfigField<z.ZodOptional<z.ZodString>>;
|
|
42
42
|
/** @default false */
|
|
43
|
-
disableBrowserSandbox: ConfigField<z.ZodDefault<z.
|
|
44
|
-
logLevel: ConfigField<z.ZodOptional<z.
|
|
43
|
+
disableBrowserSandbox: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodBoolean>>>;
|
|
44
|
+
logLevel: ConfigField<z.ZodOptional<z.ZodPreprocess<z.ZodEnum<typeof LogLevel>>>>;
|
|
45
45
|
/** @default true */
|
|
46
|
-
persistStorage: ConfigField<z.ZodDefault<z.
|
|
46
|
+
persistStorage: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodBoolean>>>;
|
|
47
47
|
/** @default './storage' */
|
|
48
48
|
storageDir: ConfigField<z.ZodDefault<z.ZodString>>;
|
|
49
|
-
containerized: ConfigField<z.ZodOptional<z.
|
|
49
|
+
containerized: ConfigField<z.ZodOptional<z.ZodPreprocess<z.ZodBoolean>>>;
|
|
50
50
|
};
|
|
51
51
|
export type FieldsInput<F extends Record<string, ConfigField>> = {
|
|
52
52
|
[K in keyof F]?: z.output<F[K]['schema']>;
|
package/configuration.js
CHANGED
|
@@ -36,7 +36,7 @@ const logLevelSchema = z.preprocess((val) => {
|
|
|
36
36
|
if (key in LogLevel)
|
|
37
37
|
return LogLevel[key];
|
|
38
38
|
return val;
|
|
39
|
-
}, z.
|
|
39
|
+
}, z.enum(LogLevel));
|
|
40
40
|
// --- Crawlee config field definitions ---
|
|
41
41
|
export const crawleeConfigFields = {
|
|
42
42
|
/** @default 'default' */
|
|
@@ -196,8 +196,8 @@ export class Configuration {
|
|
|
196
196
|
continue;
|
|
197
197
|
}
|
|
198
198
|
// 4. Schema default (by parsing undefined through the schema)
|
|
199
|
-
const
|
|
200
|
-
values[key] =
|
|
199
|
+
const parsed = fieldDef.schema.safeParse(undefined);
|
|
200
|
+
values[key] = parsed.success ? parsed.data : undefined;
|
|
201
201
|
}
|
|
202
202
|
return values;
|
|
203
203
|
}
|