Skip to content

Commit f1ba93d

Browse files
fix: size the shared crawler pool from the run, not from a request
1 parent 370f28c commit f1ba93d

2 files changed

Lines changed: 40 additions & 6 deletions

File tree

‎src/input.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ async function getProxyConfiguration(proxyOptions: ProxyOptions) {
4646

4747
const DEFAULT_PROXY_OPTIONS = ragWebBrowserInputSchema.properties.proxyConfiguration.default as ProxyOptions;
4848

49+
/**
50+
* How many pages the shared crawlers fetch in parallel to begin with. Set from the run's own input at
51+
* startup, never from a request: see `processInput`.
52+
*/
53+
let runDesiredConcurrency = ragWebBrowserInputSchema.properties.desiredConcurrency.default;
54+
4955
/**
5056
* The configuration every crawler is built with. It resolves the proxy from the request Crawlee is
5157
* about to send rather than from the input the crawler was built with, so one crawler serves callers
@@ -92,6 +98,7 @@ async function validateProxyConfiguration(proxyOptions: ProxyOptions, abortOnFai
9298
export async function processStandbyInput(originalInput: Partial<Input>) {
9399
const { input, searchCrawlerOptions, contentScraperSettings } = await processInputInternal(originalInput, true);
94100

101+
runDesiredConcurrency = input.desiredConcurrency;
95102
await validateProxyConfiguration(input.proxyConfiguration, true);
96103
const contentCrawlerOptions: ContentCrawlerOptions[] = [
97104
createPlaywrightCrawlerOptions(input),
@@ -110,6 +117,13 @@ export async function processInput(originalInput: Partial<Input>) {
110117
// In Standby this runs once per request, where a proxy problem must fail that request alone. In
111118
// Normal mode it is the run's startup, where there is nothing to serve and the run ends.
112119
await validateProxyConfiguration(input.proxyConfiguration, !isActorStandby());
120+
121+
// A Standby request builds these options whenever no crawler is cached yet, at boot before the
122+
// crawlers are up or after a failed one was dropped, so whichever caller got there first would
123+
// size the pool that every later caller then shares. The run sizes it instead. Nobody loses a
124+
// guarantee: the pool is sized for the instance, not per caller, and it autoscales from here.
125+
if (isActorStandby()) input.desiredConcurrency = runDesiredConcurrency;
126+
113127
const contentCrawlerOptions: ContentCrawlerOptions = input.scrapingTool === 'raw-http'
114128
? createCheerioCrawlerOptions(input, false)
115129
: createPlaywrightCrawlerOptions(input, false);

‎tests/input.test.ts‎

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Actor } from 'apify';
12
import { log } from 'crawlee';
23
import { afterEach, describe, expect, it, vi } from 'vitest';
34

@@ -6,6 +7,12 @@ import { parseParameters } from '../src/utils.js';
67

78
process.env.ACTOR_FULL_NAME = 'apify/rag-web-browser';
89

10+
// A custom proxy keeps `Actor.createProxyConfiguration` off the network.
11+
const CALLER_PROXY = encodeURIComponent(JSON.stringify({
12+
useApifyProxy: false,
13+
proxyUrls: ['http://caller.invalid:8000'],
14+
}));
15+
916
describe('warnIfLowMemoryForPlaywright', () => {
1017
afterEach(() => {
1118
vi.restoreAllMocks();
@@ -38,15 +45,28 @@ describe('warnIfLowMemoryForPlaywright', () => {
3845

3946
describe('the log level', () => {
4047
it('ignores a caller\'s debugMode, which would switch logging for everyone sharing the run', async () => {
41-
// A custom proxy keeps `Actor.createProxyConfiguration` off the network.
42-
const proxy = encodeURIComponent(JSON.stringify({
43-
useApifyProxy: false,
44-
proxyUrls: ['http://log-level.invalid:8000'],
45-
}));
4648
log.setLevel(log.LEVELS.INFO);
4749

48-
await processInput(parseParameters(`?query=hello&debugMode=true&proxyConfiguration=${proxy}`));
50+
await processInput(parseParameters(`?query=hello&debugMode=true&proxyConfiguration=${CALLER_PROXY}`));
4951

5052
expect(log.getLevel()).toBe(log.LEVELS.INFO);
5153
});
5254
});
55+
56+
describe('the shared crawler pool', () => {
57+
afterEach(() => {
58+
vi.restoreAllMocks();
59+
});
60+
61+
// A request builds these options whenever no crawler is cached yet, so without this the first
62+
// caller through that window would size the pool every later caller shares.
63+
it('is sized by the run, not by whichever caller happens to build it', async () => {
64+
vi.spyOn(Actor, 'getEnv').mockReturnValue({ metaOrigin: 'STANDBY' } as never);
65+
66+
const { contentCrawlerOptions } = await processInput(
67+
parseParameters(`?query=hello&desiredConcurrency=17&proxyConfiguration=${CALLER_PROXY}`),
68+
);
69+
70+
expect(contentCrawlerOptions.crawlerOptions.autoscaledPoolOptions?.desiredConcurrency).toBe(5);
71+
});
72+
});

0 commit comments

Comments
 (0)