Skip to content

Commit ca5564e

Browse files
fix: serve every standby request from the crawlers started at boot
1 parent 174e78b commit ca5564e

13 files changed

Lines changed: 277 additions & 123 deletions

‎src/const.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,17 @@ export enum ContentCrawlerTypes {
1818
CHEERIO = 'cheerio',
1919
}
2020

21+
/** Identifies which of the crawlers a key belongs to. */
22+
export type CrawlerKind = 'search' | ContentCrawlerTypes;
23+
24+
/**
25+
* The crawlers are shared by every Standby request, so these cannot be built from one request's input.
26+
* They are the widest the input schemas allow; each request then narrows them down to its own values,
27+
* `maxRequestRetries` through `Request.maxRetries` and the timeout through a pre-navigation hook.
28+
*/
29+
export const CRAWLER_MAX_REQUEST_RETRIES = 5;
30+
export const CRAWLER_REQUEST_HANDLER_TIMEOUT_SECS = 300;
31+
2132
export const PLAYWRIGHT_REQUEST_TIMEOUT_NORMAL_MODE_SECS = 60;
2233

2334
export const GOOGLE_STANDARD_RESULTS_PER_PAGE = 10;

‎src/crawlers.ts‎

Lines changed: 72 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { createHash } from 'node:crypto';
12
import { readFile } from 'node:fs/promises';
23

34
import { ImpitHttpClient } from '@crawlee/impit-client';
@@ -18,6 +19,7 @@ import {
1819
} from 'crawlee';
1920

