@crawlee/core 4.0.0-beta.104 → 4.0.0-beta.106
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 +85 -85
- package/autoscaling/client_load_signal.d.ts +1 -5
- package/autoscaling/client_load_signal.js +20 -20
- package/autoscaling/concurrency_system.d.ts +5 -20
- package/autoscaling/concurrency_system.js +81 -80
- package/autoscaling/cpu_load_signal.d.ts +1 -2
- package/autoscaling/cpu_load_signal.js +10 -10
- package/autoscaling/event_loop_load_signal.d.ts +1 -4
- package/autoscaling/event_loop_load_signal.js +18 -18
- package/autoscaling/load_signal.d.ts +1 -1
- package/autoscaling/load_signal.js +12 -11
- package/autoscaling/memory_load_signal.d.ts +3 -12
- package/autoscaling/memory_load_signal.js +40 -41
- package/autoscaling/snapshotter.d.ts +1 -4
- package/autoscaling/snapshotter.js +12 -12
- package/autoscaling/system_status.d.ts +1 -3
- package/autoscaling/system_status.js +11 -11
- package/configuration.d.ts +1 -1
- package/configuration.js +3 -3
- package/crawlers/context_pipeline.js +6 -6
- package/crawlers/crawler_commons.d.ts +6 -56
- package/crawlers/crawler_commons.js +1 -107
- package/crawlers/index.d.ts +1 -1
- package/crawlers/index.js +0 -1
- package/crawlers/statistics.d.ts +1 -8
- package/crawlers/statistics.js +45 -44
- package/events/event_manager.d.ts +1 -1
- package/events/event_manager.js +3 -3
- package/events/local_event_manager.d.ts +1 -1
- package/events/local_event_manager.js +3 -3
- package/log.js +5 -1
- package/memory-storage/memory-storage.d.ts +1 -5
- package/memory-storage/memory-storage.js +2 -2
- package/memory-storage/resource-clients/dataset.d.ts +1 -1
- package/memory-storage/resource-clients/dataset.js +6 -5
- package/memory-storage/resource-clients/key-value-store.d.ts +1 -1
- package/memory-storage/resource-clients/key-value-store.js +13 -12
- package/memory-storage/resource-clients/request-queue.d.ts +4 -23
- package/memory-storage/resource-clients/request-queue.js +59 -58
- package/owned_or_injected.d.ts +1 -3
- package/owned_or_injected.js +17 -17
- package/package.json +5 -5
- package/proxy_configuration.d.ts +1 -3
- package/proxy_configuration.js +8 -8
- package/recoverable_state.d.ts +1 -10
- package/recoverable_state.js +41 -41
- package/request.d.ts +1 -2
- package/request.js +10 -13
- package/router.d.ts +1 -4
- package/router.js +23 -23
- package/serialization.js +8 -9
- package/service_locator.d.ts +1 -10
- package/service_locator.js +48 -48
- package/session_pool/session.d.ts +1 -12
- package/session_pool/session.js +50 -50
- package/session_pool/session_pool.d.ts +2 -11
- package/session_pool/session_pool.js +59 -58
- package/storages/dataset.d.ts +12 -1
- package/storages/dataset.js +121 -22
- package/storages/index.d.ts +1 -1
- package/storages/index.js +1 -1
- package/storages/key_value_store.d.ts +19 -4
- package/storages/key_value_store.js +174 -48
- package/storages/request_dedup_cache.d.ts +1 -2
- package/storages/request_dedup_cache.js +9 -9
- package/storages/request_list.d.ts +2 -22
- package/storages/request_list.js +74 -73
- 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 +256 -53
- package/storages/sitemap_request_loader.d.ts +1 -44
- package/storages/sitemap_request_loader.js +87 -87
- 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/transaction.d.ts +252 -0
- package/storages/transaction.js +251 -0
- package/system-info/runtime.js +7 -7
- package/storages/access_checking.d.ts +0 -12
- package/storages/access_checking.js +0 -17
|
@@ -2,12 +2,13 @@ import { inspect } from 'node:util';
|
|
|
2
2
|
import { downloadListOfUrls, isAsyncIterable, isIterable, sleep } from '@crawlee/utils';
|
|
3
3
|
import ow from 'ow';
|
|
4
4
|
import { LruCache } from '@apify/datastructures';
|
|
5
|
+
import { tryCancel } from '@apify/timeout';
|
|
5
6
|
import { Configuration } from '../configuration.js';
|
|
6
7
|
import { getObjectType } from '../debug.js';
|
|
7
8
|
import { chunkedAsyncIterable, peekableAsyncIterable } from '../iterables.js';
|
|
8
9
|
import { Request } from '../request.js';
|
|
9
10
|
import { serviceLocator } from '../service_locator.js';
|
|
10
|
-
import {
|
|
11
|
+
import { activeStorageTransaction, rejectOperationInTransaction, withDirectStorageAccess } from './transaction.js';
|
|
11
12
|
import { StorageStatsTracker } from './storage_stats.js';
|
|
12
13
|
import { resolveStorageIdentifier } from './storage_instance_manager.js';
|
|
13
14
|
import { getRequestId, purgeDefaultStorages } from './utils.js';
|
|
@@ -55,26 +56,28 @@ export class RequestQueue {
|
|
|
55
56
|
id;
|
|
56
57
|
name;
|
|
57
58
|
backend;
|
|
58
|
-
proxyConfiguration;
|
|
59
|
+
#proxyConfiguration;
|
|
59
60
|
log;
|
|
61
|
+
// kept as TS-private: request_queue tests read this cache directly
|
|
60
62
|
requestCache;
|
|
61
63
|
/**
|
|
62
64
|
* Remembers the `requestId` of every request already submitted to the client — including background
|
|
63
65
|
* batches that `requestCache` skips — so overlapping URL sets aren't re-submitted.
|
|
64
66
|
* See {@link RequestDeduplicationCache} for why this is a separate, cheaper cache.
|
|
65
67
|
*/
|
|
66
|
-
requestSeenCache;
|
|
67
|
-
queuePausedForMigration = false;
|
|
68
|
+
#requestSeenCache;
|
|
69
|
+
#queuePausedForMigration = false;
|
|
70
|
+
// kept as TS-private: packages/core/test request-queue tests write this counter directly
|
|
68
71
|
inProgressRequestBatchCount = 0;
|
|
69
72
|
/**
|
|
70
73
|
* The largest expected request-processing time (in seconds) seen so far via
|
|
71
74
|
* {@link setExpectedRequestProcessingTimeSecs}. Used to ensure that value is only ever raised, never
|
|
72
75
|
* lowered, before being forwarded to the storage backend.
|
|
73
76
|
*/
|
|
74
|
-
expectedRequestProcessingSecs = 0;
|
|
75
|
-
httpClient;
|
|
76
|
-
events;
|
|
77
|
-
statsTracker = new StorageStatsTracker({
|
|
77
|
+
#expectedRequestProcessingSecs = 0;
|
|
78
|
+
#httpClient;
|
|
79
|
+
#events;
|
|
80
|
+
#statsTracker = new StorageStatsTracker({
|
|
78
81
|
writeCount: 0,
|
|
79
82
|
headItemReadCount: 0,
|
|
80
83
|
});
|
|
@@ -83,7 +86,7 @@ export class RequestQueue {
|
|
|
83
86
|
* queue-head reads issued to the underlying storage backend). Counted per backend call.
|
|
84
87
|
*/
|
|
85
88
|
get stats() {
|
|
86
|
-
return this
|
|
89
|
+
return this.#statsTracker.current;
|
|
87
90
|
}
|
|
88
91
|
/**
|
|
89
92
|
* @internal
|
|
@@ -91,14 +94,14 @@ export class RequestQueue {
|
|
|
91
94
|
constructor(options) {
|
|
92
95
|
this.id = options.metadata.id;
|
|
93
96
|
this.name = options.metadata.name;
|
|
94
|
-
this
|
|
97
|
+
this.#events = serviceLocator.getEventManager();
|
|
95
98
|
this.backend = options.backend;
|
|
96
|
-
this
|
|
99
|
+
this.#proxyConfiguration = options.proxyConfiguration;
|
|
97
100
|
this.requestCache = new LruCache({ maxLength: MAX_CACHED_REQUESTS });
|
|
98
|
-
this
|
|
101
|
+
this.#requestSeenCache = new RequestDeduplicationCache();
|
|
99
102
|
this.log = serviceLocator.getLogger().child({ prefix: `RequestQueue(${this.id}, ${this.name ?? 'no-name'})` });
|
|
100
|
-
this
|
|
101
|
-
this
|
|
103
|
+
this.#events.on("migrating" /* EventType.MIGRATING */, async () => {
|
|
104
|
+
this.#queuePausedForMigration = true;
|
|
102
105
|
});
|
|
103
106
|
}
|
|
104
107
|
/**
|
|
@@ -134,7 +137,7 @@ export class RequestQueue {
|
|
|
134
137
|
* @param [options] Request queue operation options.
|
|
135
138
|
*/
|
|
136
139
|
async addRequest(requestLike, options = {}) {
|
|
137
|
-
|
|
140
|
+
const transaction = activeStorageTransaction();
|
|
138
141
|
ow(requestLike, ow.object);
|
|
139
142
|
ow(options, ow.object.exactShape({
|
|
140
143
|
forefront: ow.optional.boolean,
|
|
@@ -150,10 +153,14 @@ export class RequestQueue {
|
|
|
150
153
|
id: ow.undefined,
|
|
151
154
|
}));
|
|
152
155
|
const request = requestLike instanceof Request ? requestLike : new Request(requestLike);
|
|
156
|
+
if (transaction?.policy.requestQueue === 'deferred') {
|
|
157
|
+
return this.addRequestDeferred(transaction, request, forefront);
|
|
158
|
+
}
|
|
153
159
|
const cacheKey = getRequestId(request.uniqueKey);
|
|
154
160
|
const cachedInfo = this.requestCache.get(cacheKey);
|
|
155
161
|
if (cachedInfo) {
|
|
156
162
|
request.id = cachedInfo.id;
|
|
163
|
+
this.recordRequestJournalEntry(transaction, [request], forefront, true);
|
|
157
164
|
return {
|
|
158
165
|
wasAlreadyPresent: true,
|
|
159
166
|
// We may assume that if request is in local cache then also the information if the
|
|
@@ -164,17 +171,163 @@ export class RequestQueue {
|
|
|
164
171
|
forefront,
|
|
165
172
|
};
|
|
166
173
|
}
|
|
167
|
-
this
|
|
174
|
+
this.#statsTracker.add('writeCount');
|
|
168
175
|
const { processedRequests } = await this.backend.addBatchOfRequests([request], { forefront });
|
|
176
|
+
this.recordRequestJournalEntry(transaction, [request], forefront, true);
|
|
169
177
|
const queueOperationInfo = {
|
|
170
178
|
...processedRequests[0],
|
|
171
179
|
uniqueKey: request.uniqueKey,
|
|
172
180
|
forefront,
|
|
173
181
|
};
|
|
174
182
|
this.cacheRequest(cacheKey, queueOperationInfo);
|
|
175
|
-
this
|
|
183
|
+
this.#requestSeenCache.add(cacheKey, request.id);
|
|
176
184
|
return queueOperationInfo;
|
|
177
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Journals an addition for introspection only; these entries are never replayed. A no-op unless the
|
|
188
|
+
* transaction is open, so detached and outliving writers stay out of the journal.
|
|
189
|
+
*/
|
|
190
|
+
recordRequestJournalEntry(transaction, requests, forefront, writeThrough) {
|
|
191
|
+
if (!transaction?.isActive || requests.length === 0)
|
|
192
|
+
return;
|
|
193
|
+
transaction.recordJournalEntry({
|
|
194
|
+
type: 'requestQueue',
|
|
195
|
+
participant: this,
|
|
196
|
+
requests: requests.map((request) => ({
|
|
197
|
+
url: request.url,
|
|
198
|
+
uniqueKey: request.uniqueKey,
|
|
199
|
+
label: request.label,
|
|
200
|
+
})),
|
|
201
|
+
forefront,
|
|
202
|
+
writeThrough,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* The requests buffered by the given transaction for this queue, keyed by `uniqueKey` — a dedup
|
|
207
|
+
* index derived from the transaction journal.
|
|
208
|
+
*/
|
|
209
|
+
bufferedRequests(transaction) {
|
|
210
|
+
const buffered = new Map();
|
|
211
|
+
// Only `deferred` records snapshots, so scanning the journal under `writeThrough` never finds any.
|
|
212
|
+
if (transaction.policy.requestQueue !== 'deferred')
|
|
213
|
+
return buffered;
|
|
214
|
+
for (const entry of transaction.journal) {
|
|
215
|
+
if (entry.type !== 'requestQueue' || entry.participant !== this)
|
|
216
|
+
continue;
|
|
217
|
+
for (const request of entry.requests) {
|
|
218
|
+
if (request.snapshot !== undefined)
|
|
219
|
+
buffered.set(request.uniqueKey, request.snapshot);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return buffered;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Adds a request under the `deferred` policy: journaled now, really added by the commit replay.
|
|
226
|
+
* A new request's `requestId` is the local `uniqueKey` hash and is **provisional** — never write it
|
|
227
|
+
* to `request.id` or the dedup caches. Dedup is cheapest-first: buffer, caches, then a backend probe.
|
|
228
|
+
*/
|
|
229
|
+
async addRequestDeferred(transaction, request, forefront, buffered = this.bufferedRequests(transaction)) {
|
|
230
|
+
// This transaction's own buffered adds; the shared caches never see them (provisional ids).
|
|
231
|
+
if (buffered.has(request.uniqueKey)) {
|
|
232
|
+
this.recordRequestJournalEntry(transaction, [request], forefront, false);
|
|
233
|
+
return {
|
|
234
|
+
wasAlreadyPresent: true,
|
|
235
|
+
wasAlreadyHandled: false,
|
|
236
|
+
requestId: getRequestId(request.uniqueKey),
|
|
237
|
+
uniqueKey: request.uniqueKey,
|
|
238
|
+
forefront,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
// The caches hold real backend ids. Only *writing* provisional ids to them would be wrong;
|
|
242
|
+
// reading saves a probe. Same lookup as the write-through path.
|
|
243
|
+
const cacheKey = getRequestId(request.uniqueKey);
|
|
244
|
+
const cachedInfo = this.requestCache.get(cacheKey);
|
|
245
|
+
const knownRequestId = cachedInfo?.id ?? this.#requestSeenCache.get(cacheKey);
|
|
246
|
+
if (knownRequestId) {
|
|
247
|
+
this.recordRequestJournalEntry(transaction, [request], forefront, false);
|
|
248
|
+
return {
|
|
249
|
+
wasAlreadyPresent: true,
|
|
250
|
+
// The dedup cache doesn't track the handled state; only the full record does.
|
|
251
|
+
wasAlreadyHandled: cachedInfo?.isHandled ?? false,
|
|
252
|
+
requestId: knownRequestId,
|
|
253
|
+
uniqueKey: request.uniqueKey,
|
|
254
|
+
forefront,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
// The caches are bounded, so a miss is not proof of absence - probe for an accurate answer.
|
|
258
|
+
const existing = await this.backend.getRequest(request.uniqueKey);
|
|
259
|
+
if (existing) {
|
|
260
|
+
this.recordRequestJournalEntry(transaction, [request], forefront, false);
|
|
261
|
+
return {
|
|
262
|
+
wasAlreadyPresent: true,
|
|
263
|
+
wasAlreadyHandled: existing.handledAt != null,
|
|
264
|
+
requestId: existing.id,
|
|
265
|
+
uniqueKey: request.uniqueKey,
|
|
266
|
+
forefront,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
// The entry below *is* the write, so a transaction closed during the probe must not receive it -
|
|
270
|
+
// pass through instead, per the closed-transaction rule. Under `deferred` that can land an
|
|
271
|
+
// addition a rollback would have discarded; dedup bounds that cost, silent loss is unbounded.
|
|
272
|
+
if (!transaction.isActive) {
|
|
273
|
+
return await this.addRequest(request, { forefront });
|
|
274
|
+
}
|
|
275
|
+
const snapshot = JSON.parse(JSON.stringify(request));
|
|
276
|
+
// Strip-list, not allow-list: every user-facing field flows through, including ones added to
|
|
277
|
+
// `Request` in the future. The exceptions are `id` and `handledAt`, the two backend-owned
|
|
278
|
+
// lifecycle fields.
|
|
279
|
+
delete snapshot.id;
|
|
280
|
+
delete snapshot.handledAt;
|
|
281
|
+
transaction.recordJournalEntry({
|
|
282
|
+
type: 'requestQueue',
|
|
283
|
+
participant: this,
|
|
284
|
+
requests: [{ url: request.url, uniqueKey: request.uniqueKey, label: request.label, snapshot }],
|
|
285
|
+
forefront,
|
|
286
|
+
writeThrough: false,
|
|
287
|
+
});
|
|
288
|
+
buffered.set(request.uniqueKey, snapshot);
|
|
289
|
+
return {
|
|
290
|
+
wasAlreadyPresent: false,
|
|
291
|
+
wasAlreadyHandled: false,
|
|
292
|
+
requestId: getRequestId(request.uniqueKey),
|
|
293
|
+
uniqueKey: request.uniqueKey,
|
|
294
|
+
forefront,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
/** @internal */
|
|
298
|
+
async commitJournalEntries(entries) {
|
|
299
|
+
// Replay through `backend.addBatchOfRequests`, *not* the batched frontend wrapper - the wrapper
|
|
300
|
+
// resolves after the first chunk and sleeps between the rest, neither of which commit may
|
|
301
|
+
// inherit. One call per `forefront` flag; the order of forefront additions is arbitrary anyway.
|
|
302
|
+
for (const forefront of [false, true]) {
|
|
303
|
+
const requests = entries.flatMap((entry) => entry.type === 'requestQueue' && entry.forefront === forefront
|
|
304
|
+
? // Requests without a snapshot were deduplicated or written through; nothing to replay.
|
|
305
|
+
entry.requests
|
|
306
|
+
.filter((journaled) => journaled.snapshot !== undefined)
|
|
307
|
+
.map((journaled) => new Request(journaled.snapshot))
|
|
308
|
+
: []);
|
|
309
|
+
if (requests.length === 0)
|
|
310
|
+
continue;
|
|
311
|
+
this.#statsTracker.add('writeCount');
|
|
312
|
+
const { processedRequests, unprocessedRequests } = await this.backend.addBatchOfRequests(requests, {
|
|
313
|
+
forefront,
|
|
314
|
+
});
|
|
315
|
+
// Only now, with the real backend-assigned ids, may the shared dedup caches be populated.
|
|
316
|
+
for (const processed of processedRequests) {
|
|
317
|
+
const cacheKey = getRequestId(processed.uniqueKey);
|
|
318
|
+
this.cacheRequest(cacheKey, { ...processed, forefront });
|
|
319
|
+
this.#requestSeenCache.add(cacheKey, processed.requestId);
|
|
320
|
+
}
|
|
321
|
+
if (unprocessedRequests.length > 0) {
|
|
322
|
+
// Warn and skip, rather than retry or fail. `unprocessedRequests` is what remains after
|
|
323
|
+
// the backend's own transient-error handling - a semantic rejection that retrying here
|
|
324
|
+
// would only re-poke. And failing the commit would let one malformed request hold the
|
|
325
|
+
// whole transaction hostage.
|
|
326
|
+
this.log.warning('Some requests were rejected by the request queue while committing a storage transaction and will be skipped. ' +
|
|
327
|
+
"This usually means the request data is malformed (e.g. an invalid 'userData' shape).", { unprocessedRequests });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
178
331
|
/**
|
|
179
332
|
* Adds requests to the queue in batches of 25. This method will wait till all the requests are added
|
|
180
333
|
* to the queue before resolving. You should prefer using `queue.addRequestsBatched()` or `crawler.addRequests()`
|
|
@@ -190,7 +343,7 @@ export class RequestQueue {
|
|
|
190
343
|
* @param [options] Request queue operation options.
|
|
191
344
|
*/
|
|
192
345
|
async addRequests(requestsLike, options = {}) {
|
|
193
|
-
|
|
346
|
+
const transaction = activeStorageTransaction();
|
|
194
347
|
ow(requestsLike, ow.object
|
|
195
348
|
.is((value) => isIterable(value) || isAsyncIterable(value))
|
|
196
349
|
.message((value) => `Expected an iterable or async iterable, got ${getObjectType(value)}`));
|
|
@@ -225,12 +378,20 @@ export class RequestQueue {
|
|
|
225
378
|
requests.push(requestLike instanceof Request ? requestLike : new Request(requestLike));
|
|
226
379
|
}
|
|
227
380
|
}
|
|
381
|
+
if (transaction?.policy.requestQueue === 'deferred') {
|
|
382
|
+
const buffered = this.bufferedRequests(transaction);
|
|
383
|
+
for (const request of requests) {
|
|
384
|
+
results.processedRequests.push(await this.addRequestDeferred(transaction, request, forefront, buffered));
|
|
385
|
+
}
|
|
386
|
+
return results;
|
|
387
|
+
}
|
|
388
|
+
this.recordRequestJournalEntry(transaction, requests, forefront, true);
|
|
228
389
|
const requestsToAdd = new Map();
|
|
229
390
|
for (const request of requests) {
|
|
230
391
|
const cacheKey = getCachedRequestId(request.uniqueKey);
|
|
231
392
|
// Prefer the full `requestCache` record; fall back to the dedup cache for background batches it skips.
|
|
232
393
|
const cachedInfo = this.requestCache.get(cacheKey);
|
|
233
|
-
const knownRequestId = cachedInfo?.id ?? this
|
|
394
|
+
const knownRequestId = cachedInfo?.id ?? this.#requestSeenCache.get(cacheKey);
|
|
234
395
|
if (knownRequestId) {
|
|
235
396
|
request.id = knownRequestId;
|
|
236
397
|
results.processedRequests.push({
|
|
@@ -249,7 +410,7 @@ export class RequestQueue {
|
|
|
249
410
|
if (!requestsToAdd.size) {
|
|
250
411
|
return results;
|
|
251
412
|
}
|
|
252
|
-
this
|
|
413
|
+
this.#statsTracker.add('writeCount');
|
|
253
414
|
const apiResults = await this.backend.addBatchOfRequests([...requestsToAdd.values()], { forefront });
|
|
254
415
|
// Report unprocessed requests
|
|
255
416
|
results.unprocessedRequests = apiResults.unprocessedRequests;
|
|
@@ -262,7 +423,7 @@ export class RequestQueue {
|
|
|
262
423
|
this.cacheRequest(cacheKey, { ...newRequest, forefront });
|
|
263
424
|
}
|
|
264
425
|
// Unlike `requestCache`, populate this on every batch (including background ones).
|
|
265
|
-
this
|
|
426
|
+
this.#requestSeenCache.add(cacheKey, newRequest.requestId);
|
|
266
427
|
}
|
|
267
428
|
return results;
|
|
268
429
|
}
|
|
@@ -276,7 +437,8 @@ export class RequestQueue {
|
|
|
276
437
|
* @param options Options for the request queue
|
|
277
438
|
*/
|
|
278
439
|
async addRequestsBatched(requests, options = {}) {
|
|
279
|
-
|
|
440
|
+
const transaction = activeStorageTransaction();
|
|
441
|
+
const deferred = transaction?.policy.requestQueue === 'deferred';
|
|
280
442
|
ow(requests, ow.object
|
|
281
443
|
.is((value) => isIterable(value) || isAsyncIterable(value))
|
|
282
444
|
.message((value) => `Expected an iterable or async iterable, got ${getObjectType(value)}`));
|
|
@@ -313,7 +475,9 @@ export class RequestQueue {
|
|
|
313
475
|
}
|
|
314
476
|
}
|
|
315
477
|
}
|
|
316
|
-
const { batchSize = 1000,
|
|
478
|
+
const { batchSize = 1000, maxNewRequests = undefined } = options;
|
|
479
|
+
// Under `deferred` no chunk performs backend I/O, so pacing them would only stall the handler.
|
|
480
|
+
const waitBetweenBatchesMillis = deferred ? 0 : (options.waitBetweenBatchesMillis ?? 1000);
|
|
317
481
|
let remainingBudget = maxNewRequests ?? Infinity;
|
|
318
482
|
const requestsOverLimit = [];
|
|
319
483
|
// If there's a limit on the number of added requests, do not send batches bigger than the limit
|
|
@@ -373,21 +537,38 @@ export class RequestQueue {
|
|
|
373
537
|
if ((await chunksIterator.peek()) === undefined) {
|
|
374
538
|
return buildResult(addedRequests, Promise.resolve([]), requestIterator);
|
|
375
539
|
}
|
|
376
|
-
|
|
377
|
-
const promise = new Promise(async (resolve) => {
|
|
540
|
+
const processRemainingChunks = async () => {
|
|
378
541
|
const finalAddedRequests = [];
|
|
379
542
|
for await (const requestChunk of chunks) {
|
|
380
543
|
finalAddedRequests.push(...(await processChunk(requestChunk, false)));
|
|
381
544
|
await sleep(waitBetweenBatchesMillis);
|
|
382
545
|
}
|
|
383
|
-
|
|
546
|
+
return finalAddedRequests;
|
|
547
|
+
};
|
|
548
|
+
// maxNewRequests needs all batches to report skipped requests accurately; `deferred` needs them
|
|
549
|
+
// too - a writer that finishes after commit would have nowhere to put its journal entries.
|
|
550
|
+
const awaitsRemainingChunks = options.waitForAllRequestsToBeAdded || maxNewRequests !== undefined || deferred;
|
|
551
|
+
// eslint-disable-next-line no-async-promise-executor
|
|
552
|
+
const promise = new Promise(async (resolve) => {
|
|
553
|
+
if (awaitsRemainingChunks) {
|
|
554
|
+
// Awaited below, i.e. still within the caller's transaction scope, so the additions are
|
|
555
|
+
// journaled like the initial chunk - introspection must not depend on where the chunk
|
|
556
|
+
// boundary happened to fall.
|
|
557
|
+
resolve(await processRemainingChunks());
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
// Nobody awaits this writer, so it outlives the transaction scope it inherits and must
|
|
561
|
+
// not record into a transaction that may already be closed. It writes directly - its
|
|
562
|
+
// write-through additions were never going to be rolled back anyway - which means the
|
|
563
|
+
// requests it adds are not journaled. See `StorageTransactionView.enqueuedUrls`.
|
|
564
|
+
resolve(await withDirectStorageAccess(processRemainingChunks));
|
|
565
|
+
}
|
|
384
566
|
});
|
|
385
567
|
this.inProgressRequestBatchCount += 1;
|
|
386
568
|
void promise.finally(() => {
|
|
387
569
|
this.inProgressRequestBatchCount -= 1;
|
|
388
570
|
});
|
|
389
|
-
|
|
390
|
-
if (options.waitForAllRequestsToBeAdded || maxNewRequests !== undefined) {
|
|
571
|
+
if (awaitsRemainingChunks) {
|
|
391
572
|
addedRequests.push(...(await promise));
|
|
392
573
|
}
|
|
393
574
|
return buildResult(addedRequests, promise, requestIterator);
|
|
@@ -399,8 +580,13 @@ export class RequestQueue {
|
|
|
399
580
|
* @returns Returns the request object, or `null` if it was not found.
|
|
400
581
|
*/
|
|
401
582
|
async getRequest(uniqueKey) {
|
|
402
|
-
|
|
583
|
+
const transaction = activeStorageTransaction();
|
|
403
584
|
ow(uniqueKey, ow.string);
|
|
585
|
+
// Requests buffered by the active transaction (under the `deferred` write policy) are visible to it.
|
|
586
|
+
const buffered = transaction && this.bufferedRequests(transaction).get(uniqueKey);
|
|
587
|
+
if (buffered) {
|
|
588
|
+
return new Request(buffered);
|
|
589
|
+
}
|
|
404
590
|
const requestOptions = await this.backend.getRequest(uniqueKey);
|
|
405
591
|
if (!requestOptions)
|
|
406
592
|
return null;
|
|
@@ -424,11 +610,11 @@ export class RequestQueue {
|
|
|
424
610
|
* Returns the request object or `null` if there are no more pending requests.
|
|
425
611
|
*/
|
|
426
612
|
async fetchNextRequest() {
|
|
427
|
-
|
|
428
|
-
if (this
|
|
613
|
+
rejectOperationInTransaction('RequestQueue.fetchNextRequest()', 'it is part of the crawler request-processing bookkeeping, which a transaction must not affect.');
|
|
614
|
+
if (this.#queuePausedForMigration) {
|
|
429
615
|
return null;
|
|
430
616
|
}
|
|
431
|
-
this
|
|
617
|
+
this.#statsTracker.add('headItemReadCount');
|
|
432
618
|
const requestOptions = await this.backend.fetchNextRequest();
|
|
433
619
|
if (!requestOptions)
|
|
434
620
|
return null;
|
|
@@ -441,7 +627,7 @@ export class RequestQueue {
|
|
|
441
627
|
* Handled requests will never again be returned by the `fetchNextRequest` function.
|
|
442
628
|
*/
|
|
443
629
|
async markRequestAsHandled(request) {
|
|
444
|
-
|
|
630
|
+
rejectOperationInTransaction('RequestQueue.markRequestAsHandled()', 'it is part of the crawler request-processing bookkeeping, which a transaction must not affect.');
|
|
445
631
|
ow(request, ow.object.partialShape({
|
|
446
632
|
id: ow.string,
|
|
447
633
|
uniqueKey: ow.string,
|
|
@@ -449,7 +635,7 @@ export class RequestQueue {
|
|
|
449
635
|
}));
|
|
450
636
|
const forefront = this.requestCache.get(getRequestId(request.uniqueKey))?.forefront ?? false;
|
|
451
637
|
const handledAt = request.handledAt ?? new Date().toISOString();
|
|
452
|
-
this
|
|
638
|
+
this.#statsTracker.add('writeCount');
|
|
453
639
|
const processedRequest = await this.backend.markRequestAsHandled({
|
|
454
640
|
...request,
|
|
455
641
|
handledAt,
|
|
@@ -474,7 +660,7 @@ export class RequestQueue {
|
|
|
474
660
|
* For example, this lets you store the number of retries or error messages for the request.
|
|
475
661
|
*/
|
|
476
662
|
async reclaimRequest(request, options = {}) {
|
|
477
|
-
|
|
663
|
+
rejectOperationInTransaction('RequestQueue.reclaimRequest()', 'it is part of the crawler request-processing bookkeeping, which a transaction must not affect.');
|
|
478
664
|
ow(request, ow.object.partialShape({
|
|
479
665
|
id: ow.string,
|
|
480
666
|
uniqueKey: ow.string,
|
|
@@ -483,7 +669,7 @@ export class RequestQueue {
|
|
|
483
669
|
forefront: ow.optional.boolean,
|
|
484
670
|
}));
|
|
485
671
|
const { forefront = false } = options;
|
|
486
|
-
this
|
|
672
|
+
this.#statsTracker.add('writeCount');
|
|
487
673
|
const processedRequest = await this.backend.reclaimRequest(request, { forefront });
|
|
488
674
|
// The request was not in progress — nothing to reclaim.
|
|
489
675
|
if (!processedRequest) {
|
|
@@ -508,7 +694,11 @@ export class RequestQueue {
|
|
|
508
694
|
* {@link RequestQueue.isFinished}.
|
|
509
695
|
*/
|
|
510
696
|
async isEmpty() {
|
|
511
|
-
|
|
697
|
+
const transaction = activeStorageTransaction();
|
|
698
|
+
// Requests buffered by the active transaction count as pending from its point of view.
|
|
699
|
+
if (transaction && this.bufferedRequests(transaction).size > 0) {
|
|
700
|
+
return false;
|
|
701
|
+
}
|
|
512
702
|
return this.backend.isEmpty();
|
|
513
703
|
}
|
|
514
704
|
/**
|
|
@@ -520,11 +710,15 @@ export class RequestQueue {
|
|
|
520
710
|
* a false negative, but it shall never return a false positive.
|
|
521
711
|
*/
|
|
522
712
|
async isFinished() {
|
|
523
|
-
|
|
713
|
+
const transaction = activeStorageTransaction();
|
|
524
714
|
// We are not finished if we're still adding new requests in the background.
|
|
525
715
|
if (this.inProgressRequestBatchCount > 0) {
|
|
526
716
|
return false;
|
|
527
717
|
}
|
|
718
|
+
// Requests buffered by the active transaction count as pending from its point of view.
|
|
719
|
+
if (transaction && this.bufferedRequests(transaction).size > 0) {
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
528
722
|
return this.backend.isFinished();
|
|
529
723
|
}
|
|
530
724
|
/**
|
|
@@ -538,10 +732,10 @@ export class RequestQueue {
|
|
|
538
732
|
* short the reservation of a long-lived one and have its in-flight request stolen.
|
|
539
733
|
*/
|
|
540
734
|
async setExpectedRequestProcessingTimeSecs(secs) {
|
|
541
|
-
if (secs <= this
|
|
735
|
+
if (secs <= this.#expectedRequestProcessingSecs) {
|
|
542
736
|
return;
|
|
543
737
|
}
|
|
544
|
-
this
|
|
738
|
+
this.#expectedRequestProcessingSecs = secs;
|
|
545
739
|
await this.backend.setExpectedRequestProcessingTimeSecs?.(secs);
|
|
546
740
|
}
|
|
547
741
|
/**
|
|
@@ -564,7 +758,7 @@ export class RequestQueue {
|
|
|
564
758
|
* depending on the mode of operation.
|
|
565
759
|
*/
|
|
566
760
|
async drop() {
|
|
567
|
-
|
|
761
|
+
rejectOperationInTransaction('RequestQueue.drop()');
|
|
568
762
|
await this.backend.drop();
|
|
569
763
|
serviceLocator.getStorageInstanceManager().removeFromCache(this);
|
|
570
764
|
}
|
|
@@ -573,16 +767,16 @@ export class RequestQueue {
|
|
|
573
767
|
* so it can be reused (e.g. across multiple `crawler.run()` calls).
|
|
574
768
|
*/
|
|
575
769
|
async purge() {
|
|
576
|
-
|
|
770
|
+
rejectOperationInTransaction('RequestQueue.purge()');
|
|
577
771
|
await this.backend.purge();
|
|
578
772
|
// Reset in-memory bookkeeping so the queue behaves as if freshly opened.
|
|
579
773
|
this.requestCache.clear();
|
|
580
|
-
this
|
|
774
|
+
this.#requestSeenCache.clear();
|
|
581
775
|
this.inProgressRequestBatchCount = 0;
|
|
582
776
|
// Reset the expected-processing-time high-water mark too, otherwise the monotonic-raise guard
|
|
583
777
|
// in `setExpectedRequestProcessingTimeSecs` would let a value raised in an earlier run leak into a
|
|
584
778
|
// later one and silently swallow a lower hint (the queue is meant to be reusable across runs).
|
|
585
|
-
this
|
|
779
|
+
this.#expectedRequestProcessingSecs = 0;
|
|
586
780
|
}
|
|
587
781
|
/**
|
|
588
782
|
* @inheritdoc
|
|
@@ -630,8 +824,17 @@ export class RequestQueue {
|
|
|
630
824
|
* @throws If the underlying storage no longer exists (e.g. it was deleted externally).
|
|
631
825
|
*/
|
|
632
826
|
async getInfo() {
|
|
633
|
-
|
|
634
|
-
|
|
827
|
+
const transaction = activeStorageTransaction();
|
|
828
|
+
const metadata = await this.backend.getMetadata();
|
|
829
|
+
const bufferedCount = transaction ? this.bufferedRequests(transaction).size : 0;
|
|
830
|
+
if (bufferedCount > 0) {
|
|
831
|
+
return {
|
|
832
|
+
...metadata,
|
|
833
|
+
totalRequestCount: metadata.totalRequestCount + bufferedCount,
|
|
834
|
+
pendingRequestCount: metadata.pendingRequestCount + bufferedCount,
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
return metadata;
|
|
635
838
|
}
|
|
636
839
|
/**
|
|
637
840
|
* Fetches URLs from requestsFromUrl and returns them in format of list of requests
|
|
@@ -641,10 +844,10 @@ export class RequestQueue {
|
|
|
641
844
|
// Download remote resource and parse URLs.
|
|
642
845
|
let urlsArr;
|
|
643
846
|
try {
|
|
644
|
-
urlsArr = await this.
|
|
847
|
+
urlsArr = await this.downloadListOfUrls({
|
|
645
848
|
url: requestsFromUrl,
|
|
646
849
|
urlRegExp: regex,
|
|
647
|
-
proxyUrl: (await this
|
|
850
|
+
proxyUrl: (await this.#proxyConfiguration?.newProxyInfo())?.url,
|
|
648
851
|
});
|
|
649
852
|
}
|
|
650
853
|
catch (err) {
|
|
@@ -676,10 +879,10 @@ export class RequestQueue {
|
|
|
676
879
|
/**
|
|
677
880
|
* @internal wraps public utility for mocking purposes
|
|
678
881
|
*/
|
|
679
|
-
async
|
|
882
|
+
async downloadListOfUrls(options) {
|
|
680
883
|
return downloadListOfUrls({
|
|
681
884
|
...options,
|
|
682
|
-
httpClient: this
|
|
885
|
+
httpClient: this.#httpClient,
|
|
683
886
|
});
|
|
684
887
|
}
|
|
685
888
|
/**
|
|
@@ -700,7 +903,7 @@ export class RequestQueue {
|
|
|
700
903
|
* @param [options] Open Request Queue options.
|
|
701
904
|
*/
|
|
702
905
|
static async open(identifier, options = {}) {
|
|
703
|
-
|
|
906
|
+
tryCancel();
|
|
704
907
|
ow(options, ow.object.exactShape({
|
|
705
908
|
configuration: ow.optional.object.instanceOf(Configuration),
|
|
706
909
|
storageBackend: ow.optional.object,
|
|
@@ -718,8 +921,8 @@ export class RequestQueue {
|
|
|
718
921
|
backendOpener: () => storageBackend.createRequestQueueBackend(resolved),
|
|
719
922
|
backendCacheKey: storageBackend.getStorageBackendCacheKey?.() ?? storageBackend.constructor.name,
|
|
720
923
|
});
|
|
721
|
-
queue
|
|
722
|
-
queue
|
|
924
|
+
queue.#proxyConfiguration = options.proxyConfiguration;
|
|
925
|
+
queue.#httpClient = options.httpClient;
|
|
723
926
|
return queue;
|
|
724
927
|
}
|
|
725
928
|
}
|
|
@@ -80,55 +80,12 @@ export interface SitemapRequestLoaderOptions extends UrlConstraints {
|
|
|
80
80
|
* The loading of the sitemap is performed in the background so that crawling can start before the sitemap is fully loaded.
|
|
81
81
|
*/
|
|
82
82
|
export declare class SitemapRequestLoader implements IRequestLoader {
|
|
83
|
+
#private;
|
|
83
84
|
/**
|
|
84
85
|
* Set of URLs that were returned by `fetchNextRequest()` and not marked as handled yet.
|
|
85
86
|
* @internal
|
|
86
87
|
*/
|
|
87
88
|
inProgress: Set<string>;
|
|
88
|
-
/**
|
|
89
|
-
* Map of returned Request objects that have not been marked as handled yet.
|
|
90
|
-
*
|
|
91
|
-
* We use this to persist custom user fields on the in-progress requests.
|
|
92
|
-
*/
|
|
93
|
-
private requestData;
|
|
94
|
-
/**
|
|
95
|
-
* Object for keeping track of the sitemap parsing progress.
|
|
96
|
-
*/
|
|
97
|
-
private sitemapParsingProgress;
|
|
98
|
-
/**
|
|
99
|
-
* Object stream of URLs parsed from the sitemaps.
|
|
100
|
-
* Using `highWaterMark`, this can manage the speed of the sitemap loading.
|
|
101
|
-
*
|
|
102
|
-
* Fetch the next URL to be processed using `fetchNextRequest()`.
|
|
103
|
-
*/
|
|
104
|
-
private urlQueueStream;
|
|
105
|
-
/**
|
|
106
|
-
* Indicates whether the request list sitemap loading was aborted.
|
|
107
|
-
*
|
|
108
|
-
* If the loading was aborted before the sitemaps were fully loaded, the request list might be missing some URLs.
|
|
109
|
-
* The `isSitemapFullyLoaded` method can be used to check if the sitemaps were fully loaded.
|
|
110
|
-
*
|
|
111
|
-
* If the loading is aborted and all the requests are handled, `isFinished()` will return `true`.
|
|
112
|
-
*/
|
|
113
|
-
private abortLoading;
|
|
114
|
-
/** Number of URLs that were marked as handled */
|
|
115
|
-
private handledUrlCount;
|
|
116
|
-
private persistStateKey?;
|
|
117
|
-
private store?;
|
|
118
|
-
private closed;
|
|
119
|
-
/**
|
|
120
|
-
* Proxy URL to be used for sitemap loading.
|
|
121
|
-
*/
|
|
122
|
-
private proxyUrl?;
|
|
123
|
-
/**
|
|
124
|
-
* Logger instance.
|
|
125
|
-
*/
|
|
126
|
-
private log;
|
|
127
|
-
private urlExcludePatternObjects;
|
|
128
|
-
private urlPatternObjects;
|
|
129
|
-
/** EventManager used to handle persistence */
|
|
130
|
-
private events;
|
|
131
|
-
private persistenceOptions;
|
|
132
89
|
/** @internal */
|
|
133
90
|
private constructor();
|
|
134
91
|
/**
|