From ad4afef13daa54a3b8a1a0265fd2e9f49660f0e0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 5 Jun 2026 16:07:02 +0000 Subject: [PATCH 1/6] Use Effect schedule for relay install locks Co-authored-by: Julius Marminge --- packages/shared/src/relayClient.test.ts | 111 +++++++++++++++++++++++- packages/shared/src/relayClient.ts | 75 +++++++++++----- 2 files changed, 162 insertions(+), 24 deletions(-) diff --git a/packages/shared/src/relayClient.test.ts b/packages/shared/src/relayClient.test.ts index 7e552194dae..690238e7a28 100644 --- a/packages/shared/src/relayClient.test.ts +++ b/packages/shared/src/relayClient.test.ts @@ -1,13 +1,17 @@ import { sha256 } from "@noble/hashes/sha2"; import * as NodeServices from "@effect/platform-node/NodeServices"; -import { describe, expect, it } from "@effect/vitest"; +import { assert, describe, expect, it } from "@effect/vitest"; import * as ConfigProvider from "effect/ConfigProvider"; +import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Encoding from "effect/Encoding"; +import * as Fiber from "effect/Fiber"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; +import * as Path from "effect/Path"; import * as Sink from "effect/Sink"; import * as Stream from "effect/Stream"; +import * as TestClock from "effect/testing/TestClock"; import { HttpClient, HttpClientResponse } from "effect/unstable/http"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; @@ -227,6 +231,111 @@ describe("RelayClient", () => { ); }); + it.effect("fails with install_locked after the lock retry schedule is exhausted", () => { + const commands: Array = []; + const bytes = new TextEncoder().encode("test-cloudflared-binary"); + return Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const baseDir = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3-cloudflared-test-locked-", + }); + const managedPath = resolveManagedCloudflaredPath({ + baseDir, + platform: "linux", + arch: "x64", + }); + const lockPath = `${managedPath}.lock`; + const path = yield* Path.Path; + yield* fileSystem.makeDirectory(path.dirname(managedPath), { recursive: true }); + yield* fileSystem.writeFileString(lockPath, "locked"); + const manager = yield* makeCloudflaredRelayClient({ + baseDir, + platform: "linux", + arch: "x64", + releaseAsset: { + url: "https://example.test/cloudflared", + sha256: Encoding.encodeHex(sha256(bytes)), + archive: "binary", + }, + configProvider: emptyConfigProvider, + }); + + const install = yield* manager.install.pipe(Effect.flip, Effect.forkScoped); + for (let attempt = 0; attempt < 100; attempt += 1) { + yield* TestClock.adjust(Duration.millis(100)); + yield* Effect.yieldNow; + } + const error = yield* Fiber.join(install); + + assert.ok(error instanceof RelayClientInstallError); + assert.equal(error.reason, "install_locked"); + assert.equal(commands.length, 0); + }).pipe( + Effect.scoped, + Effect.provide( + Layer.mergeAll( + NodeServices.layer, + TestClock.layer(), + makeHttpClientLayer(bytes), + makeSpawnerLayer(commands), + ), + ), + ); + }); + + it.effect("removes stale install locks before downloading the managed executable", () => { + const commands: Array = []; + const bytes = new TextEncoder().encode("test-cloudflared-binary"); + return Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const baseDir = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3-cloudflared-test-stale-lock-", + }); + const managedPath = resolveManagedCloudflaredPath({ + baseDir, + platform: "linux", + arch: "x64", + }); + const lockPath = `${managedPath}.lock`; + const path = yield* Path.Path; + yield* fileSystem.makeDirectory(path.dirname(managedPath), { recursive: true }); + yield* fileSystem.writeFileString(lockPath, "stale"); + yield* fileSystem.utimes(lockPath, 0, 0); + const manager = yield* makeCloudflaredRelayClient({ + baseDir, + platform: "linux", + arch: "x64", + releaseAsset: { + url: "https://example.test/cloudflared", + sha256: Encoding.encodeHex(sha256(bytes)), + archive: "binary", + }, + configProvider: emptyConfigProvider, + }); + + yield* TestClock.adjust(Duration.minutes(6)); + const installed = yield* manager.install; + + assert.deepStrictEqual(installed, { + status: "available", + executablePath: managedPath, + source: "managed", + version: CLOUDFLARED_VERSION, + }); + assert.equal(commands.length, 1); + }).pipe( + Effect.scoped, + Effect.provide( + Layer.mergeAll( + NodeServices.layer, + TestClock.layer(), + makeHttpClientLayer(bytes), + makeSpawnerLayer(commands), + ), + ), + ); + }); + it.effect("observes PATH changes after the manager has been constructed", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; diff --git a/packages/shared/src/relayClient.ts b/packages/shared/src/relayClient.ts index 35d002466e9..a4746c9e88d 100644 --- a/packages/shared/src/relayClient.ts +++ b/packages/shared/src/relayClient.ts @@ -1,4 +1,3 @@ -import * as Clock from "effect/Clock"; import type { RelayClientInstallProgressEvent, RelayClientInstallProgressStage, @@ -8,6 +7,8 @@ import * as ConfigProvider from "effect/ConfigProvider"; import * as Context from "effect/Context"; import * as Crypto from "effect/Crypto"; import * as Data from "effect/Data"; +import * as DateTime from "effect/DateTime"; +import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Encoding from "effect/Encoding"; import * as FileSystem from "effect/FileSystem"; @@ -15,6 +16,7 @@ import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as PlatformError from "effect/PlatformError"; +import * as Schedule from "effect/Schedule"; import * as Semaphore from "effect/Semaphore"; import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; @@ -62,6 +64,12 @@ class CloudflaredCommandError extends Data.TaggedError("CloudflaredCommandError" readonly exitCode: number; }> {} +class RelayClientInstallLockUnavailable extends Data.TaggedError( + "RelayClientInstallLockUnavailable", +)<{ + readonly lockPath: string; +}> {} + export interface CloudflaredReleaseAsset { readonly url: string; readonly sha256: string; @@ -99,8 +107,9 @@ const CLOUDFLARED_RELEASE_ASSETS: Readonly< }; const INSTALL_LOCK_RETRY_COUNT = 100; -const INSTALL_LOCK_RETRY_DELAY = "100 millis"; -const INSTALL_LOCK_STALE_MS = 5 * 60 * 1_000; +const INSTALL_LOCK_RETRY_DELAY = Duration.millis(100); +const INSTALL_LOCK_STALE_AFTER = Duration.minutes(5); +const INSTALL_LOCK_RETRY_SCHEDULE = Schedule.spaced(INSTALL_LOCK_RETRY_DELAY); const trimmedString = (name: string) => Config.string(name).pipe( @@ -354,28 +363,48 @@ export const makeCloudflaredRelayClient = Effect.fn("cloudflared.make")(function const acquireInstallLock = Effect.fn("cloudflared.acquireInstallLock")(function* ( lockPath: string, ) { - for (let attempt = 0; attempt < INSTALL_LOCK_RETRY_COUNT; attempt += 1) { - const acquired = yield* fileSystem.writeFileString(lockPath, "", { flag: "wx" }).pipe( - Effect.as(true), - Effect.catch((error) => - isAlreadyExists(error) ? Effect.succeed(false) : Effect.fail(error), - ), - ); - if (acquired) return; - - const now = yield* Clock.currentTimeMillis; - const lockInfo = yield* fileSystem.stat(lockPath).pipe(Effect.option); - const mtime = Option.flatMap(lockInfo, (info) => info.mtime); - if (Option.isSome(mtime) && now - mtime.value.getTime() > INSTALL_LOCK_STALE_MS) { - yield* fileSystem.remove(lockPath, { force: true }); - continue; + const acquireInstallLockOnce = Effect.fn("cloudflared.acquireInstallLockOnce")(function* () { + while (true) { + const acquired = yield* fileSystem.writeFileString(lockPath, "", { flag: "wx" }).pipe( + Effect.as(true), + Effect.catch((error) => + isAlreadyExists(error) ? Effect.succeed(false) : Effect.fail(error), + ), + ); + if (acquired) return; + + const now = yield* DateTime.now; + const lockInfo = yield* fileSystem.stat(lockPath).pipe(Effect.option); + const mtime = Option.flatMap(lockInfo, (info) => info.mtime); + const staleBefore = DateTime.subtractDuration(now, INSTALL_LOCK_STALE_AFTER); + if ( + Option.isSome(mtime) && + DateTime.Order(DateTime.fromDateUnsafe(mtime.value), staleBefore) < 0 + ) { + yield* fileSystem.remove(lockPath, { force: true }); + continue; + } + return yield* new RelayClientInstallLockUnavailable({ lockPath }); } - yield* Effect.sleep(INSTALL_LOCK_RETRY_DELAY); - } - return yield* new RelayClientInstallError({ - reason: "install_locked", - message: "Another relay client installation is still in progress.", }); + + return yield* acquireInstallLockOnce().pipe( + Effect.retry({ + times: INSTALL_LOCK_RETRY_COUNT - 1, + schedule: INSTALL_LOCK_RETRY_SCHEDULE, + while: (error) => error instanceof RelayClientInstallLockUnavailable, + }), + Effect.catchTag( + "RelayClientInstallLockUnavailable", + () => + Effect.fail( + new RelayClientInstallError({ + reason: "install_locked", + message: "Another relay client installation is still in progress.", + }), + ), + ), + ); }); const installUnlocked = Effect.fn("cloudflared.installUnlocked")(function* ( From b7c09464584e79d9f97d17b41318822a26ee5992 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 5 Jun 2026 16:10:59 +0000 Subject: [PATCH 2/6] Bound relay install lock retry schedule Co-authored-by: Julius Marminge --- packages/shared/src/relayClient.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/shared/src/relayClient.ts b/packages/shared/src/relayClient.ts index a4746c9e88d..c3b949878d9 100644 --- a/packages/shared/src/relayClient.ts +++ b/packages/shared/src/relayClient.ts @@ -109,7 +109,9 @@ const CLOUDFLARED_RELEASE_ASSETS: Readonly< const INSTALL_LOCK_RETRY_COUNT = 100; const INSTALL_LOCK_RETRY_DELAY = Duration.millis(100); const INSTALL_LOCK_STALE_AFTER = Duration.minutes(5); -const INSTALL_LOCK_RETRY_SCHEDULE = Schedule.spaced(INSTALL_LOCK_RETRY_DELAY); +const INSTALL_LOCK_RETRY_SCHEDULE = Schedule.spaced(INSTALL_LOCK_RETRY_DELAY).pipe( + Schedule.both(Schedule.recurs(INSTALL_LOCK_RETRY_COUNT - 1)), +); const trimmedString = (name: string) => Config.string(name).pipe( @@ -390,7 +392,6 @@ export const makeCloudflaredRelayClient = Effect.fn("cloudflared.make")(function return yield* acquireInstallLockOnce().pipe( Effect.retry({ - times: INSTALL_LOCK_RETRY_COUNT - 1, schedule: INSTALL_LOCK_RETRY_SCHEDULE, while: (error) => error instanceof RelayClientInstallLockUnavailable, }), From 9d9ecd1b24f77b01a5530d747980f6c6465b7239 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 5 Jun 2026 16:16:32 +0000 Subject: [PATCH 3/6] Make relay lock retry test deterministic Co-authored-by: Julius Marminge --- packages/shared/src/relayClient.test.ts | 70 +++++++++++++++++++------ 1 file changed, 53 insertions(+), 17 deletions(-) diff --git a/packages/shared/src/relayClient.test.ts b/packages/shared/src/relayClient.test.ts index 690238e7a28..783ddee8f68 100644 --- a/packages/shared/src/relayClient.test.ts +++ b/packages/shared/src/relayClient.test.ts @@ -8,7 +8,9 @@ import * as Encoding from "effect/Encoding"; import * as Fiber from "effect/Fiber"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; import * as Path from "effect/Path"; +import * as PlatformError from "effect/PlatformError"; import * as Sink from "effect/Sink"; import * as Stream from "effect/Stream"; import * as TestClock from "effect/testing/TestClock"; @@ -61,6 +63,51 @@ const makeSpawnerLayer = (commands: Array) => ), ); +const lockedFileInfo: FileSystem.File.Info = { + type: "File", + mtime: Option.none(), + atime: Option.none(), + birthtime: Option.none(), + dev: 0, + ino: Option.none(), + mode: 0o644, + nlink: Option.none(), + uid: Option.none(), + gid: Option.none(), + rdev: Option.none(), + size: FileSystem.Size(0), + blksize: Option.none(), + blocks: Option.none(), +}; + +const fileSystemError = ( + tag: "AlreadyExists" | "NotFound", + method: string, + path: string, +) => + PlatformError.systemError({ + _tag: tag, + module: "FileSystem", + method, + pathOrDescriptor: path, + description: tag === "AlreadyExists" ? "File already exists" : "No such file or directory", + }); + +const makeLockedFileSystemLayer = (attempts: { count: number }) => + Layer.mock(FileSystem.FileSystem, { + makeDirectory: () => Effect.void, + remove: () => Effect.void, + stat: (path) => + path.endsWith(".lock") + ? Effect.succeed(lockedFileInfo) + : Effect.fail(fileSystemError("NotFound", "stat", path)), + writeFileString: (path) => + Effect.gen(function* () { + attempts.count += 1; + return yield* Effect.fail(fileSystemError("AlreadyExists", "writeFileString", path)); + }), + }); + describe("RelayClient", () => { it.effect("resolves explicit overrides before managed and PATH executables", () => Effect.gen(function* () { @@ -234,20 +281,9 @@ describe("RelayClient", () => { it.effect("fails with install_locked after the lock retry schedule is exhausted", () => { const commands: Array = []; const bytes = new TextEncoder().encode("test-cloudflared-binary"); + const attempts = { count: 0 }; return Effect.gen(function* () { - const fileSystem = yield* FileSystem.FileSystem; - const baseDir = yield* fileSystem.makeTempDirectoryScoped({ - prefix: "t3-cloudflared-test-locked-", - }); - const managedPath = resolveManagedCloudflaredPath({ - baseDir, - platform: "linux", - arch: "x64", - }); - const lockPath = `${managedPath}.lock`; - const path = yield* Path.Path; - yield* fileSystem.makeDirectory(path.dirname(managedPath), { recursive: true }); - yield* fileSystem.writeFileString(lockPath, "locked"); + const baseDir = "/tmp/t3-cloudflared-test-locked"; const manager = yield* makeCloudflaredRelayClient({ baseDir, platform: "linux", @@ -261,20 +297,20 @@ describe("RelayClient", () => { }); const install = yield* manager.install.pipe(Effect.flip, Effect.forkScoped); - for (let attempt = 0; attempt < 100; attempt += 1) { - yield* TestClock.adjust(Duration.millis(100)); - yield* Effect.yieldNow; - } + yield* Effect.yieldNow; + yield* TestClock.adjust(Duration.seconds(10)); const error = yield* Fiber.join(install); assert.ok(error instanceof RelayClientInstallError); assert.equal(error.reason, "install_locked"); + assert.equal(attempts.count, 100); assert.equal(commands.length, 0); }).pipe( Effect.scoped, Effect.provide( Layer.mergeAll( NodeServices.layer, + makeLockedFileSystemLayer(attempts), TestClock.layer(), makeHttpClientLayer(bytes), makeSpawnerLayer(commands), From 5de7504b05af73e4deb28ee01369033958778622 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 5 Jun 2026 16:16:53 +0000 Subject: [PATCH 4/6] Use noop FileSystem layer for relay lock test Co-authored-by: Julius Marminge --- packages/shared/src/relayClient.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/relayClient.test.ts b/packages/shared/src/relayClient.test.ts index 783ddee8f68..f1a1a133a18 100644 --- a/packages/shared/src/relayClient.test.ts +++ b/packages/shared/src/relayClient.test.ts @@ -94,7 +94,7 @@ const fileSystemError = ( }); const makeLockedFileSystemLayer = (attempts: { count: number }) => - Layer.mock(FileSystem.FileSystem, { + FileSystem.layerNoop({ makeDirectory: () => Effect.void, remove: () => Effect.void, stat: (path) => From 77bb590bd8ab2a8e867a67b03952f564315d2f30 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 5 Jun 2026 16:18:15 +0000 Subject: [PATCH 5/6] Format relay client lock changes Co-authored-by: Julius Marminge --- packages/shared/src/relayClient.test.ts | 6 +----- packages/shared/src/relayClient.ts | 16 +++++++--------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/packages/shared/src/relayClient.test.ts b/packages/shared/src/relayClient.test.ts index f1a1a133a18..c2b0687b76b 100644 --- a/packages/shared/src/relayClient.test.ts +++ b/packages/shared/src/relayClient.test.ts @@ -80,11 +80,7 @@ const lockedFileInfo: FileSystem.File.Info = { blocks: Option.none(), }; -const fileSystemError = ( - tag: "AlreadyExists" | "NotFound", - method: string, - path: string, -) => +const fileSystemError = (tag: "AlreadyExists" | "NotFound", method: string, path: string) => PlatformError.systemError({ _tag: tag, module: "FileSystem", diff --git a/packages/shared/src/relayClient.ts b/packages/shared/src/relayClient.ts index c3b949878d9..0e6f41f7a81 100644 --- a/packages/shared/src/relayClient.ts +++ b/packages/shared/src/relayClient.ts @@ -395,15 +395,13 @@ export const makeCloudflaredRelayClient = Effect.fn("cloudflared.make")(function schedule: INSTALL_LOCK_RETRY_SCHEDULE, while: (error) => error instanceof RelayClientInstallLockUnavailable, }), - Effect.catchTag( - "RelayClientInstallLockUnavailable", - () => - Effect.fail( - new RelayClientInstallError({ - reason: "install_locked", - message: "Another relay client installation is still in progress.", - }), - ), + Effect.catchTag("RelayClientInstallLockUnavailable", () => + Effect.fail( + new RelayClientInstallError({ + reason: "install_locked", + message: "Another relay client installation is still in progress.", + }), + ), ), ); }); From 33319ce2a8a6d29f34d11a88411ce8984eeab096 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 5 Jun 2026 16:19:19 +0000 Subject: [PATCH 6/6] Address relay lock test diagnostic Co-authored-by: Julius Marminge --- packages/shared/src/relayClient.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/relayClient.test.ts b/packages/shared/src/relayClient.test.ts index c2b0687b76b..ed081017544 100644 --- a/packages/shared/src/relayClient.test.ts +++ b/packages/shared/src/relayClient.test.ts @@ -100,7 +100,7 @@ const makeLockedFileSystemLayer = (attempts: { count: number }) => writeFileString: (path) => Effect.gen(function* () { attempts.count += 1; - return yield* Effect.fail(fileSystemError("AlreadyExists", "writeFileString", path)); + return yield* fileSystemError("AlreadyExists", "writeFileString", path); }), });