From 0b31a8b57cdeee4cead83db6c48febf48b988f6f Mon Sep 17 00:00:00 2001 From: Anas Khan Date: Thu, 23 Jul 2026 12:15:55 +0530 Subject: [PATCH 1/2] fix(core): unblock backpressured sitemap load on persistState (#3863) Solves a deadlock caused by a never-drained stream on `SitemapRequestList.persistState`. Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com> --- .../core/src/storages/sitemap_request_list.ts | 12 +++++-- test/core/sitemap_request_list.test.ts | 36 +++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/packages/core/src/storages/sitemap_request_list.ts b/packages/core/src/storages/sitemap_request_list.ts index 1b9903f05d9d..5d77e02c14bb 100644 --- a/packages/core/src/storages/sitemap_request_list.ts +++ b/packages/core/src/storages/sitemap_request_list.ts @@ -505,18 +505,26 @@ export class SitemapRequestList implements IRequestList { // Create a new stream, as we have read all the URLs from the current one. // Pushing the urls back to the original stream might not be possible if it has been ended. - const newStream = this.createNewStream(this.urlQueueStream.readableHighWaterMark); + const previousStream = this.urlQueueStream; + const newStream = this.createNewStream(previousStream.readableHighWaterMark); for (const url of urlQueue) { newStream.push(url); } - if (this.urlQueueStream.writableEnded) { + if (previousStream.writableEnded) { newStream.end(); } this.urlQueueStream = newStream; + // A `pushNextUrl()` call may be blocked on backpressure, waiting for a `readdata` event on the + // previous stream. That event is only ever emitted by `readNextUrl()` on the current stream, so + // after the swap the waiter would never be notified and the background sitemap loading would hang. + // Re-emit `readdata` on the previous stream to release any such pending waiter (its URL has already + // been transferred to the new stream above). + previousStream.emit('readdata'); + await this.store.setValue(this.persistStateKey, { sitemapParsingProgress: { pendingSitemapUrls: Array.from(this.sitemapParsingProgress.pendingSitemapUrls), diff --git a/test/core/sitemap_request_list.test.ts b/test/core/sitemap_request_list.test.ts index 5daca1aec70c..ecf453162b06 100644 --- a/test/core/sitemap_request_list.test.ts +++ b/test/core/sitemap_request_list.test.ts @@ -690,4 +690,40 @@ describe('SitemapRequestList', () => { // The strategy is persisted on the request so it keeps being enforced after navigation. expect((request as any).enqueueStrategy).toBe('same-hostname'); }); + + test('persistState does not deadlock a backpressured sitemap load', async () => { + // `maxBufferSize: 1` makes the background loader block on backpressure right after + // pushing the first URL. Persisting the state at that moment swaps the underlying stream, + // which used to orphan the pending push and hang the loading indefinitely. + const list = await SitemapRequestList.open({ + sitemapUrls: [`${url}/sitemap.xml`], + persistStateKey: 'backpressure-persist', + maxBufferSize: 1, + }); + + // Wait until the first URL is buffered, i.e. the loader is parked on backpressure. + while (await list.isEmpty()) { + await sleep(20); + } + + await list.persistState(); + + const urls = new Set(); + for await (const request of list) { + await list.markRequestHandled(request); + urls.add(request.url); + } + + expect(list.isSitemapFullyLoaded()).toBe(true); + await expect(list.isFinished()).resolves.toBe(true); + expect(urls).toEqual( + new Set([ + 'http://not-exists.com/', + 'http://not-exists.com/catalog?item=12&desc=vacation_hawaii', + 'http://not-exists.com/catalog?item=73&desc=vacation_new_zealand', + 'http://not-exists.com/catalog?item=74&desc=vacation_newfoundland', + 'http://not-exists.com/catalog?item=83&desc=vacation_usa', + ]), + ); + }, 10_000); }); From d0d8fa15cc42a0b8f74c065761f4c0e33ce8e66d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20B=C3=A4r?= Date: Thu, 23 Jul 2026 11:27:23 +0200 Subject: [PATCH 2/2] chore(test): sitemap backpressure test hangs on cross-hostname URLs (#3885) The default same-hostname enqueue strategy silently drops the test's not-exists.com URLs, so the loader never pushes anything and the test hangs waiting. Regressed by the interaction between #3797 and #3863. --- test/core/sitemap_request_list.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/core/sitemap_request_list.test.ts b/test/core/sitemap_request_list.test.ts index ecf453162b06..24397b9ec1c9 100644 --- a/test/core/sitemap_request_list.test.ts +++ b/test/core/sitemap_request_list.test.ts @@ -699,6 +699,7 @@ describe('SitemapRequestList', () => { sitemapUrls: [`${url}/sitemap.xml`], persistStateKey: 'backpressure-persist', maxBufferSize: 1, + enqueueStrategy: 'all', }); // Wait until the first URL is buffered, i.e. the loader is parked on backpressure.