@crawlee/core 4.0.0-beta.99 → 4.0.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/autoscaling/autoscaled_pool.d.ts +3 -21
- package/autoscaling/autoscaled_pool.js +105 -97
- package/autoscaling/concurrency_system.d.ts +7 -22
- package/autoscaling/concurrency_system.js +112 -100
- package/autoscaling/cpu_load_signal.d.ts +1 -2
- package/autoscaling/cpu_load_signal.js +11 -10
- package/autoscaling/event_loop_load_signal.d.ts +1 -4
- package/autoscaling/event_loop_load_signal.js +18 -18
- package/autoscaling/index.d.ts +1 -1
- package/autoscaling/index.js +1 -1
- package/autoscaling/load_signal.d.ts +8 -7
- package/autoscaling/load_signal.js +14 -12
- package/autoscaling/memory_load_signal.d.ts +3 -12
- package/autoscaling/memory_load_signal.js +41 -41
- package/autoscaling/snapshotter.d.ts +7 -10
- package/autoscaling/snapshotter.js +18 -18
- package/autoscaling/{client_load_signal.d.ts → storage_backend_load_signal.d.ts} +14 -17
- package/autoscaling/{client_load_signal.js → storage_backend_load_signal.js} +27 -27
- package/autoscaling/system_status.d.ts +9 -11
- package/autoscaling/system_status.js +13 -13
- package/configuration.d.ts +16 -16
- package/configuration.js +6 -6
- package/crawlers/context_pipeline.js +6 -6
- package/crawlers/crawler_commons.d.ts +14 -112
- package/crawlers/crawler_commons.js +1 -107
- package/crawlers/error_snapshotter.d.ts +2 -4
- package/crawlers/error_snapshotter.js +5 -5
- package/crawlers/index.d.ts +1 -1
- package/crawlers/index.js +0 -1
- package/crawlers/statistics.d.ts +164 -45
- package/crawlers/statistics.js +339 -153
- package/debug.js +4 -4
- package/enqueue_links/enqueue_links.d.ts +51 -159
- package/enqueue_links/enqueue_links.js +38 -231
- package/enqueue_links/shared.d.ts +34 -24
- package/enqueue_links/shared.js +54 -36
- package/errors.d.ts +43 -12
- package/errors.js +48 -9
- package/events/event_manager.d.ts +2 -2
- package/events/event_manager.js +7 -6
- package/events/local_event_manager.d.ts +1 -1
- package/events/local_event_manager.js +5 -5
- package/http.d.ts +9 -0
- package/http.js +28 -0
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/log.js +5 -1
- package/memory-storage/memory-storage.d.ts +3 -11
- package/memory-storage/memory-storage.js +42 -48
- package/memory-storage/resource-clients/dataset.d.ts +1 -1
- package/memory-storage/resource-clients/dataset.js +8 -13
- package/memory-storage/resource-clients/key-value-store.d.ts +1 -1
- package/memory-storage/resource-clients/key-value-store.js +36 -38
- package/memory-storage/resource-clients/request-queue.d.ts +4 -23
- package/memory-storage/resource-clients/request-queue.js +68 -80
- package/owned_or_injected.d.ts +1 -3
- package/owned_or_injected.js +17 -17
- package/package.json +8 -9
- package/proxy_configuration.d.ts +1 -3
- package/proxy_configuration.js +18 -14
- package/recoverable_state.d.ts +80 -45
- package/recoverable_state.js +163 -75
- package/request.d.ts +3 -4
- package/request.js +57 -44
- package/router.d.ts +6 -9
- package/router.js +23 -23
- package/serialization.js +14 -13
- package/service_locator.d.ts +19 -10
- package/service_locator.js +57 -48
- package/session_pool/session.d.ts +3 -14
- package/session_pool/session.js +76 -74
- package/session_pool/session_pool.d.ts +6 -12
- package/session_pool/session_pool.js +85 -77
- package/storages/batched_adds.d.ts +37 -0
- package/storages/batched_adds.js +73 -0
- package/storages/dataset.d.ts +12 -1
- package/storages/dataset.js +133 -32
- package/storages/index.d.ts +2 -1
- package/storages/index.js +2 -1
- package/storages/key_value_store.d.ts +19 -4
- package/storages/key_value_store.js +204 -75
- package/storages/request_dedup_cache.d.ts +1 -2
- package/storages/request_dedup_cache.js +9 -9
- package/storages/request_list.d.ts +4 -23
- package/storages/request_list.js +103 -96
- package/storages/request_loader.d.ts +5 -0
- package/storages/request_manager_tandem.d.ts +1 -10
- package/storages/request_manager_tandem.js +27 -27
- package/storages/request_queue.d.ts +21 -18
- package/storages/request_queue.js +318 -177
- package/storages/sitemap_request_loader.d.ts +22 -70
- package/storages/sitemap_request_loader.js +122 -116
- package/storages/storage_instance_manager.d.ts +1 -2
- package/storages/storage_instance_manager.js +17 -17
- package/storages/storage_stats.d.ts +1 -1
- package/storages/storage_stats.js +4 -4
- package/storages/throttling_request_manager.d.ts +239 -0
- package/storages/throttling_request_manager.js +646 -0
- package/storages/transaction.d.ts +252 -0
- package/storages/transaction.js +251 -0
- package/storages/utils.d.ts +6 -3
- package/storages/utils.js +11 -2
- package/system-info/runtime.js +7 -7
- package/url.d.ts +9 -0
- package/url.js +11 -0
- package/validators.d.ts +22 -25
- package/validators.js +13 -25
- package/storages/access_checking.d.ts +0 -12
- package/storages/access_checking.js +0 -17
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
import { AsyncQueue } from '@sapphire/async-queue';
|
|
2
|
-
import
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { EventType } from '../events/event_manager.js';
|
|
3
4
|
import { serviceLocator } from '../service_locator.js';
|
|
4
5
|
import { KeyValueStore } from '../storages/key_value_store.js';
|
|
6
|
+
import { parseArgument, schemas, validators } from '../validators.js';
|
|
5
7
|
import { MAX_POOL_SIZE, PERSIST_STATE_KEY } from './consts.js';
|
|
6
8
|
import { createDefaultSessionFingerprint } from './fingerprint.js';
|
|
7
9
|
import { Session } from './session.js';
|
|
8
10
|
const SESSION_REUSE_STRATEGIES = ['random', 'round-robin', 'use-until-failure'];
|
|
11
|
+
// `schemas.anyObject` passes values through by reference (object schemas return a pruned plain
|
|
12
|
+
// copy), so class instances like loggers keep their prototype.
|
|
13
|
+
const sessionPoolOptionsSchema = z.strictObject({
|
|
14
|
+
id: z.union([schemas.anyNumber, z.string()]).optional(),
|
|
15
|
+
maxPoolSize: schemas.anyNumber.default(MAX_POOL_SIZE),
|
|
16
|
+
persistStateKeyValueStoreId: z.string().optional(),
|
|
17
|
+
persistStateKey: z.string().optional(),
|
|
18
|
+
createSessionFunction: schemas.anyFunction.optional(),
|
|
19
|
+
sessionOptions: schemas.anyObject.default(() => ({})),
|
|
20
|
+
log: validators.logger.default(() => serviceLocator.getLogger()),
|
|
21
|
+
persistenceOptions: schemas.anyObject.default(() => ({ enable: true })),
|
|
22
|
+
sessionReuseStrategy: z.enum(SESSION_REUSE_STRATEGIES).default('random'),
|
|
23
|
+
});
|
|
24
|
+
const createSessionOptionsSchema = z.strictObject({
|
|
25
|
+
sessionOptions: schemas.anyObject.default(() => ({})),
|
|
26
|
+
});
|
|
9
27
|
/**
|
|
10
28
|
* Handles the rotation, creation and persistence of user-like sessions.
|
|
11
29
|
* Creates a pool of {@link Session} instances, that are randomly rotated.
|
|
@@ -60,53 +78,41 @@ const SESSION_REUSE_STRATEGIES = ['random', 'round-robin', 'use-until-failure'];
|
|
|
60
78
|
* @category Scaling
|
|
61
79
|
*/
|
|
62
80
|
export class SessionPool {
|
|
63
|
-
static nextId = 0;
|
|
81
|
+
static #nextId = 0;
|
|
64
82
|
id;
|
|
65
|
-
log;
|
|
83
|
+
#log;
|
|
84
|
+
#sessions = [];
|
|
85
|
+
// kept as TS-private: session_pool tests read/override the members below directly
|
|
66
86
|
maxPoolSize;
|
|
67
87
|
createSessionFunction;
|
|
68
88
|
keyValueStore;
|
|
69
|
-
sessions = [];
|
|
70
89
|
sessionMap = new Map();
|
|
71
90
|
sessionOptions;
|
|
72
91
|
persistStateKeyValueStoreId;
|
|
73
92
|
persistStateKey;
|
|
74
|
-
listener;
|
|
75
|
-
events;
|
|
76
|
-
persistenceOptions;
|
|
77
|
-
sessionReuseStrategy;
|
|
78
|
-
initPromise;
|
|
79
|
-
queue = new AsyncQueue();
|
|
80
|
-
roundRobinIndex = 0;
|
|
93
|
+
#listener;
|
|
94
|
+
#events;
|
|
95
|
+
#persistenceOptions;
|
|
96
|
+
#sessionReuseStrategy;
|
|
97
|
+
#initPromise;
|
|
98
|
+
#queue = new AsyncQueue();
|
|
99
|
+
#roundRobinIndex = 0;
|
|
81
100
|
constructor(options = {}) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
sessionOptions: ow.optional.object,
|
|
89
|
-
log: ow.optional.object,
|
|
90
|
-
persistenceOptions: ow.optional.object,
|
|
91
|
-
sessionReuseStrategy: ow.optional.string.oneOf([...SESSION_REUSE_STRATEGIES]),
|
|
92
|
-
}));
|
|
93
|
-
const { id, maxPoolSize = MAX_POOL_SIZE, persistStateKeyValueStoreId, persistStateKey, createSessionFunction, sessionOptions = {}, log = serviceLocator.getLogger(), persistenceOptions = {
|
|
94
|
-
enable: true,
|
|
95
|
-
}, sessionReuseStrategy = 'random', } = options;
|
|
96
|
-
this.id = id != null ? String(id) : String(SessionPool.nextId++);
|
|
97
|
-
this.sessionReuseStrategy = sessionReuseStrategy;
|
|
98
|
-
this.events = serviceLocator.getEventManager();
|
|
99
|
-
this.log = log.child({ prefix: 'SessionPool' });
|
|
100
|
-
this.persistenceOptions = persistenceOptions;
|
|
101
|
+
const { id, maxPoolSize, persistStateKeyValueStoreId, persistStateKey, createSessionFunction, sessionOptions, log, persistenceOptions, sessionReuseStrategy, } = parseArgument(options, sessionPoolOptionsSchema);
|
|
102
|
+
this.id = id != null ? String(id) : String(SessionPool.#nextId++);
|
|
103
|
+
this.#sessionReuseStrategy = sessionReuseStrategy;
|
|
104
|
+
this.#events = serviceLocator.getEventManager();
|
|
105
|
+
this.#log = log.child({ prefix: 'SessionPool' });
|
|
106
|
+
this.#persistenceOptions = persistenceOptions;
|
|
101
107
|
// Pool Configuration
|
|
102
108
|
this.maxPoolSize = maxPoolSize;
|
|
103
109
|
this.createSessionFunction = createSessionFunction || this.defaultCreateSessionFunction;
|
|
104
110
|
// Session configuration. The pool-scoped logger is merged into per-call sessionOptions inside
|
|
105
|
-
// `
|
|
111
|
+
// `invokeCreateSessionFunction`, so every Session inherits it without custom createSessionFunctions
|
|
106
112
|
// having to know about it.
|
|
107
113
|
this.sessionOptions = {
|
|
108
114
|
...sessionOptions,
|
|
109
|
-
log: this
|
|
115
|
+
log: this.#log,
|
|
110
116
|
};
|
|
111
117
|
// Session keyValueStore
|
|
112
118
|
this.persistStateKeyValueStoreId = persistStateKeyValueStoreId;
|
|
@@ -117,39 +123,39 @@ export class SessionPool {
|
|
|
117
123
|
*/
|
|
118
124
|
async usableSessionsCount() {
|
|
119
125
|
await this.ensureInitialized();
|
|
120
|
-
return this
|
|
126
|
+
return this.#sessions.filter((session) => session.isUsable()).length;
|
|
121
127
|
}
|
|
122
128
|
/**
|
|
123
129
|
* Gets count of retired sessions in the pool.
|
|
124
130
|
*/
|
|
125
131
|
async retiredSessionsCount() {
|
|
126
132
|
await this.ensureInitialized();
|
|
127
|
-
return this
|
|
133
|
+
return this.#sessions.filter((session) => !session.isUsable()).length;
|
|
128
134
|
}
|
|
129
135
|
/**
|
|
130
136
|
* Starts periodic state persistence and potentially loads SessionPool state from {@link KeyValueStore}.
|
|
131
137
|
* Called automatically on first use of any public method.
|
|
132
138
|
*/
|
|
133
139
|
async ensureInitialized() {
|
|
134
|
-
if (!this
|
|
135
|
-
this
|
|
140
|
+
if (!this.#initPromise) {
|
|
141
|
+
this.#initPromise = this.setupPool();
|
|
136
142
|
}
|
|
137
|
-
return this
|
|
143
|
+
return this.#initPromise;
|
|
138
144
|
}
|
|
139
145
|
async setupPool() {
|
|
140
|
-
if (!this
|
|
146
|
+
if (!this.#persistenceOptions.enable) {
|
|
141
147
|
return;
|
|
142
148
|
}
|
|
143
149
|
this.keyValueStore = await KeyValueStore.open(this.persistStateKeyValueStoreId ? { id: this.persistStateKeyValueStoreId } : null, {
|
|
144
150
|
configuration: serviceLocator.getConfiguration(),
|
|
145
151
|
});
|
|
146
152
|
if (!this.persistStateKeyValueStoreId) {
|
|
147
|
-
this
|
|
153
|
+
this.#log.debug(`No 'persistStateKeyValueStoreId' options specified, this session pool's data has been saved in the KeyValueStore with the id: ${this.keyValueStore.id}`);
|
|
148
154
|
}
|
|
149
155
|
// in case of migration happened and SessionPool state should be restored from the keyValueStore.
|
|
150
156
|
await this.maybeLoadSessionPool();
|
|
151
|
-
this
|
|
152
|
-
this
|
|
157
|
+
this.#listener = this.persistState.bind(this);
|
|
158
|
+
this.#events.on(EventType.PERSIST_STATE, this.#listener);
|
|
153
159
|
}
|
|
154
160
|
/**
|
|
155
161
|
* Adds a new session to the session pool. The pool automatically creates sessions up to the maximum size of the pool,
|
|
@@ -169,8 +175,8 @@ export class SessionPool {
|
|
|
169
175
|
if (!this.hasSpaceForSession()) {
|
|
170
176
|
this.removeRetiredSessions();
|
|
171
177
|
}
|
|
172
|
-
const newSession = options instanceof Session ? options : await this.
|
|
173
|
-
this
|
|
178
|
+
const newSession = options instanceof Session ? options : await this.invokeCreateSessionFunction(options);
|
|
179
|
+
this.#log.debug(`Adding new Session - ${newSession.id}`);
|
|
174
180
|
this.registerSession(newSession);
|
|
175
181
|
}
|
|
176
182
|
/**
|
|
@@ -181,7 +187,7 @@ export class SessionPool {
|
|
|
181
187
|
*/
|
|
182
188
|
async newSession(sessionOptions) {
|
|
183
189
|
await this.ensureInitialized();
|
|
184
|
-
const newSession = await this.
|
|
190
|
+
const newSession = await this.invokeCreateSessionFunction(sessionOptions);
|
|
185
191
|
this.registerSession(newSession);
|
|
186
192
|
return newSession;
|
|
187
193
|
}
|
|
@@ -194,7 +200,7 @@ export class SessionPool {
|
|
|
194
200
|
*/
|
|
195
201
|
async getSession(sessionId) {
|
|
196
202
|
await this.ensureInitialized();
|
|
197
|
-
await this
|
|
203
|
+
await this.#queue.wait();
|
|
198
204
|
try {
|
|
199
205
|
if (sessionId) {
|
|
200
206
|
const session = this.sessionMap.get(sessionId);
|
|
@@ -212,14 +218,14 @@ export class SessionPool {
|
|
|
212
218
|
return await this.createSession();
|
|
213
219
|
}
|
|
214
220
|
finally {
|
|
215
|
-
this
|
|
221
|
+
this.#queue.shift();
|
|
216
222
|
}
|
|
217
223
|
}
|
|
218
224
|
/**
|
|
219
225
|
* @param options - Override the persistence options provided in the constructor
|
|
220
226
|
*/
|
|
221
227
|
async resetStore(options) {
|
|
222
|
-
if (!this
|
|
228
|
+
if (!this.#persistenceOptions.enable && !options?.enable) {
|
|
223
229
|
return;
|
|
224
230
|
}
|
|
225
231
|
await this.ensureInitialized();
|
|
@@ -234,7 +240,7 @@ export class SessionPool {
|
|
|
234
240
|
return {
|
|
235
241
|
usableSessionsCount: await this.usableSessionsCount(),
|
|
236
242
|
retiredSessionsCount: await this.retiredSessionsCount(),
|
|
237
|
-
sessions: this
|
|
243
|
+
sessions: this.#sessions.map((session) => session.getState()),
|
|
238
244
|
};
|
|
239
245
|
}
|
|
240
246
|
/**
|
|
@@ -243,40 +249,43 @@ export class SessionPool {
|
|
|
243
249
|
* @param options - Override the persistence options provided in the constructor
|
|
244
250
|
*/
|
|
245
251
|
async persistState(options) {
|
|
246
|
-
if (!this
|
|
252
|
+
if (!this.#persistenceOptions.enable && !options?.enable) {
|
|
247
253
|
return;
|
|
248
254
|
}
|
|
249
255
|
await this.ensureInitialized();
|
|
250
|
-
this
|
|
256
|
+
this.#log.debug('Persisting state', {
|
|
251
257
|
persistStateKeyValueStoreId: this.persistStateKeyValueStoreId,
|
|
252
258
|
persistStateKey: this.persistStateKey,
|
|
253
259
|
});
|
|
254
260
|
await this.keyValueStore
|
|
255
261
|
?.setValue(this.persistStateKey, await this.getState())
|
|
256
|
-
.catch((error) => this
|
|
262
|
+
.catch((error) => this.#log.warning(`Failed to persist the session pool stats to ${this.persistStateKey}`, { error }));
|
|
257
263
|
}
|
|
258
264
|
/**
|
|
259
265
|
* Removes listener from `persistState` event.
|
|
260
266
|
* This function should be called after you are done with using the `SessionPool` instance.
|
|
267
|
+
* @param options - Set `persistState` to false when the final state was already persisted by the event manager.
|
|
261
268
|
*/
|
|
262
|
-
async teardown() {
|
|
263
|
-
if (!this
|
|
269
|
+
async teardown({ persistState = true } = {}) {
|
|
270
|
+
if (!this.#initPromise)
|
|
264
271
|
return;
|
|
265
272
|
await this.ensureInitialized();
|
|
266
|
-
if (this
|
|
267
|
-
this
|
|
273
|
+
if (this.#listener) {
|
|
274
|
+
this.#events.off(EventType.PERSIST_STATE, this.#listener);
|
|
275
|
+
}
|
|
276
|
+
if (persistState) {
|
|
277
|
+
await this.persistState();
|
|
268
278
|
}
|
|
269
|
-
await this.persistState();
|
|
270
279
|
}
|
|
271
280
|
/**
|
|
272
281
|
* Removes retired `Session` instances from `SessionPool`.
|
|
273
282
|
*/
|
|
274
283
|
removeRetiredSessions() {
|
|
275
|
-
this
|
|
284
|
+
this.#sessions = this.#sessions.filter((storedSession) => {
|
|
276
285
|
if (storedSession.isUsable())
|
|
277
286
|
return true;
|
|
278
287
|
this.sessionMap.delete(storedSession.id);
|
|
279
|
-
this
|
|
288
|
+
this.#log.debug(`Removed Session - ${storedSession.id}`);
|
|
280
289
|
return false;
|
|
281
290
|
});
|
|
282
291
|
}
|
|
@@ -285,14 +294,14 @@ export class SessionPool {
|
|
|
285
294
|
* @param newSession `Session` instance to be added.
|
|
286
295
|
*/
|
|
287
296
|
registerSession(newSession) {
|
|
288
|
-
this
|
|
297
|
+
this.#sessions.push(newSession);
|
|
289
298
|
this.sessionMap.set(newSession.id, newSession);
|
|
290
299
|
}
|
|
291
300
|
/**
|
|
292
301
|
* Gets random index.
|
|
293
302
|
*/
|
|
294
303
|
getRandomIndex() {
|
|
295
|
-
return Math.floor(Math.random() * this
|
|
304
|
+
return Math.floor(Math.random() * this.#sessions.length);
|
|
296
305
|
}
|
|
297
306
|
/**
|
|
298
307
|
* Creates new session without any extra behavior.
|
|
@@ -301,8 +310,7 @@ export class SessionPool {
|
|
|
301
310
|
* @returns New session.
|
|
302
311
|
*/
|
|
303
312
|
async defaultCreateSessionFunction(options = {}) {
|
|
304
|
-
|
|
305
|
-
const { sessionOptions = {} } = options;
|
|
313
|
+
const { sessionOptions } = parseArgument(options, createSessionOptionsSchema);
|
|
306
314
|
return new Session(sessionOptions);
|
|
307
315
|
}
|
|
308
316
|
/**
|
|
@@ -315,7 +323,7 @@ export class SessionPool {
|
|
|
315
323
|
* through `maybeLoadSessionPool` naturally wins because it arrives in
|
|
316
324
|
* `perCallOptions`.
|
|
317
325
|
*/
|
|
318
|
-
async
|
|
326
|
+
async invokeCreateSessionFunction(perCallOptions) {
|
|
319
327
|
const sessionOptions = {
|
|
320
328
|
fingerprint: createDefaultSessionFingerprint(),
|
|
321
329
|
...this.sessionOptions,
|
|
@@ -328,35 +336,35 @@ export class SessionPool {
|
|
|
328
336
|
* @returns Newly created `Session` instance.
|
|
329
337
|
*/
|
|
330
338
|
async createSession() {
|
|
331
|
-
const newSession = await this.
|
|
339
|
+
const newSession = await this.invokeCreateSessionFunction();
|
|
332
340
|
this.registerSession(newSession);
|
|
333
|
-
this
|
|
341
|
+
this.#log.debug(`Created new Session - ${newSession.id}`);
|
|
334
342
|
return newSession;
|
|
335
343
|
}
|
|
336
344
|
/**
|
|
337
345
|
* Decides whether there is enough space for creating new session.
|
|
338
346
|
*/
|
|
339
347
|
hasSpaceForSession() {
|
|
340
|
-
return this
|
|
348
|
+
return this.#sessions.length < this.maxPoolSize;
|
|
341
349
|
}
|
|
342
350
|
/**
|
|
343
351
|
* Picks a session from the `SessionPool` according to the configured `sessionReuseStrategy`.
|
|
344
352
|
* Returns `undefined` when no session should be reused and a new one should be created instead.
|
|
345
353
|
*/
|
|
346
354
|
pickSession() {
|
|
347
|
-
if (this
|
|
355
|
+
if (this.#sessionReuseStrategy !== 'use-until-failure' && this.hasSpaceForSession())
|
|
348
356
|
return undefined;
|
|
349
|
-
if (this
|
|
350
|
-
return this
|
|
357
|
+
if (this.#sessionReuseStrategy === 'use-until-failure') {
|
|
358
|
+
return this.#sessions.find((session) => session.isUsable());
|
|
351
359
|
}
|
|
352
360
|
let picked;
|
|
353
|
-
if (this
|
|
354
|
-
const index = this
|
|
355
|
-
this
|
|
356
|
-
picked = this
|
|
361
|
+
if (this.#sessionReuseStrategy === 'round-robin') {
|
|
362
|
+
const index = this.#roundRobinIndex % this.#sessions.length;
|
|
363
|
+
this.#roundRobinIndex = index + 1;
|
|
364
|
+
picked = this.#sessions[index];
|
|
357
365
|
}
|
|
358
366
|
else {
|
|
359
|
-
picked = this
|
|
367
|
+
picked = this.#sessions[this.getRandomIndex()];
|
|
360
368
|
}
|
|
361
369
|
return picked.isUsable() ? picked : undefined;
|
|
362
370
|
}
|
|
@@ -369,18 +377,18 @@ export class SessionPool {
|
|
|
369
377
|
if (!loadedSessionPool)
|
|
370
378
|
return;
|
|
371
379
|
// Invalidate old sessions and load active sessions only
|
|
372
|
-
this
|
|
380
|
+
this.#log.debug('Recreating state from KeyValueStore', {
|
|
373
381
|
persistStateKeyValueStoreId: this.persistStateKeyValueStoreId,
|
|
374
382
|
persistStateKey: this.persistStateKey,
|
|
375
383
|
});
|
|
376
384
|
for (const sessionObject of loadedSessionPool.sessions) {
|
|
377
385
|
sessionObject.createdAt = new Date(sessionObject.createdAt);
|
|
378
386
|
sessionObject.expiresAt = new Date(sessionObject.expiresAt);
|
|
379
|
-
const recreatedSession = await this.
|
|
387
|
+
const recreatedSession = await this.invokeCreateSessionFunction(sessionObject);
|
|
380
388
|
if (recreatedSession.isUsable()) {
|
|
381
389
|
this.registerSession(recreatedSession);
|
|
382
390
|
}
|
|
383
391
|
}
|
|
384
|
-
this
|
|
392
|
+
this.#log.debug(`${this.#sessions.length} active sessions loaded from KeyValueStore`);
|
|
385
393
|
}
|
|
386
394
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ProcessedRequest } from '@crawlee/types';
|
|
2
|
+
import type { Source } from '../request.js';
|
|
3
|
+
import type { AddRequestsBatchedResult } from './request_queue.js';
|
|
4
|
+
export interface DrainRequestBatchesOptions<TItem extends Source> {
|
|
5
|
+
/**
|
|
6
|
+
* The requests to add, already normalized by the caller. Consumed lazily: an unbounded or expensive
|
|
7
|
+
* iterable is only pulled from as far as the batching (and any `maxNewRequests` budget) requires.
|
|
8
|
+
*/
|
|
9
|
+
items: AsyncGenerator<TItem>;
|
|
10
|
+
batchSize: number;
|
|
11
|
+
waitBetweenBatchesMillis: number;
|
|
12
|
+
waitForAllRequestsToBeAdded: boolean;
|
|
13
|
+
maxNewRequests?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Adds a single chunk and reports what it processed.
|
|
16
|
+
*
|
|
17
|
+
* @param isInitial Whether this is the first chunk, which is added before this function returns. Later
|
|
18
|
+
* chunks land in the background, which is why some callers cache only the first.
|
|
19
|
+
*/
|
|
20
|
+
processChunk: (chunk: TItem[], isInitial: boolean) => Promise<ProcessedRequest[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Called with the promise covering every chunk after the first, so the caller can keep its own
|
|
23
|
+
* `isFinished` honest while batches are still landing.
|
|
24
|
+
*/
|
|
25
|
+
trackBackgroundBatches?: (batches: Promise<unknown>) => void;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Drives the chunk-by-chunk half of `addRequestsBatched`: the first chunk is added before returning and the
|
|
29
|
+
* rest continue in the background, paced by `waitBetweenBatchesMillis`.
|
|
30
|
+
*
|
|
31
|
+
* Callers differ only in how a chunk is added and how the input is normalized, so that is all
|
|
32
|
+
* {@link DrainRequestBatchesOptions} asks for - the budget arithmetic, the lazy chunking, the
|
|
33
|
+
* over-limit reporting and the transaction handling are identical for everyone and live here. In
|
|
34
|
+
* particular, every caller has to keep its background chunks out of a transaction they will outlive,
|
|
35
|
+
* so that is read from the ambient transaction rather than asked of the caller.
|
|
36
|
+
*/
|
|
37
|
+
export declare function drainRequestBatches<TItem extends Source>(options: DrainRequestBatchesOptions<TItem>): Promise<AddRequestsBatchedResult>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
2
|
+
import { chunkedAsyncIterable, peekableAsyncIterable } from '../iterables.js';
|
|
3
|
+
import { activeStorageTransaction, withDirectStorageAccess } from './transaction.js';
|
|
4
|
+
/**
|
|
5
|
+
* Drives the chunk-by-chunk half of `addRequestsBatched`: the first chunk is added before returning and the
|
|
6
|
+
* rest continue in the background, paced by `waitBetweenBatchesMillis`.
|
|
7
|
+
*
|
|
8
|
+
* Callers differ only in how a chunk is added and how the input is normalized, so that is all
|
|
9
|
+
* {@link DrainRequestBatchesOptions} asks for - the budget arithmetic, the lazy chunking, the
|
|
10
|
+
* over-limit reporting and the transaction handling are identical for everyone and live here. In
|
|
11
|
+
* particular, every caller has to keep its background chunks out of a transaction they will outlive,
|
|
12
|
+
* so that is read from the ambient transaction rather than asked of the caller.
|
|
13
|
+
*/
|
|
14
|
+
export async function drainRequestBatches(options) {
|
|
15
|
+
const { items, batchSize, waitBetweenBatchesMillis, waitForAllRequestsToBeAdded, maxNewRequests, processChunk, trackBackgroundBatches, } = options;
|
|
16
|
+
const deferred = activeStorageTransaction()?.policy.requestQueue === 'deferred';
|
|
17
|
+
let remainingBudget = maxNewRequests ?? Infinity;
|
|
18
|
+
const requestsOverLimit = [];
|
|
19
|
+
// Never hand a chunk more than the budget allows, so an over-large final batch cannot overshoot.
|
|
20
|
+
const effectiveChunkSize = maxNewRequests !== undefined ? () => Math.min(batchSize, remainingBudget) : batchSize;
|
|
21
|
+
const chunks = peekableAsyncIterable(chunkedAsyncIterable(items, effectiveChunkSize));
|
|
22
|
+
const chunksIterator = chunks[Symbol.asyncIterator]();
|
|
23
|
+
const addChunk = async (chunk, isInitial) => {
|
|
24
|
+
const processedRequests = await processChunk(chunk, isInitial);
|
|
25
|
+
if (maxNewRequests !== undefined) {
|
|
26
|
+
remainingBudget -= processedRequests.filter((request) => !request.wasAlreadyPresent).length;
|
|
27
|
+
}
|
|
28
|
+
return processedRequests;
|
|
29
|
+
};
|
|
30
|
+
const buildResult = async (addedRequests, waitForAll) => {
|
|
31
|
+
if (maxNewRequests !== undefined) {
|
|
32
|
+
// `chunkedAsyncIterable` stops pulling once the budget-derived chunk size hits zero, so whatever
|
|
33
|
+
// is left is still sitting in `items` rather than in a chunk we have seen.
|
|
34
|
+
for await (const item of items) {
|
|
35
|
+
requestsOverLimit.push(item);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { addedRequests, waitForAllRequestsToBeAdded: waitForAll, requestsOverLimit };
|
|
39
|
+
};
|
|
40
|
+
const initialChunk = await chunksIterator.peek();
|
|
41
|
+
if (initialChunk === undefined) {
|
|
42
|
+
return buildResult([], Promise.resolve([]));
|
|
43
|
+
}
|
|
44
|
+
const addedRequests = await addChunk(initialChunk, true);
|
|
45
|
+
await chunksIterator.next();
|
|
46
|
+
if ((await chunksIterator.peek()) === undefined) {
|
|
47
|
+
return buildResult(addedRequests, Promise.resolve([]));
|
|
48
|
+
}
|
|
49
|
+
const processRemainingChunks = async () => {
|
|
50
|
+
const added = [];
|
|
51
|
+
for await (const chunk of chunks) {
|
|
52
|
+
added.push(...(await addChunk(chunk, false)));
|
|
53
|
+
// Under `deferred` no chunk performs backend I/O, so pacing them would only stall the handler.
|
|
54
|
+
await sleep(deferred ? 0 : waitBetweenBatchesMillis);
|
|
55
|
+
}
|
|
56
|
+
return added;
|
|
57
|
+
};
|
|
58
|
+
// With a budget we must drain everything before we can report what went over it; under `deferred` a
|
|
59
|
+
// writer that finishes after commit would have nowhere to put its journal entries.
|
|
60
|
+
const awaitsRemainder = waitForAllRequestsToBeAdded || maxNewRequests !== undefined || deferred;
|
|
61
|
+
// An un-awaited writer outlives the transaction scope it inherits, so it must not record into a
|
|
62
|
+
// transaction that may already be closed. It writes directly - its write-through additions were never
|
|
63
|
+
// going to be rolled back anyway - which means the requests it adds are not journaled.
|
|
64
|
+
// See `StorageTransactionView.enqueuedUrls`.
|
|
65
|
+
const remainder = awaitsRemainder ? processRemainingChunks() : withDirectStorageAccess(processRemainingChunks);
|
|
66
|
+
// The caller is not obliged to await `remainder`, so give it a handler of its own - an unhandled
|
|
67
|
+
// rejection here would otherwise take the process down.
|
|
68
|
+
trackBackgroundBatches?.(remainder.catch(() => { }));
|
|
69
|
+
if (awaitsRemainder) {
|
|
70
|
+
addedRequests.push(...(await remainder));
|
|
71
|
+
}
|
|
72
|
+
return buildResult(addedRequests, remainder);
|
|
73
|
+
}
|
package/storages/dataset.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Awaitable, DatasetBackend, DatasetInfo, Dictionary } from '@crawlee/types';
|
|
2
2
|
import { Configuration } from '../configuration.js';
|
|
3
3
|
import type { CrawleeLogger } from '../log.js';
|
|
4
|
+
import type { JournalEntry } from './transaction.js';
|
|
4
5
|
import type { DatasetStats } from './storage_stats.js';
|
|
5
6
|
import type { StorageOpenOptions } from './utils.js';
|
|
6
7
|
import type { StorageIdentifier } from './storage_instance_manager.js';
|
|
@@ -139,12 +140,12 @@ export interface DatasetExportToOptions extends DatasetExportOptions {
|
|
|
139
140
|
* @category Result Stores
|
|
140
141
|
*/
|
|
141
142
|
export declare class Dataset<Data extends Dictionary = Dictionary> {
|
|
143
|
+
#private;
|
|
142
144
|
readonly configuration: Configuration;
|
|
143
145
|
id: string;
|
|
144
146
|
name?: string;
|
|
145
147
|
backend: DatasetBackend<Data>;
|
|
146
148
|
log: CrawleeLogger;
|
|
147
|
-
private readonly statsTracker;
|
|
148
149
|
/**
|
|
149
150
|
* @internal
|
|
150
151
|
*/
|
|
@@ -170,6 +171,16 @@ export declare class Dataset<Data extends Dictionary = Dictionary> {
|
|
|
170
171
|
* Returns {@link DatasetContent} object holding the items in the dataset based on the provided parameters.
|
|
171
172
|
*/
|
|
172
173
|
getData(options?: DatasetDataOptions): Promise<DatasetContent<Data>>;
|
|
174
|
+
/**
|
|
175
|
+
* The single transaction-aware page read all dataset read paths go through — both `getData()` and
|
|
176
|
+
* the private `fetchPages()`. Returns the real page concatenated with the current transaction's
|
|
177
|
+
* buffered items, with `offset` / `limit` / `desc` windowing applied across the concatenation.
|
|
178
|
+
*/
|
|
179
|
+
private readPage;
|
|
180
|
+
/** The active transaction's buffered writes to this dataset, derived from its journal. */
|
|
181
|
+
private bufferedJournalEntries;
|
|
182
|
+
/** @internal */
|
|
183
|
+
commitJournalEntries(entries: JournalEntry[]): Promise<void>;
|
|
173
184
|
/**
|
|
174
185
|
* Returns all the data from the dataset. This will iterate through the whole dataset
|
|
175
186
|
* via the `listItems()` client method, which gives you only paginated results.
|