@crawlee/core 4.0.0-beta.9 → 4.0.0-beta.91
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/README.md +17 -13
- package/autoscaling/autoscaled_pool.d.ts +66 -153
- package/autoscaling/autoscaled_pool.js +95 -251
- package/autoscaling/client_load_signal.d.ts +59 -0
- package/autoscaling/client_load_signal.js +73 -0
- package/autoscaling/concurrency_system.d.ts +283 -0
- package/autoscaling/concurrency_system.js +350 -0
- package/autoscaling/cpu_load_signal.d.ts +44 -0
- package/autoscaling/cpu_load_signal.js +46 -0
- package/autoscaling/event_loop_load_signal.d.ts +54 -0
- package/autoscaling/event_loop_load_signal.js +60 -0
- package/autoscaling/index.d.ts +6 -1
- package/autoscaling/index.js +6 -1
- package/autoscaling/load_signal.d.ts +99 -0
- package/autoscaling/load_signal.js +103 -0
- package/autoscaling/memory_load_signal.d.ts +56 -0
- package/autoscaling/memory_load_signal.js +106 -0
- package/autoscaling/snapshotter.d.ts +61 -163
- package/autoscaling/snapshotter.js +45 -263
- package/autoscaling/system_status.d.ts +63 -83
- package/autoscaling/system_status.js +90 -120
- package/configuration.d.ts +90 -223
- package/configuration.js +164 -222
- package/cookie_utils.d.ts +4 -3
- package/cookie_utils.js +20 -13
- package/crawlers/context_pipeline.d.ts +70 -0
- package/crawlers/context_pipeline.js +122 -0
- package/crawlers/crawler_commons.d.ts +76 -33
- package/crawlers/crawler_commons.js +15 -24
- package/crawlers/crawler_utils.d.ts +2 -3
- package/crawlers/crawler_utils.js +1 -2
- package/crawlers/error_snapshotter.d.ts +3 -3
- package/crawlers/error_snapshotter.js +2 -3
- package/crawlers/error_tracker.d.ts +2 -2
- package/crawlers/error_tracker.js +0 -1
- package/crawlers/index.d.ts +1 -2
- package/crawlers/index.js +1 -2
- package/crawlers/internals/types.d.ts +7 -0
- package/crawlers/internals/types.js +1 -0
- package/crawlers/statistics.d.ts +28 -23
- package/crawlers/statistics.js +38 -34
- package/debug.d.ts +36 -0
- package/debug.js +70 -0
- package/enqueue_links/enqueue_links.d.ts +44 -22
- package/enqueue_links/enqueue_links.js +51 -30
- package/enqueue_links/index.d.ts +0 -1
- package/enqueue_links/index.js +0 -1
- package/enqueue_links/shared.d.ts +25 -9
- package/enqueue_links/shared.js +69 -38
- package/errors.d.ts +53 -4
- package/errors.js +70 -5
- package/events/event_manager.d.ts +34 -8
- package/events/event_manager.js +8 -10
- package/events/index.d.ts +0 -1
- package/events/index.js +0 -1
- package/events/local_event_manager.d.ts +15 -3
- package/events/local_event_manager.js +37 -11
- package/index.d.ts +6 -3
- package/index.js +5 -2
- package/log.d.ts +82 -3
- package/log.js +102 -1
- package/memory-storage/consts.d.ts +4 -0
- package/memory-storage/consts.js +4 -0
- package/memory-storage/index.d.ts +1 -0
- package/memory-storage/index.js +1 -0
- package/memory-storage/memory-storage.d.ts +46 -0
- package/memory-storage/memory-storage.js +136 -0
- package/memory-storage/resource-clients/common/base-client.d.ts +4 -0
- package/memory-storage/resource-clients/common/base-client.js +6 -0
- package/memory-storage/resource-clients/dataset.d.ts +40 -0
- package/memory-storage/resource-clients/dataset.js +113 -0
- package/memory-storage/resource-clients/key-value-store.d.ts +63 -0
- package/memory-storage/resource-clients/key-value-store.js +203 -0
- package/memory-storage/resource-clients/request-queue.d.ts +96 -0
- package/memory-storage/resource-clients/request-queue.js +421 -0
- package/memory-storage/utils.d.ts +17 -0
- package/memory-storage/utils.js +42 -0
- package/owned_or_injected.d.ts +60 -0
- package/owned_or_injected.js +98 -0
- package/package.json +12 -10
- package/proxy_configuration.d.ts +29 -152
- package/proxy_configuration.js +21 -173
- package/recoverable_state.d.ts +120 -0
- package/recoverable_state.js +143 -0
- package/request.d.ts +85 -15
- package/request.js +107 -28
- package/router.d.ts +144 -19
- package/router.js +125 -30
- package/serialization.d.ts +0 -1
- package/serialization.js +1 -2
- package/service_locator.d.ts +156 -0
- package/service_locator.js +238 -0
- package/session_pool/consts.d.ts +1 -2
- package/session_pool/consts.js +1 -2
- package/session_pool/errors.d.ts +0 -1
- package/session_pool/errors.js +0 -1
- package/session_pool/fingerprint.d.ts +9 -0
- package/session_pool/fingerprint.js +30 -0
- package/session_pool/index.d.ts +0 -2
- package/session_pool/index.js +0 -2
- package/session_pool/session.d.ts +37 -75
- package/session_pool/session.js +49 -102
- package/session_pool/session_pool.d.ts +85 -90
- package/session_pool/session_pool.js +131 -120
- package/storages/access_checking.d.ts +1 -2
- package/storages/access_checking.js +5 -2
- package/storages/dataset.d.ts +103 -54
- package/storages/dataset.js +174 -132
- package/storages/index.d.ts +8 -7
- package/storages/index.js +6 -7
- package/storages/key_value_store.d.ts +167 -39
- package/storages/key_value_store.js +274 -127
- package/storages/key_value_store_codec.d.ts +32 -0
- package/storages/key_value_store_codec.js +113 -0
- package/storages/request_dedup_cache.d.ts +23 -0
- package/storages/request_dedup_cache.js +48 -0
- package/storages/request_list.d.ts +54 -98
- package/storages/request_list.js +99 -75
- package/storages/request_loader.d.ts +96 -0
- package/storages/request_loader.js +1 -0
- package/storages/request_manager.d.ts +33 -0
- package/storages/request_manager.js +1 -0
- package/storages/request_manager_tandem.d.ts +106 -0
- package/storages/request_manager_tandem.js +197 -0
- package/storages/request_queue.d.ts +290 -47
- package/storages/request_queue.js +629 -215
- package/storages/{sitemap_request_list.d.ts → sitemap_request_loader.d.ts} +28 -24
- package/storages/{sitemap_request_list.js → sitemap_request_loader.js} +49 -47
- package/storages/storage_instance_manager.d.ts +90 -0
- package/storages/storage_instance_manager.js +258 -0
- package/storages/storage_stats.d.ts +48 -0
- package/storages/storage_stats.js +29 -0
- package/storages/utils.d.ts +54 -9
- package/storages/utils.js +64 -13
- package/system-info/cpu-info.d.ts +67 -0
- package/system-info/cpu-info.js +216 -0
- package/system-info/memory-info.d.ts +31 -0
- package/system-info/memory-info.js +115 -0
- package/system-info/ps-tree.d.ts +17 -0
- package/system-info/ps-tree.js +144 -0
- package/system-info/runtime.d.ts +14 -0
- package/system-info/runtime.js +80 -0
- package/typedefs.d.ts +0 -6
- package/typedefs.js +0 -1
- package/validators.d.ts +8 -1
- package/validators.js +10 -3
- package/autoscaling/autoscaled_pool.d.ts.map +0 -1
- package/autoscaling/autoscaled_pool.js.map +0 -1
- package/autoscaling/index.d.ts.map +0 -1
- package/autoscaling/index.js.map +0 -1
- package/autoscaling/snapshotter.d.ts.map +0 -1
- package/autoscaling/snapshotter.js.map +0 -1
- package/autoscaling/system_status.d.ts.map +0 -1
- package/autoscaling/system_status.js.map +0 -1
- package/configuration.d.ts.map +0 -1
- package/configuration.js.map +0 -1
- package/cookie_utils.d.ts.map +0 -1
- package/cookie_utils.js.map +0 -1
- package/crawlers/crawler_commons.d.ts.map +0 -1
- package/crawlers/crawler_commons.js.map +0 -1
- package/crawlers/crawler_extension.d.ts +0 -12
- package/crawlers/crawler_extension.d.ts.map +0 -1
- package/crawlers/crawler_extension.js +0 -14
- package/crawlers/crawler_extension.js.map +0 -1
- package/crawlers/crawler_utils.d.ts.map +0 -1
- package/crawlers/crawler_utils.js.map +0 -1
- package/crawlers/error_snapshotter.d.ts.map +0 -1
- package/crawlers/error_snapshotter.js.map +0 -1
- package/crawlers/error_tracker.d.ts.map +0 -1
- package/crawlers/error_tracker.js.map +0 -1
- package/crawlers/index.d.ts.map +0 -1
- package/crawlers/index.js.map +0 -1
- package/crawlers/statistics.d.ts.map +0 -1
- package/crawlers/statistics.js.map +0 -1
- package/enqueue_links/enqueue_links.d.ts.map +0 -1
- package/enqueue_links/enqueue_links.js.map +0 -1
- package/enqueue_links/index.d.ts.map +0 -1
- package/enqueue_links/index.js.map +0 -1
- package/enqueue_links/shared.d.ts.map +0 -1
- package/enqueue_links/shared.js.map +0 -1
- package/errors.d.ts.map +0 -1
- package/errors.js.map +0 -1
- package/events/event_manager.d.ts.map +0 -1
- package/events/event_manager.js.map +0 -1
- package/events/index.d.ts.map +0 -1
- package/events/index.js.map +0 -1
- package/events/local_event_manager.d.ts.map +0 -1
- package/events/local_event_manager.js.map +0 -1
- package/http_clients/base-http-client.d.ts +0 -134
- package/http_clients/base-http-client.d.ts.map +0 -1
- package/http_clients/base-http-client.js +0 -33
- package/http_clients/base-http-client.js.map +0 -1
- package/http_clients/form-data-like.d.ts +0 -67
- package/http_clients/form-data-like.d.ts.map +0 -1
- package/http_clients/form-data-like.js +0 -5
- package/http_clients/form-data-like.js.map +0 -1
- package/http_clients/got-scraping-http-client.d.ts +0 -15
- package/http_clients/got-scraping-http-client.d.ts.map +0 -1
- package/http_clients/got-scraping-http-client.js +0 -69
- package/http_clients/got-scraping-http-client.js.map +0 -1
- package/http_clients/index.d.ts +0 -3
- package/http_clients/index.d.ts.map +0 -1
- package/http_clients/index.js +0 -3
- package/http_clients/index.js.map +0 -1
- package/index.d.ts.map +0 -1
- package/index.js.map +0 -1
- package/log.d.ts.map +0 -1
- package/log.js.map +0 -1
- package/proxy_configuration.d.ts.map +0 -1
- package/proxy_configuration.js.map +0 -1
- package/request.d.ts.map +0 -1
- package/request.js.map +0 -1
- package/router.d.ts.map +0 -1
- package/router.js.map +0 -1
- package/serialization.d.ts.map +0 -1
- package/serialization.js.map +0 -1
- package/session_pool/consts.d.ts.map +0 -1
- package/session_pool/consts.js.map +0 -1
- package/session_pool/errors.d.ts.map +0 -1
- package/session_pool/errors.js.map +0 -1
- package/session_pool/events.d.ts +0 -3
- package/session_pool/events.d.ts.map +0 -1
- package/session_pool/events.js +0 -3
- package/session_pool/events.js.map +0 -1
- package/session_pool/index.d.ts.map +0 -1
- package/session_pool/index.js.map +0 -1
- package/session_pool/session.d.ts.map +0 -1
- package/session_pool/session.js.map +0 -1
- package/session_pool/session_pool.d.ts.map +0 -1
- package/session_pool/session_pool.js.map +0 -1
- package/storages/access_checking.d.ts.map +0 -1
- package/storages/access_checking.js.map +0 -1
- package/storages/dataset.d.ts.map +0 -1
- package/storages/dataset.js.map +0 -1
- package/storages/index.d.ts.map +0 -1
- package/storages/index.js.map +0 -1
- package/storages/key_value_store.d.ts.map +0 -1
- package/storages/key_value_store.js.map +0 -1
- package/storages/request_list.d.ts.map +0 -1
- package/storages/request_list.js.map +0 -1
- package/storages/request_provider.d.ts +0 -307
- package/storages/request_provider.d.ts.map +0 -1
- package/storages/request_provider.js +0 -555
- package/storages/request_provider.js.map +0 -1
- package/storages/request_queue.d.ts.map +0 -1
- package/storages/request_queue.js.map +0 -1
- package/storages/request_queue_v2.d.ts +0 -87
- package/storages/request_queue_v2.d.ts.map +0 -1
- package/storages/request_queue_v2.js +0 -438
- package/storages/request_queue_v2.js.map +0 -1
- package/storages/sitemap_request_list.d.ts.map +0 -1
- package/storages/sitemap_request_list.js.map +0 -1
- package/storages/storage_manager.d.ts +0 -58
- package/storages/storage_manager.d.ts.map +0 -1
- package/storages/storage_manager.js +0 -105
- package/storages/storage_manager.js.map +0 -1
- package/storages/utils.d.ts.map +0 -1
- package/storages/utils.js.map +0 -1
- package/tsconfig.build.tsbuildinfo +0 -1
- package/typedefs.d.ts.map +0 -1
- package/typedefs.js.map +0 -1
- package/validators.d.ts.map +0 -1
- package/validators.js.map +0 -1
|
@@ -1,31 +1,23 @@
|
|
|
1
1
|
import ow from 'ow';
|
|
2
2
|
import { addTimeoutToPromise } from '@apify/timeout';
|
|
3
3
|
import { betterClearInterval, betterSetInterval } from '@apify/utilities';
|
|
4
|
-
import { Configuration } from '../configuration.js';
|
|
5
4
|
import { CriticalError } from '../errors.js';
|
|
6
|
-
import {
|
|
7
|
-
import { Snapshotter } from './snapshotter.js';
|
|
8
|
-
import { SystemStatus } from './system_status.js';
|
|
5
|
+
import { serviceLocator } from '../service_locator.js';
|
|
9
6
|
/**
|
|
10
7
|
* Manages a pool of asynchronous resource-intensive tasks that are executed in parallel.
|
|
11
|
-
* The pool only starts new tasks
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* The information about the CPU and memory usage is obtained by the {@link Snapshotter} class,
|
|
15
|
-
* which makes regular snapshots of system resources that may be either local
|
|
16
|
-
* or from the Apify cloud infrastructure in case the process is running on the Apify platform.
|
|
17
|
-
* Meaningful data gathered from these snapshots is provided to `AutoscaledPool` by the {@link SystemStatus} class.
|
|
8
|
+
* The pool only starts new tasks while its {@link IConcurrencySystem|concurrency system} reports free capacity —
|
|
9
|
+
* that governor is what monitors CPU, memory and event loop load and autoscales the concurrency budget.
|
|
18
10
|
*
|
|
19
11
|
* Before running the pool, you need to implement the following three functions:
|
|
20
|
-
* {@link AutoscaledPoolOptions.runTaskFunction},
|
|
21
|
-
* {@link
|
|
22
|
-
* {@link
|
|
12
|
+
* {@link AutoscaledPoolOptions.runTaskFunction|`runTaskFunction`},
|
|
13
|
+
* {@link AutoscaledPoolPredicateOptions.isTaskReadyFunction|`isTaskReadyFunction`} and
|
|
14
|
+
* {@link AutoscaledPoolPredicateOptions.isFinishedFunction|`isFinishedFunction`}.
|
|
23
15
|
*
|
|
24
16
|
* The auto-scaled pool is started by calling the {@link AutoscaledPool.run} function.
|
|
25
|
-
* The pool periodically queries
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
17
|
+
* The pool periodically queries `isTaskReadyFunction` for more tasks, managing optimal concurrency, until the function
|
|
18
|
+
* resolves to `false`. The pool then queries `isFinishedFunction`. If it resolves to `true`, the run finishes after all
|
|
19
|
+
* running tasks complete. If it resolves to `false`, it assumes there will be more tasks available later and keeps
|
|
20
|
+
* periodically querying for tasks.
|
|
29
21
|
* If any of the tasks throws then the {@link AutoscaledPool.run} function rejects the promise with an error.
|
|
30
22
|
*
|
|
31
23
|
* The pool evaluates whether it should start a new task every time one of the tasks finishes
|
|
@@ -34,8 +26,12 @@ import { SystemStatus } from './system_status.js';
|
|
|
34
26
|
* **Example usage:**
|
|
35
27
|
*
|
|
36
28
|
* ```javascript
|
|
29
|
+
* const concurrencySystem = new ConcurrencySystem({ maxConcurrency: 50 });
|
|
30
|
+
* await concurrencySystem.start();
|
|
31
|
+
*
|
|
37
32
|
* const pool = new AutoscaledPool({
|
|
38
|
-
*
|
|
33
|
+
* concurrencySystem,
|
|
34
|
+
* consumer: { id: 'my-pool' },
|
|
39
35
|
* runTaskFunction: async () => {
|
|
40
36
|
* // Run some resource-intensive asynchronous operation here.
|
|
41
37
|
* },
|
|
@@ -50,177 +46,115 @@ import { SystemStatus } from './system_status.js';
|
|
|
50
46
|
* }
|
|
51
47
|
* });
|
|
52
48
|
*
|
|
53
|
-
*
|
|
49
|
+
* try {
|
|
50
|
+
* await pool.run();
|
|
51
|
+
* } finally {
|
|
52
|
+
* await concurrencySystem.stop();
|
|
53
|
+
* }
|
|
54
54
|
* ```
|
|
55
55
|
* @category Scaling
|
|
56
56
|
*/
|
|
57
57
|
export class AutoscaledPool {
|
|
58
|
-
config;
|
|
59
58
|
log;
|
|
60
59
|
// Configurable properties.
|
|
61
|
-
desiredConcurrencyRatio;
|
|
62
|
-
scaleUpStepRatio;
|
|
63
|
-
scaleDownStepRatio;
|
|
64
60
|
maybeRunIntervalMillis;
|
|
65
|
-
loggingIntervalMillis;
|
|
66
|
-
autoscaleIntervalMillis;
|
|
67
61
|
taskTimeoutMillis;
|
|
68
62
|
runTaskFunction;
|
|
69
63
|
isFinishedFunction;
|
|
70
64
|
isTaskReadyFunction;
|
|
71
|
-
|
|
65
|
+
concurrencySystem;
|
|
66
|
+
consumer;
|
|
72
67
|
// Internal properties.
|
|
73
|
-
_minConcurrency;
|
|
74
|
-
_maxConcurrency;
|
|
75
|
-
_desiredConcurrency;
|
|
76
|
-
_currentConcurrency = 0;
|
|
77
68
|
isStopped = false;
|
|
78
|
-
lastLoggingTime;
|
|
79
69
|
resolve = null;
|
|
80
70
|
reject = null;
|
|
81
|
-
snapshotter;
|
|
82
|
-
systemStatus;
|
|
83
|
-
autoscaleInterval;
|
|
84
71
|
maybeRunInterval;
|
|
85
72
|
queryingIsTaskReady;
|
|
86
73
|
queryingIsFinished;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
74
|
+
/**
|
|
75
|
+
* This pool's *own* in-flight task count, as opposed to {@link AutoscaledPool.currentConcurrency}, which is the
|
|
76
|
+
* (possibly shared) governor's total. `pause()` and `maybeFinish()` care only about this pool draining.
|
|
77
|
+
*/
|
|
78
|
+
ownConcurrency = 0;
|
|
79
|
+
constructor(options) {
|
|
91
80
|
ow(options, ow.object.exactShape({
|
|
92
81
|
runTaskFunction: ow.function,
|
|
93
82
|
isFinishedFunction: ow.function,
|
|
94
83
|
isTaskReadyFunction: ow.function,
|
|
95
|
-
maxConcurrency: ow.optional.number.integer.greaterThanOrEqual(1),
|
|
96
|
-
minConcurrency: ow.optional.number.integer.greaterThanOrEqual(1),
|
|
97
|
-
desiredConcurrency: ow.optional.number.integer.greaterThanOrEqual(1),
|
|
98
|
-
desiredConcurrencyRatio: ow.optional.number.greaterThan(0).lessThan(1),
|
|
99
|
-
scaleUpStepRatio: ow.optional.number.greaterThan(0).lessThan(1),
|
|
100
|
-
scaleDownStepRatio: ow.optional.number.greaterThan(0).lessThan(1),
|
|
101
84
|
maybeRunIntervalSecs: ow.optional.number.greaterThan(0),
|
|
102
|
-
loggingIntervalSecs: ow.any(ow.number.greaterThan(0), ow.nullOrUndefined),
|
|
103
|
-
autoscaleIntervalSecs: ow.optional.number.greaterThan(0),
|
|
104
85
|
taskTimeoutSecs: ow.optional.number.greaterThanOrEqual(0),
|
|
105
|
-
systemStatusOptions: ow.optional.object,
|
|
106
|
-
snapshotterOptions: ow.optional.object,
|
|
107
86
|
log: ow.optional.object,
|
|
108
|
-
|
|
87
|
+
concurrencySystem: ow.object,
|
|
88
|
+
consumer: ow.object.partialShape({ id: ow.string.nonEmpty }),
|
|
109
89
|
}));
|
|
110
|
-
const { runTaskFunction, isFinishedFunction, isTaskReadyFunction,
|
|
90
|
+
const { runTaskFunction, isFinishedFunction, isTaskReadyFunction, maybeRunIntervalSecs = 0.5, taskTimeoutSecs = 0, log = serviceLocator.getLogger(), concurrencySystem, consumer, } = options;
|
|
111
91
|
this.log = log.child({ prefix: 'AutoscaledPool' });
|
|
112
92
|
// Configurable properties.
|
|
113
|
-
this.desiredConcurrencyRatio = desiredConcurrencyRatio;
|
|
114
|
-
this.scaleUpStepRatio = scaleUpStepRatio;
|
|
115
|
-
this.scaleDownStepRatio = scaleDownStepRatio;
|
|
116
93
|
this.maybeRunIntervalMillis = maybeRunIntervalSecs * 1000;
|
|
117
|
-
this.loggingIntervalMillis = loggingIntervalSecs * 1000;
|
|
118
|
-
this.autoscaleIntervalMillis = autoscaleIntervalSecs * 1000;
|
|
119
94
|
this.taskTimeoutMillis = taskTimeoutSecs * 1000;
|
|
120
95
|
this.runTaskFunction = runTaskFunction;
|
|
121
96
|
this.isFinishedFunction = isFinishedFunction;
|
|
122
97
|
this.isTaskReadyFunction = isTaskReadyFunction;
|
|
123
|
-
this.
|
|
98
|
+
this.concurrencySystem = concurrencySystem;
|
|
99
|
+
this.consumer = consumer;
|
|
124
100
|
// Internal properties.
|
|
125
|
-
this._minConcurrency = minConcurrency;
|
|
126
|
-
this._maxConcurrency = maxConcurrency;
|
|
127
|
-
this._desiredConcurrency = Math.min(desiredConcurrency ?? minConcurrency, maxConcurrency);
|
|
128
|
-
this._currentConcurrency = 0;
|
|
129
101
|
this.isStopped = false;
|
|
130
102
|
this.resolve = null;
|
|
131
103
|
this.reject = null;
|
|
132
|
-
this.
|
|
133
|
-
this._maybeRunTask = this._maybeRunTask.bind(this);
|
|
134
|
-
this._incrementTasksDonePerSecond = this._incrementTasksDonePerSecond.bind(this);
|
|
135
|
-
// Create instances with correct options.
|
|
136
|
-
const ssoCopy = { ...systemStatusOptions };
|
|
137
|
-
ssoCopy.snapshotter ??= new Snapshotter({
|
|
138
|
-
...snapshotterOptions,
|
|
139
|
-
log: this.log,
|
|
140
|
-
config: this.config,
|
|
141
|
-
client: this.config.getStorageClient(),
|
|
142
|
-
});
|
|
143
|
-
ssoCopy.config ??= this.config;
|
|
144
|
-
this.snapshotter = ssoCopy.snapshotter;
|
|
145
|
-
this.systemStatus = new SystemStatus(ssoCopy);
|
|
104
|
+
this.maybeRunTask = this.maybeRunTask.bind(this);
|
|
146
105
|
}
|
|
147
106
|
/**
|
|
148
|
-
*
|
|
149
|
-
|
|
150
|
-
get minConcurrency() {
|
|
151
|
-
return this._minConcurrency;
|
|
152
|
-
}
|
|
153
|
-
/**
|
|
154
|
-
* Sets the minimum number of tasks running in parallel.
|
|
107
|
+
* The governor backing this pool, as supplied to the constructor — exposed as the read-only
|
|
108
|
+
* {@link IConcurrencySystem} contract.
|
|
155
109
|
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*/
|
|
159
|
-
set minConcurrency(value) {
|
|
160
|
-
ow(value, ow.optional.number.integer.greaterThanOrEqual(1));
|
|
161
|
-
this._minConcurrency = value;
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Gets the maximum number of tasks running in parallel.
|
|
110
|
+
* This and the two getters below are telemetry only: concurrency is configured and tuned on the concrete
|
|
111
|
+
* {@link ConcurrencySystem} its owner holds, never through the pool.
|
|
165
112
|
*/
|
|
166
|
-
get
|
|
167
|
-
return this.
|
|
113
|
+
get system() {
|
|
114
|
+
return this.concurrencySystem;
|
|
168
115
|
}
|
|
169
|
-
/**
|
|
170
|
-
* Sets the maximum number of tasks running in parallel.
|
|
171
|
-
*/
|
|
172
|
-
set maxConcurrency(value) {
|
|
173
|
-
ow(value, ow.optional.number.integer.greaterThanOrEqual(1));
|
|
174
|
-
this._maxConcurrency = value;
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* Gets the desired concurrency for the pool,
|
|
178
|
-
* which is an estimated number of parallel tasks that the system can currently support.
|
|
179
|
-
*/
|
|
116
|
+
/** The estimated number of parallel tasks the governor can currently support. */
|
|
180
117
|
get desiredConcurrency() {
|
|
181
|
-
return this.
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Sets the desired concurrency for the pool, i.e. the number of tasks that should be running
|
|
185
|
-
* in parallel if there's large enough supply of tasks.
|
|
186
|
-
*/
|
|
187
|
-
set desiredConcurrency(value) {
|
|
188
|
-
ow(value, ow.optional.number.integer.greaterThanOrEqual(1));
|
|
189
|
-
this._desiredConcurrency = value;
|
|
118
|
+
return this.concurrencySystem.desiredConcurrency;
|
|
190
119
|
}
|
|
191
120
|
/**
|
|
192
|
-
*
|
|
121
|
+
* The number of parallel tasks currently booked against the governor. When it is shared, this counts every
|
|
122
|
+
* borrowing pool's tasks, not just this one's.
|
|
193
123
|
*/
|
|
194
124
|
get currentConcurrency() {
|
|
195
|
-
return this.
|
|
125
|
+
return this.concurrencySystem.currentConcurrency;
|
|
196
126
|
}
|
|
197
127
|
/**
|
|
198
128
|
* Runs the auto-scaled pool. Returns a promise that gets resolved or rejected once
|
|
199
129
|
* all the tasks are finished or one of them fails.
|
|
130
|
+
*
|
|
131
|
+
* Throws if the {@link IConcurrencySystem|concurrency system} it borrows was never started — the pool assumes
|
|
132
|
+
* a running governor and cannot start one it does not own.
|
|
200
133
|
*/
|
|
201
134
|
async run() {
|
|
135
|
+
// Checked here, on an awaited path — the capacity queries inside the task loop run from intervals and
|
|
136
|
+
// `setImmediate`, where a throw would become an unhandled rejection and hang `run()` forever.
|
|
137
|
+
if (!this.concurrencySystem.isRunning) {
|
|
138
|
+
throw new CriticalError('The ConcurrencySystem this AutoscaledPool borrows has not been started, so system load would not be ' +
|
|
139
|
+
'monitored and the concurrency would never be adjusted. Whoever creates a ConcurrencySystem owns ' +
|
|
140
|
+
'its lifecycle: call `await concurrencySystem.start()` before running the pools or crawlers that ' +
|
|
141
|
+
'use it, and `await concurrencySystem.stop()` once they are all done.');
|
|
142
|
+
}
|
|
202
143
|
const poolPromise = new Promise((resolve, reject) => {
|
|
203
144
|
this.resolve = resolve;
|
|
204
145
|
this.reject = reject;
|
|
205
146
|
});
|
|
206
|
-
await this.snapshotter.start();
|
|
207
|
-
// This interval checks the system status and updates the desired concurrency accordingly.
|
|
208
|
-
this.autoscaleInterval = betterSetInterval(this._autoscale, this.autoscaleIntervalMillis);
|
|
209
147
|
// This is here because if we scale down to let's say 1, then after each promise is finished
|
|
210
|
-
// this.
|
|
148
|
+
// this.maybeRunTask() doesn't trigger another one. So if that 1 instance gets stuck it results
|
|
211
149
|
// in the crawler getting stuck and even after scaling up it never triggers another promise.
|
|
212
|
-
this.maybeRunInterval = betterSetInterval(this.
|
|
213
|
-
if (this.maxTasksPerMinute !== Infinity) {
|
|
214
|
-
// Start the interval that resets the counter of tasks per minute.
|
|
215
|
-
this.tasksDonePerSecondInterval = betterSetInterval(this._incrementTasksDonePerSecond, 1000);
|
|
216
|
-
}
|
|
150
|
+
this.maybeRunInterval = betterSetInterval(this.maybeRunTask, this.maybeRunIntervalMillis);
|
|
217
151
|
try {
|
|
218
152
|
await poolPromise;
|
|
219
153
|
}
|
|
220
154
|
finally {
|
|
221
155
|
// If resolve is null, the pool is already destroyed.
|
|
222
156
|
if (this.resolve)
|
|
223
|
-
await this.
|
|
157
|
+
await this.destroy();
|
|
224
158
|
}
|
|
225
159
|
}
|
|
226
160
|
/**
|
|
@@ -238,7 +172,7 @@ export class AutoscaledPool {
|
|
|
238
172
|
this.isStopped = true;
|
|
239
173
|
if (this.resolve) {
|
|
240
174
|
this.resolve();
|
|
241
|
-
await this.
|
|
175
|
+
await this.destroy();
|
|
242
176
|
}
|
|
243
177
|
}
|
|
244
178
|
/**
|
|
@@ -251,6 +185,10 @@ export class AutoscaledPool {
|
|
|
251
185
|
*
|
|
252
186
|
* The promise returned from the {@link AutoscaledPool.run} function will not resolve
|
|
253
187
|
* when `.pause()` is invoked (unlike abort, which resolves it).
|
|
188
|
+
*
|
|
189
|
+
* > *NOTE:* Pausing the pool does not suspend the (possibly shared) {@link ConcurrencySystem} — its
|
|
190
|
+
* autoscaling and resource monitoring keep running, since other pools borrowing it may still be active. To silence
|
|
191
|
+
* it during a long pause, its owner can `stop()` and `start()` it again.
|
|
254
192
|
*/
|
|
255
193
|
async pause(timeoutSecs) {
|
|
256
194
|
if (this.isStopped)
|
|
@@ -258,15 +196,18 @@ export class AutoscaledPool {
|
|
|
258
196
|
this.isStopped = true;
|
|
259
197
|
await new Promise((resolve, reject) => {
|
|
260
198
|
let timeout;
|
|
199
|
+
let interval;
|
|
261
200
|
if (timeoutSecs) {
|
|
262
201
|
timeout = setTimeout(() => {
|
|
202
|
+
// Clean up the polling interval to prevent it from leaking on timeout.
|
|
203
|
+
clearInterval(interval);
|
|
263
204
|
const err = new Error("The pool's running tasks did not finish" +
|
|
264
205
|
`in ${timeoutSecs} secs after pool.pause() invocation.`);
|
|
265
206
|
reject(err);
|
|
266
207
|
}, timeoutSecs);
|
|
267
208
|
}
|
|
268
|
-
|
|
269
|
-
if (this.
|
|
209
|
+
interval = setInterval(() => {
|
|
210
|
+
if (this.ownConcurrency <= 0) {
|
|
270
211
|
// Clean up timeout and interval to prevent process hanging.
|
|
271
212
|
if (timeout)
|
|
272
213
|
clearTimeout(timeout);
|
|
@@ -290,7 +231,7 @@ export class AutoscaledPool {
|
|
|
290
231
|
* every `maybeRunIntervalSecs` seconds. If you want to trigger the processing immediately, use this method.
|
|
291
232
|
*/
|
|
292
233
|
async notify() {
|
|
293
|
-
setImmediate(this.
|
|
234
|
+
setImmediate(this.maybeRunTask);
|
|
294
235
|
}
|
|
295
236
|
/**
|
|
296
237
|
* Starts a new task
|
|
@@ -300,7 +241,7 @@ export class AutoscaledPool {
|
|
|
300
241
|
*
|
|
301
242
|
* It doesn't allow multiple concurrent runs of this method.
|
|
302
243
|
*/
|
|
303
|
-
async
|
|
244
|
+
async maybeRunTask(intervalCallback) {
|
|
304
245
|
this.log.perf('Attempting to run a task.');
|
|
305
246
|
// Check if the function was invoked by the maybeRunInterval and use an empty function if not.
|
|
306
247
|
const done = intervalCallback || (() => { });
|
|
@@ -315,17 +256,14 @@ export class AutoscaledPool {
|
|
|
315
256
|
this.log.perf('Task will not run. Waiting for a ready task.');
|
|
316
257
|
return done();
|
|
317
258
|
}
|
|
318
|
-
// -
|
|
319
|
-
if (this.
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if (!isSystemIdle && this._currentConcurrency >= this._minConcurrency) {
|
|
327
|
-
this.log.perf('Task will not be run. System is overloaded.', currentStatus);
|
|
328
|
-
return done();
|
|
259
|
+
// - the budget has room for us.
|
|
260
|
+
if (!this.concurrencySystem.hasCapacityForTask(this.consumer)) {
|
|
261
|
+
done();
|
|
262
|
+
// A shared governor's budget can stay saturated by another pool indefinitely, so we still have to be able
|
|
263
|
+
// to notice that *this* pool has run out of work — `maybeFinish()` is the only thing that ever resolves
|
|
264
|
+
// `run()`. It no-ops while this pool has tasks of its own in flight, which is every case in which an
|
|
265
|
+
// unshared governor reports no capacity.
|
|
266
|
+
return this.maybeFinish();
|
|
329
267
|
}
|
|
330
268
|
// - a task is ready.
|
|
331
269
|
this.queryingIsTaskReady = true;
|
|
@@ -351,22 +289,19 @@ export class AutoscaledPool {
|
|
|
351
289
|
this.log.perf('Task will not run. No tasks are ready.');
|
|
352
290
|
done();
|
|
353
291
|
// No tasks could mean that we're finished with all tasks.
|
|
354
|
-
return this.
|
|
292
|
+
return this.maybeFinish();
|
|
355
293
|
}
|
|
356
|
-
// -
|
|
357
|
-
//
|
|
358
|
-
|
|
359
|
-
if (this._isOverMaxRequestLimit) {
|
|
360
|
-
this.log.perf('Task will not run. Maximum tasks per minute reached.');
|
|
294
|
+
// - the budget still has room. Re-checked atomically, because another pool sharing the governor may have taken
|
|
295
|
+
// the last free slot while we awaited `isTaskReadyFunction` above.
|
|
296
|
+
if (!this.concurrencySystem.tryRegisterTaskStart(this.consumer)) {
|
|
361
297
|
return done();
|
|
362
298
|
}
|
|
299
|
+
this.ownConcurrency++;
|
|
363
300
|
try {
|
|
364
301
|
// Everything's fine. Run task.
|
|
365
|
-
this._currentConcurrency++;
|
|
366
|
-
this._tasksPerMinute[0]++;
|
|
367
302
|
// Try to run next task to build up concurrency,
|
|
368
303
|
// but defer it so it doesn't create a cycle.
|
|
369
|
-
setImmediate(this.
|
|
304
|
+
setImmediate(this.maybeRunTask);
|
|
370
305
|
// We need to restart interval here, so that it doesn't get blocked by a stalled task.
|
|
371
306
|
done();
|
|
372
307
|
// Execute the current task.
|
|
@@ -378,9 +313,9 @@ export class AutoscaledPool {
|
|
|
378
313
|
await this.runTaskFunction();
|
|
379
314
|
}
|
|
380
315
|
this.log.perf('Task finished.');
|
|
381
|
-
|
|
382
|
-
//
|
|
383
|
-
setImmediate(this.
|
|
316
|
+
// Run task after the previous one finished. Only on success: a failed task rejects the pool, and
|
|
317
|
+
// nudging the loop afterwards could start work on an already destroyed pool.
|
|
318
|
+
setImmediate(this.maybeRunTask);
|
|
384
319
|
}
|
|
385
320
|
catch (e) {
|
|
386
321
|
const err = e;
|
|
@@ -396,85 +331,11 @@ export class AutoscaledPool {
|
|
|
396
331
|
this.reject(err);
|
|
397
332
|
}
|
|
398
333
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
* Gets called every autoScaleIntervalSecs and evaluates the current system status.
|
|
403
|
-
* If the system IS NOT overloaded and the settings allow it, it scales up.
|
|
404
|
-
* If the system IS overloaded and the settings allow it, it scales down.
|
|
405
|
-
*/
|
|
406
|
-
_autoscale(intervalCallback) {
|
|
407
|
-
// Don't scale if paused.
|
|
408
|
-
if (this.isStopped)
|
|
409
|
-
return intervalCallback();
|
|
410
|
-
// Don't scale if we've hit the maximum requests per minute
|
|
411
|
-
if (this._isOverMaxRequestLimit)
|
|
412
|
-
return intervalCallback();
|
|
413
|
-
// Only scale up if:
|
|
414
|
-
// - system has not been overloaded lately.
|
|
415
|
-
const systemStatus = this.systemStatus.getHistoricalStatus();
|
|
416
|
-
const { isSystemIdle } = systemStatus;
|
|
417
|
-
// - we're not already at max concurrency.
|
|
418
|
-
const weAreNotAtMax = this._desiredConcurrency < this._maxConcurrency;
|
|
419
|
-
// - current concurrency reaches at least the given ratio of desired concurrency.
|
|
420
|
-
const minCurrentConcurrency = Math.floor(this._desiredConcurrency * this.desiredConcurrencyRatio);
|
|
421
|
-
const weAreReachingDesiredConcurrency = this._currentConcurrency >= minCurrentConcurrency;
|
|
422
|
-
if (isSystemIdle && weAreNotAtMax && weAreReachingDesiredConcurrency)
|
|
423
|
-
this._scaleUp(systemStatus);
|
|
424
|
-
// Always scale down if:
|
|
425
|
-
// - the system has been overloaded lately.
|
|
426
|
-
const isSystemOverloaded = !isSystemIdle;
|
|
427
|
-
// - we're over min concurrency.
|
|
428
|
-
const weAreNotAtMin = this._desiredConcurrency > this._minConcurrency;
|
|
429
|
-
if (isSystemOverloaded && weAreNotAtMin)
|
|
430
|
-
this._scaleDown(systemStatus);
|
|
431
|
-
// On periodic intervals, print comprehensive log information
|
|
432
|
-
if (this.loggingIntervalMillis > 0) {
|
|
433
|
-
const now = Date.now();
|
|
434
|
-
if (this.lastLoggingTime == null) {
|
|
435
|
-
this.lastLoggingTime = now;
|
|
436
|
-
}
|
|
437
|
-
else if (now > this.lastLoggingTime + this.loggingIntervalMillis) {
|
|
438
|
-
this.lastLoggingTime = now;
|
|
439
|
-
this.log.info('state', {
|
|
440
|
-
currentConcurrency: this._currentConcurrency,
|
|
441
|
-
desiredConcurrency: this._desiredConcurrency,
|
|
442
|
-
systemStatus,
|
|
443
|
-
});
|
|
444
|
-
}
|
|
334
|
+
finally {
|
|
335
|
+
this.concurrencySystem.registerTaskEnd(this.consumer);
|
|
336
|
+
this.ownConcurrency--;
|
|
445
337
|
}
|
|
446
|
-
|
|
447
|
-
return intervalCallback();
|
|
448
|
-
}
|
|
449
|
-
/**
|
|
450
|
-
* Scales the pool up by increasing
|
|
451
|
-
* the desired concurrency by the scaleUpStepRatio.
|
|
452
|
-
*
|
|
453
|
-
* @param systemStatus for logging
|
|
454
|
-
*/
|
|
455
|
-
_scaleUp(systemStatus) {
|
|
456
|
-
const step = Math.ceil(this._desiredConcurrency * this.scaleUpStepRatio);
|
|
457
|
-
this._desiredConcurrency = Math.min(this._maxConcurrency, this._desiredConcurrency + step);
|
|
458
|
-
this.log.debug('scaling up', {
|
|
459
|
-
oldConcurrency: this._desiredConcurrency - step,
|
|
460
|
-
newConcurrency: this._desiredConcurrency,
|
|
461
|
-
systemStatus,
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
/**
|
|
465
|
-
* Scales the pool down by decreasing
|
|
466
|
-
* the desired concurrency by the scaleDownStepRatio.
|
|
467
|
-
*
|
|
468
|
-
* @param systemStatus for logging
|
|
469
|
-
*/
|
|
470
|
-
_scaleDown(systemStatus) {
|
|
471
|
-
const step = Math.ceil(this._desiredConcurrency * this.scaleDownStepRatio);
|
|
472
|
-
this._desiredConcurrency = Math.max(this._minConcurrency, this._desiredConcurrency - step);
|
|
473
|
-
this.log.debug('scaling down', {
|
|
474
|
-
oldConcurrency: this._desiredConcurrency + step,
|
|
475
|
-
newConcurrency: this._desiredConcurrency,
|
|
476
|
-
systemStatus,
|
|
477
|
-
});
|
|
338
|
+
return undefined;
|
|
478
339
|
}
|
|
479
340
|
/**
|
|
480
341
|
* If there are no running tasks and this.isFinishedFunction() returns true then closes
|
|
@@ -482,10 +343,10 @@ export class AutoscaledPool {
|
|
|
482
343
|
*
|
|
483
344
|
* It doesn't allow multiple concurrent runs of this method.
|
|
484
345
|
*/
|
|
485
|
-
async
|
|
346
|
+
async maybeFinish() {
|
|
486
347
|
if (this.queryingIsFinished)
|
|
487
348
|
return;
|
|
488
|
-
if (this.
|
|
349
|
+
if (this.ownConcurrency > 0)
|
|
489
350
|
return;
|
|
490
351
|
this.queryingIsFinished = true;
|
|
491
352
|
try {
|
|
@@ -508,26 +369,9 @@ export class AutoscaledPool {
|
|
|
508
369
|
/**
|
|
509
370
|
* Cleans up resources.
|
|
510
371
|
*/
|
|
511
|
-
async
|
|
372
|
+
async destroy() {
|
|
512
373
|
this.resolve = null;
|
|
513
374
|
this.reject = null;
|
|
514
|
-
betterClearInterval(this.autoscaleInterval);
|
|
515
375
|
betterClearInterval(this.maybeRunInterval);
|
|
516
|
-
if (this.tasksDonePerSecondInterval)
|
|
517
|
-
betterClearInterval(this.tasksDonePerSecondInterval);
|
|
518
|
-
if (this.snapshotter)
|
|
519
|
-
await this.snapshotter.stop();
|
|
520
|
-
}
|
|
521
|
-
_incrementTasksDonePerSecond(intervalCallback) {
|
|
522
|
-
this._tasksPerMinute.unshift(0);
|
|
523
|
-
this._tasksPerMinute.pop();
|
|
524
|
-
return intervalCallback();
|
|
525
|
-
}
|
|
526
|
-
get _isOverMaxRequestLimit() {
|
|
527
|
-
if (this.maxTasksPerMinute === Infinity) {
|
|
528
|
-
return false;
|
|
529
|
-
}
|
|
530
|
-
return this._tasksPerMinute.reduce((acc, curr) => acc + curr, 0) >= this.maxTasksPerMinute;
|
|
531
376
|
}
|
|
532
377
|
}
|
|
533
|
-
//# sourceMappingURL=autoscaled_pool.js.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { LoadSignal, LoadSignalStartContext, LoadSnapshot } from './load_signal.js';
|
|
2
|
+
/**
|
|
3
|
+
* A snapshot produced by the built-in client (rate-limit) signal.
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
export interface ClientSnapshot extends LoadSnapshot {
|
|
7
|
+
rateLimitErrorCount: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Tuning for the built-in **client** (rate-limit) load signal, as accepted both by {@link ClientLoadSignal} and by
|
|
11
|
+
* the {@link LoadSignalsOptions.client|`client`} shorthand on {@link LoadSignalsOptions}.
|
|
12
|
+
*/
|
|
13
|
+
export interface ClientLoadSignalOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Defines the interval of checking the current state of the remote API client, in seconds.
|
|
16
|
+
* @default 1
|
|
17
|
+
*/
|
|
18
|
+
snapshotIntervalSecs?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Defines the maximum number of new rate limit errors within the given interval.
|
|
21
|
+
* @default 3
|
|
22
|
+
*/
|
|
23
|
+
maxErrors?: number;
|
|
24
|
+
/**
|
|
25
|
+
* Maximum ratio of overloaded snapshots in a sample before the client counts as overloaded.
|
|
26
|
+
* @default 0.3
|
|
27
|
+
*/
|
|
28
|
+
overloadedRatio?: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Periodically checks the storage backend for rate-limit errors (HTTP 429) and reports overload when the error delta
|
|
32
|
+
* exceeds a threshold.
|
|
33
|
+
*
|
|
34
|
+
* Built by default; construct one yourself only to wrap or adapt it — see {@link LoadSignal}.
|
|
35
|
+
*
|
|
36
|
+
* Switch it off entirely ({@link LoadSignalsOptions.client|`client: false`}) if the storage backend reports no
|
|
37
|
+
* rate-limit statistics, since it otherwise polls it every second to no purpose.
|
|
38
|
+
*
|
|
39
|
+
* @category Scaling
|
|
40
|
+
*/
|
|
41
|
+
export declare class ClientLoadSignal implements LoadSignal {
|
|
42
|
+
readonly name = "clientInfo";
|
|
43
|
+
readonly overloadedRatio: number;
|
|
44
|
+
private readonly store;
|
|
45
|
+
private readonly intervalMillis;
|
|
46
|
+
private readonly maxErrors;
|
|
47
|
+
private interval?;
|
|
48
|
+
private client?;
|
|
49
|
+
constructor(options?: ClientLoadSignalOptions);
|
|
50
|
+
start(context: LoadSignalStartContext): Promise<void>;
|
|
51
|
+
stop(): Promise<void>;
|
|
52
|
+
getSample(sampleDurationMillis?: number): LoadSnapshot[];
|
|
53
|
+
/**
|
|
54
|
+
* Records one snapshot, overloaded when rate-limit errors grew by more than the configured limit since the
|
|
55
|
+
* previous one.
|
|
56
|
+
* @internal Also lets tests drive the measurement without waiting on a timer.
|
|
57
|
+
*/
|
|
58
|
+
handle(intervalCallback: () => unknown): void;
|
|
59
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { betterClearInterval, betterSetInterval } from '@apify/utilities';
|
|
2
|
+
import { serviceLocator } from '../service_locator.js';
|
|
3
|
+
import { SnapshotStore } from './load_signal.js';
|
|
4
|
+
const CLIENT_RATE_LIMIT_ERROR_RETRY_COUNT = 2;
|
|
5
|
+
/**
|
|
6
|
+
* Periodically checks the storage backend for rate-limit errors (HTTP 429) and reports overload when the error delta
|
|
7
|
+
* exceeds a threshold.
|
|
8
|
+
*
|
|
9
|
+
* Built by default; construct one yourself only to wrap or adapt it — see {@link LoadSignal}.
|
|
10
|
+
*
|
|
11
|
+
* Switch it off entirely ({@link LoadSignalsOptions.client|`client: false`}) if the storage backend reports no
|
|
12
|
+
* rate-limit statistics, since it otherwise polls it every second to no purpose.
|
|
13
|
+
*
|
|
14
|
+
* @category Scaling
|
|
15
|
+
*/
|
|
16
|
+
export class ClientLoadSignal {
|
|
17
|
+
name = 'clientInfo';
|
|
18
|
+
overloadedRatio;
|
|
19
|
+
store = new SnapshotStore();
|
|
20
|
+
intervalMillis;
|
|
21
|
+
maxErrors;
|
|
22
|
+
interval;
|
|
23
|
+
client;
|
|
24
|
+
constructor(options = {}) {
|
|
25
|
+
this.overloadedRatio = options.overloadedRatio ?? 0.3;
|
|
26
|
+
this.intervalMillis = (options.snapshotIntervalSecs ?? 1) * 1000;
|
|
27
|
+
this.maxErrors = options.maxErrors ?? 3;
|
|
28
|
+
this.handle = this.handle.bind(this);
|
|
29
|
+
}
|
|
30
|
+
async start(context) {
|
|
31
|
+
this.store.useSampleWindow(context.maxSampleWindowMillis);
|
|
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 the client is resolved afresh just below.
|
|
34
|
+
this.store.clear();
|
|
35
|
+
// Resolved here rather than in the constructor, where asking for the backend would instantiate a default one
|
|
36
|
+
// as a side effect - long before the crawler that owns the run has had a chance to register its own.
|
|
37
|
+
this.client = serviceLocator.getStorageBackend();
|
|
38
|
+
this.interval = betterSetInterval(this.handle, this.intervalMillis);
|
|
39
|
+
}
|
|
40
|
+
async stop() {
|
|
41
|
+
if (this.interval)
|
|
42
|
+
betterClearInterval(this.interval);
|
|
43
|
+
this.interval = undefined;
|
|
44
|
+
this.client = undefined;
|
|
45
|
+
}
|
|
46
|
+
getSample(sampleDurationMillis) {
|
|
47
|
+
return this.store.getSample(sampleDurationMillis);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Records one snapshot, overloaded when rate-limit errors grew by more than the configured limit since the
|
|
51
|
+
* previous one.
|
|
52
|
+
* @internal Also lets tests drive the measurement without waiting on a timer.
|
|
53
|
+
*/
|
|
54
|
+
handle(intervalCallback) {
|
|
55
|
+
const now = new Date();
|
|
56
|
+
const allErrorCounts = this.client?.stats?.rateLimitErrors ?? [];
|
|
57
|
+
const currentErrCount = allErrorCounts[CLIENT_RATE_LIMIT_ERROR_RETRY_COUNT] || 0;
|
|
58
|
+
const snapshot = {
|
|
59
|
+
createdAt: now,
|
|
60
|
+
isOverloaded: false,
|
|
61
|
+
rateLimitErrorCount: currentErrCount,
|
|
62
|
+
};
|
|
63
|
+
const all = this.store.getAll();
|
|
64
|
+
const previousSnapshot = all[all.length - 1];
|
|
65
|
+
if (previousSnapshot) {
|
|
66
|
+
const delta = currentErrCount - previousSnapshot.rateLimitErrorCount;
|
|
67
|
+
if (delta > this.maxErrors)
|
|
68
|
+
snapshot.isOverloaded = true;
|
|
69
|
+
}
|
|
70
|
+
this.store.push(snapshot, now);
|
|
71
|
+
intervalCallback();
|
|
72
|
+
}
|
|
73
|
+
}
|