authorgravatar for 153842990+yashranaway@users.noreply.github.comAditya Garud <153842990+yashranaway@users.noreply.github.com> 2026-10-09 01:52:58+05:30
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-08 13:22:58-07:00
logc377d1ca17dbce42008fbb05fccd95468b13d329
tree9239b726ceb25fb06c790ddb1e334a0f81171ed3
parentbd565bd1794f49f176ad137555faa46224716138
signature Signed by PGP key B5690EEEBB952194

fix(shared): release relay install locks on cancellation (#10585)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

2 files changed, 138 insertions(+), 3 deletions(-)

packages/shared/src/relayClient.test.ts+133
...@@ -4,6 +4,7 @@ import { describe, expect, it } from "@effect/vitest";...@@ -4,6 +4,7 @@ import { describe, expect, it } from "@effect/vitest";
4import * as ConfigProvider from "effect/ConfigProvider";4import * as ConfigProvider from "effect/ConfigProvider";
5import * as Deferred from "effect/Deferred";5import * as Deferred from "effect/Deferred";
6import * as Effect from "effect/Effect";6import * as Effect from "effect/Effect";
7import * as Fiber from "effect/Fiber";
7import * as Hex from "effect/encoding/Hex";8import * as Hex from "effect/encoding/Hex";
8import * as Fiber from "effect/Fiber";9import * as Fiber from "effect/Fiber";
9import * as FileSystem from "effect/FileSystem";10import * as FileSystem from "effect/FileSystem";
...@@ -154,6 +155,133 @@ const managedDirectory = (baseDir: string, platform: NodeJS.Platform) =>...@@ -154,6 +155,133 @@ const managedDirectory = (baseDir: string, platform: NodeJS.Platform) =>
154 `${baseDir}/tools/cloudflared/${RelayClient.CLOUDFLARED_VERSION}/${platform}-x64`;155 `${baseDir}/tools/cloudflared/${RelayClient.CLOUDFLARED_VERSION}/${platform}-x64`;
155156
156describe("RelayClient", () => {157describe("RelayClient", () => {
158 it.effect("cancels a contended install without removing the other installer's lock", () =>
159 Effect.gen(function* () {
160 const fileSystem = yield* FileSystem.FileSystem;
161 const baseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-cloudflared-test-" });
162 const directory = `${baseDir}/tools/cloudflared/${RelayClient.CLOUDFLARED_VERSION}/linux-x64`;
163 const lockPath = `${directory}/cloudflared.lock`;
164 yield* fileSystem.makeDirectory(directory, { recursive: true });
165 yield* fileSystem.writeFileString(lockPath, "other-installer");
166 const contended = yield* Deferred.make<void>();
167 const manager = yield* RelayClient.makeCloudflaredRelayClient({ baseDir }).pipe(
168 Effect.provideService(FileSystem.FileSystem, {
169 ...fileSystem,
170 writeFileString: (path, contents, options) =>
171 fileSystem
172 .writeFileString(path, contents, options)
173 .pipe(
174 Effect.tapError(() =>
175 path === lockPath ? Deferred.succeed(contended, undefined) : Effect.void,
176 ),
177 ),
178 }),
179 );
180 const installing = yield* manager.install.pipe(Effect.forkChild);
181 yield* Deferred.await(contended);
182 yield* Fiber.interrupt(installing);
183 expect(yield* fileSystem.readFileString(lockPath)).toBe("other-installer");
184 }).pipe(
185 Effect.scoped,
186 Effect.provide(
187 Layer.mergeAll(
188 NodeServices.layer,
189 layerHttpClient(new Uint8Array()),
190 layerSpawner([]),
191 layerHostRuntime({ PATH: "" }),
192 ),
193 ),
194 ),
195 );
196
197 it.effect("releases the install lock when a download is cancelled", () =>
198 Effect.gen(function* () {
199 const fileSystem = yield* FileSystem.FileSystem;
200 const baseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-cloudflared-test-" });
201 const lockPath = `${baseDir}/tools/cloudflared/${RelayClient.CLOUDFLARED_VERSION}/linux-x64/cloudflared.lock`;
202 const downloading = yield* Deferred.make<void>();
203 const manager = yield* RelayClient.makeCloudflaredRelayClient({ baseDir });
204 const installing = yield* manager
205 .installWithProgress((event) =>
206 event.type === "progress" && event.stage === "downloading"
207 ? Deferred.succeed(downloading, undefined).pipe(Effect.andThen(Effect.never))
208 : Effect.void,
209 )
210 .pipe(Effect.forkChild);
211 yield* Deferred.await(downloading);
212 expect(yield* fileSystem.exists(lockPath)).toBe(true);
213 yield* Fiber.interrupt(installing);
214 expect(yield* fileSystem.exists(lockPath)).toBe(false);
215 }).pipe(
216 Effect.scoped,
217 Effect.provide(
218 Layer.mergeAll(
219 NodeServices.layer,
220 layerHttpClient(new Uint8Array()),
221 layerSpawner([]),
222 layerHostRuntime({ PATH: "" }),
223 ),
224 ),
225 ),
226 );
227
228 it.effect.skipIf(windowsHost)("releases a lock acquired while installation is cancelled", () =>
229 Effect.gen(function* () {
230 const fileSystem = yield* FileSystem.FileSystem;
231 const baseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-cloudflared-test-" });
232 const lockPath = `${baseDir}/tools/cloudflared/${RelayClient.CLOUDFLARED_VERSION}/linux-x64/cloudflared.lock`;
233 const acquired = yield* Deferred.make<void>();
234 const completeWrite = yield* Deferred.make<void>();
235 let pauseWrite = true;
236 const manager = yield* RelayClient.makeCloudflaredRelayClient({
237 baseDir,
238 releaseAsset: {
239 url: "https://example.test/cloudflared",
240 sha256: Hex.encode(sha256(new TextEncoder().encode("test-binary"))),
241 archive: "binary",
242 },
243 }).pipe(
244 Effect.provideService(FileSystem.FileSystem, {
245 ...fileSystem,
246 writeFileString: (path, contents, options) =>
247 fileSystem
248 .writeFileString(path, contents, options)
249 .pipe(
250 Effect.tap(() =>
251 path === lockPath && pauseWrite
252 ? Deferred.succeed(acquired, undefined).pipe(
253 Effect.andThen(Deferred.await(completeWrite)),
254 )
255 : Effect.void,
256 ),
257 ),
258 }),
259 );
260
261 const installing = yield* manager.install.pipe(Effect.forkChild);
262 yield* Deferred.await(acquired);
263 const cancelling = yield* Fiber.interrupt(installing).pipe(
264 Effect.forkChild({ startImmediately: true }),
265 );
266 yield* Deferred.succeed(completeWrite, undefined);
267 yield* Fiber.join(cancelling);
268 expect(yield* fileSystem.exists(lockPath)).toBe(false);
269
270 pauseWrite = false;
271 expect(yield* manager.install).toMatchObject({ status: "available" });
272 expect(yield* fileSystem.exists(lockPath)).toBe(false);
273 }).pipe(
274 Effect.scoped,
275 Effect.provide(
276 Layer.mergeAll(
277 NodeServices.layer,
278 layerHttpClient(new TextEncoder().encode("test-binary")),
279 layerSpawner([]),
280 layerHostRuntime({ PATH: "" }),
281 ),
282 ),
283 ),
284 );
157 it.effect.skipIf(windowsHost)(285 it.effect.skipIf(windowsHost)(
158 "resolves explicit overrides before managed and PATH executables",286 "resolves explicit overrides before managed and PATH executables",
159 () =>287 () =>
...@@ -274,6 +402,11 @@ describe("RelayClient", () => {...@@ -274,6 +402,11 @@ describe("RelayClient", () => {
274 const error = yield* manager.install.pipe(Effect.flip);402 const error = yield* manager.install.pipe(Effect.flip);
275 expect(error).toBeInstanceOf(RelayClient.RelayClientInstallError);403 expect(error).toBeInstanceOf(RelayClient.RelayClientInstallError);
276 expect(error.reason).toBe("invalid_checksum");404 expect(error.reason).toBe("invalid_checksum");
405 expect(
406 yield* fileSystem.exists(
407 `${baseDir}/tools/cloudflared/${RelayClient.CLOUDFLARED_VERSION}/linux-x64/cloudflared.lock`,
408 ),
409 ).toBe(false);
277 }).pipe(410 }).pipe(
278 Effect.scoped,411 Effect.scoped,
279 Effect.provide(412 Effect.provide(
packages/shared/src/relayClient.ts+5-3
...@@ -487,7 +487,10 @@ export const makeCloudflaredRelayClient = Effect.fn("cloudflared.make")(function...@@ -487,7 +487,10 @@ export const makeCloudflaredRelayClient = Effect.fn("cloudflared.make")(function
487 lockPath: string,487 lockPath: string,
488 ) {488 ) {
489 for (let attempt = 0; attempt < INSTALL_LOCK_RETRY_COUNT; attempt += 1) {489 for (let attempt = 0; attempt < INSTALL_LOCK_RETRY_COUNT; attempt += 1) {
490 const acquired = yield* fileSystem.writeFileString(lockPath, "", { flag: "wx" }).pipe(490 const acquired = yield* Effect.acquireRelease(
491 fileSystem.writeFileString(lockPath, "", { flag: "wx" }),
492 () => fileSystem.remove(lockPath, { force: true }).pipe(Effect.ignore),
493 ).pipe(
491 Effect.as(true),494 Effect.as(true),
492 Effect.catchIf(isAlreadyExists, () => Effect.succeed(false)),495 Effect.catchIf(isAlreadyExists, () => Effect.succeed(false)),
493 );496 );
...@@ -610,7 +613,6 @@ export const makeCloudflaredRelayClient = Effect.fn("cloudflared.make")(function...@@ -610,7 +613,6 @@ export const makeCloudflaredRelayClient = Effect.fn("cloudflared.make")(function
610 } satisfies AvailableRelayClient;613 } satisfies AvailableRelayClient;
611 }).pipe(614 }).pipe(
612 Effect.scoped,615 Effect.scoped,
613 Effect.ensuring(fileSystem.remove(lockPath, { force: true }).pipe(Effect.ignore)),
614 Effect.catchIf(616 Effect.catchIf(
615 (cause) => !(cause instanceof RelayClientInstallError),617 (cause) => !(cause instanceof RelayClientInstallError),
616 (cause) =>618 (cause) =>
...@@ -631,7 +633,7 @@ export const makeCloudflaredRelayClient = Effect.fn("cloudflared.make")(function...@@ -631,7 +633,7 @@ export const makeCloudflaredRelayClient = Effect.fn("cloudflared.make")(function
631 type: "progress",633 type: "progress",
632 stage,634 stage,
633 }),635 }),
634 ),636 ).pipe(Effect.scoped),
635 );637 );
636 const install = installWithProgress(() => Effect.void);638 const install = installWithProgress(() => Effect.void);
637639