2021
import { chargeFetch, chargeSearch } from './charging.js';
22+
import type { CrawlerKind } from './const.js';
2123
import { ContentCrawlerTypes, GOOGLE_STANDARD_RESULTS_PER_PAGE } from './const.js';
2224
import { deduplicateResults, scrapeOrganicResults } from './google-search/google-extractors-urls.js';
2325
import {
@@ -27,10 +29,19 @@ import {
2729
TEXT_DOCUMENT_CONTENT_TYPES,
2830
} from './request-handler.js';
2931
import { addEmptyResultToResponse, addResultToResponse, sendResponseError, sendResponseIfFinished } from './responses.js';
30-
import type { ContentCrawlerOptions, ContentCrawlerUserData, Output, SearchCrawlerUserData } from './types.js';
32+
import type {
33+
ContentCrawlerOptions,
34+
ContentCrawlerUserData,
35+
Output,
36+
ProxyOptions,
37+
SearchCrawlerOptions,
38+
SearchCrawlerUserData,
39+
} from './types.js';
3140
import { addTimeMeasureEvent, createRequest, createSearchRequest } from './utils.js';
3241

33-
const crawlers = new Map<string, CheerioCrawler | PlaywrightCrawler>();
42+
type Crawler = CheerioCrawler | PlaywrightCrawler;
43+
44+
const crawlers = new Map<string, Crawler>();
3445
const client = new MemoryStorage({ persistStorage: false });
3546

3647
const contentCrawlerHttpClient = new ImpitHttpClient({
@@ -56,26 +67,58 @@ async function getGhosteryBlocker(): Promise<PlaywrightBlocker | undefined> {
5667
}
5768
}
5869

59-
export function getCrawlerKey(crawlerOptions: CheerioCrawlerOptions | PlaywrightCrawlerOptions) {
60-
return JSON.stringify(crawlerOptions);
70+
/**
71+
* Identifies a crawler in the `crawlers` cache, and names its request queue. Keyed off the caller's
72+
* proxy options rather than the built `ProxyConfiguration`, whose child logger snapshots the log level
73+
* and whose URL rotation state mutates - both of which kept changing the key. Hashed because the key
74+
* is logged and used as a queue name, while the options can hold credentials.
75+
*/
76+
export function getCrawlerKey(kind: CrawlerKind, proxyOptions: ProxyOptions): string {
77+
const hash = createHash('sha1').update(JSON.stringify(proxyOptions)).digest('hex');
78+
return `${kind}-${hash.slice(0, 12)}`;
79+
}
80+
81+
/** How many crawlers are alive. Exported so tests can assert that requests do not fork new ones. */
82+
export function getCrawlerCount() {
83+
return crawlers.size;
84+
}
85+
86+
async function getOrCreateCrawler(key: string, kind: CrawlerKind, startCrawler: boolean, build: () => Promise<Crawler>) {
87+
const cached = crawlers.get(key);
88+
if (cached) {
89+
return cached;
90+
}
91+
92+
log.info(`Creating new ${kind} crawler with key ${key}`);
93+
const crawler = await build();
94+
if (startCrawler) {
95+
crawler.run().then(
96+
() => log.warning(`Crawler ${kind} has finished`),
97+
// eslint-disable-next-line @typescript-eslint/no-empty-function
98+
() => { },
99+
);
100+
log.info(`Crawler ${kind} has started 💪🏼`);
101+
}
102+
crawlers.set(key, crawler);
103+
log.info(`Number of crawlers ${crawlers.size}`);
104+
return crawler;
61105
}
62106

63107
/**
64-
* Adds a content crawl request to selected content crawler.
65-
* Get existing crawler based on crawlerOptions and scraperSettings, if not present -> create new
108+
* Adds a content crawl request to the content crawler identified by `contentCrawlerKey`.
66109
*/
67110
export const addContentCrawlRequest = async (
68111
request: RequestOptions<ContentCrawlerUserData>,
69112
responseId: string,
70113
contentCrawlerKey: string,
71114
) => {
72115
const crawler = crawlers.get(contentCrawlerKey);
73-
const name = crawler instanceof PlaywrightCrawler ? 'playwright' : 'cheerio';
74-
75116
if (!crawler) {
76-
log.error(`Content crawler not found: key ${contentCrawlerKey}`);
117+
log.error(`Content crawler not found: ${contentCrawlerKey}`);
77118
return;
78119
}
120+
121+
const name = crawler instanceof PlaywrightCrawler ? 'playwright' : 'cheerio';
79122
try {
80123
await crawler.requestQueue!.addRequest(request);
81124
// create an empty result in search request response
@@ -92,17 +135,13 @@ export const addContentCrawlRequest = async (
92135
* A crawler won't be created if it already exists.
93136
*/
94137
export async function createAndStartSearchCrawler(
95-
searchCrawlerOptions: CheerioCrawlerOptions,
138+
searchCrawlerOptions: SearchCrawlerOptions,
96139
startCrawler = true,
97140
) {
98-
const key = getCrawlerKey(searchCrawlerOptions);
99-
if (crawlers.has(key)) {
100-
return { key, crawler: crawlers.get(key) };
101-
}
102-
103-
log.info(`Creating new cheerio crawler with key ${key}`);
104-
const crawler = new CheerioCrawler({
105-
...(searchCrawlerOptions as CheerioCrawlerOptions),
141+
const { crawlerOptions, proxyOptions } = searchCrawlerOptions;
142+
const key = getCrawlerKey('search', proxyOptions);
143+
const crawler = await getOrCreateCrawler(key, 'search', startCrawler, async () => new CheerioCrawler({
144+
...crawlerOptions,
106145
requestQueue: await RequestQueue.open(key, { storageClient: client }),
107146
requestHandler: async ({ request, $: _$, addRequests }: CheerioCrawlingContext<SearchCrawlerUserData>) => {
108147
// NOTE: we need to cast this to fix `cheerio` type errors
@@ -150,7 +189,7 @@ export async function createAndStartSearchCrawler(
150189
collectedResults: deduplicated,
151190
currentPage: nextPage,
152191
},
153-
searchCrawlerOptions.proxyConfiguration,
192+
proxyOptions,
154193
nextOffset,
155194
);
156195
await addRequests([nextRequest]);
@@ -182,17 +221,8 @@ export async function createAndStartSearchCrawler(
182221
const errorResponse = { errorMessage: err.message };
183222
sendResponseError(request.uniqueKey, JSON.stringify(errorResponse));
184223
},
185-
});
186-
if (startCrawler) {
187-
crawler.run().then(
188-
() => log.warning('Google-search-crawler has finished'),
189-
// eslint-disable-next-line @typescript-eslint/no-empty-function
190-
() => { },
191-
);
192-
log.info('Google-search-crawler has started 🫡');
193-
}
194-
crawlers.set(key, crawler);
195-
log.info(`Number of crawlers ${crawlers.size}`);
224+
}));
225+
196226
return { key, crawler };
197227
}
198228

@@ -205,27 +235,15 @@ export async function createAndStartContentCrawler(
205235
contentCrawlerOptions: ContentCrawlerOptions,
206236
startCrawler = true,
207237
) {
208-
const { type: crawlerType, crawlerOptions } = contentCrawlerOptions;
238+
const { type: crawlerType, crawlerOptions, proxyOptions } = contentCrawlerOptions;
209239

210-
const key = getCrawlerKey(crawlerOptions);
211-
if (crawlers.has(key)) {
212-
return { key, crawler: crawlers.get(key) };
213-
}
240+
const key = getCrawlerKey(crawlerType, proxyOptions);
241+
const crawler = await getOrCreateCrawler(key, crawlerType, startCrawler, async () => (
242+
crawlerType === ContentCrawlerTypes.PLAYWRIGHT
243+
? createPlaywrightContentCrawler(crawlerOptions, key)
244+
: createCheerioContentCrawler(crawlerOptions, key)
245+
));
214246

215-
const crawler = crawlerType === 'playwright'
216-
? await createPlaywrightContentCrawler(crawlerOptions, key)
217-
: await createCheerioContentCrawler(crawlerOptions, key);
218-
219-
if (startCrawler) {
220-
crawler.run().then(
221-
() => log.warning(`Crawler ${crawlerType} has finished`),
222-
// eslint-disable-next-line @typescript-eslint/no-empty-function
223-
() => { },
224-
);
225-
log.info(`Crawler ${crawlerType} has started 💪🏼`);
226-
}
227-
crawlers.set(key, crawler);
228-
log.info(`Number of crawlers ${crawlers.size}`);
229247
return { key, crawler };
230248
}
231249

@@ -261,7 +279,6 @@ async function createPlaywrightContentCrawler(
261279
crawlerOptions: PlaywrightCrawlerOptions,
262280
key: string,
263281
): Promise<PlaywrightCrawler> {
264-
log.info(`Creating new playwright crawler with key ${key}`);
265282
const blocker = await getGhosteryBlocker();
266283
return new PlaywrightCrawler({
267284
...crawlerOptions,
@@ -284,7 +301,6 @@ async function createCheerioContentCrawler(
284301
crawlerOptions: CheerioCrawlerOptions,
285302
key: string,
286303
): Promise<CheerioCrawler> {
287-
log.info(`Creating new cheerio crawler with key ${key}`);
288304
return new CheerioCrawler({
289305
...crawlerOptions,
290306
keepAlive: crawlerOptions.keepAlive,
@@ -305,20 +321,19 @@ async function createCheerioContentCrawler(
305321
}
306322

307323
/**
308-
* Adds a search request to the Google search crawler.
324+
* Adds a search request to the search crawler identified by `searchCrawlerKey`.
309325
* Create a response for the request and set the desired number of results (maxResults).
310326
*/
311327
export const addSearchRequest = async (
312328
request: RequestOptions<ContentCrawlerUserData>,
313-
searchCrawlerOptions: CheerioCrawlerOptions,
329+
searchCrawlerKey: string,
314330
) => {
315-
const key = getCrawlerKey(searchCrawlerOptions);
316-
const crawler = crawlers.get(key);
317-
331+
const crawler = crawlers.get(searchCrawlerKey);
318332
if (!crawler) {
319-
log.error(`Cheerio crawler not found: key ${key}`);
333+
log.error(`Search crawler not found: ${searchCrawlerKey}`);
320334
return;
321335
}
336+
322337
addTimeMeasureEvent(request.userData!, 'before-cheerio-queue-add');
323338
await crawler.requestQueue!.addRequest(request);
324339
log.info(`Added request to cheerio-google-search-crawler: ${request.url}`);

0 commit comments

Comments
 (0)