authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-08-06 05:02:27-07:00
committergravatar for me@theo.liTheo Browne <me@theo.li> 2026-08-06 08:09:51-04:00
log64a3cd6d7daab21f6bfbc00faf0104b6fd361267
treebe25610986c31fde5b30aaea2ff2e7f1f9fa8615
parent48e2c27f2fef10520512fcfc482d022477ed18ab

fix(server): separate trial cleanup from handoff retention


3 files changed, 20 insertions(+), 14 deletions(-)

apps/server/src/cloud/http.test.ts+2
...@@ -35,6 +35,7 @@ import {...@@ -35,6 +35,7 @@ import {
35 consumeCloudReplayGuards,35 consumeCloudReplayGuards,
36 isSupportedLinkProviderKind,36 isSupportedLinkProviderKind,
37 linkProofScopes,37 linkProofScopes,
38 pendingServiceUpdateExists,
38 reconcileDesiredCloudLink,39 reconcileDesiredCloudLink,
39 releaseManagedTunnelOnShutdown,40 releaseManagedTunnelOnShutdown,
40} from "./http.ts";41} from "./http.ts";
...@@ -474,6 +475,7 @@ describe("releaseManagedTunnelOnShutdown", () => {...@@ -474,6 +475,7 @@ describe("releaseManagedTunnelOnShutdown", () => {
474 const config = yield* ServerConfigModule.ServerConfig;475 const config = yield* ServerConfigModule.ServerConfig;
475 yield* fs.writeFileString(path.join(config.baseDir, "runtime", SERVICE_STOP_MARKER_FILE), "");476 yield* fs.writeFileString(path.join(config.baseDir, "runtime", SERVICE_STOP_MARKER_FILE), "");
476477
478 expect(yield* pendingServiceUpdateExists).toBe(true);
477 const released = yield* releaseManagedTunnelOnShutdown();479 const released = yield* releaseManagedTunnelOnShutdown();
478480
479 expect(released).toBe(true);481 expect(released).toBe(true);
apps/server/src/cloud/http.ts+16-12
...@@ -635,17 +635,9 @@ export const reconcileDesiredCloudLink = Effect.fn("environment.cloud.reconcileD...@@ -635,17 +635,9 @@ export const reconcileDesiredCloudLink = Effect.fn("environment.cloud.reconcileD
635 },635 },
636);636);
637637
638// The launcher records an in-flight update in its durable state file. Read638// The launcher owns this durable state, so read it directly both when a trial
639// straight from disk: the launcher owns that file, and this runs while the639// decides whether it owns pre-activation cleanup and while a server tears down.
640// server is tearing down. Unreadable or unparseable state means no pending640export const pendingServiceUpdateExists = Effect.gen(function* () {
641// update — the release then proceeds as before.
642//
643// A pending update alone is not proof a replacement server is coming: an
644// explicit launcher stop (`t3 service uninstall`, `systemctl stop`) during
645// the pending window also tears this server down, with nothing starting
646// afterwards. The launcher marks that case with a stop-marker file just
647// before it signals the child, so pending + no marker is the update handoff.
648export const pendingUpdateHandoffExists = Effect.gen(function* () {
649 const config = yield* ServerConfig.ServerConfig;641 const config = yield* ServerConfig.ServerConfig;
650 const fs = yield* FileSystem.FileSystem;642 const fs = yield* FileSystem.FileSystem;
651 const path = yield* Path.Path;643 const path = yield* Path.Path;
...@@ -653,9 +645,21 @@ export const pendingUpdateHandoffExists = Effect.gen(function* () {...@@ -653,9 +645,21 @@ export const pendingUpdateHandoffExists = Effect.gen(function* () {
653 const stateText = yield* fs645 const stateText = yield* fs
654 .readFileString(path.join(runtimeDir, SERVICE_STATE_FILE))646 .readFileString(path.join(runtimeDir, SERVICE_STATE_FILE))
655 .pipe(Effect.option);647 .pipe(Effect.option);
656 if (Option.isNone(stateText) || !serviceStateHasPendingUpdate(stateText.value)) {648 return Option.isSome(stateText) && serviceStateHasPendingUpdate(stateText.value);
649});
650
651// A pending update alone is not proof a replacement server is coming: an
652// explicit launcher stop (`t3 service uninstall`, `systemctl stop`) during
653// the pending window also tears this server down. The launcher marks that case
654// just before it signals the child, so pending + no marker is the handoff.
655const pendingUpdateHandoffExists = Effect.gen(function* () {
656 if (!(yield* pendingServiceUpdateExists)) {
657 return false;657 return false;
658 }658 }
659 const config = yield* ServerConfig.ServerConfig;
660 const fs = yield* FileSystem.FileSystem;
661 const path = yield* Path.Path;
662 const runtimeDir = path.join(config.baseDir, "runtime");
659 const stopping = yield* fs663 const stopping = yield* fs
660 .exists(path.join(runtimeDir, SERVICE_STOP_MARKER_FILE))664 .exists(path.join(runtimeDir, SERVICE_STOP_MARKER_FILE))
661 .pipe(Effect.orElseSucceed(() => false));665 .pipe(Effect.orElseSucceed(() => false));
apps/server/src/server.ts+2-2
...@@ -83,7 +83,7 @@ import * as ServerSecretStore from "./auth/ServerSecretStore.ts";...@@ -83,7 +83,7 @@ import * as ServerSecretStore from "./auth/ServerSecretStore.ts";
83import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts";83import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts";
84import {84import {
85 connectHttpApiLayer,85 connectHttpApiLayer,
86 pendingUpdateHandoffExists,86 pendingServiceUpdateExists,
87 reconcileDesiredCloudLink,87 reconcileDesiredCloudLink,
88 releaseManagedTunnelOnShutdown,88 releaseManagedTunnelOnShutdown,
89} from "./cloud/http.ts";89} from "./cloud/http.ts";
...@@ -579,7 +579,7 @@ export const makeServerLayer = Layer.unwrap(...@@ -579,7 +579,7 @@ export const makeServerLayer = Layer.unwrap(
579 // while the launcher's explicit-stop marker allows it to be released.579 // while the launcher's explicit-stop marker allows it to be released.
580 // Other runtimes wait for activation so a failed standby cannot tear580 // Other runtimes wait for activation so a failed standby cannot tear
581 // down the active runtime's tunnel.581 // down the active runtime's tunnel.
582 const cleanupBeforeActivation = yield* pendingUpdateHandoffExists;582 const cleanupBeforeActivation = yield* pendingServiceUpdateExists;
583 if (cleanupBeforeActivation) {583 if (cleanupBeforeActivation) {
584 yield* Effect.addFinalizer(() => releaseManagedTunnel);584 yield* Effect.addFinalizer(() => releaseManagedTunnel);
585 }585 }