From 2ef97e030bd09a88773842aa5e4fa1f152f6b1d0 Mon Sep 17 00:00:00 2001 From: Igor Loskutov Date: Fri, 20 Mar 2026 19:30:46 -0400 Subject: [PATCH] fix: preserve recentlyHandledRequestsCache across watchdog reset to prevent handled request refetch --- .../core/src/storages/request_provider.ts | 5 ++- test/core/storages/request_queue.test.ts | 35 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/core/src/storages/request_provider.ts b/packages/core/src/storages/request_provider.ts index dc1204abf5aa..5e29c8de7e97 100644 --- a/packages/core/src/storages/request_provider.ts +++ b/packages/core/src/storages/request_provider.ts @@ -676,7 +676,10 @@ export abstract class RequestProvider implements IStorage, IRequestManager { protected _reset() { this.lastActivity = new Date(); this.queueHeadIds.clear(); - this.recentlyHandledRequestsCache.clear(); + // NOTE: recentlyHandledRequestsCache is intentionally NOT cleared here. + // Clearing it causes handled requests to be refetched from the server + // after a watchdog reset, since listHead() may still return them due to + // server-side consistency delays. this.assumedTotalCount = 0; this.assumedHandledCount = 0; this.requestCache.clear(); diff --git a/test/core/storages/request_queue.test.ts b/test/core/storages/request_queue.test.ts index 881c4a91bf86..a6bdb553674b 100644 --- a/test/core/storages/request_queue.test.ts +++ b/test/core/storages/request_queue.test.ts @@ -702,6 +702,41 @@ describe('RequestQueue remote', () => { expect(deleteMock).toHaveBeenLastCalledWith(); }); + test('should not refetch handled requests after watchdog reset (V3)', async () => { + const queue = new RequestQueue({ id: 'v3-test', client: storageClient }); + const requestA = new Request({ url: 'http://example.com/a' }); + + vitest.spyOn(queue.client, 'addRequest').mockResolvedValueOnce({ + requestId: 'a', + wasAlreadyHandled: false, + wasAlreadyPresent: false, + }); + await queue.addRequest(requestA, { forefront: true }); + + vitest.spyOn(queue.client, 'getRequest').mockResolvedValueOnce({ ...requestA, id: 'a' }); + const fetched = await queue.fetchNextRequest(); + + vitest.spyOn(queue.client, 'updateRequest').mockResolvedValueOnce({ + requestId: 'a', + wasAlreadyHandled: false, + wasAlreadyPresent: true, + }); + await queue.markRequestHandled(fetched!); + + // Simulate watchdog reset (isFinished() calls _reset() after inactivity timeout) + queue['_reset'](); + + // Cache must survive reset — it's the only guard against server returning stale handled requests + expect(queue['recentlyHandledRequestsCache'].get('a')).toBe(true); + + vitest.spyOn(queue.client, 'listHead').mockResolvedValueOnce({ + items: [{ id: 'a', uniqueKey: 'aaa' }], + } as never); + + const refetched = await queue.fetchNextRequest(); + expect(refetched).toBe(null); + }); + test('Request.userData.__crawlee internal object is non-enumerable and always defined', async () => { const url = 'http://example.com'; const method = 'POST';