From b0c8a8c827b46fba95f0946e06a06ff61b284596 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 28 Aug 2026 17:18:32 -0400 Subject: [PATCH] fix(util): refresh flock heartbeat time (#45720) --- packages/core/test/util/effect-flock.test.ts | 23 ++++++++++++++++++++ packages/util/src/effect-flock.ts | 15 +++++++------ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/packages/core/test/util/effect-flock.test.ts b/packages/core/test/util/effect-flock.test.ts index 2813a623218..4af0445c8c3 100644 --- a/packages/core/test/util/effect-flock.test.ts +++ b/packages/core/test/util/effect-flock.test.ts @@ -156,6 +156,29 @@ describe("util.effect-flock", () => { }), ) + it.live( + "advances the heartbeat timestamp while the lock is held", + Effect.gen(function* () { + const flock = yield* EffectFlock.Service + const tmp = yield* Effect.promise(() => fs.mkdtemp(path.join(os.tmpdir(), "eflock-test-"))) + const dir = path.join(tmp, "locks") + const heartbeat = path.join(lock(dir, "eflock:heartbeat"), "heartbeat") + yield* Effect.addFinalizer(() => Effect.promise(() => fs.rm(tmp, { recursive: true, force: true }))) + + yield* Effect.scoped( + Effect.gen(function* () { + yield* flock.acquire("eflock:heartbeat", dir, { staleMs: 300 }) + yield* Effect.sleep(150) + const first = yield* Effect.promise(() => fs.stat(heartbeat)) + yield* Effect.sleep(250) + const second = yield* Effect.promise(() => fs.stat(heartbeat)) + + expect(second.mtimeMs).toBeGreaterThan(first.mtimeMs) + }), + ) + }), + ) + it.live( "breaks stale lock dirs", Effect.gen(function* () { diff --git a/packages/util/src/effect-flock.ts b/packages/util/src/effect-flock.ts index 788fd81d0d5..d35b685ca62 100644 --- a/packages/util/src/effect-flock.ts +++ b/packages/util/src/effect-flock.ts @@ -284,13 +284,14 @@ export namespace EffectFlock { ) // Heartbeat fiber — scoped, so it's interrupted before release runs - yield* fs - .utimes(handle.heartbeatPath, new Date(), new Date()) - .pipe( - Effect.ignore, - Effect.repeat(Schedule.spaced(Math.max(100, Math.floor(staleMs / 3)))), - Effect.forkScoped, - ) + yield* Effect.suspend(() => { + const now = new Date() + return fs.utimes(handle.heartbeatPath, now, now) + }).pipe( + Effect.ignore, + Effect.repeat(Schedule.spaced(Math.max(100, Math.floor(staleMs / 3)))), + Effect.forkScoped, + ) }) const withLock: Interface["withLock"] = Function.dual(