1import * as machines from "@distilled.cloud/fly-io/machines";
2import * as Retry from "@distilled.cloud/fly-io/Retry";
3import * as Fly from "@/Fly";
4import { makeMachineLeases } from "@/Fly/leases";
5import * as Test from "@/Test/Alchemy";
6import { expect } from "alchemy-test";
7import * as Deferred from "effect/Deferred";
8import * as Effect from "effect/Effect";
9import * as Exit from "effect/Exit";
10import * as Fiber from "effect/Fiber";
11import * as Scope from "effect/Scope";
12import * as HttpClient from "effect/http/HttpClient";
13import { assertAppGone } from "./fixtures/bluegreen.ts";
14import { sanitizeExecFailure } from "./fixtures/exec-lease.ts";
15
16const { test } = Test.make({ providers: Fly.providers() });
17
18test.provider(
19 "native lease cleanup waits for an in-flight renewal before releasing authority",
20 (stack) =>
21 Effect.gen(function* () {
22 yield* stack.destroy();
23 const target = yield* stack.deploy(
24 Effect.gen(function* () {
25 const app = yield* Fly.App("Site");
26 return yield* Fly.Machine("Worker", {
27 app,
28 image: "nginx:alpine",
29 region: "iad",
30 skipLaunch: true,
31 });
32 }),
33 );
34 try {
35 yield* Effect.gen(function* () {
36 const entered = yield* Deferred.make<void>();
37 const resume = yield* Deferred.make<void>();
38 const closed = yield* Deferred.make<void>();
39 const client = yield* HttpClient.HttpClient;
40 let acquisitions = 0;
41 let releases = 0;
42 const observed = HttpClient.transform(client, (response, request) =>
43 Effect.gen(function* () {
44 const path = request.url.split("?")[0]!;
45 if (path.endsWith(`/machines/${target.machineId}/lease`)) {
46 if (request.method === "POST") {
47 acquisitions++;
48 if (acquisitions === 2) {
49 yield* Deferred.succeed(entered, undefined);
50 yield* Deferred.await(resume);
51 }
52 } else if (request.method === "DELETE") releases++;
53 }
54 return yield* response;
55 }),
56 );
57 yield* Effect.gen(function* () {
58 const scope = yield* Scope.make();
59 yield* Effect.addFinalizer((exit) =>
60 Deferred.succeed(resume, undefined).pipe(
61 Effect.andThen(Scope.close(scope, exit)),
62 ),
63 );
64 const leases = yield* makeMachineLeases(target.appName).pipe(
65 Effect.provideService(Scope.Scope, scope),
66 );
67 yield* leases.acquire([target.machineId]);
68 yield* Effect.logInfo("Lease renewal probe", {
69 phase: "acquired",
70 acquisitions,
71 releases,
72 });
73 yield* Deferred.await(entered).pipe(
74 Effect.timeout("40 seconds"),
75 Effect.tapError((error) =>
76 Effect.logInfo("Lease renewal probe", {
77 phase: "await renewal",
78 acquisitions,
79 releases,
80 tag: error._tag,
81 }),
82 ),
83 );
84 const closing = yield* Scope.close(scope, Exit.void).pipe(
85 Effect.tap(() => Deferred.succeed(closed, undefined)),
86 Effect.forkScoped,
87 );
88 yield* Effect.sleep("200 millis");
89 expect(yield* Deferred.isDone(closed)).toBe(false);
90 expect(releases).toBe(0);
91 const held = yield* machines
92 .getMachineLease({
93 app_name: target.appName,
94 machine_id: target.machineId,
95 })
96 .pipe(Retry.none, Effect.timeout("10 seconds"));
97 expect(typeof held.data?.nonce === "string").toBe(true);
98 yield* Deferred.succeed(resume, undefined);
99 yield* Fiber.join(closing).pipe(
100 Effect.timeout("20 seconds"),
101 Effect.tapError((error) =>
102 Effect.logInfo("Lease renewal probe", {
103 phase: "await close",
104 acquisitions,
105 releases,
106 tag: error._tag,
107 }),
108 ),
109 );
110 expect(releases).toBe(1);
111 expect(acquisitions).toBe(2);
112 const absent = yield* machines
113 .getMachineLease({
114 app_name: target.appName,
115 machine_id: target.machineId,
116 })
117 .pipe(
118 Retry.none,
119 Effect.as(false),
120 Effect.catchTag("NotFound", () => Effect.succeed(true)),
121 Effect.timeout("10 seconds"),
122 );
123 expect(absent).toBe(true);
124 }).pipe(
125 Effect.provideService(HttpClient.HttpClient, observed),
126 Effect.scoped,
127 );
128 }).pipe(Effect.scoped);
129 } finally {
130 yield* stack.destroy();
131 yield* assertAppGone(target.appName);
132 }
133 }).pipe(sanitizeExecFailure),
134 {
135 tags: ["provider:fly", "provider:fly:app", "provider:fly:machine", "live"],
136 timeout: 240_000,
137 },
138);