| author | |
| committer | |
| log | 64a3cd6d7daab21f6bfbc00faf0104b6fd361267 |
| tree | be25610986c31fde5b30aaea2ff2e7f1f9fa8615 |
| parent | 48e2c27f2fef10520512fcfc482d022477ed18ab |
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), ""); |
| 476 | 477 | ||
| 478 | expect(yield* pendingServiceUpdateExists).toBe(true); | ||
| 477 | const released = yield* releaseManagedTunnelOnShutdown(); | 479 | const released = yield* releaseManagedTunnelOnShutdown(); |
| 478 | 480 | ||
| 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 | ); |
| 637 | 637 | ||
| 638 | // The launcher records an in-flight update in its durable state file. Read | 638 | // 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 the | 639 | // decides whether it owns pre-activation cleanup and while a server tears down. |
| 640 | // server is tearing down. Unreadable or unparseable state means no pending | 640 | export 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. | ||
| 648 | export 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* fs | 645 | 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. | ||
| 655 | const 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* fs | 663 | 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"; |
| 83 | import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts"; | 83 | import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts"; |
| 84 | import { | 84 | import { |
| 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 tear | 580 | // 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 | } |