@crawlee/core 4.0.0-beta.143 → 4.0.0-beta.145
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/autoscaling/autoscaled_pool.d.ts +8 -7
- package/autoscaling/autoscaled_pool.js +1 -1
- package/autoscaling/concurrency_system.d.ts +0 -1
- package/autoscaling/concurrency_system.js +8 -9
- package/autoscaling/load_signal.d.ts +0 -2
- package/autoscaling/load_signal.js +3 -5
- package/crawlers/statistics.d.ts +0 -1
- package/crawlers/statistics.js +2 -3
- package/log.d.ts +2 -3
- package/log.js +14 -16
- package/package.json +6 -6
- package/session_pool/session_pool.d.ts +0 -7
- package/session_pool/session_pool.js +31 -32
- package/storages/dataset.js +1 -0
- package/storages/key_value_store.d.ts +1 -1
- package/storages/key_value_store.js +0 -1
- package/storages/request_list.d.ts +0 -1
- package/storages/request_list.js +9 -10
- package/storages/request_queue.d.ts +0 -2
- package/storages/request_queue.js +14 -16
|
@@ -20,8 +20,15 @@ export interface TaskLoopPredicates {
|
|
|
20
20
|
*/
|
|
21
21
|
isFinishedFunction?: () => Promise<boolean>;
|
|
22
22
|
}
|
|
23
|
+
export interface TaskLoopOptions extends TaskLoopPredicates {
|
|
24
|
+
/**
|
|
25
|
+
* How often the pool should check if a new task is ready, in seconds.
|
|
26
|
+
* @default 0.5
|
|
27
|
+
*/
|
|
28
|
+
maybeRunIntervalSecs?: number;
|
|
29
|
+
}
|
|
23
30
|
/** @internal */
|
|
24
|
-
export interface AutoscaledPoolOptions extends
|
|
31
|
+
export interface AutoscaledPoolOptions extends TaskLoopOptions {
|
|
25
32
|
/**
|
|
26
33
|
* The governor that decides whether there is free compute for one more task. Typically a
|
|
27
34
|
* {@link ConcurrencySystem}, but any {@link IConcurrencySystem} works. Share a single instance across
|
|
@@ -41,12 +48,6 @@ export interface AutoscaledPoolOptions extends TaskLoopPredicates {
|
|
|
41
48
|
* The function must either be labeled `async` or return a promise.
|
|
42
49
|
*/
|
|
43
50
|
runTaskFunction?: () => Promise<unknown>;
|
|
44
|
-
/**
|
|
45
|
-
* Indicates how often the pool should call the `runTaskFunction()` to start a new task, in seconds.
|
|
46
|
-
* This has no effect on starting new tasks immediately after a task completes.
|
|
47
|
-
* @default 0.5
|
|
48
|
-
*/
|
|
49
|
-
maybeRunIntervalSecs?: number;
|
|
50
51
|
/**
|
|
51
52
|
* Timeout in which the `runTaskFunction` needs to finish, given in seconds.
|
|
52
53
|
* @default 0
|
|
@@ -19,7 +19,7 @@ const autoscaledPoolOptionsSchema = z.strictObject({
|
|
|
19
19
|
.default(0),
|
|
20
20
|
log: validators.logger.default(() => serviceLocator.getLogger()),
|
|
21
21
|
concurrencySystem: schemas.anyObject,
|
|
22
|
-
consumer: schemas.anyObject.refine((value) => typeof value
|
|
22
|
+
consumer: schemas.anyObject.refine((value) => typeof value?.id === 'string' && value.id.length > 0, "Expected an object with a non-empty string 'id'"),
|
|
23
23
|
});
|
|
24
24
|
/**
|
|
25
25
|
* Manages a pool of asynchronous resource-intensive tasks that are executed in parallel.
|
|
@@ -158,7 +158,6 @@ export declare class ConcurrencySystem implements IConcurrencySystem {
|
|
|
158
158
|
private readonly scaleUpStepRatio;
|
|
159
159
|
private readonly scaleDownStepRatio;
|
|
160
160
|
private readonly maxTasksPerMinute;
|
|
161
|
-
private _currentConcurrency;
|
|
162
161
|
private readonly snapshotter;
|
|
163
162
|
private readonly systemStatus;
|
|
164
163
|
constructor(options?: ConcurrencySystemOptions);
|
|
@@ -54,8 +54,7 @@ export class ConcurrencySystem {
|
|
|
54
54
|
#minConcurrency;
|
|
55
55
|
#maxConcurrency;
|
|
56
56
|
#desiredConcurrency;
|
|
57
|
-
|
|
58
|
-
_currentConcurrency = 0;
|
|
57
|
+
#currentConcurrency = 0;
|
|
59
58
|
#lastLoggingTime;
|
|
60
59
|
#tasksPerMinute = Array.from({ length: 60 }, () => 0);
|
|
61
60
|
snapshotter;
|
|
@@ -158,7 +157,7 @@ export class ConcurrencySystem {
|
|
|
158
157
|
this.#desiredConcurrency = Math.min(atLeastMin, this.#maxConcurrency);
|
|
159
158
|
}
|
|
160
159
|
get currentConcurrency() {
|
|
161
|
-
return this
|
|
160
|
+
return this.#currentConcurrency;
|
|
162
161
|
}
|
|
163
162
|
/** Whether the system is currently monitoring load and autoscaling the budget. */
|
|
164
163
|
get isRunning() {
|
|
@@ -247,13 +246,13 @@ export class ConcurrencySystem {
|
|
|
247
246
|
*/
|
|
248
247
|
hasCapacityForTask(_consumer) {
|
|
249
248
|
this.warnIfNotRunning();
|
|
250
|
-
if (this
|
|
249
|
+
if (this.#currentConcurrency >= this.#desiredConcurrency) {
|
|
251
250
|
this.log.perf('Task will not run. Desired concurrency achieved.');
|
|
252
251
|
return false;
|
|
253
252
|
}
|
|
254
253
|
const currentStatus = this.systemStatus.getCurrentStatus();
|
|
255
254
|
const { isSystemIdle } = currentStatus;
|
|
256
|
-
if (!isSystemIdle && this
|
|
255
|
+
if (!isSystemIdle && this.#currentConcurrency >= this.#minConcurrency) {
|
|
257
256
|
this.log.perf('Task will not be run. System is overloaded.', currentStatus);
|
|
258
257
|
return false;
|
|
259
258
|
}
|
|
@@ -283,13 +282,13 @@ export class ConcurrencySystem {
|
|
|
283
282
|
this.log.perf('Task will not run. Maximum tasks per minute reached.');
|
|
284
283
|
return false;
|
|
285
284
|
}
|
|
286
|
-
this
|
|
285
|
+
this.#currentConcurrency++;
|
|
287
286
|
this.#tasksPerMinute[0]++;
|
|
288
287
|
return true;
|
|
289
288
|
}
|
|
290
289
|
/** Returns a slot to the shared budget, whoever booked it. */
|
|
291
290
|
registerTaskEnd(_consumer) {
|
|
292
|
-
this
|
|
291
|
+
this.#currentConcurrency--;
|
|
293
292
|
}
|
|
294
293
|
/**
|
|
295
294
|
* What the system currently makes of the machine: the per-signal overload verdicts, evaluated over the
|
|
@@ -310,7 +309,7 @@ export class ConcurrencySystem {
|
|
|
310
309
|
const { isSystemIdle } = systemStatus;
|
|
311
310
|
const weAreNotAtMax = this.#desiredConcurrency < this.#maxConcurrency;
|
|
312
311
|
const minCurrentConcurrency = Math.floor(this.#desiredConcurrency * this.desiredConcurrencyRatio);
|
|
313
|
-
const weAreReachingDesiredConcurrency = this
|
|
312
|
+
const weAreReachingDesiredConcurrency = this.#currentConcurrency >= minCurrentConcurrency;
|
|
314
313
|
if (isSystemIdle && weAreNotAtMax && weAreReachingDesiredConcurrency)
|
|
315
314
|
this.scaleUp(systemStatus);
|
|
316
315
|
const isSystemOverloaded = !isSystemIdle;
|
|
@@ -325,7 +324,7 @@ export class ConcurrencySystem {
|
|
|
325
324
|
else if (now > this.#lastLoggingTime + this.#loggingIntervalMillis) {
|
|
326
325
|
this.#lastLoggingTime = now;
|
|
327
326
|
this.log.info('state', {
|
|
328
|
-
currentConcurrency: this
|
|
327
|
+
currentConcurrency: this.#currentConcurrency,
|
|
329
328
|
desiredConcurrency: this.#desiredConcurrency,
|
|
330
329
|
systemStatus,
|
|
331
330
|
});
|
|
@@ -61,8 +61,6 @@ export interface LoadSignal {
|
|
|
61
61
|
*/
|
|
62
62
|
export declare class SnapshotStore<T extends LoadSnapshot = LoadSnapshot> {
|
|
63
63
|
#private;
|
|
64
|
-
/** Retention window in milliseconds. Unbounded until {@link SnapshotStore.useSampleWindow|`useSampleWindow()`}. */
|
|
65
|
-
private historyMillis;
|
|
66
64
|
/**
|
|
67
65
|
* Sizes retention to the window the signal will be sampled over, as handed to it in
|
|
68
66
|
* {@link LoadSignal.start|`start()`}. Until this is called nothing is pruned at all, so a signal that ignores
|
|
@@ -5,16 +5,14 @@ import { weightedAvg } from './weighted_avg.js';
|
|
|
5
5
|
*/
|
|
6
6
|
export class SnapshotStore {
|
|
7
7
|
#snapshots = [];
|
|
8
|
-
|
|
9
|
-
// kept as TS-private: concurrency_system tests read this retention window directly
|
|
10
|
-
historyMillis = Infinity;
|
|
8
|
+
#historyMillis = Infinity;
|
|
11
9
|
/**
|
|
12
10
|
* Sizes retention to the window the signal will be sampled over, as handed to it in
|
|
13
11
|
* {@link LoadSignal.start|`start()`}. Until this is called nothing is pruned at all, so a signal that ignores
|
|
14
12
|
* its start context grows unboundedly.
|
|
15
13
|
*/
|
|
16
14
|
useSampleWindow(maxSampleWindowMillis) {
|
|
17
|
-
this
|
|
15
|
+
this.#historyMillis = maxSampleWindowMillis;
|
|
18
16
|
}
|
|
19
17
|
/**
|
|
20
18
|
* Add a snapshot and prune entries older than the history window.
|
|
@@ -24,7 +22,7 @@ export class SnapshotStore {
|
|
|
24
22
|
let oldCount = 0;
|
|
25
23
|
for (let i = 0; i < this.#snapshots.length; i++) {
|
|
26
24
|
const { createdAt } = this.#snapshots[i];
|
|
27
|
-
if (now.getTime() - new Date(createdAt).getTime() > this
|
|
25
|
+
if (now.getTime() - new Date(createdAt).getTime() > this.#historyMillis)
|
|
28
26
|
oldCount++;
|
|
29
27
|
else
|
|
30
28
|
break;
|
package/crawlers/statistics.d.ts
CHANGED
|
@@ -89,7 +89,6 @@ export interface CalculatedStatistics {
|
|
|
89
89
|
*/
|
|
90
90
|
export declare class Statistics<StateExtension extends object = {}, PersistedStateExtension extends object = StateExtension> implements IStatistics<StateExtension> {
|
|
91
91
|
#private;
|
|
92
|
-
private static id;
|
|
93
92
|
/**
|
|
94
93
|
* An error tracker for final retry errors.
|
|
95
94
|
*/
|
package/crawlers/statistics.js
CHANGED
|
@@ -158,8 +158,7 @@ function buildStatisticStateCodec(statistics) {
|
|
|
158
158
|
* @category Crawlers
|
|
159
159
|
*/
|
|
160
160
|
export class Statistics {
|
|
161
|
-
|
|
162
|
-
static id = 0;
|
|
161
|
+
static #id = 0;
|
|
163
162
|
/**
|
|
164
163
|
* An error tracker for final retry errors.
|
|
165
164
|
*/
|
|
@@ -201,7 +200,7 @@ export class Statistics {
|
|
|
201
200
|
*/
|
|
202
201
|
constructor(options = {}) {
|
|
203
202
|
const { logIntervalSecs, logMessage, log, keyValueStore, persistenceOptions, saveErrorSnapshots, id, stateExtension, } = parseArgument(options, statisticsOptionsSchema);
|
|
204
|
-
this.id = id ?? String(Statistics
|
|
203
|
+
this.id = id ?? String(Statistics.#id++);
|
|
205
204
|
this.#persistStateKey = `CRAWLEE_CRAWLER_STATISTICS_${this.id}`;
|
|
206
205
|
this.log = (log ?? serviceLocator.getLogger()).child({ prefix: 'Statistics' });
|
|
207
206
|
this.errorTracker = new ErrorTracker({ ...errorTrackerConfig, saveErrorSnapshots });
|
package/log.d.ts
CHANGED
|
@@ -33,8 +33,7 @@ export type { CrawleeLogger, CrawleeLoggerOptions };
|
|
|
33
33
|
* ```
|
|
34
34
|
*/
|
|
35
35
|
export declare abstract class BaseCrawleeLogger implements CrawleeLogger {
|
|
36
|
-
private
|
|
37
|
-
private readonly warningsLogged;
|
|
36
|
+
#private;
|
|
38
37
|
constructor(options?: Partial<CrawleeLoggerOptions>);
|
|
39
38
|
/**
|
|
40
39
|
* Core logging method. Subclasses must implement this to dispatch log messages
|
|
@@ -73,7 +72,7 @@ export declare abstract class BaseCrawleeLogger implements CrawleeLogger {
|
|
|
73
72
|
* Users who want to use a different logging library should implement {@link BaseCrawleeLogger} directly.
|
|
74
73
|
*/
|
|
75
74
|
export declare class ApifyLogAdapter extends BaseCrawleeLogger {
|
|
76
|
-
private
|
|
75
|
+
#private;
|
|
77
76
|
constructor(apifyLog: Log, options?: Partial<CrawleeLoggerOptions>);
|
|
78
77
|
logWithLevel(level: number, message: string, data?: Record<string, unknown>): void;
|
|
79
78
|
protected createChild(options: Partial<CrawleeLoggerOptions>): CrawleeLogger;
|
package/log.js
CHANGED
|
@@ -30,18 +30,18 @@ import log, { Log, Logger, LoggerJson, LoggerText, LogLevel } from '@apify/log';
|
|
|
30
30
|
* ```
|
|
31
31
|
*/
|
|
32
32
|
export class BaseCrawleeLogger {
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
options;
|
|
36
|
-
warningsLogged = new Set();
|
|
33
|
+
// Note: If wrapping logger in a Proxy, unbound methods calling #-fields throw TypeError
|
|
34
|
+
// unless bound to the target (see createLogProxy in adaptive-playwright-crawler.ts).
|
|
35
|
+
#options;
|
|
36
|
+
#warningsLogged = new Set();
|
|
37
37
|
constructor(options = {}) {
|
|
38
|
-
this
|
|
38
|
+
this.#options = options;
|
|
39
39
|
}
|
|
40
40
|
getOptions() {
|
|
41
|
-
return this
|
|
41
|
+
return this.#options;
|
|
42
42
|
}
|
|
43
43
|
setOptions(options) {
|
|
44
|
-
this
|
|
44
|
+
this.#options = { ...this.#options, ...options };
|
|
45
45
|
}
|
|
46
46
|
child(options) {
|
|
47
47
|
return this.createChild(options);
|
|
@@ -63,8 +63,8 @@ export class BaseCrawleeLogger {
|
|
|
63
63
|
this.logWithLevel(LogLevel.WARNING, message, data);
|
|
64
64
|
}
|
|
65
65
|
warningOnce(message) {
|
|
66
|
-
if (!this
|
|
67
|
-
this
|
|
66
|
+
if (!this.#warningsLogged.has(message)) {
|
|
67
|
+
this.#warningsLogged.add(message);
|
|
68
68
|
this.warning(message);
|
|
69
69
|
}
|
|
70
70
|
}
|
|
@@ -88,18 +88,16 @@ export class BaseCrawleeLogger {
|
|
|
88
88
|
* Users who want to use a different logging library should implement {@link BaseCrawleeLogger} directly.
|
|
89
89
|
*/
|
|
90
90
|
export class ApifyLogAdapter extends BaseCrawleeLogger {
|
|
91
|
-
apifyLog;
|
|
92
|
-
constructor(
|
|
93
|
-
// kept as a TS-private parameter property: reached through the adaptive crawler's log proxy, see above
|
|
94
|
-
apifyLog, options) {
|
|
91
|
+
#apifyLog;
|
|
92
|
+
constructor(apifyLog, options) {
|
|
95
93
|
super(options ?? {});
|
|
96
|
-
this
|
|
94
|
+
this.#apifyLog = apifyLog;
|
|
97
95
|
}
|
|
98
96
|
logWithLevel(level, message, data) {
|
|
99
|
-
this
|
|
97
|
+
this.#apifyLog.internal(level, message, data);
|
|
100
98
|
}
|
|
101
99
|
createChild(options) {
|
|
102
|
-
return new ApifyLogAdapter(this
|
|
100
|
+
return new ApifyLogAdapter(this.#apifyLog.child({ prefix: options.prefix ?? null }), {
|
|
103
101
|
...this.getOptions(),
|
|
104
102
|
...options,
|
|
105
103
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crawlee/core",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.145",
|
|
4
4
|
"description": "The scalable web crawling and scraping library for JavaScript/Node.js. Enables development of data extraction and web automation jobs (not only) with headless Chrome and Puppeteer.",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.0.0"
|
|
@@ -52,10 +52,10 @@
|
|
|
52
52
|
"@apify/log": "^2.5.18",
|
|
53
53
|
"@apify/timeout": "^0.4.4",
|
|
54
54
|
"@apify/utilities": "^2.15.5",
|
|
55
|
-
"@crawlee/fs-storage": "4.0.0-beta.
|
|
56
|
-
"@crawlee/http-client": "4.0.0-beta.
|
|
57
|
-
"@crawlee/types": "4.0.0-beta.
|
|
58
|
-
"@crawlee/utils": "4.0.0-beta.
|
|
55
|
+
"@crawlee/fs-storage": "4.0.0-beta.145",
|
|
56
|
+
"@crawlee/http-client": "4.0.0-beta.145",
|
|
57
|
+
"@crawlee/types": "4.0.0-beta.145",
|
|
58
|
+
"@crawlee/utils": "4.0.0-beta.145",
|
|
59
59
|
"@sapphire/async-queue": "^1.5.5",
|
|
60
60
|
"@standard-schema/spec": "^1.0.0",
|
|
61
61
|
"@vladfrangu/async_event_emitter": "^2.4.6",
|
|
@@ -78,5 +78,5 @@
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
},
|
|
81
|
-
"gitHead": "
|
|
81
|
+
"gitHead": "e8b4d50265b9e433ae61c69d16cc1fee00a4d7e9"
|
|
82
82
|
}
|
|
@@ -114,13 +114,6 @@ export interface SessionPoolOptions {
|
|
|
114
114
|
export declare class SessionPool implements ISessionPool {
|
|
115
115
|
#private;
|
|
116
116
|
readonly id: string;
|
|
117
|
-
private maxPoolSize;
|
|
118
|
-
private createSessionFunction;
|
|
119
|
-
private keyValueStore?;
|
|
120
|
-
private sessionMap;
|
|
121
|
-
private sessionOptions;
|
|
122
|
-
private persistStateKeyValueStoreId?;
|
|
123
|
-
private persistStateKey;
|
|
124
117
|
constructor(options?: SessionPoolOptions);
|
|
125
118
|
/**
|
|
126
119
|
* Gets count of usable sessions in the pool.
|
|
@@ -82,14 +82,13 @@ export class SessionPool {
|
|
|
82
82
|
id;
|
|
83
83
|
#log;
|
|
84
84
|
#sessions = [];
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
persistStateKey;
|
|
85
|
+
#maxPoolSize;
|
|
86
|
+
#createSessionFunction;
|
|
87
|
+
#keyValueStore;
|
|
88
|
+
#sessionMap = new Map();
|
|
89
|
+
#sessionOptions;
|
|
90
|
+
#persistStateKeyValueStoreId;
|
|
91
|
+
#persistStateKey;
|
|
93
92
|
#listener;
|
|
94
93
|
#events;
|
|
95
94
|
#persistenceOptions;
|
|
@@ -105,18 +104,18 @@ export class SessionPool {
|
|
|
105
104
|
this.#log = log.child({ prefix: 'SessionPool' });
|
|
106
105
|
this.#persistenceOptions = persistenceOptions;
|
|
107
106
|
// Pool Configuration
|
|
108
|
-
this
|
|
109
|
-
this
|
|
107
|
+
this.#maxPoolSize = maxPoolSize;
|
|
108
|
+
this.#createSessionFunction = createSessionFunction || this.defaultCreateSessionFunction;
|
|
110
109
|
// Session configuration. The pool-scoped logger is merged into per-call sessionOptions inside
|
|
111
110
|
// `invokeCreateSessionFunction`, so every Session inherits it without custom createSessionFunctions
|
|
112
111
|
// having to know about it.
|
|
113
|
-
this
|
|
112
|
+
this.#sessionOptions = {
|
|
114
113
|
...sessionOptions,
|
|
115
114
|
log: this.#log,
|
|
116
115
|
};
|
|
117
116
|
// Session keyValueStore
|
|
118
|
-
this
|
|
119
|
-
this
|
|
117
|
+
this.#persistStateKeyValueStoreId = persistStateKeyValueStoreId;
|
|
118
|
+
this.#persistStateKey = persistStateKey ?? `${PERSIST_STATE_KEY}_${this.id}`;
|
|
120
119
|
}
|
|
121
120
|
/**
|
|
122
121
|
* Gets count of usable sessions in the pool.
|
|
@@ -146,11 +145,11 @@ export class SessionPool {
|
|
|
146
145
|
if (!this.#persistenceOptions.enable) {
|
|
147
146
|
return;
|
|
148
147
|
}
|
|
149
|
-
this
|
|
148
|
+
this.#keyValueStore = await KeyValueStore.open(this.#persistStateKeyValueStoreId ? { id: this.#persistStateKeyValueStoreId } : null, {
|
|
150
149
|
configuration: serviceLocator.getConfiguration(),
|
|
151
150
|
});
|
|
152
|
-
if (!this
|
|
153
|
-
this.#log.debug(`No 'persistStateKeyValueStoreId' options specified, this session pool's data has been saved in the KeyValueStore with the id: ${this
|
|
151
|
+
if (!this.#persistStateKeyValueStoreId) {
|
|
152
|
+
this.#log.debug(`No 'persistStateKeyValueStoreId' options specified, this session pool's data has been saved in the KeyValueStore with the id: ${this.#keyValueStore.id}`);
|
|
154
153
|
}
|
|
155
154
|
// in case of migration happened and SessionPool state should be restored from the keyValueStore.
|
|
156
155
|
await this.maybeLoadSessionPool();
|
|
@@ -167,7 +166,7 @@ export class SessionPool {
|
|
|
167
166
|
await this.ensureInitialized();
|
|
168
167
|
const { id } = options;
|
|
169
168
|
if (id) {
|
|
170
|
-
const sessionExists = this
|
|
169
|
+
const sessionExists = this.#sessionMap.has(id);
|
|
171
170
|
if (sessionExists) {
|
|
172
171
|
throw new Error(`Cannot add session with id '${id}' as it already exists in the pool`);
|
|
173
172
|
}
|
|
@@ -203,7 +202,7 @@ export class SessionPool {
|
|
|
203
202
|
await this.#queue.wait();
|
|
204
203
|
try {
|
|
205
204
|
if (sessionId) {
|
|
206
|
-
const session = this
|
|
205
|
+
const session = this.#sessionMap.get(sessionId);
|
|
207
206
|
if (session?.isUsable())
|
|
208
207
|
return session;
|
|
209
208
|
return undefined;
|
|
@@ -229,7 +228,7 @@ export class SessionPool {
|
|
|
229
228
|
return;
|
|
230
229
|
}
|
|
231
230
|
await this.ensureInitialized();
|
|
232
|
-
await this
|
|
231
|
+
await this.#keyValueStore?.setValue(this.#persistStateKey, null);
|
|
233
232
|
}
|
|
234
233
|
/**
|
|
235
234
|
* Returns an object representing the internal state of the `SessionPool` instance.
|
|
@@ -254,12 +253,12 @@ export class SessionPool {
|
|
|
254
253
|
}
|
|
255
254
|
await this.ensureInitialized();
|
|
256
255
|
this.#log.debug('Persisting state', {
|
|
257
|
-
persistStateKeyValueStoreId: this
|
|
258
|
-
persistStateKey: this
|
|
256
|
+
persistStateKeyValueStoreId: this.#persistStateKeyValueStoreId,
|
|
257
|
+
persistStateKey: this.#persistStateKey,
|
|
259
258
|
});
|
|
260
|
-
await this
|
|
261
|
-
?.setValue(this
|
|
262
|
-
.catch((error) => this.#log.warning(`Failed to persist the session pool stats to ${this
|
|
259
|
+
await this.#keyValueStore
|
|
260
|
+
?.setValue(this.#persistStateKey, await this.getState())
|
|
261
|
+
.catch((error) => this.#log.warning(`Failed to persist the session pool stats to ${this.#persistStateKey}`, { error }));
|
|
263
262
|
}
|
|
264
263
|
async [Symbol.asyncDispose]() {
|
|
265
264
|
await this.teardown({ persistState: true });
|
|
@@ -287,7 +286,7 @@ export class SessionPool {
|
|
|
287
286
|
this.#sessions = this.#sessions.filter((storedSession) => {
|
|
288
287
|
if (storedSession.isUsable())
|
|
289
288
|
return true;
|
|
290
|
-
this
|
|
289
|
+
this.#sessionMap.delete(storedSession.id);
|
|
291
290
|
this.#log.debug(`Removed Session - ${storedSession.id}`);
|
|
292
291
|
return false;
|
|
293
292
|
});
|
|
@@ -298,7 +297,7 @@ export class SessionPool {
|
|
|
298
297
|
*/
|
|
299
298
|
registerSession(newSession) {
|
|
300
299
|
this.#sessions.push(newSession);
|
|
301
|
-
this
|
|
300
|
+
this.#sessionMap.set(newSession.id, newSession);
|
|
302
301
|
}
|
|
303
302
|
/**
|
|
304
303
|
* Gets random index.
|
|
@@ -329,10 +328,10 @@ export class SessionPool {
|
|
|
329
328
|
async invokeCreateSessionFunction(perCallOptions) {
|
|
330
329
|
const sessionOptions = {
|
|
331
330
|
fingerprint: createDefaultSessionFingerprint(),
|
|
332
|
-
...this
|
|
331
|
+
...this.#sessionOptions,
|
|
333
332
|
...perCallOptions,
|
|
334
333
|
};
|
|
335
|
-
return this
|
|
334
|
+
return this.#createSessionFunction({ sessionOptions });
|
|
336
335
|
}
|
|
337
336
|
/**
|
|
338
337
|
* Creates new session and adds it to the pool.
|
|
@@ -348,7 +347,7 @@ export class SessionPool {
|
|
|
348
347
|
* Decides whether there is enough space for creating new session.
|
|
349
348
|
*/
|
|
350
349
|
hasSpaceForSession() {
|
|
351
|
-
return this.#sessions.length < this
|
|
350
|
+
return this.#sessions.length < this.#maxPoolSize;
|
|
352
351
|
}
|
|
353
352
|
/**
|
|
354
353
|
* Picks a session from the `SessionPool` according to the configured `sessionReuseStrategy`.
|
|
@@ -376,13 +375,13 @@ export class SessionPool {
|
|
|
376
375
|
* If the state was persisted it loads the `SessionPool` from the persisted state.
|
|
377
376
|
*/
|
|
378
377
|
async maybeLoadSessionPool() {
|
|
379
|
-
const loadedSessionPool = await this
|
|
378
|
+
const loadedSessionPool = await this.#keyValueStore?.getValue(this.#persistStateKey);
|
|
380
379
|
if (!loadedSessionPool)
|
|
381
380
|
return;
|
|
382
381
|
// Invalidate old sessions and load active sessions only
|
|
383
382
|
this.#log.debug('Recreating state from KeyValueStore', {
|
|
384
|
-
persistStateKeyValueStoreId: this
|
|
385
|
-
persistStateKey: this
|
|
383
|
+
persistStateKeyValueStoreId: this.#persistStateKeyValueStoreId,
|
|
384
|
+
persistStateKey: this.#persistStateKey,
|
|
386
385
|
});
|
|
387
386
|
for (const sessionObject of loadedSessionPool.sessions) {
|
|
388
387
|
sessionObject.createdAt = new Date(sessionObject.createdAt);
|
package/storages/dataset.js
CHANGED
|
@@ -126,6 +126,7 @@ export class Dataset {
|
|
|
126
126
|
* The objects must be serializable to JSON.
|
|
127
127
|
*/
|
|
128
128
|
async pushData(data) {
|
|
129
|
+
tryCancel();
|
|
129
130
|
const transaction = activeStorageTransaction();
|
|
130
131
|
parseArgument(data, schemas.anyObject);
|
|
131
132
|
// Normalize to array and validate each item
|
|
@@ -86,7 +86,6 @@ export class KeyValueStore {
|
|
|
86
86
|
configuration;
|
|
87
87
|
id;
|
|
88
88
|
name;
|
|
89
|
-
// kept as TS-private: key_value_store tests spy on the backend directly
|
|
90
89
|
backend;
|
|
91
90
|
#persistStateEventStarted = false;
|
|
92
91
|
/** Cache for persistent (auto-saved) values. When we try to set such value, the cache will be updated automatically. */
|
|
@@ -239,7 +239,6 @@ export declare class RequestList implements IRequestLoader {
|
|
|
239
239
|
* @internal
|
|
240
240
|
*/
|
|
241
241
|
areRequestsPersisted: boolean;
|
|
242
|
-
private sources;
|
|
243
242
|
/**
|
|
244
243
|
* To create new instance of `RequestList` we need to use `RequestList.open()` factory method.
|
|
245
244
|
* @param options All `RequestList` configuration options
|
package/storages/request_list.js
CHANGED
|
@@ -131,8 +131,7 @@ export class RequestList {
|
|
|
131
131
|
#initialState;
|
|
132
132
|
#store;
|
|
133
133
|
#keepDuplicateUrls;
|
|
134
|
-
|
|
135
|
-
sources;
|
|
134
|
+
#sources;
|
|
136
135
|
#sourcesFunction;
|
|
137
136
|
#proxyConfiguration;
|
|
138
137
|
#httpClient;
|
|
@@ -153,7 +152,7 @@ export class RequestList {
|
|
|
153
152
|
// If this option is set then all requests will get a pre-generated unique ID and duplicate URLs will be kept in the list.
|
|
154
153
|
this.#keepDuplicateUrls = keepDuplicateUrls;
|
|
155
154
|
// Will be empty after initialization to save memory.
|
|
156
|
-
this
|
|
155
|
+
this.#sources = sources ? [...sources] : [];
|
|
157
156
|
this.#sourcesFunction = sourcesFunction;
|
|
158
157
|
// The proxy configuration used for `requestsFromUrl` requests.
|
|
159
158
|
this.#proxyConfiguration = proxyConfiguration;
|
|
@@ -195,11 +194,11 @@ export class RequestList {
|
|
|
195
194
|
async addPersistedRequests(persistedRequests) {
|
|
196
195
|
// We don't need the sources so we purge them to
|
|
197
196
|
// prevent them from hanging in memory.
|
|
198
|
-
for (let i = 0; i < this
|
|
197
|
+
for (let i = 0; i < this.#sources.length; i++) {
|
|
199
198
|
// oxlint-disable-next-line typescript/no-array-delete -- intentional, drop the slot so V8 can collect the object
|
|
200
|
-
delete this
|
|
199
|
+
delete this.#sources[i];
|
|
201
200
|
}
|
|
202
|
-
this
|
|
201
|
+
this.#sources = [];
|
|
203
202
|
this.areRequestsPersisted = true;
|
|
204
203
|
const requestStream = createDeserialize(persistedRequests);
|
|
205
204
|
for await (const request of requestStream) {
|
|
@@ -214,13 +213,13 @@ export class RequestList {
|
|
|
214
213
|
*/
|
|
215
214
|
async addRequestsFromSources() {
|
|
216
215
|
// We'll load all sources in sequence to ensure that they get loaded in the right order.
|
|
217
|
-
const sourcesCount = this
|
|
216
|
+
const sourcesCount = this.#sources.length;
|
|
218
217
|
for (let i = 0; i < sourcesCount; i++) {
|
|
219
|
-
const source = this
|
|
218
|
+
const source = this.#sources[i];
|
|
220
219
|
// Using delete here to drop the original object ASAP to free memory
|
|
221
220
|
// .pop would reverse the array and .shift is SLOW.
|
|
222
221
|
// oxlint-disable-next-line typescript/no-array-delete
|
|
223
|
-
delete this
|
|
222
|
+
delete this.#sources[i];
|
|
224
223
|
if (typeof source === 'object' && source.requestsFromUrl) {
|
|
225
224
|
const fetchedRequests = await this.fetchRequestsFromUrl(source);
|
|
226
225
|
await this.addFetchedRequests(source, fetchedRequests);
|
|
@@ -230,7 +229,7 @@ export class RequestList {
|
|
|
230
229
|
}
|
|
231
230
|
}
|
|
232
231
|
// Drop the original array full of empty indexes.
|
|
233
|
-
this
|
|
232
|
+
this.#sources = [];
|
|
234
233
|
if (this.#sourcesFunction) {
|
|
235
234
|
try {
|
|
236
235
|
const sourcesFromFunction = await this.#sourcesFunction();
|
|
@@ -49,8 +49,6 @@ export declare class RequestQueue implements IStorage, IRequestManager {
|
|
|
49
49
|
readonly name?: string;
|
|
50
50
|
readonly backend: RequestQueueBackend;
|
|
51
51
|
readonly log: CrawleeLogger;
|
|
52
|
-
private requestCache;
|
|
53
|
-
private inProgressRequestBatchCount;
|
|
54
52
|
/**
|
|
55
53
|
* Backend-independent usage counters tracked for this request queue (write operations and
|
|
56
54
|
* queue-head reads issued to the underlying storage backend). Counted per backend call.
|
|
@@ -98,8 +98,7 @@ export class RequestQueue {
|
|
|
98
98
|
backend;
|
|
99
99
|
#proxyConfiguration;
|
|
100
100
|
log;
|
|
101
|
-
|
|
102
|
-
requestCache;
|
|
101
|
+
#requestCache;
|
|
103
102
|
/**
|
|
104
103
|
* Remembers the `requestId` of every request already submitted to the client — including background
|
|
105
104
|
* batches that `requestCache` skips — so overlapping URL sets aren't re-submitted.
|
|
@@ -107,8 +106,7 @@ export class RequestQueue {
|
|
|
107
106
|
*/
|
|
108
107
|
#requestSeenCache;
|
|
109
108
|
#queuePausedForMigration = false;
|
|
110
|
-
|
|
111
|
-
inProgressRequestBatchCount = 0;
|
|
109
|
+
#inProgressRequestBatchCount = 0;
|
|
112
110
|
/**
|
|
113
111
|
* The largest expected request-processing time (in seconds) seen so far via
|
|
114
112
|
* {@link setExpectedRequestProcessingTimeSecs}. Used to ensure that value is only ever raised, never
|
|
@@ -137,7 +135,7 @@ export class RequestQueue {
|
|
|
137
135
|
this.#events = serviceLocator.getEventManager();
|
|
138
136
|
this.backend = options.backend;
|
|
139
137
|
this.#proxyConfiguration = options.proxyConfiguration;
|
|
140
|
-
this
|
|
138
|
+
this.#requestCache = new LruCache({ maxLength: MAX_CACHED_REQUESTS });
|
|
141
139
|
this.#requestSeenCache = new RequestDeduplicationCache();
|
|
142
140
|
this.log = serviceLocator.getLogger().child({ prefix: `RequestQueue(${this.id}, ${this.name ?? 'no-name'})` });
|
|
143
141
|
this.#events.on(EventType.MIGRATING, async () => {
|
|
@@ -191,7 +189,7 @@ export class RequestQueue {
|
|
|
191
189
|
return this.addRequestDeferred(transaction, request, forefront);
|
|
192
190
|
}
|
|
193
191
|
const cacheKey = getRequestId(request.uniqueKey);
|
|
194
|
-
const cachedInfo = this
|
|
192
|
+
const cachedInfo = this.#requestCache.get(cacheKey);
|
|
195
193
|
if (cachedInfo) {
|
|
196
194
|
request.id = cachedInfo.id;
|
|
197
195
|
this.recordRequestJournalEntry(transaction, [request], forefront, true);
|
|
@@ -275,7 +273,7 @@ export class RequestQueue {
|
|
|
275
273
|
// The caches hold real backend ids. Only *writing* provisional ids to them would be wrong;
|
|
276
274
|
// reading saves a probe. Same lookup as the write-through path.
|
|
277
275
|
const cacheKey = getRequestId(request.uniqueKey);
|
|
278
|
-
const cachedInfo = this
|
|
276
|
+
const cachedInfo = this.#requestCache.get(cacheKey);
|
|
279
277
|
const knownRequestId = cachedInfo?.id ?? this.#requestSeenCache.get(cacheKey);
|
|
280
278
|
if (knownRequestId) {
|
|
281
279
|
this.recordRequestJournalEntry(transaction, [request], forefront, false);
|
|
@@ -418,7 +416,7 @@ export class RequestQueue {
|
|
|
418
416
|
for (const request of requests) {
|
|
419
417
|
const cacheKey = getCachedRequestId(request.uniqueKey);
|
|
420
418
|
// Prefer the full `requestCache` record; fall back to the dedup cache for background batches it skips.
|
|
421
|
-
const cachedInfo = this
|
|
419
|
+
const cachedInfo = this.#requestCache.get(cacheKey);
|
|
422
420
|
const knownRequestId = cachedInfo?.id ?? this.#requestSeenCache.get(cacheKey);
|
|
423
421
|
if (knownRequestId) {
|
|
424
422
|
request.id = knownRequestId;
|
|
@@ -517,9 +515,9 @@ export class RequestQueue {
|
|
|
517
515
|
return processedRequests;
|
|
518
516
|
},
|
|
519
517
|
trackBackgroundBatches: (batches) => {
|
|
520
|
-
this
|
|
518
|
+
this.#inProgressRequestBatchCount += 1;
|
|
521
519
|
void batches.finally(() => {
|
|
522
|
-
this
|
|
520
|
+
this.#inProgressRequestBatchCount -= 1;
|
|
523
521
|
});
|
|
524
522
|
},
|
|
525
523
|
});
|
|
@@ -580,7 +578,7 @@ export class RequestQueue {
|
|
|
580
578
|
async markRequestAsHandled(request) {
|
|
581
579
|
rejectOperationInTransaction('RequestQueue.markRequestAsHandled()', 'it is part of the crawler request-processing bookkeeping, which a transaction must not affect.');
|
|
582
580
|
parseArgument(request, handledRequestSchema);
|
|
583
|
-
const forefront = this
|
|
581
|
+
const forefront = this.#requestCache.get(getRequestId(request.uniqueKey))?.forefront ?? false;
|
|
584
582
|
const handledAt = request.handledAt ?? new Date().toISOString();
|
|
585
583
|
this.#statsTracker.add('writeCount');
|
|
586
584
|
const processedRequest = await this.backend.markRequestAsHandled({
|
|
@@ -655,7 +653,7 @@ export class RequestQueue {
|
|
|
655
653
|
async isFinished() {
|
|
656
654
|
const transaction = activeStorageTransaction();
|
|
657
655
|
// We are not finished if we're still adding new requests in the background.
|
|
658
|
-
if (this
|
|
656
|
+
if (this.#inProgressRequestBatchCount > 0) {
|
|
659
657
|
return false;
|
|
660
658
|
}
|
|
661
659
|
// Requests buffered by the active transaction count as pending from its point of view.
|
|
@@ -686,8 +684,8 @@ export class RequestQueue {
|
|
|
686
684
|
*/
|
|
687
685
|
cacheRequest(cacheKey, queueOperationInfo) {
|
|
688
686
|
// Remove the previous entry, as otherwise our cache will never update 👀
|
|
689
|
-
this
|
|
690
|
-
this
|
|
687
|
+
this.#requestCache.remove(cacheKey);
|
|
688
|
+
this.#requestCache.add(cacheKey, {
|
|
691
689
|
id: queueOperationInfo.requestId,
|
|
692
690
|
isHandled: queueOperationInfo.wasAlreadyHandled,
|
|
693
691
|
uniqueKey: queueOperationInfo.uniqueKey,
|
|
@@ -713,9 +711,9 @@ export class RequestQueue {
|
|
|
713
711
|
rejectOperationInTransaction('RequestQueue.purge()');
|
|
714
712
|
await this.backend.purge();
|
|
715
713
|
// Reset in-memory bookkeeping so the queue behaves as if freshly opened.
|
|
716
|
-
this
|
|
714
|
+
this.#requestCache.clear();
|
|
717
715
|
this.#requestSeenCache.clear();
|
|
718
|
-
this
|
|
716
|
+
this.#inProgressRequestBatchCount = 0;
|
|
719
717
|
// Reset the expected-processing-time high-water mark too, otherwise the monotonic-raise guard
|
|
720
718
|
// in `setExpectedRequestProcessingTimeSecs` would let a value raised in an earlier run leak into a
|
|
721
719
|
// later one and silently swallow a lower hint (the queue is meant to be reusable across runs).
|