@crawlee/core 4.0.0-beta.13 → 4.0.0-beta.131
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 +67 -172
- package/autoscaling/autoscaled_pool.js +182 -329
- package/autoscaling/concurrency_system.d.ts +268 -0
- package/autoscaling/concurrency_system.js +362 -0
- package/autoscaling/cpu_load_signal.d.ts +43 -0
- package/autoscaling/cpu_load_signal.js +47 -0
- package/autoscaling/event_loop_load_signal.d.ts +51 -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 +100 -0
- package/autoscaling/load_signal.js +105 -0
- package/autoscaling/memory_load_signal.d.ts +47 -0
- package/autoscaling/memory_load_signal.js +106 -0
- package/autoscaling/snapshotter.d.ts +58 -163
- package/autoscaling/snapshotter.js +45 -263
- package/autoscaling/storage_backend_load_signal.d.ts +56 -0
- package/autoscaling/storage_backend_load_signal.js +73 -0
- package/autoscaling/system_status.d.ts +67 -89
- package/autoscaling/system_status.js +92 -122
- package/autoscaling/weighted_avg.d.ts +5 -0
- package/autoscaling/weighted_avg.js +14 -0
- package/byte_utils.d.ts +17 -0
- package/byte_utils.js +42 -0
- package/configuration.d.ts +96 -223
- package/configuration.js +170 -222
- package/cookie_utils.d.ts +3 -2
- package/cookie_utils.js +18 -7
- package/crawlers/context_pipeline.d.ts +10 -1
- package/crawlers/context_pipeline.js +31 -8
- package/crawlers/crawler_commons.d.ts +90 -129
- package/crawlers/crawler_commons.js +1 -116
- package/crawlers/error_snapshotter.d.ts +2 -5
- package/crawlers/error_snapshotter.js +7 -8
- package/crawlers/error_tracker.d.ts +0 -1
- package/crawlers/error_tracker.js +0 -1
- package/crawlers/index.d.ts +1 -3
- package/crawlers/index.js +0 -3
- package/crawlers/internals/types.d.ts +0 -1
- package/crawlers/internals/types.js +0 -1
- package/crawlers/statistics.d.ts +187 -63
- package/crawlers/statistics.js +354 -164
- package/debug.d.ts +36 -0
- package/debug.js +70 -0
- package/enqueue_links/enqueue_links.d.ts +68 -154
- package/enqueue_links/enqueue_links.js +38 -210
- package/enqueue_links/index.d.ts +0 -1
- package/enqueue_links/index.js +0 -1
- package/enqueue_links/shared.d.ts +52 -26
- package/enqueue_links/shared.js +115 -66
- package/errors.d.ts +72 -4
- package/errors.js +89 -5
- package/events/event_manager.d.ts +35 -9
- package/events/event_manager.js +12 -13
- 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 +39 -13
- package/http.d.ts +9 -0
- package/http.js +28 -0
- package/index.d.ts +7 -4
- package/index.js +6 -3
- package/iterables.d.ts +79 -0
- package/iterables.js +134 -0
- package/log.d.ts +82 -3
- package/log.js +106 -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 +38 -0
- package/memory-storage/memory-storage.js +130 -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 +108 -0
- package/memory-storage/resource-clients/key-value-store.d.ts +63 -0
- package/memory-storage/resource-clients/key-value-store.js +201 -0
- package/memory-storage/resource-clients/request-queue.d.ts +77 -0
- package/memory-storage/resource-clients/request-queue.js +409 -0
- package/memory-storage/utils.d.ts +16 -0
- package/memory-storage/utils.js +41 -0
- package/owned_or_injected.d.ts +58 -0
- package/owned_or_injected.js +98 -0
- package/package.json +14 -13
- package/proxy_configuration.d.ts +24 -132
- package/proxy_configuration.js +32 -147
- package/recoverable_state.d.ts +155 -0
- package/recoverable_state.js +231 -0
- package/request.d.ts +80 -16
- package/request.js +147 -68
- package/router.d.ts +193 -21
- package/router.js +188 -43
- package/serialization.d.ts +0 -1
- package/serialization.js +15 -15
- package/service_locator.d.ts +165 -0
- package/service_locator.js +253 -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 +35 -88
- package/session_pool/session.js +101 -159
- package/session_pool/session_pool.d.ts +73 -91
- package/session_pool/session_pool.js +172 -165
- package/storages/batched_adds.d.ts +37 -0
- package/storages/batched_adds.js +73 -0
- package/storages/dataset.d.ts +114 -54
- package/storages/dataset.js +291 -148
- package/storages/index.d.ts +10 -8
- package/storages/index.js +8 -8
- package/storages/key_value_store.d.ts +185 -42
- package/storages/key_value_store.js +445 -169
- 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 +22 -0
- package/storages/request_dedup_cache.js +48 -0
- package/storages/request_list.d.ts +52 -115
- package/storages/request_list.js +183 -152
- package/storages/request_loader.d.ts +101 -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 +97 -0
- package/storages/request_manager_tandem.js +197 -0
- package/storages/request_queue.d.ts +290 -47
- package/storages/request_queue.js +762 -216
- package/storages/{sitemap_request_list.d.ts → sitemap_request_loader.d.ts} +45 -89
- package/storages/sitemap_request_loader.js +438 -0
- package/storages/storage_instance_manager.d.ts +87 -0
- package/storages/storage_instance_manager.js +256 -0
- package/storages/storage_stats.d.ts +48 -0
- package/storages/storage_stats.js +29 -0
- 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 +59 -11
- package/storages/utils.js +75 -15
- 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/url.d.ts +9 -0
- package/url.js +11 -0
- package/validators.d.ts +22 -18
- package/validators.js +13 -18
- 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/context_pipeline.d.ts.map +0 -1
- package/crawlers/context_pipeline.js.map +0 -1
- package/crawlers/crawler_commons.d.ts.map +0 -1
- package/crawlers/crawler_commons.js.map +0 -1
- package/crawlers/crawler_utils.d.ts +0 -10
- package/crawlers/crawler_utils.d.ts.map +0 -1
- package/crawlers/crawler_utils.js +0 -12
- 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/internals/types.d.ts.map +0 -1
- package/crawlers/internals/types.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 -140
- package/http_clients/base-http-client.d.ts.map +0 -1
- package/http_clients/base-http-client.js +0 -40
- 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 -20
- package/http_clients/got-scraping-http-client.d.ts.map +0 -1
- package/http_clients/got-scraping-http-client.js +0 -82
- 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 +0 -13
- package/storages/access_checking.d.ts.map +0 -1
- package/storages/access_checking.js +0 -14
- 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 -308
- 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 +0 -430
- 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
package/crawlers/statistics.js
CHANGED
|
@@ -1,22 +1,34 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { convertStateSync, RecoverableState } from '../recoverable_state.js';
|
|
3
|
+
import { serviceLocator } from '../service_locator.js';
|
|
4
4
|
import { KeyValueStore } from '../storages/key_value_store.js';
|
|
5
|
+
import { parseArgument, schemas, validators } from '../validators.js';
|
|
5
6
|
import { ErrorTracker } from './error_tracker.js';
|
|
6
7
|
/**
|
|
7
8
|
* @ignore
|
|
8
9
|
*/
|
|
9
10
|
class Job {
|
|
10
|
-
lastRunAt = null;
|
|
11
|
-
durationMillis;
|
|
11
|
+
#lastRunAt = null;
|
|
12
|
+
#durationMillis;
|
|
12
13
|
run() {
|
|
13
|
-
this
|
|
14
|
+
this.#lastRunAt = Date.now();
|
|
14
15
|
}
|
|
15
16
|
finish() {
|
|
16
|
-
this
|
|
17
|
-
return this
|
|
17
|
+
this.#durationMillis = Date.now() - this.#lastRunAt;
|
|
18
|
+
return this.#durationMillis;
|
|
18
19
|
}
|
|
19
20
|
}
|
|
21
|
+
const statisticsOptionsSchema = z.strictObject({
|
|
22
|
+
logIntervalSecs: schemas.anyNumber.default(60),
|
|
23
|
+
logMessage: z.string().default('Statistics'),
|
|
24
|
+
log: validators.logger.optional(),
|
|
25
|
+
keyValueStore: z.instanceof(KeyValueStore).optional(),
|
|
26
|
+
// `schemas.anyObject` passes values through by reference (object schemas return a pruned plain copy).
|
|
27
|
+
persistenceOptions: schemas.anyObject.default(() => ({ enable: true })),
|
|
28
|
+
saveErrorSnapshots: z.boolean().default(false),
|
|
29
|
+
id: z.union([schemas.anyNumber, z.string()]).optional(),
|
|
30
|
+
stateExtension: schemas.anyObject.default(() => ({})),
|
|
31
|
+
});
|
|
20
32
|
const errorTrackerConfig = {
|
|
21
33
|
showErrorCode: true,
|
|
22
34
|
showErrorName: true,
|
|
@@ -25,17 +37,128 @@ const errorTrackerConfig = {
|
|
|
25
37
|
showErrorMessage: true,
|
|
26
38
|
showFullMessage: false,
|
|
27
39
|
};
|
|
40
|
+
/**
|
|
41
|
+
* The persisted record, in the order it is written - the schema rebuilds the object on the way out, so the field
|
|
42
|
+
* order here *is* the record's field order (guarded by a test).
|
|
43
|
+
*
|
|
44
|
+
* JSON has no infinity, so the three fields that are `Infinity` until the first request settles are written as
|
|
45
|
+
* `null`. Both {@link Statistics.serializeState} and {@link Statistics.deserializeState} run through this,
|
|
46
|
+
* which is what keeps them describing the same record.
|
|
47
|
+
*
|
|
48
|
+
* Nothing is optional on purpose: the record has always carried every field, so one missing a field is not one of
|
|
49
|
+
* ours and is discarded whole rather than partially trusted - a counter restored as a string would poison every
|
|
50
|
+
* later increment.
|
|
51
|
+
*
|
|
52
|
+
* Custom fields are not this schema's business either way: they are added to the record after the encode and
|
|
53
|
+
* validated by their own conversion on the way back, so the keys it does not know about are simply dropped here.
|
|
54
|
+
*/
|
|
55
|
+
const persistedStatisticState = z.object({
|
|
56
|
+
requestsFinished: z.number(),
|
|
57
|
+
requestsFailed: z.number(),
|
|
58
|
+
requestsRetries: z.number(),
|
|
59
|
+
requestsFailedPerMinute: z.number().nullable(),
|
|
60
|
+
requestsFinishedPerMinute: z.number().nullable(),
|
|
61
|
+
requestMinDurationMillis: z.number().nullable(),
|
|
62
|
+
requestMaxDurationMillis: z.number(),
|
|
63
|
+
requestTotalFailedDurationMillis: z.number(),
|
|
64
|
+
requestTotalFinishedDurationMillis: z.number(),
|
|
65
|
+
crawlerStartedAt: z.string().nullable(),
|
|
66
|
+
crawlerFinishedAt: z.string().nullable(),
|
|
67
|
+
statsPersistedAt: z.string(),
|
|
68
|
+
crawlerRuntimeMillis: z.number(),
|
|
69
|
+
crawlerLastStartTimestamp: z.number(),
|
|
70
|
+
// A retry count that never occurred leaves a hole in the live histogram, written out as a `null`. We
|
|
71
|
+
// once saw a record whose histogram was not an array at all and crashed the crawler on load.
|
|
72
|
+
requestRetryHistogram: z.array(z.number().nullable()),
|
|
73
|
+
statsId: z.string(),
|
|
74
|
+
requestAvgFailedDurationMillis: z.number().nullable(),
|
|
75
|
+
requestAvgFinishedDurationMillis: z.number().nullable(),
|
|
76
|
+
requestTotalDurationMillis: z.number(),
|
|
77
|
+
requestsTotal: z.number(),
|
|
78
|
+
requestsWithStatusCode: z.record(z.string(), z.number()),
|
|
79
|
+
errors: z.record(z.string(), z.unknown()),
|
|
80
|
+
retryErrors: z.record(z.string(), z.unknown()),
|
|
81
|
+
});
|
|
82
|
+
/** `Infinity` is what the statistics use for "nothing to average yet"; JSON has only `null` for it. */
|
|
83
|
+
function finiteOrNull(value) {
|
|
84
|
+
return Number.isFinite(value) ? value : null;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The conversion between the live state and the record above, in both directions.
|
|
88
|
+
*
|
|
89
|
+
* Built per instance rather than kept as a constant because a record carries three things the state does not: the
|
|
90
|
+
* instance `id`, the derived aggregates of the overridable {@link Statistics.calculate}, and - on the way back -
|
|
91
|
+
* the fields that are rebuilt from the instance's default state rather than restored, the error trackers among them.
|
|
92
|
+
*
|
|
93
|
+
* The model side is deliberately opaque: zod rebuilds what it validates, and `state.errors` has to stay the very
|
|
94
|
+
* object the error trackers write into, not a copy of it.
|
|
95
|
+
*/
|
|
96
|
+
function buildStatisticStateCodec(statistics) {
|
|
97
|
+
return z.codec(persistedStatisticState, z.custom(), {
|
|
98
|
+
decode: (record) => ({
|
|
99
|
+
...statistics.defaultState(),
|
|
100
|
+
requestsFinished: record.requestsFinished,
|
|
101
|
+
requestsFailed: record.requestsFailed,
|
|
102
|
+
requestsRetries: record.requestsRetries,
|
|
103
|
+
requestTotalFailedDurationMillis: record.requestTotalFailedDurationMillis,
|
|
104
|
+
requestTotalFinishedDurationMillis: record.requestTotalFinishedDurationMillis,
|
|
105
|
+
// Restoring the `null` as-is would make every later `duration < min` comparison fail, leaving the
|
|
106
|
+
// minimum `null` for the rest of the run.
|
|
107
|
+
requestMinDurationMillis: record.requestMinDurationMillis ?? Infinity,
|
|
108
|
+
requestMaxDurationMillis: record.requestMaxDurationMillis,
|
|
109
|
+
crawlerRuntimeMillis: record.crawlerRuntimeMillis,
|
|
110
|
+
// A `null` stands for the zero requests that reached that retry count - restore it as such.
|
|
111
|
+
requestRetryHistogram: record.requestRetryHistogram.map((count) => count ?? 0),
|
|
112
|
+
// The record keeps ISO strings, the live state keeps `Date`s.
|
|
113
|
+
crawlerStartedAt: record.crawlerStartedAt === null ? null : new Date(record.crawlerStartedAt),
|
|
114
|
+
crawlerFinishedAt: record.crawlerFinishedAt === null ? null : new Date(record.crawlerFinishedAt),
|
|
115
|
+
statsPersistedAt: new Date(record.statsPersistedAt),
|
|
116
|
+
// Rebased so that the runtime reported by `calculate()` spans the migration instead of restarting.
|
|
117
|
+
instanceStart: Date.now() - (new Date(record.statsPersistedAt).getTime() - record.crawlerLastStartTimestamp),
|
|
118
|
+
}),
|
|
119
|
+
encode: (state) => {
|
|
120
|
+
const { requestsWithStatusCode, errors, retryErrors, requestRetryHistogram, instanceStart, ...counters } = state;
|
|
121
|
+
// Every rate and average `calculate()` derives is `Infinity` until the run is long enough, or until
|
|
122
|
+
// something has finished or failed, to divide by.
|
|
123
|
+
const { requestAvgFailedDurationMillis, requestAvgFinishedDurationMillis, requestsFailedPerMinute, requestsFinishedPerMinute, ...aggregates } = statistics.calculate();
|
|
124
|
+
return {
|
|
125
|
+
...counters,
|
|
126
|
+
requestMinDurationMillis: finiteOrNull(state.requestMinDurationMillis),
|
|
127
|
+
crawlerStartedAt: state.crawlerStartedAt ? new Date(state.crawlerStartedAt).toISOString() : null,
|
|
128
|
+
crawlerFinishedAt: state.crawlerFinishedAt ? new Date(state.crawlerFinishedAt).toISOString() : null,
|
|
129
|
+
statsPersistedAt: new Date().toISOString(),
|
|
130
|
+
crawlerLastStartTimestamp: instanceStart,
|
|
131
|
+
// `Array.from`, not `map` - a hole left by a retry count no request reached is skipped by `map`
|
|
132
|
+
// and would stay a hole, which is not something the record can carry.
|
|
133
|
+
requestRetryHistogram: Array.from(requestRetryHistogram, (count) => count ?? null),
|
|
134
|
+
statsId: statistics.statsId,
|
|
135
|
+
...aggregates,
|
|
136
|
+
requestAvgFailedDurationMillis: finiteOrNull(requestAvgFailedDurationMillis),
|
|
137
|
+
requestAvgFinishedDurationMillis: finiteOrNull(requestAvgFinishedDurationMillis),
|
|
138
|
+
requestsFailedPerMinute: finiteOrNull(requestsFailedPerMinute),
|
|
139
|
+
requestsFinishedPerMinute: finiteOrNull(requestsFinishedPerMinute),
|
|
140
|
+
requestsWithStatusCode,
|
|
141
|
+
errors,
|
|
142
|
+
retryErrors,
|
|
143
|
+
};
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
28
147
|
/**
|
|
29
148
|
* The statistics class provides an interface to collecting and logging run
|
|
30
149
|
* statistics for requests.
|
|
31
150
|
*
|
|
32
151
|
* All statistic information is saved on key value store
|
|
33
|
-
* under the key `
|
|
152
|
+
* under the key `CRAWLEE_CRAWLER_STATISTICS_*`, persists between
|
|
34
153
|
* migrations and abort/resurrect
|
|
35
154
|
*
|
|
155
|
+
* Custom fields are tracked by passing {@link StatisticsOptions.stateExtension|`stateExtension`} - the extra fields are then part
|
|
156
|
+
* of {@link Statistics.state|`state`}, persisted and restored along with the built-in ones.
|
|
157
|
+
*
|
|
36
158
|
* @category Crawlers
|
|
37
159
|
*/
|
|
38
160
|
export class Statistics {
|
|
161
|
+
// kept as TS-private: statistics tests read the static counter directly
|
|
39
162
|
static id = 0;
|
|
40
163
|
/**
|
|
41
164
|
* An error tracker for final retry errors.
|
|
@@ -48,66 +171,120 @@ export class Statistics {
|
|
|
48
171
|
/**
|
|
49
172
|
* Statistic instance id.
|
|
50
173
|
*/
|
|
51
|
-
id
|
|
174
|
+
id;
|
|
175
|
+
#persistStateKey;
|
|
176
|
+
#stateCodec;
|
|
177
|
+
#recoverableState;
|
|
178
|
+
#stateExtension;
|
|
179
|
+
#defaultStateExtension;
|
|
180
|
+
#stateExtensionKeys;
|
|
181
|
+
#logIntervalMillis;
|
|
182
|
+
#logMessage;
|
|
183
|
+
#requestsInProgress = new Map();
|
|
184
|
+
log;
|
|
185
|
+
#logInterval;
|
|
52
186
|
/**
|
|
53
187
|
* Current statistic state used for doing calculations on {@link Statistics.calculate} calls
|
|
54
188
|
*/
|
|
55
|
-
state
|
|
189
|
+
get state() {
|
|
190
|
+
return this.#recoverableState.currentValue;
|
|
191
|
+
}
|
|
56
192
|
/**
|
|
57
193
|
* Contains the current retries histogram. Index 0 means 0 retries, index 2, 2 retries, and so on
|
|
58
194
|
*/
|
|
59
|
-
requestRetryHistogram
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
*/
|
|
63
|
-
config;
|
|
64
|
-
keyValueStore = undefined;
|
|
65
|
-
persistStateKey = `SDK_CRAWLER_STATISTICS_${this.id}`;
|
|
66
|
-
logIntervalMillis;
|
|
67
|
-
logMessage;
|
|
68
|
-
listener;
|
|
69
|
-
requestsInProgress = new Map();
|
|
70
|
-
log;
|
|
71
|
-
instanceStart;
|
|
72
|
-
logInterval;
|
|
73
|
-
events;
|
|
74
|
-
persistenceOptions;
|
|
195
|
+
get requestRetryHistogram() {
|
|
196
|
+
return this.state.requestRetryHistogram;
|
|
197
|
+
}
|
|
75
198
|
/**
|
|
76
|
-
*
|
|
199
|
+
* Construct a statistics instance to pass to a crawler via its `statistics` option, e.g. to preconfigure
|
|
200
|
+
* persistence or error snapshots, share it across sequential runs, or track extra fields via `state`.
|
|
77
201
|
*/
|
|
78
202
|
constructor(options = {}) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
keyValueStore: ow.optional.object,
|
|
84
|
-
config: ow.optional.object,
|
|
85
|
-
persistenceOptions: ow.optional.object,
|
|
86
|
-
saveErrorSnapshots: ow.optional.boolean,
|
|
87
|
-
}));
|
|
88
|
-
const { logIntervalSecs = 60, logMessage = 'Statistics', keyValueStore, config = Configuration.getGlobalConfig(), persistenceOptions = {
|
|
89
|
-
enable: true,
|
|
90
|
-
}, saveErrorSnapshots = false, } = options;
|
|
91
|
-
this.log = (options.log ?? defaultLog).child({ prefix: 'Statistics' });
|
|
203
|
+
const { logIntervalSecs, logMessage, log, keyValueStore, persistenceOptions, saveErrorSnapshots, id, stateExtension, } = parseArgument(options, statisticsOptionsSchema);
|
|
204
|
+
this.id = id ?? String(Statistics.id++);
|
|
205
|
+
this.#persistStateKey = `CRAWLEE_CRAWLER_STATISTICS_${this.id}`;
|
|
206
|
+
this.log = (log ?? serviceLocator.getLogger()).child({ prefix: 'Statistics' });
|
|
92
207
|
this.errorTracker = new ErrorTracker({ ...errorTrackerConfig, saveErrorSnapshots });
|
|
93
208
|
this.errorTrackerRetry = new ErrorTracker({ ...errorTrackerConfig, saveErrorSnapshots });
|
|
94
|
-
this
|
|
95
|
-
this
|
|
96
|
-
this
|
|
97
|
-
this
|
|
98
|
-
this
|
|
99
|
-
this
|
|
100
|
-
|
|
209
|
+
this.#logIntervalMillis = logIntervalSecs * 1000;
|
|
210
|
+
this.#logMessage = logMessage;
|
|
211
|
+
this.#stateExtension = stateExtension;
|
|
212
|
+
this.#defaultStateExtension = this.#resolveDefaultStateExtension(this.#stateExtension);
|
|
213
|
+
this.#stateExtensionKeys = Object.keys(this.#defaultStateExtension());
|
|
214
|
+
for (const key of this.#stateExtensionKeys) {
|
|
215
|
+
if (key in this.#builtInDefaultState()) {
|
|
216
|
+
throw new Error(`The custom statistics field \`${String(key)}\` collides with a built-in one - it would shadow ` +
|
|
217
|
+
'the value the crawler tracks. Rename it in `stateExtension`.');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// `calculate()` is late-bound on purpose - it is an override point, and a subclass's must be the one that runs.
|
|
221
|
+
this.#stateCodec = buildStatisticStateCodec({
|
|
222
|
+
statsId: this.id,
|
|
223
|
+
defaultState: () => this.#defaultState(),
|
|
224
|
+
calculate: () => this.calculate(),
|
|
225
|
+
});
|
|
226
|
+
this.#recoverableState = new RecoverableState({
|
|
227
|
+
persistStateKey: this.#persistStateKey,
|
|
228
|
+
persistenceEnabled: persistenceOptions.enable,
|
|
229
|
+
keyValueStore,
|
|
230
|
+
logger: this.log,
|
|
231
|
+
defaultState: () => this.#defaultState(),
|
|
232
|
+
serialize: (state) => this.#serializeState(state),
|
|
233
|
+
deserialize: (persistedState) => this.#deserializeState(persistedState),
|
|
234
|
+
});
|
|
101
235
|
// initialize by "resetting"
|
|
102
236
|
this.reset();
|
|
103
237
|
}
|
|
104
238
|
/**
|
|
105
|
-
* Set the current statistic instance to pristine values
|
|
239
|
+
* Set the current statistic instance to pristine values.
|
|
240
|
+
*
|
|
241
|
+
* The persisted record is left alone - use {@link Statistics.resetStore} to clear that as well.
|
|
106
242
|
*/
|
|
107
243
|
reset() {
|
|
108
244
|
this.errorTracker.reset();
|
|
109
245
|
this.errorTrackerRetry.reset();
|
|
110
|
-
this.
|
|
246
|
+
this.#recoverableState.reset();
|
|
247
|
+
this.#requestsInProgress.clear();
|
|
248
|
+
}
|
|
249
|
+
/** The pristine state a new instance starts with and {@link Statistics.reset} restores. */
|
|
250
|
+
#defaultState() {
|
|
251
|
+
return {
|
|
252
|
+
...this.#builtInDefaultState(),
|
|
253
|
+
...this.#defaultStateExtension(),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* The factory behind the custom half of the default state - it has to hand out a fresh object every time, or a
|
|
258
|
+
* `reset()` would write through to the defaults of the next one.
|
|
259
|
+
*
|
|
260
|
+
* With no `defaultState` given, the defaults are what `deserialize` makes of an empty record. That keeps a
|
|
261
|
+
* single declaration of the custom fields - a schema with a `.default()` per field is enough - and the defaults
|
|
262
|
+
* cannot then disagree with the conversion that has to accept them back.
|
|
263
|
+
*/
|
|
264
|
+
#resolveDefaultStateExtension(options) {
|
|
265
|
+
const { defaultState, deserialize } = options;
|
|
266
|
+
if (typeof defaultState === 'function') {
|
|
267
|
+
return defaultState;
|
|
268
|
+
}
|
|
269
|
+
if (defaultState !== undefined) {
|
|
270
|
+
return () => structuredClone(defaultState);
|
|
271
|
+
}
|
|
272
|
+
if (deserialize === undefined) {
|
|
273
|
+
return () => ({});
|
|
274
|
+
}
|
|
275
|
+
return () => {
|
|
276
|
+
try {
|
|
277
|
+
return convertStateSync(deserialize, {}, this.#persistStateKey);
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
throw new Error('Could not derive the default values of the custom statistics fields from `stateExtension.deserialize` - ' +
|
|
281
|
+
'give every field a default, or declare `stateExtension.defaultState` explicitly.', { cause: error });
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
/** The built-in half of {@link Statistics.defaultState}, before any custom fields are merged over it. */
|
|
286
|
+
#builtInDefaultState() {
|
|
287
|
+
return {
|
|
111
288
|
requestsFinished: 0,
|
|
112
289
|
requestsFailed: 0,
|
|
113
290
|
requestsRetries: 0,
|
|
@@ -122,25 +299,20 @@ export class Statistics {
|
|
|
122
299
|
statsPersistedAt: null,
|
|
123
300
|
crawlerRuntimeMillis: 0,
|
|
124
301
|
requestsWithStatusCode: {},
|
|
302
|
+
// Aliases, not copies - the trackers keep writing into these objects.
|
|
125
303
|
errors: this.errorTracker.result,
|
|
126
304
|
retryErrors: this.errorTrackerRetry.result,
|
|
305
|
+
requestRetryHistogram: [],
|
|
306
|
+
instanceStart: Date.now(),
|
|
127
307
|
};
|
|
128
|
-
this.requestRetryHistogram.length = 0;
|
|
129
|
-
this.requestsInProgress.clear();
|
|
130
|
-
this.instanceStart = Date.now();
|
|
131
|
-
this._teardown();
|
|
132
308
|
}
|
|
133
309
|
/**
|
|
134
|
-
*
|
|
310
|
+
* Clear the persisted statistics record, leaving the in-memory state alone.
|
|
311
|
+
*
|
|
312
|
+
* Throws while capturing - the next PERSIST_STATE event would write the record straight back.
|
|
135
313
|
*/
|
|
136
|
-
async resetStore(
|
|
137
|
-
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
if (!this.keyValueStore) {
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
await this.keyValueStore.setValue(this.persistStateKey, null);
|
|
314
|
+
async resetStore() {
|
|
315
|
+
await this.#recoverableState.resetStore();
|
|
144
316
|
}
|
|
145
317
|
/**
|
|
146
318
|
* Increments the status code counter.
|
|
@@ -157,49 +329,57 @@ export class Statistics {
|
|
|
157
329
|
* @ignore
|
|
158
330
|
*/
|
|
159
331
|
startJob(id) {
|
|
160
|
-
let job = this
|
|
332
|
+
let job = this.#requestsInProgress.get(id);
|
|
161
333
|
if (!job)
|
|
162
334
|
job = new Job();
|
|
163
335
|
job.run();
|
|
164
|
-
this
|
|
336
|
+
this.#requestsInProgress.set(id, job);
|
|
165
337
|
}
|
|
166
338
|
/**
|
|
167
339
|
* Mark job as finished and sets the state
|
|
168
340
|
* @ignore
|
|
169
341
|
*/
|
|
170
342
|
finishJob(id, retryCount) {
|
|
171
|
-
const job = this
|
|
343
|
+
const job = this.#requestsInProgress.get(id);
|
|
172
344
|
if (!job)
|
|
173
345
|
return;
|
|
174
346
|
const jobDurationMillis = job.finish();
|
|
175
347
|
this.state.requestsFinished++;
|
|
176
348
|
this.state.requestTotalFinishedDurationMillis += jobDurationMillis;
|
|
177
|
-
this.
|
|
349
|
+
this.saveRetryCountForJob(retryCount);
|
|
178
350
|
if (jobDurationMillis < this.state.requestMinDurationMillis)
|
|
179
351
|
this.state.requestMinDurationMillis = jobDurationMillis;
|
|
180
352
|
if (jobDurationMillis > this.state.requestMaxDurationMillis)
|
|
181
353
|
this.state.requestMaxDurationMillis = jobDurationMillis;
|
|
182
|
-
this
|
|
354
|
+
this.#requestsInProgress.delete(id);
|
|
183
355
|
}
|
|
184
356
|
/**
|
|
185
357
|
* Mark job as failed and sets the state
|
|
186
358
|
* @ignore
|
|
187
359
|
*/
|
|
188
360
|
failJob(id, retryCount) {
|
|
189
|
-
const job = this
|
|
361
|
+
const job = this.#requestsInProgress.get(id);
|
|
190
362
|
if (!job)
|
|
191
363
|
return;
|
|
192
364
|
this.state.requestTotalFailedDurationMillis += job.finish();
|
|
193
365
|
this.state.requestsFailed++;
|
|
194
|
-
this.
|
|
195
|
-
this
|
|
366
|
+
this.saveRetryCountForJob(retryCount);
|
|
367
|
+
this.#requestsInProgress.delete(id);
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Discards a started job without affecting the finished/failed counters, e.g. when a request
|
|
371
|
+
* turns out to be skipped (robots.txt, enqueue strategy) after `startJob` was already called for it.
|
|
372
|
+
* @ignore
|
|
373
|
+
*/
|
|
374
|
+
discardJob(id) {
|
|
375
|
+
this.#requestsInProgress.delete(id);
|
|
196
376
|
}
|
|
197
377
|
/**
|
|
198
378
|
* Calculate the current statistics
|
|
199
379
|
*/
|
|
200
380
|
calculate() {
|
|
201
381
|
const { requestsFailed, requestsFinished, requestTotalFailedDurationMillis, requestTotalFinishedDurationMillis, } = this.state;
|
|
202
|
-
const totalMillis = Date.now() - this.instanceStart;
|
|
382
|
+
const totalMillis = Date.now() - this.state.instanceStart;
|
|
203
383
|
const totalMinutes = totalMillis / 1000 / 60;
|
|
204
384
|
return {
|
|
205
385
|
requestAvgFailedDurationMillis: Math.round(requestTotalFailedDurationMillis / requestsFailed) || Infinity,
|
|
@@ -216,131 +396,141 @@ export class Statistics {
|
|
|
216
396
|
* displaying the current state in predefined intervals
|
|
217
397
|
*/
|
|
218
398
|
async startCapturing() {
|
|
219
|
-
|
|
399
|
+
// A single instance drives one logging interval and one PERSIST_STATE listener, so a second concurrent
|
|
400
|
+
// capture (e.g. sharing one instance across crawlers running at once) would orphan the first. Fail loudly.
|
|
401
|
+
if (this.#logInterval) {
|
|
402
|
+
throw new Error('Statistics.startCapturing() was already called - this instance is already capturing.');
|
|
403
|
+
}
|
|
404
|
+
await this.#recoverableState.initialize();
|
|
405
|
+
// After the load, so that a restored record keeps the timestamp of the run it belongs to.
|
|
220
406
|
if (this.state.crawlerStartedAt === null) {
|
|
221
407
|
this.state.crawlerStartedAt = new Date();
|
|
222
408
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
this.events.on("persistState" /* EventType.PERSIST_STATE */, this.listener);
|
|
226
|
-
}
|
|
227
|
-
this.logInterval = setInterval(() => {
|
|
228
|
-
this.log.info(this.logMessage, {
|
|
409
|
+
this.#logInterval = setInterval(() => {
|
|
410
|
+
this.log.info(this.#logMessage, {
|
|
229
411
|
...this.calculate(),
|
|
230
412
|
retryHistogram: this.requestRetryHistogram,
|
|
231
413
|
});
|
|
232
|
-
}, this
|
|
414
|
+
}, this.#logIntervalMillis);
|
|
233
415
|
}
|
|
234
416
|
/**
|
|
235
417
|
* Stops logging and remove event listeners, then persist
|
|
236
418
|
*/
|
|
237
419
|
async stopCapturing() {
|
|
238
|
-
this
|
|
420
|
+
this.#stopLogging();
|
|
239
421
|
this.state.crawlerFinishedAt = new Date();
|
|
240
|
-
await this.
|
|
422
|
+
await this.#recoverableState.teardown();
|
|
241
423
|
}
|
|
242
|
-
|
|
424
|
+
saveRetryCountForJob(retryCount) {
|
|
243
425
|
if (retryCount > 0)
|
|
244
426
|
this.state.requestsRetries++;
|
|
245
427
|
this.requestRetryHistogram[retryCount] ??= 0;
|
|
246
428
|
this.requestRetryHistogram[retryCount]++;
|
|
247
429
|
}
|
|
248
430
|
/**
|
|
249
|
-
* Persist internal state to the key value store
|
|
250
|
-
*
|
|
431
|
+
* Persist internal state to the key value store.
|
|
432
|
+
*
|
|
433
|
+
* Statistics are bookkeeping - a store that refuses the write is worth a warning, not a failed crawl. The
|
|
434
|
+
* crawler calls this from its migration handler, where a rejection would go unhandled.
|
|
251
435
|
*/
|
|
252
|
-
async persistState(
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
436
|
+
async persistState() {
|
|
437
|
+
await this.#recoverableState
|
|
438
|
+
.persistState()
|
|
439
|
+
.catch((error) => this.log.warning(`Failed to persist the statistics to ${this.#persistStateKey}`, { error }));
|
|
440
|
+
}
|
|
441
|
+
/** Rebuilds the state from a persisted record. */
|
|
442
|
+
#deserializeState(persistedState) {
|
|
443
|
+
// The cast covers the custom fields, whose type is open here - and the record is an unvalidated blob off the
|
|
444
|
+
// key-value store either way, which is what the decode is for. Their keys are not in the schema, so the
|
|
445
|
+
// decode drops them; `#restoreStateExtension` is what brings them back.
|
|
446
|
+
const restored = z.safeDecode(this.#stateCodec, persistedState);
|
|
447
|
+
if (!restored.success) {
|
|
448
|
+
// Statistics are bookkeeping - a record that cannot be made sense of is worth a warning and a fresh
|
|
449
|
+
// start, not a failed crawl.
|
|
450
|
+
this.log.warning('Received invalid state from Key-value store, starting the statistics from scratch.', {
|
|
451
|
+
persistStateKey: this.#persistStateKey,
|
|
452
|
+
issues: restored.error.issues,
|
|
453
|
+
});
|
|
454
|
+
return this.#defaultState();
|
|
259
455
|
}
|
|
260
|
-
this.log.debug('
|
|
261
|
-
|
|
262
|
-
const persistStateIntervalMillis = this.config.get('persistStateIntervalMillis');
|
|
263
|
-
const timeoutSecs = persistStateIntervalMillis / 2_000;
|
|
264
|
-
await this.keyValueStore
|
|
265
|
-
.setValue(this.persistStateKey, this.toJSON(), {
|
|
266
|
-
timeoutSecs,
|
|
267
|
-
doNotRetryTimeouts: true,
|
|
268
|
-
})
|
|
269
|
-
.catch((error) => this.log.warning(`Failed to persist the statistics to ${this.persistStateKey}`, { error }));
|
|
456
|
+
this.log.debug('Recreating state from KeyValueStore', { persistStateKey: this.#persistStateKey });
|
|
457
|
+
return { ...this.#defaultState(), ...restored.data, ...this.#restoreStateExtension(persistedState) };
|
|
270
458
|
}
|
|
271
459
|
/**
|
|
272
|
-
*
|
|
460
|
+
* The custom {@link StatisticsOptions.stateExtension|`stateExtension`} fields as they were persisted - the codec only rebuilds
|
|
461
|
+
* the built-in ones.
|
|
462
|
+
*
|
|
463
|
+
* Given a `deserialize`, the whole record goes through it, so a field the record does not carry - one declared
|
|
464
|
+
* after the record was written - comes back as whatever default the conversion gives it. Without one, the
|
|
465
|
+
* declared keys are copied over as they were persisted and a missing one keeps the default it already has.
|
|
273
466
|
*/
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (
|
|
277
|
-
|
|
467
|
+
#restoreStateExtension(persistedState) {
|
|
468
|
+
const { deserialize } = this.#stateExtension;
|
|
469
|
+
if (deserialize === undefined) {
|
|
470
|
+
const restored = {};
|
|
471
|
+
for (const key of this.#stateExtensionKeys) {
|
|
472
|
+
// The record is an unvalidated blob and there is no conversion to check it with, so this is the
|
|
473
|
+
// caller's word for it - the state ends up holding whatever was written. The two halves only line
|
|
474
|
+
// up at all because the fields are persisted as they are without a `serialize`.
|
|
475
|
+
const persistedValue = persistedState[key];
|
|
476
|
+
if (persistedValue !== undefined) {
|
|
477
|
+
restored[key] = persistedValue;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return restored;
|
|
278
481
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
this.log.warning('Received invalid
|
|
286
|
-
|
|
287
|
-
state: savedState,
|
|
288
|
-
});
|
|
482
|
+
try {
|
|
483
|
+
return convertStateSync(deserialize, persistedState, this.#persistStateKey);
|
|
484
|
+
}
|
|
485
|
+
catch (error) {
|
|
486
|
+
// Same policy as a built-in field that cannot be made sense of, but scoped to the custom ones - a
|
|
487
|
+
// corrupt counter of your own is no reason to throw away the crawler's.
|
|
488
|
+
this.log.warning('Received invalid custom statistics fields from Key-value store, starting those from scratch.', { persistStateKey: this.#persistStateKey, error });
|
|
489
|
+
return {};
|
|
289
490
|
}
|
|
290
|
-
this.log.debug('Recreating state from KeyValueStore', { persistStateKey: this.persistStateKey });
|
|
291
|
-
// the `requestRetryHistogram` array might be very large, we could end up with
|
|
292
|
-
// `RangeError: Maximum call stack size exceeded` if we use `a.push(...b)`
|
|
293
|
-
savedState.requestRetryHistogram.forEach((idx) => this.requestRetryHistogram.push(idx));
|
|
294
|
-
this.state.requestsFinished = savedState.requestsFinished;
|
|
295
|
-
this.state.requestsFailed = savedState.requestsFailed;
|
|
296
|
-
this.state.requestsRetries = savedState.requestsRetries;
|
|
297
|
-
this.state.requestTotalFailedDurationMillis = savedState.requestTotalFailedDurationMillis;
|
|
298
|
-
this.state.requestTotalFinishedDurationMillis = savedState.requestTotalFinishedDurationMillis;
|
|
299
|
-
this.state.requestMinDurationMillis = savedState.requestMinDurationMillis;
|
|
300
|
-
this.state.requestMaxDurationMillis = savedState.requestMaxDurationMillis;
|
|
301
|
-
// persisted state uses ISO date strings
|
|
302
|
-
this.state.crawlerFinishedAt = savedState.crawlerFinishedAt ? new Date(savedState.crawlerFinishedAt) : null;
|
|
303
|
-
this.state.crawlerStartedAt = savedState.crawlerStartedAt ? new Date(savedState.crawlerStartedAt) : null;
|
|
304
|
-
this.state.statsPersistedAt = savedState.statsPersistedAt ? new Date(savedState.statsPersistedAt) : null;
|
|
305
|
-
this.state.crawlerRuntimeMillis = savedState.crawlerRuntimeMillis;
|
|
306
|
-
this.instanceStart = Date.now() - (+this.state.statsPersistedAt - savedState.crawlerLastStartTimestamp);
|
|
307
|
-
this.log.debug('Loaded from KeyValueStore');
|
|
308
491
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
clearInterval(this.logInterval);
|
|
314
|
-
this.logInterval = null;
|
|
492
|
+
#stopLogging() {
|
|
493
|
+
if (this.#logInterval) {
|
|
494
|
+
clearInterval(this.#logInterval);
|
|
495
|
+
this.#logInterval = null;
|
|
315
496
|
}
|
|
316
497
|
}
|
|
498
|
+
/**
|
|
499
|
+
* Builds the record written to the key value store, merging in the derived aggregates so that a consumer
|
|
500
|
+
* reading the record does not have to reconstruct them.
|
|
501
|
+
*/
|
|
502
|
+
#serializeState(state) {
|
|
503
|
+
const { builtIn, extension } = this.#splitState(state);
|
|
504
|
+
return {
|
|
505
|
+
...z.encode(this.#stateCodec, builtIn),
|
|
506
|
+
...this.#serializeStateExtension(extension),
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
/** The custom {@link StatisticsOptions.stateExtension|`stateExtension`} fields as they go into the record. */
|
|
510
|
+
#serializeStateExtension(extension) {
|
|
511
|
+
const { serialize } = this.#stateExtension;
|
|
512
|
+
if (serialize === undefined) {
|
|
513
|
+
return extension;
|
|
514
|
+
}
|
|
515
|
+
// Unlike the way back, a failure here throws: the value is the caller's own, and a record written from a
|
|
516
|
+
// state that does not match its own declaration is not worth having.
|
|
517
|
+
return convertStateSync(serialize, extension, this.#persistStateKey);
|
|
518
|
+
}
|
|
519
|
+
/** Separates the declared custom fields from the built-in ones, so that each half goes through its own conversion. */
|
|
520
|
+
#splitState(state) {
|
|
521
|
+
const builtIn = { ...state };
|
|
522
|
+
const extension = {};
|
|
523
|
+
for (const key of this.#stateExtensionKeys) {
|
|
524
|
+
extension[key] = state[key];
|
|
525
|
+
delete builtIn[key];
|
|
526
|
+
}
|
|
527
|
+
return { builtIn, extension };
|
|
528
|
+
}
|
|
317
529
|
/**
|
|
318
530
|
* Make this class serializable when called with `JSON.stringify(statsInstance)` directly
|
|
319
531
|
* or through `keyValueStore.setValue('KEY', statsInstance)`
|
|
320
532
|
*/
|
|
321
533
|
toJSON() {
|
|
322
|
-
|
|
323
|
-
// without the need to reconstruct for the sake of stats.calculate()
|
|
324
|
-
// omit duplicated information
|
|
325
|
-
const result = {
|
|
326
|
-
...this.state,
|
|
327
|
-
crawlerLastStartTimestamp: this.instanceStart,
|
|
328
|
-
crawlerFinishedAt: this.state.crawlerFinishedAt
|
|
329
|
-
? new Date(this.state.crawlerFinishedAt).toISOString()
|
|
330
|
-
: null,
|
|
331
|
-
crawlerStartedAt: this.state.crawlerStartedAt ? new Date(this.state.crawlerStartedAt).toISOString() : null,
|
|
332
|
-
requestRetryHistogram: this.requestRetryHistogram,
|
|
333
|
-
statsId: this.id,
|
|
334
|
-
statsPersistedAt: new Date().toISOString(),
|
|
335
|
-
...this.calculate(),
|
|
336
|
-
};
|
|
337
|
-
Reflect.deleteProperty(result, 'requestsWithStatusCode');
|
|
338
|
-
Reflect.deleteProperty(result, 'errors');
|
|
339
|
-
Reflect.deleteProperty(result, 'retryErrors');
|
|
340
|
-
result.requestsWithStatusCode = this.state.requestsWithStatusCode;
|
|
341
|
-
result.errors = this.state.errors;
|
|
342
|
-
result.retryErrors = this.state.retryErrors;
|
|
343
|
-
return result;
|
|
534
|
+
return this.#serializeState(this.state);
|
|
344
535
|
}
|
|
345
536
|
}
|
|
346
|
-
//# sourceMappingURL=statistics.js.map
|