@crawlee/core 4.0.0-beta.145 → 4.0.0-beta.147
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/package.json +6 -6
- package/storages/request_list.d.ts +2 -6
- package/storages/request_list.js +6 -9
- package/storages/request_loader.d.ts +47 -15
- package/storages/request_loader.js +36 -1
- package/storages/request_manager.d.ts +76 -0
- package/storages/request_manager_tandem.d.ts +12 -9
- package/storages/request_manager_tandem.js +29 -20
- package/storages/request_queue.d.ts +14 -17
- package/storages/request_queue.js +23 -28
- package/storages/sitemap_request_loader.d.ts +2 -6
- package/storages/sitemap_request_loader.js +11 -10
- package/storages/throttling_request_manager.d.ts +45 -62
- package/storages/throttling_request_manager.js +226 -92
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.147",
|
|
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.147",
|
|
56
|
+
"@crawlee/http-client": "4.0.0-beta.147",
|
|
57
|
+
"@crawlee/types": "4.0.0-beta.147",
|
|
58
|
+
"@crawlee/utils": "4.0.0-beta.147",
|
|
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": "bb04462b453da49b08ea0af0038cb734eabe3d01"
|
|
82
82
|
}
|
|
@@ -3,7 +3,7 @@ import type { Dictionary } from '@crawlee/types';
|
|
|
3
3
|
import type { Configuration } from '../configuration.js';
|
|
4
4
|
import type { IProxyConfiguration } from '../proxy_configuration.js';
|
|
5
5
|
import { Request, type RequestOptions, type Source } from '../request.js';
|
|
6
|
-
import type { IRequestLoader } from './request_loader.js';
|
|
6
|
+
import type { IRequestLoader, RequestLoaderStatus } from './request_loader.js';
|
|
7
7
|
import type { IRequestManager } from './request_manager.js';
|
|
8
8
|
/** @internal */
|
|
9
9
|
export declare const STATE_PERSISTENCE_KEY = "REQUEST_LIST_STATE";
|
|
@@ -296,11 +296,7 @@ export declare class RequestList implements IRequestLoader {
|
|
|
296
296
|
/**
|
|
297
297
|
* @inheritDoc
|
|
298
298
|
*/
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* @inheritDoc
|
|
302
|
-
*/
|
|
303
|
-
isFinished(): Promise<boolean>;
|
|
299
|
+
checkReadiness(): Promise<RequestLoaderStatus>;
|
|
304
300
|
/**
|
|
305
301
|
* @inheritDoc
|
|
306
302
|
*/
|
package/storages/request_list.js
CHANGED
|
@@ -381,16 +381,13 @@ export class RequestList {
|
|
|
381
381
|
/**
|
|
382
382
|
* @inheritDoc
|
|
383
383
|
*/
|
|
384
|
-
async
|
|
384
|
+
async checkReadiness() {
|
|
385
385
|
this.ensureIsInitialized();
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
async isFinished() {
|
|
392
|
-
this.ensureIsInitialized();
|
|
393
|
-
return this.inProgress.size === 0 && this.#nextIndex >= this.requests.length;
|
|
386
|
+
if (this.#requestsToRetry.length > 0 || this.#nextIndex < this.requests.length) {
|
|
387
|
+
return { status: 'ready' };
|
|
388
|
+
}
|
|
389
|
+
// `#requestsToRetry` is a subset of `inProgress`, so nothing in progress means nothing left to re-serve.
|
|
390
|
+
return this.inProgress.size === 0 ? { status: 'finished' } : { status: 'waiting' };
|
|
394
391
|
}
|
|
395
392
|
/**
|
|
396
393
|
* @inheritDoc
|
|
@@ -2,6 +2,44 @@ import type { Dictionary } from '@crawlee/types';
|
|
|
2
2
|
import type { Request } from '../request.js';
|
|
3
3
|
import type { IRequestManager } from './request_manager.js';
|
|
4
4
|
import type { RequestQueueOperationInfo } from './request_queue.js';
|
|
5
|
+
/**
|
|
6
|
+
* A request source's own availability, in a single answer.
|
|
7
|
+
*
|
|
8
|
+
* - `ready` — the next {@link IRequestLoader.fetchNextRequest} is expected to hand something over.
|
|
9
|
+
* - `waiting` — nothing to fetch right now, but the source is not done: requests are in progress, are being
|
|
10
|
+
* added in the background, or are held back until `readyAt`.
|
|
11
|
+
* - `stalled` — the source holds requests it cannot make progress on. Only a manager that paces its own
|
|
12
|
+
* dispatch can reach this; see {@link ThrottlingRequestManager}.
|
|
13
|
+
* - `finished` — everything has been handled.
|
|
14
|
+
*/
|
|
15
|
+
export type RequestSourceStatus = {
|
|
16
|
+
status: 'ready';
|
|
17
|
+
} | {
|
|
18
|
+
status: 'waiting';
|
|
19
|
+
/**
|
|
20
|
+
* A `Date.now()` timestamp at which the source expects to become `ready`. Absent when the wait has
|
|
21
|
+
* no clock (an in-progress request, a background add), leaving a consumer to poll.
|
|
22
|
+
*/
|
|
23
|
+
readyAt?: number;
|
|
24
|
+
} | {
|
|
25
|
+
status: 'stalled';
|
|
26
|
+
reason: string;
|
|
27
|
+
} | {
|
|
28
|
+
status: 'finished';
|
|
29
|
+
};
|
|
30
|
+
/** Loaders never stall — only a manager that paces its own dispatch can. */
|
|
31
|
+
export type RequestLoaderStatus = Exclude<RequestSourceStatus, {
|
|
32
|
+
status: 'stalled';
|
|
33
|
+
}>;
|
|
34
|
+
/**
|
|
35
|
+
* Combines two request sources' statuses, with the precedence `ready` > `stalled` > `waiting` > `finished`.
|
|
36
|
+
*
|
|
37
|
+
* Binary rather than variadic on purpose: it is on the task loop's probe path and folding a pair allocates
|
|
38
|
+
* nothing.
|
|
39
|
+
*
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
export declare function joinRequestSourceStatuses(a: RequestSourceStatus, b: RequestSourceStatus): RequestSourceStatus;
|
|
5
43
|
/**
|
|
6
44
|
* An abstract interface defining a read-only stream of requests to crawl.
|
|
7
45
|
*
|
|
@@ -24,7 +62,7 @@ import type { RequestQueueOperationInfo } from './request_queue.js';
|
|
|
24
62
|
* - **Restarts and migrations:** loaders that persist their state (see {@link IRequestLoader.persistState})
|
|
25
63
|
* treat in-progress requests as interrupted and re-serve them after a restart. A request that is fetched
|
|
26
64
|
* but never marked handled will be crawled again.
|
|
27
|
-
* - **Termination detection:** {@link IRequestLoader.
|
|
65
|
+
* - **Termination detection:** {@link IRequestLoader.checkReadiness} only reports `finished` once nothing is
|
|
28
66
|
* in progress. Leaving a request unmarked keeps the crawler running indefinitely.
|
|
29
67
|
* - **Bookkeeping:** the handled and pending counts are derived from the set of in-progress requests, so
|
|
30
68
|
* skipping {@link IRequestLoader.markRequestAsHandled} corrupts {@link IRequestLoader.getHandledCount}
|
|
@@ -47,20 +85,13 @@ export interface IRequestLoader {
|
|
|
47
85
|
*/
|
|
48
86
|
getHandledCount(): Promise<number>;
|
|
49
87
|
/**
|
|
50
|
-
*
|
|
51
|
-
|
|
52
|
-
isFinished(): Promise<boolean>;
|
|
53
|
-
/**
|
|
54
|
-
* Resolves to `true` if the next call to {@link IRequestLoader.fetchNextRequest} function
|
|
55
|
-
* would return `null`, otherwise it resolves to `false`.
|
|
56
|
-
* Note that even if the loader is empty, there might be some pending requests currently being processed.
|
|
88
|
+
* Reports whether the loader has a request to hand over, is waiting on one, or is done — see
|
|
89
|
+
* {@link RequestSourceStatus}.
|
|
57
90
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* requests for a while (as {@link ThrottlingRequestManager} does for a rate-limited domain) is empty
|
|
61
|
-
* for as long as it will not hand anything over. Use `isFinished()` to ask whether the work is done.
|
|
91
|
+
* A consumer's task loop is gated on this, so implementations MUST answer `ready` before evaluating
|
|
92
|
+
* anything else. `finished` may arrive late behind distributed storage, but it is never wrong.
|
|
62
93
|
*/
|
|
63
|
-
|
|
94
|
+
checkReadiness(): Promise<RequestSourceStatus>;
|
|
64
95
|
/**
|
|
65
96
|
* Gets the next {@link Request} to process, or `null` if there are no more pending requests.
|
|
66
97
|
*
|
|
@@ -81,8 +112,9 @@ export interface IRequestLoader {
|
|
|
81
112
|
*
|
|
82
113
|
* Call this once you are done with the request — whether processing succeeded or was abandoned after
|
|
83
114
|
* exhausting retries. Because a loader cannot take a request back, marking it handled is the only way to
|
|
84
|
-
* signal completion; failing to do so prevents {@link IRequestLoader.
|
|
85
|
-
* `
|
|
115
|
+
* signal completion; failing to do so prevents {@link IRequestLoader.checkReadiness} from ever reporting
|
|
116
|
+
* `finished` and skews the handled and pending counts. See the request lifecycle contract on
|
|
117
|
+
* {@link IRequestLoader}.
|
|
86
118
|
*/
|
|
87
119
|
markRequestAsHandled(request: Request): Promise<RequestQueueOperationInfo | void | null>;
|
|
88
120
|
/**
|
|
@@ -1 +1,36 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Combines two request sources' statuses, with the precedence `ready` > `stalled` > `waiting` > `finished`.
|
|
3
|
+
*
|
|
4
|
+
* Binary rather than variadic on purpose: it is on the task loop's probe path and folding a pair allocates
|
|
5
|
+
* nothing.
|
|
6
|
+
*
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export function joinRequestSourceStatuses(a, b) {
|
|
10
|
+
if (a.status === 'ready') {
|
|
11
|
+
return a;
|
|
12
|
+
}
|
|
13
|
+
if (b.status === 'ready') {
|
|
14
|
+
return b;
|
|
15
|
+
}
|
|
16
|
+
// `ready` outranking `stalled` masks a stalled source while the other still has work. That is parity with
|
|
17
|
+
// the crawler before this was a single answer: the stall check was only reached from `isFinishedFunction`,
|
|
18
|
+
// which the task loop calls only when nothing is in flight and nothing is ready. "Fixing" the masking
|
|
19
|
+
// turns a crawl that is progressing elsewhere into a `PersistentRateLimitError`. `stalled` outranking
|
|
20
|
+
// `waiting` is the same parity - that call site fired regardless of other domains' clocks.
|
|
21
|
+
if (a.status === 'stalled') {
|
|
22
|
+
return a;
|
|
23
|
+
}
|
|
24
|
+
if (b.status === 'stalled') {
|
|
25
|
+
return b;
|
|
26
|
+
}
|
|
27
|
+
if (a.status === 'waiting') {
|
|
28
|
+
// The earlier of the two known wake-up times - unknown only if neither source announced one.
|
|
29
|
+
if (b.status !== 'waiting' || b.readyAt === undefined) {
|
|
30
|
+
return a;
|
|
31
|
+
}
|
|
32
|
+
return a.readyAt !== undefined && a.readyAt <= b.readyAt ? a : b;
|
|
33
|
+
}
|
|
34
|
+
// `a` is finished, so `b` decides.
|
|
35
|
+
return b;
|
|
36
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { LiteralUnion } from 'type-fest';
|
|
1
2
|
import type { Request, Source } from '../request.js';
|
|
2
3
|
import type { IRequestLoader } from './request_loader.js';
|
|
3
4
|
import type { AddRequestsBatchedOptions, AddRequestsBatchedResult, RequestQueueOperationInfo, RequestQueueOperationOptions } from './request_queue.js';
|
|
@@ -30,4 +31,79 @@ export interface IRequestManager extends IRequestLoader {
|
|
|
30
31
|
* this hint may leave it `undefined`.
|
|
31
32
|
*/
|
|
32
33
|
setExpectedRequestProcessingTimeSecs?(secs: number): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Records something said about the pace requests should go out at, so that a manager which paces its own
|
|
36
|
+
* dispatch can hold requests back.
|
|
37
|
+
*
|
|
38
|
+
* Required rather than optional, so that a wrapping manager always forwards it and a pacer nested in a
|
|
39
|
+
* composition still receives it; a manager that does not pace returns `false`.
|
|
40
|
+
*
|
|
41
|
+
* @returns `true` if anything in the composition took responsibility for the signal.
|
|
42
|
+
*/
|
|
43
|
+
recordPacingSignal(signal: PacingSignal): boolean;
|
|
33
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* How much of the URL space a {@link PacingSignal} covers.
|
|
47
|
+
*
|
|
48
|
+
* Open on purpose: `'hostname'` and `'registrableDomain'` are what Crawlee's own reporters send and what
|
|
49
|
+
* {@link ThrottlingRequestManager} understands, but any string is accepted, so a pacer keyed on something
|
|
50
|
+
* else can be reported to in its own vocabulary.
|
|
51
|
+
*/
|
|
52
|
+
export type PacingScope = LiteralUnion<'hostname' | 'registrableDomain', string>;
|
|
53
|
+
/**
|
|
54
|
+
* Something said about the pace requests should go out at, reported to a request manager through
|
|
55
|
+
* {@link IRequestManager.recordPacingSignal}.
|
|
56
|
+
*
|
|
57
|
+
* One shape rather than a method per channel: a pacing manager switches on `reason`, a wrapping one forwards the
|
|
58
|
+
* value without knowing what is in it, and a new kind of signal costs the interface nothing. The `url` travels
|
|
59
|
+
* inside the value because the crawl-wide variant has none. Nothing here names the mechanism a signal came
|
|
60
|
+
* from - status codes, headers and robots.txt are the crawler's business - and every delay is in milliseconds.
|
|
61
|
+
*
|
|
62
|
+
* ## Scope
|
|
63
|
+
*
|
|
64
|
+
* A manager may apply a signal to a **wider** scope than it was given - a floor that holds for one host still
|
|
65
|
+
* holds when a whole site is paced by it - but never to a narrower one, which would leave some of the URLs the
|
|
66
|
+
* signal covers running unpaced. A manager that can only do the latter, or that does not recognise the scope at
|
|
67
|
+
* all, MUST throw rather than quietly under-apply it.
|
|
68
|
+
*/
|
|
69
|
+
export type PacingSignal = {
|
|
70
|
+
/**
|
|
71
|
+
* The source turned a request away because we were going too fast — an HTTP 429 or 503, an exhausted
|
|
72
|
+
* quota. Reactive and transient: a pacer typically backs off while refusals continue, and lets that
|
|
73
|
+
* decay once they stop.
|
|
74
|
+
*/
|
|
75
|
+
reason: 'rateLimited';
|
|
76
|
+
/** The URL that was turned away. */
|
|
77
|
+
url: string;
|
|
78
|
+
/** How long the source asked us to wait before trying again, if it said. */
|
|
79
|
+
waitMs?: number;
|
|
80
|
+
/**
|
|
81
|
+
* How far this refusal reaches, if the reporter can tell — a 429 rarely says. Left out, it asks the
|
|
82
|
+
* manager to apply the signal however it happens to group requests.
|
|
83
|
+
*/
|
|
84
|
+
scope?: PacingScope;
|
|
85
|
+
} | {
|
|
86
|
+
/**
|
|
87
|
+
* The source declared a standing floor on how often it may be requested — a robots.txt `Crawl-delay`,
|
|
88
|
+
* a documented quota. A property of the source rather than of the run, so a pacer keeps it for the
|
|
89
|
+
* whole crawl.
|
|
90
|
+
*/
|
|
91
|
+
reason: 'minInterval';
|
|
92
|
+
/** A URL of the source that declared the interval. */
|
|
93
|
+
url: string;
|
|
94
|
+
/** The declared minimum interval between two requests to the source. */
|
|
95
|
+
intervalMs: number;
|
|
96
|
+
/** Required, since whoever declares an interval knows what it applies to. */
|
|
97
|
+
scope: PacingScope;
|
|
98
|
+
} | {
|
|
99
|
+
/**
|
|
100
|
+
* A standing floor under the pace of **every** domain the manager dispatches to, declared by whoever
|
|
101
|
+
* owns the crawl rather than by a source — a crawler's `sameDomainDelaySecs`. A manager that paces only
|
|
102
|
+
* some of its domains MUST throw rather than under-apply it.
|
|
103
|
+
*/
|
|
104
|
+
reason: 'minIntervalEverywhere';
|
|
105
|
+
/** The declared minimum interval between two requests to any one source. */
|
|
106
|
+
intervalMs: number;
|
|
107
|
+
/** At what granularity the floor applies. */
|
|
108
|
+
scope: PacingScope;
|
|
109
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Dictionary } from '@crawlee/types';
|
|
2
2
|
import type { Request, Source } from '../request.js';
|
|
3
|
-
import type { IRequestLoader } from './request_loader.js';
|
|
4
|
-
import type { IRequestManager, RequestsLike } from './request_manager.js';
|
|
3
|
+
import type { IRequestLoader, RequestSourceStatus } from './request_loader.js';
|
|
4
|
+
import type { IRequestManager, PacingSignal, RequestsLike } from './request_manager.js';
|
|
5
5
|
import type { AddRequestsBatchedOptions, AddRequestsBatchedResult, RequestQueueOperationInfo, RequestQueueOperationOptions } from './request_queue.js';
|
|
6
6
|
/**
|
|
7
7
|
* A request manager that combines a {@link IRequestLoader} (such as a `RequestList`) with a writable
|
|
@@ -32,19 +32,15 @@ export declare class RequestManagerTandem implements IRequestManager {
|
|
|
32
32
|
*/
|
|
33
33
|
private transferNextRequestToQueue;
|
|
34
34
|
/**
|
|
35
|
-
* Fetches the next request from the
|
|
36
|
-
* is not finished, it will transfer a request from the loader to the manager first.
|
|
35
|
+
* Fetches the next request, transferring one from the loader first if the loader still has work.
|
|
37
36
|
* @inheritdoc
|
|
38
37
|
*/
|
|
39
38
|
fetchNextRequest<T extends Dictionary = Dictionary>(): Promise<Request<T> | null>;
|
|
40
39
|
/**
|
|
40
|
+
* The loader and the manager read as one source.
|
|
41
41
|
* @inheritdoc
|
|
42
42
|
*/
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* @inheritdoc
|
|
46
|
-
*/
|
|
47
|
-
isEmpty(): Promise<boolean>;
|
|
43
|
+
checkReadiness(): Promise<RequestSourceStatus>;
|
|
48
44
|
/**
|
|
49
45
|
* @inheritdoc
|
|
50
46
|
*/
|
|
@@ -94,4 +90,11 @@ export declare class RequestManagerTandem implements IRequestManager {
|
|
|
94
90
|
* @inheritdoc
|
|
95
91
|
*/
|
|
96
92
|
setExpectedRequestProcessingTimeSecs(secs: number): Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* Forwards a pacing signal to the writable manager - the loader side is read-only and dispatches nothing of
|
|
95
|
+
* its own. Only a resolved manager is signalled; the tandem will not open a queue to answer a question about
|
|
96
|
+
* pacing.
|
|
97
|
+
* @inheritdoc
|
|
98
|
+
*/
|
|
99
|
+
recordPacingSignal(signal: PacingSignal): boolean;
|
|
97
100
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { serviceLocator } from '../service_locator.js';
|
|
2
|
+
import { joinRequestSourceStatuses } from './request_loader.js';
|
|
2
3
|
/**
|
|
3
4
|
* A request manager that combines a {@link IRequestLoader} (such as a `RequestList`) with a writable
|
|
4
5
|
* {@link IRequestManager} (such as a `RequestQueue`).
|
|
@@ -24,7 +25,14 @@ export class RequestManagerTandem {
|
|
|
24
25
|
constructor(requestLoader, requestManager) {
|
|
25
26
|
this.#log = serviceLocator.getLogger().child({ prefix: 'RequestManagerTandem' });
|
|
26
27
|
this.#requestLoader = requestLoader;
|
|
27
|
-
|
|
28
|
+
if (typeof requestManager === 'function') {
|
|
29
|
+
this.#requestManagerFactory = requestManager;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
// Nothing to open, so mark it resolved up front - synchronous pacing signals can then reach it.
|
|
33
|
+
this.#resolvedRequestManager = requestManager;
|
|
34
|
+
this.#requestManagerFactory = () => requestManager;
|
|
35
|
+
}
|
|
28
36
|
}
|
|
29
37
|
/**
|
|
30
38
|
* Resolves the writable request manager, opening it lazily (via the factory) on first use and memoizing the result.
|
|
@@ -69,17 +77,13 @@ export class RequestManagerTandem {
|
|
|
69
77
|
}
|
|
70
78
|
}
|
|
71
79
|
/**
|
|
72
|
-
* Fetches the next request from the
|
|
73
|
-
* is not finished, it will transfer a request from the loader to the manager first.
|
|
80
|
+
* Fetches the next request, transferring one from the loader first if the loader still has work.
|
|
74
81
|
* @inheritdoc
|
|
75
82
|
*/
|
|
76
83
|
async fetchNextRequest() {
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
this.#requestLoader.isFinished(),
|
|
81
|
-
]);
|
|
82
|
-
if (!listEmpty && !listFinished) {
|
|
84
|
+
// Only the loader's own state decides this: a manager waiting out a backoff must not freeze the
|
|
85
|
+
// loader's unrelated requests for the length of it.
|
|
86
|
+
if ((await this.#requestLoader.checkReadiness()).status === 'ready') {
|
|
83
87
|
// If the transfer failed, the request was dropped; don't fetch from the manager this round (matching
|
|
84
88
|
// crawlee-python behaviour). The next `fetchNextRequest()` call will pick up where we left off.
|
|
85
89
|
if (!(await this.transferNextRequestToQueue())) {
|
|
@@ -90,20 +94,16 @@ export class RequestManagerTandem {
|
|
|
90
94
|
return (await this.getRequestManager()).fetchNextRequest();
|
|
91
95
|
}
|
|
92
96
|
/**
|
|
97
|
+
* The loader and the manager read as one source.
|
|
93
98
|
* @inheritdoc
|
|
94
99
|
*/
|
|
95
|
-
async
|
|
100
|
+
async checkReadiness() {
|
|
96
101
|
const requestManager = await this.getRequestManager();
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
*/
|
|
103
|
-
async isEmpty() {
|
|
104
|
-
const requestManager = await this.getRequestManager();
|
|
105
|
-
const storagesEmpty = await Promise.all([this.#requestLoader.isEmpty(), requestManager.isEmpty()]);
|
|
106
|
-
return storagesEmpty.every(Boolean);
|
|
102
|
+
const [loaderStatus, managerStatus] = await Promise.all([
|
|
103
|
+
this.#requestLoader.checkReadiness(),
|
|
104
|
+
requestManager.checkReadiness(),
|
|
105
|
+
]);
|
|
106
|
+
return joinRequestSourceStatuses(loaderStatus, managerStatus);
|
|
107
107
|
}
|
|
108
108
|
/**
|
|
109
109
|
* @inheritdoc
|
|
@@ -194,4 +194,13 @@ export class RequestManagerTandem {
|
|
|
194
194
|
this.#expectedRequestProcessingSecs = secs;
|
|
195
195
|
await this.#resolvedRequestManager?.setExpectedRequestProcessingTimeSecs?.(secs);
|
|
196
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Forwards a pacing signal to the writable manager - the loader side is read-only and dispatches nothing of
|
|
199
|
+
* its own. Only a resolved manager is signalled; the tandem will not open a queue to answer a question about
|
|
200
|
+
* pacing.
|
|
201
|
+
* @inheritdoc
|
|
202
|
+
*/
|
|
203
|
+
recordPacingSignal(signal) {
|
|
204
|
+
return this.#resolvedRequestManager?.recordPacingSignal(signal) ?? false;
|
|
205
|
+
}
|
|
197
206
|
}
|
|
@@ -5,7 +5,8 @@ import type { IProxyConfiguration } from '../proxy_configuration.js';
|
|
|
5
5
|
import type { Source } from '../request.js';
|
|
6
6
|
import { Request } from '../request.js';
|
|
7
7
|
import type { JournalEntry } from './transaction.js';
|
|
8
|
-
import type {
|
|
8
|
+
import type { RequestLoaderStatus } from './request_loader.js';
|
|
9
|
+
import type { IRequestManager, PacingSignal, RequestsLike } from './request_manager.js';
|
|
9
10
|
import type { RequestQueueStats } from './storage_stats.js';
|
|
10
11
|
import type { IStorage, StorageIdentifier } from './storage_instance_manager.js';
|
|
11
12
|
import type { StorageOpenOptions } from './utils.js';
|
|
@@ -147,7 +148,7 @@ export declare class RequestQueue implements IStorage, IRequestManager {
|
|
|
147
148
|
* Note that the `null` return value doesn't mean the queue processing finished,
|
|
148
149
|
* it means there are currently no pending requests.
|
|
149
150
|
* To check whether all requests in queue were finished,
|
|
150
|
-
* use {@link RequestQueue.
|
|
151
|
+
* use {@link RequestQueue.checkReadiness} instead.
|
|
151
152
|
*
|
|
152
153
|
* @returns
|
|
153
154
|
* Returns the request object or `null` if there are no more pending requests.
|
|
@@ -168,25 +169,21 @@ export declare class RequestQueue implements IStorage, IRequestManager {
|
|
|
168
169
|
*/
|
|
169
170
|
reclaimRequest(request: Request, options?: RequestQueueOperationOptions): Promise<RequestQueueOperationInfo | null>;
|
|
170
171
|
/**
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
* Note that even if the queue is empty, there might be some requests currently being processed
|
|
175
|
-
* (fetched but not yet handled or reclaimed). An empty queue therefore does not mean crawling is
|
|
176
|
-
* finished — those in-progress requests may still be reclaimed, and background tasks may still be
|
|
177
|
-
* adding more requests. To check whether all activity in the queue has finished, use
|
|
178
|
-
* {@link RequestQueue.isFinished}.
|
|
172
|
+
* A queue hands requests out as fast as they are asked for; pacing is a job for a manager wrapped around it,
|
|
173
|
+
* such as {@link ThrottlingRequestManager}.
|
|
174
|
+
* @inheritdoc
|
|
179
175
|
*/
|
|
180
|
-
|
|
176
|
+
recordPacingSignal(_signal: PacingSignal): boolean;
|
|
181
177
|
/**
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
178
|
+
* Reports whether the queue has a request to hand over, is waiting on one, or is done.
|
|
179
|
+
*
|
|
180
|
+
* `waiting` means requests are in progress (fetched but not yet handled or reclaimed, possibly by another
|
|
181
|
+
* client sharing the queue) or a background add is still landing; neither has a clock, so no `readyAt`.
|
|
185
182
|
*
|
|
186
|
-
* Due to the nature of distributed storage used by the queue,
|
|
187
|
-
*
|
|
183
|
+
* Due to the nature of distributed storage used by the queue, `finished` may occasionally arrive a probe or
|
|
184
|
+
* two late, but it is never reported early.
|
|
188
185
|
*/
|
|
189
|
-
|
|
186
|
+
checkReadiness(): Promise<RequestLoaderStatus>;
|
|
190
187
|
/**
|
|
191
188
|
* Tells the queue how long a consumer expects to hold a fetched request before marking it handled
|
|
192
189
|
* or reclaiming it (typically the request-handler timeout plus padding), so that a storage backend
|
|
@@ -553,7 +553,7 @@ export class RequestQueue {
|
|
|
553
553
|
* Note that the `null` return value doesn't mean the queue processing finished,
|
|
554
554
|
* it means there are currently no pending requests.
|
|
555
555
|
* To check whether all requests in queue were finished,
|
|
556
|
-
* use {@link RequestQueue.
|
|
556
|
+
* use {@link RequestQueue.checkReadiness} instead.
|
|
557
557
|
*
|
|
558
558
|
* @returns
|
|
559
559
|
* Returns the request object or `null` if there are no more pending requests.
|
|
@@ -625,42 +625,37 @@ export class RequestQueue {
|
|
|
625
625
|
return queueOperationInfo;
|
|
626
626
|
}
|
|
627
627
|
/**
|
|
628
|
-
*
|
|
629
|
-
*
|
|
630
|
-
*
|
|
631
|
-
* Note that even if the queue is empty, there might be some requests currently being processed
|
|
632
|
-
* (fetched but not yet handled or reclaimed). An empty queue therefore does not mean crawling is
|
|
633
|
-
* finished — those in-progress requests may still be reclaimed, and background tasks may still be
|
|
634
|
-
* adding more requests. To check whether all activity in the queue has finished, use
|
|
635
|
-
* {@link RequestQueue.isFinished}.
|
|
628
|
+
* A queue hands requests out as fast as they are asked for; pacing is a job for a manager wrapped around it,
|
|
629
|
+
* such as {@link ThrottlingRequestManager}.
|
|
630
|
+
* @inheritdoc
|
|
636
631
|
*/
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
// Requests buffered by the active transaction count as pending from its point of view.
|
|
640
|
-
if (transaction && this.bufferedRequests(transaction).size > 0) {
|
|
641
|
-
return false;
|
|
642
|
-
}
|
|
643
|
-
return this.backend.isEmpty();
|
|
632
|
+
recordPacingSignal(_signal) {
|
|
633
|
+
return false;
|
|
644
634
|
}
|
|
645
635
|
/**
|
|
646
|
-
*
|
|
647
|
-
*
|
|
648
|
-
*
|
|
636
|
+
* Reports whether the queue has a request to hand over, is waiting on one, or is done.
|
|
637
|
+
*
|
|
638
|
+
* `waiting` means requests are in progress (fetched but not yet handled or reclaimed, possibly by another
|
|
639
|
+
* client sharing the queue) or a background add is still landing; neither has a clock, so no `readyAt`.
|
|
649
640
|
*
|
|
650
|
-
* Due to the nature of distributed storage used by the queue,
|
|
651
|
-
*
|
|
641
|
+
* Due to the nature of distributed storage used by the queue, `finished` may occasionally arrive a probe or
|
|
642
|
+
* two late, but it is never reported early.
|
|
652
643
|
*/
|
|
653
|
-
async
|
|
644
|
+
async checkReadiness() {
|
|
654
645
|
const transaction = activeStorageTransaction();
|
|
655
|
-
// We are not finished if we're still adding new requests in the background.
|
|
656
|
-
if (this.#inProgressRequestBatchCount > 0) {
|
|
657
|
-
return false;
|
|
658
|
-
}
|
|
659
646
|
// Requests buffered by the active transaction count as pending from its point of view.
|
|
660
647
|
if (transaction && this.bufferedRequests(transaction).size > 0) {
|
|
661
|
-
return
|
|
648
|
+
return { status: 'ready' };
|
|
649
|
+
}
|
|
650
|
+
// Something fetchable outranks everything below, so this is the only backend call a probe needs.
|
|
651
|
+
if (!(await this.backend.isEmpty())) {
|
|
652
|
+
return { status: 'ready' };
|
|
653
|
+
}
|
|
654
|
+
// We are not finished if we're still adding new requests in the background.
|
|
655
|
+
if (this.#inProgressRequestBatchCount > 0) {
|
|
656
|
+
return { status: 'waiting' };
|
|
662
657
|
}
|
|
663
|
-
return this.backend.isFinished();
|
|
658
|
+
return (await this.backend.isFinished()) ? { status: 'finished' } : { status: 'waiting' };
|
|
664
659
|
}
|
|
665
660
|
/**
|
|
666
661
|
* Tells the queue how long a consumer expects to hold a fetched request before marking it handled
|
|
@@ -2,7 +2,7 @@ import type { BaseHttpClient } from '@crawlee/http-client';
|
|
|
2
2
|
import { EnqueueStrategy, type ParseSitemapOptions } from '@crawlee/utils';
|
|
3
3
|
import type { UrlPatternInput } from '../enqueue_links/shared.js';
|
|
4
4
|
import { Request } from '../request.js';
|
|
5
|
-
import type { IRequestLoader } from './request_loader.js';
|
|
5
|
+
import type { IRequestLoader, RequestLoaderStatus } from './request_loader.js';
|
|
6
6
|
import type { IRequestManager } from './request_manager.js';
|
|
7
7
|
interface UrlConstraints {
|
|
8
8
|
/**
|
|
@@ -163,11 +163,7 @@ export declare class SitemapRequestLoader implements IRequestLoader {
|
|
|
163
163
|
/**
|
|
164
164
|
* @inheritDoc
|
|
165
165
|
*/
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* @inheritDoc
|
|
169
|
-
*/
|
|
170
|
-
isEmpty(): Promise<boolean>;
|
|
166
|
+
checkReadiness(): Promise<RequestLoaderStatus>;
|
|
171
167
|
/**
|
|
172
168
|
* @inheritDoc
|
|
173
169
|
*/
|
|
@@ -72,7 +72,7 @@ export class SitemapRequestLoader {
|
|
|
72
72
|
* If the loading was aborted before the sitemaps were fully loaded, the request list might be missing some URLs.
|
|
73
73
|
* The `isSitemapFullyLoaded` method can be used to check if the sitemaps were fully loaded.
|
|
74
74
|
*
|
|
75
|
-
* If the loading is aborted and all the requests are handled, `
|
|
75
|
+
* If the loading is aborted and all the requests are handled, `checkReadiness()` will report `finished`.
|
|
76
76
|
*/
|
|
77
77
|
#abortLoading = false;
|
|
78
78
|
/** Number of URLs that were marked as handled */
|
|
@@ -300,14 +300,15 @@ export class SitemapRequestLoader {
|
|
|
300
300
|
/**
|
|
301
301
|
* @inheritDoc
|
|
302
302
|
*/
|
|
303
|
-
async
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
303
|
+
async checkReadiness() {
|
|
304
|
+
if (this.#urlQueueStream.readableLength > 0) {
|
|
305
|
+
return { status: 'ready' };
|
|
306
|
+
}
|
|
307
|
+
// The parser is still running, so more URLs may yet arrive - on no schedule, hence no `readyAt`.
|
|
308
|
+
if (!this.isSitemapFullyLoaded() && !this.#abortLoading) {
|
|
309
|
+
return { status: 'waiting' };
|
|
310
|
+
}
|
|
311
|
+
return this.inProgress.size === 0 ? { status: 'finished' } : { status: 'waiting' };
|
|
311
312
|
}
|
|
312
313
|
/**
|
|
313
314
|
* @inheritDoc
|
|
@@ -402,7 +403,7 @@ export class SitemapRequestLoader {
|
|
|
402
403
|
* @inheritDoc
|
|
403
404
|
*/
|
|
404
405
|
async *[Symbol.asyncIterator]() {
|
|
405
|
-
while (
|
|
406
|
+
while ((await this.checkReadiness()).status !== 'finished') {
|
|
406
407
|
const request = await this.fetchNextRequest();
|
|
407
408
|
if (!request)
|
|
408
409
|
break;
|