diff --git a/src/lib/google-health/__tests__/backfill-bounds.test.ts b/src/lib/google-health/__tests__/backfill-bounds.test.ts index 9860b0022..581c46ff6 100644 --- a/src/lib/google-health/__tests__/backfill-bounds.test.ts +++ b/src/lib/google-health/__tests__/backfill-bounds.test.ts @@ -351,6 +351,31 @@ describe("a collection ended short", () => { ).toMatchObject({ ...END, attempts: 2 }); }); + it("starts the count over when the retry got further back before it stalled again", async () => { + const further = { ...END, at: "2023-01-02T00:00:00.000Z" }; + const done = new Set(); + const { ended, collections } = await slice( + async () => { + noteGoogleHealthDeferred(); + await recordGoogleHealthBackfillEnd("fetchHeartRate", further); + return 4; + }, + new Map([ + ["fetchHeartRate", parked(1, 25)], + ]), + done, + ); + // Progress, then a new stall: parked again, not final. + expect(ended.get("fetchHeartRate")).toMatchObject({ + ...further, + attempts: 1, + }); + expect(done.has("fetchHeartRate")).toBe(false); + expect(collections).toEqual([ + expect.objectContaining({ key: "fetchHeartRate", status: "stopped" }), + ]); + }); + it("goes back to an ordinary walk when the second attempt gets somewhere", async () => { const { ended, collections } = await slice( async () => { diff --git a/src/lib/google-health/sync-core.ts b/src/lib/google-health/sync-core.ts index e33735991..b0ee51d1f 100644 --- a/src/lib/google-health/sync-core.ts +++ b/src/lib/google-health/sync-core.ts @@ -497,9 +497,10 @@ interface BackfillResumeTracker { ended: Map; /** * Collections walked again this attempt after an earlier one ended without - * progress, with how many attempts had ended that way. + * progress, with how many attempts in a row had ended that way and the edge + * they ended at. */ - retrying: Map; + retrying: Map; } const backfillResumeStorage = new AsyncLocalStorage(); @@ -857,14 +858,18 @@ export async function recordGoogleHealthBackfillEnd( const resume = backfillResumeStorage.getStore(); if (!resume) return; const ended = new Map(resume.ended); + // Counted across attempts that end at the same edge: a walk that got + // further back before it stalled again made progress, and its count starts + // over. Only attempts in a row that reach nowhere make the end final. + const parked = resume.retrying.get(key); + const sameEdge = + parked !== undefined && Date.parse(parked.at) === Date.parse(end.at); ended.set( key, end.reason === "no_progress" ? { ...end, - // Counted across attempts: the one walked again this attempt - // carries the count it was parked with. - attempts: (resume.retrying.get(key) ?? 0) + 1, + attempts: sameEdge ? parked.attempts + 1 : 1, endedAt: new Date().toISOString(), } : end, @@ -1073,8 +1078,12 @@ export async function runGoogleHealthCollection( if (parked) { // Its earlier attempt ended without progress and its wait is over: walk // it again from its edge. The note goes with the next write; the count - // stays with the attempt, so a second end without progress is final. - resume.retrying.set(key, parked.attempts ?? 1); + // and the edge stay with the attempt, so a second end at the same edge + // is final. + resume.retrying.set(key, { + attempts: parked.attempts ?? 1, + at: parked.at, + }); resume.ended.delete(key); } const startedAt = performance.now();