diff --git a/extensions/telegram/src/telegram-ingress-spool.test.ts b/extensions/telegram/src/telegram-ingress-spool.test.ts index aee859d1283..b61857e4c43 100644 --- a/extensions/telegram/src/telegram-ingress-spool.test.ts +++ b/extensions/telegram/src/telegram-ingress-spool.test.ts @@ -242,6 +242,37 @@ describe("Telegram ingress spool", () => { }); }); + it("handles ENOENT race when processing file is removed before recovery rename", async () => { + await withTempSpool(async (spoolDir) => { + await writeTelegramSpooledUpdate({ + spoolDir, + update: { update_id: 45, message: { text: "vanishes" } }, + }); + const update = (await listTelegramSpooledUpdates({ spoolDir }))[0]; + if (!update) { + throw new Error("Expected a spooled update"); + } + const claimed = await claimTelegramSpooledUpdate(update); + if (!claimed) { + throw new Error("Expected a claimed update"); + } + let shouldRecoverCalls = 0; + const recovered = await recoverStaleTelegramSpooledUpdateClaims({ + spoolDir, + staleMs: 0, + shouldRecover: async () => { + shouldRecoverCalls += 1; + await fs.unlink(claimed.path); + return true; + }, + }); + + expect(recovered).toBe(0); + expect(shouldRecoverCalls).toBe(1); + expect(await fs.readdir(spoolDir)).toEqual([]); + }); + }); + it("does not treat stale claims with reused pids as live-owned", () => { const now = Date.now(); expect( diff --git a/extensions/telegram/src/telegram-ingress-spool.ts b/extensions/telegram/src/telegram-ingress-spool.ts index b8af64150b0..c006a7b83ec 100644 --- a/extensions/telegram/src/telegram-ingress-spool.ts +++ b/extensions/telegram/src/telegram-ingress-spool.ts @@ -443,7 +443,19 @@ export async function recoverStaleTelegramSpooledUpdateClaims(params: { if (await pathExists(pendingPath)) { await unlinkIfPresent(claimedPath); } else { - await fs.rename(claimedPath, pendingPath); + try { + await fs.rename(claimedPath, pendingPath); + } catch (err) { + const code = (err as { code?: string }).code; + if (code === "ENOENT") { + continue; + } + if (code === "EEXIST") { + await unlinkIfPresent(claimedPath); + } else { + throw err; + } + } } recovered += 1; }