@crawlee/browser-pool 3.18.2-beta.4 → 3.18.2-beta.5

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.
@@ -41,6 +41,8 @@ export declare abstract class BrowserController<Library extends CommonLibrary =
41
41
  proxyUrl?: string;
42
42
  isActive: boolean;
43
43
  activePages: number;
44
+ private readonly closedPages;
45
+ private readonly pageTeardowns;
44
46
  totalPages: number;
45
47
  lastPageOpenedAt: number;
46
48
  private _activate;
@@ -77,6 +79,23 @@ export declare abstract class BrowserController<Library extends CommonLibrary =
77
79
  * @ignore
78
80
  */
79
81
  newPage(pageOptions?: NewPageOptions): Promise<NewPageResult>;
82
+ /**
83
+ * Accounts for a page no longer being open. Callable from either side and safe to call twice:
84
+ * the page's own `close` event, or the pool when it gives up on a close that never settles.
85
+ * Tracked in a `WeakSet` rather than via `isClosed()`, because that reports the same event.
86
+ * @internal
87
+ */
88
+ registerPageClosed(page: NewPageResult): void;
89
+ /**
90
+ * Registers cleanup belonging to a single page: an anonymizing proxy server, an incognito
91
+ * context. It runs from `registerPageClosed`, so it happens whether the page closed on its own
92
+ * or the pool gave up on a close that never settled, and it runs at most once.
93
+ *
94
+ * Hanging it off the page's `close` event instead leaks it in exactly the case this controller
95
+ * now handles, because that event does not arrive for a page the browser never destroyed.
96
+ * @ignore
97
+ */
98
+ protected registerPageTeardown(page: NewPageResult, teardown: () => Promise<void>): void;
80
99
  setCookies(page: NewPageResult, cookies: Cookie[]): Promise<void>;
81
100
  getCookies(page: NewPageResult): Promise<Cookie[]>;
82
101
  /**
@@ -82,6 +82,18 @@ class BrowserController extends tiny_typed_emitter_1.TypedEmitter {
82
82
  writable: true,
83
83
  value: 0
84
84
  });
85
+ Object.defineProperty(this, "closedPages", {
86
+ enumerable: true,
87
+ configurable: true,
88
+ writable: true,
89
+ value: new WeakSet()
90
+ });
91
+ Object.defineProperty(this, "pageTeardowns", {
92
+ enumerable: true,
93
+ configurable: true,
94
+ writable: true,
95
+ value: new WeakMap()
96
+ });
85
97
  Object.defineProperty(this, "totalPages", {
86
98
  enumerable: true,
87
99
  configurable: true,
@@ -195,6 +207,37 @@ class BrowserController extends tiny_typed_emitter_1.TypedEmitter {
195
207
  this.lastPageOpenedAt = Date.now();
196
208
  return page;
197
209
  }
210
+ /**
211
+ * Accounts for a page no longer being open. Callable from either side and safe to call twice:
212
+ * the page's own `close` event, or the pool when it gives up on a close that never settles.
213
+ * Tracked in a `WeakSet` rather than via `isClosed()`, because that reports the same event.
214
+ * @internal
215
+ */
216
+ registerPageClosed(page) {
217
+ if (this.closedPages.has(page))
218
+ return;
219
+ this.closedPages.add(page);
220
+ this.activePages--;
221
+ const teardown = this.pageTeardowns.get(page);
222
+ if (!teardown)
223
+ return;
224
+ this.pageTeardowns.delete(page);
225
+ teardown().catch((error) => {
226
+ logger_1.log.debug(`Could not clean up after a closed page.\nCause:${error.message}`, { id: this.id });
227
+ });
228
+ }
229
+ /**
230
+ * Registers cleanup belonging to a single page: an anonymizing proxy server, an incognito
231
+ * context. It runs from `registerPageClosed`, so it happens whether the page closed on its own
232
+ * or the pool gave up on a close that never settled, and it runs at most once.
233
+ *
234
+ * Hanging it off the page's `close` event instead leaks it in exactly the case this controller
235
+ * now handles, because that event does not arrive for a page the browser never destroyed.
236
+ * @ignore
237
+ */
238
+ registerPageTeardown(page, teardown) {
239
+ this.pageTeardowns.set(page, teardown);
240
+ }
198
241
  async setCookies(page, cookies) {
199
242
  return this._setCookies(page, cookies);
200
243
  }
package/browser-pool.js CHANGED
@@ -14,6 +14,7 @@ const timeout_1 = require("@apify/timeout");
14
14
  const hooks_1 = require("./fingerprinting/hooks");
15
15
  const logger_1 = require("./logger");
16
16
  const PAGE_CLOSE_KILL_TIMEOUT_MILLIS = 1000;
17
+ const PAGE_CLOSE_TIMEOUT_MILLIS = 5000;
17
18
  const BROWSER_KILLER_INTERVAL_MILLIS = 10 * 1000;
