diff --git a/packages/cloudflare/src/cacheability-probe.ts b/packages/cloudflare/src/cacheability-probe.ts index 89a7cc399..2ac6d9f02 100644 --- a/packages/cloudflare/src/cacheability-probe.ts +++ b/packages/cloudflare/src/cacheability-probe.ts @@ -726,58 +726,37 @@ export async function probeStagedWorkerCacheability(options: { reportProgress(); }; - reportProgress(); - let activeProbes = 0; - const slotWaiters: Array<() => void> = []; - const acquireProbeSlot = async (): Promise => { - if (activeProbes < concurrency) { - activeProbes++; - return; - } - await new Promise((resolve) => slotWaiters.push(resolve)); - }; - const releaseProbeSlot = (): void => { - const next = slotWaiters.shift(); - if (next) next(); - else activeProbes--; - }; - let pendingRouteMovers = groups.filter( - (group) => group.primary.route?.cacheabilityProbe?.routeMayResolve === true, - ).length; - let settleRouteMovers: (() => void) | undefined; - const routeMoversSettled = - pendingRouteMovers === 0 - ? Promise.resolve() - : new Promise((resolve) => { - settleRouteMovers = resolve; - }); - const runGroup = async (group: ConcretePathGroup): Promise => { - const mayResolveRoute = group.primary.route?.cacheabilityProbe?.routeMayResolve === true; - while (true) { - if (!mayResolveRoute && group.pattern.pruned && pendingRouteMovers > 0) { - await routeMoversSettled; - } - await acquireProbeSlot(); - if (!mayResolveRoute && group.pattern.pruned && pendingRouteMovers > 0) { - releaseProbeSlot(); - continue; + const runGroups = async (scheduledGroups: readonly ConcretePathGroup[]): Promise => { + let nextIndex = 0; + const worker = async (): Promise => { + while (!limitFailure && !phaseTimedOut && nextIndex < scheduledGroups.length) { + await classifyConcretePath(scheduledGroups[nextIndex++]); } - break; - } - try { - if (!limitFailure && !phaseTimedOut) await classifyConcretePath(group); - } finally { - releaseProbeSlot(); - if (mayResolveRoute && --pendingRouteMovers === 0) settleRouteMovers?.(); - } + }; + await Promise.all( + Array.from({ length: Math.min(concurrency, scheduledGroups.length) }, () => worker()), + ); }; - const initialPatternGroups = Array.from(patterns.values(), (pattern) => [...pattern.groups]); - await Promise.all( - initialPatternGroups.map(async ([representative, ...siblings]) => { - await runGroup(representative); - await Promise.all(siblings.map(runGroup)); - }), + + reportProgress(); + const routeMovingGroups = groups.filter( + (group) => group.primary.route?.cacheabilityProbe?.routeMayResolve === true, ); + const routeMovingGroupSet = new Set(routeMovingGroups); + const representativeGroups: ConcretePathGroup[] = []; + const siblingGroups: ConcretePathGroup[] = []; + for (const pattern of patterns.values()) { + const [representative, ...siblings] = pattern.groups; + if (representative && !routeMovingGroupSet.has(representative)) { + representativeGroups.push(representative); + } + siblingGroups.push(...siblings.filter((group) => !routeMovingGroupSet.has(group))); + } + // Resolve every route-moving public path before a destination pattern may + // be pruned, then use the same bounded worker loops as ordinary CDN warming. + await runGroups(routeMovingGroups); + if (!limitFailure && !phaseTimedOut) await runGroups(representativeGroups); + if (!limitFailure && !phaseTimedOut) await runGroups(siblingGroups); if (limitFailure) throw limitFailure; if (phaseTimedOut || Date.now() >= getDeadlineAt()) { throw new Error(`cacheability probing made no progress for ${phaseTimeoutMs}ms`); diff --git a/tests/cloudflare-cacheability-probe.test.ts b/tests/cloudflare-cacheability-probe.test.ts index 883aa8504..839bcab4f 100644 --- a/tests/cloudflare-cacheability-probe.test.ts +++ b/tests/cloudflare-cacheability-probe.test.ts @@ -478,14 +478,14 @@ describe("staged Worker cacheability probes", () => { ); }); - it("unlocks sibling paths without waiting for every pattern representative", async () => { + it("completes pattern representatives before scheduling sibling paths", async () => { const root = createProbeRoot(); const slow = target("/slow"); const fastRoute = optimizableRoute("/fast/:slug"); const fastFirst = { ...target("/fast/one"), route: fastRoute }; const fastSecond = { ...target("/fast/two"), route: fastRoute }; let slowCompleted = false; - let siblingStartedBeforeSlowCompleted = false; + let siblingStartedAfterSlowCompleted = false; const result = await probeStagedWorkerCacheability({ buildId: "application-build", concurrency: 2, @@ -495,7 +495,7 @@ describe("staged Worker cacheability probes", () => { await new Promise((resolve) => setTimeout(resolve, 20)); slowCompleted = true; } else if (pathname === "/fast/two") { - siblingStartedBeforeSlowCompleted = !slowCompleted; + siblingStartedAfterSlowCompleted = slowCompleted; } return Response.json({ kind: "app-page", @@ -512,7 +512,7 @@ describe("staged Worker cacheability probes", () => { }); expect(result).toMatchObject({ failures: [], probed: 3 }); - expect(siblingStartedBeforeSlowCompleted).toBe(true); + expect(siblingStartedAfterSlowCompleted).toBe(true); }); it("does not prune destination siblings before route-moving probes settle", async () => {