@@ -56,6 +56,15 @@ import {
5656 withWranglerPassthroughArgs ,
5757} from "./utils/utils.js" ;
5858
59+ // Maximum number of attempts to send the request
60+ export const MAX_REQUEST_RETRIES = 15 ;
61+ // Base delay for retries
62+ export const BASE_RETRY_DELAY_MS = 250 ;
63+ // Maximum delay for retries, used to calculate the backoff factor
64+ export const MAX_RETRY_DELAY_MS = 10_000 ;
65+ // Backoff factor for retries, calculated to ensure that the delay grows exponentially up to the maximum delay
66+ export const BACKOFF_FACTOR = ( MAX_RETRY_DELAY_MS / BASE_RETRY_DELAY_MS ) ** ( 1 / ( MAX_REQUEST_RETRIES - 1 ) ) ;
67+
5968/**
6069 * Implementation of the `opennextjs-cloudflare populateCache` command.
6170 *
@@ -324,14 +333,11 @@ async function populateR2IncrementalCache(
324333/**
325334 * Sends cache entries to the R2 worker, one entry per request.
326335 *
327- * Up to `concurrency` requests are in-flight at any given time.
328- * Retry logic for transient R2 write failures is handled by the worker.
329- *
330336 * @param options
331337 * @param options.workerUrl - The URL of the local R2 worker's `/populate` endpoint.
332338 * @param options.assets - The cache assets to write, as collected by {@link getCacheAssets}.
333339 * @param options.prefix - Optional prefix prepended to each R2 key.
334- * @param options.concurrency - Maximum number of concurrent in-flight requests.
340+ * @param options.maxConcurrency - Maximum number of concurrent in-flight requests.
335341 * @returns Resolves when all entries have been written successfully.
336342 * @throws {Error } If any entry fails after all retries or encounters a non-retryable error.
337343 */
@@ -343,41 +349,26 @@ async function sendEntriesToR2Worker(options: {
343349} ) : Promise < void > {
344350 const { workerUrl, assets, prefix, maxConcurrency } = options ;
345351
346- // Build the list of entries to send (key + filename).
347- // File contents are read lazily in sendEntryToR2Worker to avoid
348- // loading all cache values into memory at once.
349- const entries = assets . map ( ( { fullPath, key, buildId, isFetch } ) => ( {
350- key : computeCacheKey ( key , {
351- prefix,
352- buildId,
353- cacheType : isFetch ? "fetch" : "cache" ,
354- } ) ,
355- filename : fullPath ,
356- } ) ) ;
357-
358- // Use a concurrency-limited loop with a progress bar.
359- // `pending` tracks in-flight promises so we can cap concurrency.
360352 const pending = new Set < Promise < void > > ( ) ;
361353
362- let concurrency = 1 ;
354+ for ( const asset of tqdm ( assets ) ) {
355+ const { fullPath, key, buildId, isFetch } = asset ;
363356
364- for ( const entry of tqdm ( entries ) ) {
365357 const task = sendEntryToR2Worker ( {
366358 workerUrl,
367- key : entry . key ,
368- filename : entry . filename ,
359+ key : computeCacheKey ( key , {
360+ prefix,
361+ buildId,
362+ cacheType : isFetch ? "fetch" : "cache" ,
363+ } ) ,
364+ filename : fullPath ,
369365 } ) . finally ( ( ) => pending . delete ( task ) ) ;
370366
371367 pending . add ( task ) ;
372368
373369 // If we've reached the concurrency limit, wait for one to finish.
374- if ( pending . size >= concurrency ) {
370+ if ( pending . size >= maxConcurrency ) {
375371 await Promise . race ( pending ) ;
376- // Increase concurrency gradually to avoid overwhelming the worker
377- // with too many requests at once.
378- if ( concurrency < maxConcurrency ) {
379- concurrency ++ ;
380- }
381372 }
382373 }
383374
@@ -404,10 +395,7 @@ async function sendEntryToR2Worker(options: {
404395} ) : Promise < void > {
405396 const { workerUrl, key, filename } = options ;
406397
407- const CLIENT_RETRY_ATTEMPTS = 5 ;
408- const CLIENT_RETRY_BASE_DELAY_MS = 250 ;
409-
410- for ( let attempt = 0 ; attempt < CLIENT_RETRY_ATTEMPTS ; attempt ++ ) {
398+ for ( let attempt = 0 ; attempt < MAX_REQUEST_RETRIES ; attempt ++ ) {
411399 try {
412400 let response : Response ;
413401
@@ -426,9 +414,7 @@ async function sendEntryToR2Worker(options: {
426414 } catch ( e ) {
427415 throw new RetryableWorkerError (
428416 `Failed to send request to R2 worker: ${ e instanceof Error ? e . message : String ( e ) } ` ,
429- {
430- cause : e ,
431- }
417+ { cause : e }
432418 ) ;
433419 }
434420
@@ -438,6 +424,7 @@ async function sendEntryToR2Worker(options: {
438424 try {
439425 result = JSON . parse ( body ) as R2Response ;
440426 } catch ( e ) {
427+ // https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1102
441428 if ( body . includes ( "Worker exceeded resource limits" ) ) {
442429 throw new RetryableWorkerError ( "Worker exceeded resource limits" , { cause : e } ) ;
443430 }
@@ -454,25 +441,23 @@ async function sendEntryToR2Worker(options: {
454441 } ) ;
455442 }
456443
457- if ( ! result . success && response . status >= 500 ) {
458- throw new RetryableWorkerError ( result . error ) ;
459- }
460-
461444 if ( ! result . success ) {
462- throw new Error ( `Failed to write "${ key } " to R2: ${ result . error } ` ) ;
445+ throw response . status >= 500
446+ ? new RetryableWorkerError ( result . error )
447+ : new Error ( `Failed to write "${ key } " to R2: ${ result . error } ` ) ;
463448 }
464449
465450 return ;
466451 } catch ( e ) {
467- if ( e instanceof RetryableWorkerError && attempt < CLIENT_RETRY_ATTEMPTS - 1 ) {
452+ if ( e instanceof RetryableWorkerError && attempt < MAX_REQUEST_RETRIES - 1 ) {
468453 logger . error (
469454 `Attempt ${ attempt + 1 } to write "${ key } " failed with a retryable error: ${ e . message } . Retrying...`
470455 ) ;
471- await setTimeout ( CLIENT_RETRY_BASE_DELAY_MS * Math . pow ( 2 , attempt ) ) ;
456+ await setTimeout ( BASE_RETRY_DELAY_MS * Math . pow ( BACKOFF_FACTOR , attempt ) ) ;
472457 continue ;
473458 }
474459
475- throw new Error ( `Failed to write "${ key } " to R2 after ${ CLIENT_RETRY_ATTEMPTS } attempts` , {
460+ throw new Error ( `Failed to write "${ key } " to R2 after ${ MAX_REQUEST_RETRIES } attempts` , {
476461 cause : e ,
477462 } ) ;
478463 }
0 commit comments