18
19
  /**
19
20
  * The `BrowserPool` class is the most important class of the `browser-pool` module.
@@ -582,11 +583,48 @@ class BrowserPool extends tiny_typed_emitter_1.TypedEmitter {
582
583
  const browserController = this.pageToBrowserController.get(page);
583
584
  const pageId = this.getPageId(page);
584
585
  page.close = async (...args) => {
585
- await this._executeHooks(this.prePageCloseHooks, page, browserController);
586
- await originalPageClose.apply(page, args).catch((err) => {
587
- logger_1.log.debug(`Could not close page.\nCause:${err.message}`, { id: browserController.id });
588
- });
589
- await this._executeHooks(this.postPageCloseHooks, pageId, browserController);
586
+ // A browser can acknowledge the close and then never destroy the target, in which case
587
+ // this never settles and the page's own `close` event never fires either (Chromium
588
+ // 536385539). Bound the whole sequence so nothing here can hang the caller, then
589
+ // reconcile the pool regardless of the outcome, so a page that refuses to close cannot
590
+ // leave the pool believing it is still open. `Promise.race` rather than
591
+ // `addTimeoutToPromise`: the latter inherits its AbortController from the calling frame,
592
+ // so a timeout here would cancel the task of whoever awaited `page.close()`.
593
+ let pageClosed = false;
594
+ const closing = (async () => {
595
+ await this._executeHooks(this.prePageCloseHooks, page, browserController);
596
+ await originalPageClose.apply(page, args).catch((err) => {
597
+ logger_1.log.debug(`Could not close page.\nCause:${err.message}`, { id: browserController.id });
598
+ });
599
+ // Whether the page itself is gone. Tracked separately from the sequence finishing,
600
+ // so that a slow hook does not get the browser retired.
601
+ pageClosed = true;
602
+ await this._executeHooks(this.postPageCloseHooks, pageId, browserController);
603
+ })();
604
+ let timeout;
605
+ const finished = await Promise.race([
606
+ closing.then(() => true, (err) => {
607
+ logger_1.log.warning(`Closing a page failed, releasing it from the pool anyway.\nCause:${err.message}`, {
608
+ id: browserController.id,
609
+ pageId,
610
+ });
611
+ return true;
612
+ }),
613
+ new Promise((resolve) => {
614
+ timeout = setTimeout(() => resolve(false), PAGE_CLOSE_TIMEOUT_MILLIS);
615
+ }),
616
+ ]);
617
+ clearTimeout(timeout);
618
+ if (!finished) {
619
+ logger_1.log.warning(`Closing a page did not finish within ${PAGE_CLOSE_TIMEOUT_MILLIS / 1000} seconds, ` +
620
+ 'releasing it from the pool anyway.', { id: browserController.id, pageId });
621
+ }
622
+ if (!pageClosed) {
623
+ // The page is still attached, so this browser cannot be trusted with more work.
624
+ // Retiring it lets the reclamation below close the process once its pages drain.
625
+ this.retireBrowserController(browserController);
626
+ }
627
+ browserController.registerPageClosed(page);
590
628
  this.pages.delete(pageId);
591
629
  this._closeRetiredBrowserWithNoPages(browserController);
592
630
  this.emit("pageClosed" /* BROWSER_POOL_EVENTS.PAGE_CLOSED */, page);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlee/browser-pool",
3
- "version": "3.18.2-beta.4",
3
+ "version": "3.18.2-beta.5",
4
4
  "description": "Rotate multiple browsers using popular automation libraries such as Playwright or Puppeteer.",
5
5
  "engines": {
6
6
  "node": ">=16.0.0"
@@ -38,8 +38,8 @@
38
38
  "dependencies": {
39
39
  "@apify/log": "^2.4.0",
40
40
  "@apify/timeout": "^0.4.0",
41
- "@crawlee/core": "3.18.2-beta.4",
42
- "@crawlee/types": "3.18.2-beta.4",
41
+ "@crawlee/core": "3.18.2-beta.5",
42
+ "@crawlee/types": "3.18.2-beta.5",
43
43
  "fingerprint-generator": "^2.1.68",
44
44
  "fingerprint-injector": "^2.1.68",
45
45
  "lodash.merge": "^4.6.2",
@@ -70,5 +70,5 @@
70
70
  }
71
71
  }
72
72
  },
73
- "gitHead": "ee9640fb13d1414a03fcd21ccc5255bca3963aef"
73
+ "gitHead": "d126d83399f7aa71bd0920d6a50b6bb9e6373790"
74
74
  }
@@ -49,9 +49,9 @@ class PlaywrightController extends browser_controller_1.BrowserController {
49
49
  }
50
50
  try {
51
51
  const page = await this.browser.newPage(contextOptions);
52
- page.once('close', async () => {
53
- this.activePages--;
54
- await close();
52
+ this.registerPageTeardown(page, close);
53
+ page.once('close', () => {
54
+ this.registerPageClosed(page);
55
55
  });
56
56
  if (this.launchContext.experimentalContainers) {
57
57
  await page.goto('data:text/plain,tabid');
@@ -55,17 +55,18 @@ class PuppeteerController extends browser_controller_1.BrowserController {
55
55
  tryCancel();
56
56
  }
57
57
  */
58
- page.once('close', async () => {
59
- this.activePages--;
60
- try {
61
- await context.close();
62
- }
63
- catch (error) {
58
+ this.registerPageTeardown(page, async () => {
59
+ // The proxy server is not chained behind the context close: that can hang on
60
+ // the same target the page hung on, and the proxy runs in this process, so it
61
+ // has to be reclaimed either way.
62
+ const contextClosed = context.close().catch((error) => {
64
63
  logger_1.log.exception(error, 'Failed to close context.');
65
- }
66
- finally {
67
- await close();
68
- }
64
+ });
65
+ await close();
66
+ await contextClosed;
67
+ });
68
+ page.once('close', () => {
69
+ this.registerPageClosed(page);
69
70
  });
70
71
  return page;
71
72
  }
@@ -77,7 +78,7 @@ class PuppeteerController extends browser_controller_1.BrowserController {
77
78
  const page = await this.browser.newPage();
78
79
  (0, timeout_1.tryCancel)();
79
80
  page.once('close', () => {
80
- this.activePages--;
81
+ this.registerPageClosed(page);
81
82
  });
82
83
  return page;
83
84
  }