History

fix: harden automatic update transitions

t3-code[bot] committed 2 months ago

Commit 6ec81ac1 parent 46ed0925 files · +80 −27Browse files

Unified · Split

5 files
  1. apps/server/src/cloud/selfUpdate.test.ts +34 −0
  2. apps/server/src/cloud/selfUpdate.ts +20 −18
  3. apps/server/src/orchestration/http.ts +15 −5
  4. apps/web/src/components/ChatView.tsx +6 −2
  5. apps/web/src/components/settings/ConnectionsSettings.tsx +5 −2
apps/server/src/cloud/selfUpdate.test.ts+34−0
@@ -20,6 +20,7 @@ interface HarnessOptions { · 19 unmodified lines
2020 readonly managed?: boolean;
2121 readonly preflight?: "ready" | "blocked";
2222 readonly automaticUpdateIdle?: (order: string[]) => Effect.Effect<void, ServerSelfUpdateError>;
23+ readonly install?: (order: string[]) => Effect.Effect<void>;
2324 readonly requestUpdate?: ServiceLauncherClient.ServiceLauncherClient["Service"]["requestUpdate"];
2425 }
2526
@@ -35,6 +36,7 @@ const makeHarness = Effect.fn("test.make_self_update_harness")(function* ( · 9 unmodified lines
3536 Effect.gen(function* () {
3637 if (input.command === "npm") {
3738 order.push("install");
39+ yield* options.install?.(order) ?? Effect.void;
3840 const prefix = input.args[input.args.indexOf("--prefix") + 1];
3941 if (prefix === undefined) return yield* Effect.die("missing npm prefix");
4042 const entry = path.join(prefix, "node_modules", "t3", "dist", "bin.mjs");
@@ -236,6 +238,38 @@ it.layer(NodeServices.layer)("server self update", (it) => { · 195 unmodified lines
236238 }),
237239 );
238240
241+ it.effect("allows automatic update staging to be interrupted before handoff", () =>
242+ Effect.gen(function* () {
243+ const installStarted = yield* Deferred.make<void>();
244+ const releaseInstall = yield* Deferred.make<void>();
245+ let installs = 0;
246+ const { selfUpdate, order } = yield* makeHarness({
247+ install: () => {
248+ installs += 1;
249+ return installs === 1
250+ ? Deferred.succeed(installStarted, undefined).pipe(
251+ Effect.andThen(Deferred.await(releaseInstall)),
252+ )
253+ : Effect.void;
254+ },
255+ });
256+ const updateFiber = yield* selfUpdate
257+ .update({ targetVersion: "1.1.0", automatic: true })
258+ .pipe(Effect.forkChild);
259+ yield* Deferred.await(installStarted);
260+ const interruptFiber = yield* Fiber.interrupt(updateFiber).pipe(Effect.forkChild);
261+ yield* Deferred.succeed(releaseInstall, undefined);
262+ yield* Fiber.join(interruptFiber);
263+
264+ expect(order).toEqual(["install"]);
265+ expect(yield* selfUpdate.update({ targetVersion: "1.1.0", automatic: true })).toEqual({
266+ targetVersion: "1.1.0",
267+ method: "boot-service",
268+ updateId: "launcher-id",
269+ });
270+ }),
271+ );
272+
239273 it.effect("blocks new commands after an automatic handoff is accepted", () =>
240274 Effect.gen(function* () {
241275 const { selfUpdate } = yield* makeHarness();
apps/server/src/cloud/selfUpdate.ts+20−18
@@ -8,6 +8,7 @@ import { · 7 unmodified lines
88 import { HostProcessExecutablePath } from "@t3tools/shared/hostProcess";
99 import { hasAutomaticServerUpdateActiveWork } from "@t3tools/shared/automaticServerUpdate";
1010 import { compareSemverVersions } from "@t3tools/shared/semver";
11+import * as Cause from "effect/Cause";
1112 import * as Context from "effect/Context";
1213 import * as Duration from "effect/Duration";
1314 import * as Effect from "effect/Effect";
@@ -243,23 +244,9 @@ export const make = Effect.fn("cloud.server_self_update.make")(function* () { · 229 unmodified lines
243244 ? failWith(error.reason, error)
244245 : failWith(`Could not prepare t3@${targetVersion}.`, error),
245246 ),
247+ Effect.interruptible,
246248 );
247249
248− if (input.automatic === true) {
249− yield* commandAdmission.withPermits(1)(
250− Effect.gen(function* () {
251− yield* idleCheck.assertIdle.pipe(
252− Effect.tapError((error) =>
253− Effect.sync(() => {
254− retryWhenIdleFailure = error.retryWhenIdle === true;
255− }),
256− ),
257− );
258− yield* Ref.set(automaticHandoffPending, true);
259− }),
260− );
261− }
262−
263250 const activatePreparedUpdate = Effect.gen(function* () {
264251 yield* reportProgress("installing");
265252 const updateId = yield* launcher
@@ -282,16 +269,31 @@ export const make = Effect.fn("cloud.server_self_update.make")(function* () { · 16 unmodified lines
282269 });
283270 return { targetVersion, method: "boot-service" as const, updateId };
284271 });
285− return yield* activatePreparedUpdate;
272+ if (input.automatic !== true) return yield* activatePreparedUpdate;
273+
274+ return yield* commandAdmission
275+ .withPermits(1)(
276+ Effect.gen(function* () {
277+ yield* idleCheck.assertIdle.pipe(
278+ Effect.tapError((error) =>
279+ Effect.sync(() => {
280+ retryWhenIdleFailure = error.retryWhenIdle === true;
281+ }),
282+ ),
283+ );
284+ yield* Ref.set(automaticHandoffPending, true);
285+ }),
286+ )
287+ .pipe(Effect.andThen(activatePreparedUpdate));
286288 }).pipe(
287− Effect.onError(() =>
289+ Effect.onError((cause) =>
288290 Effect.all(
289291 [
290292 Ref.set(inFlight, false),
291293 Ref.set(automaticHandoffPending, false),
292294 Ref.set(
293295 lastFailedTarget,
294− retryWhenIdleFailure
296+ retryWhenIdleFailure || Cause.hasInterruptsOnly(cause)
295297 ? previousFailedTarget
296298 : input.automatic === true ||
297299 previousFailedTarget === null ||
apps/server/src/orchestration/http.ts+15−5
@@ -95,13 +95,23 @@ export const orchestrationHttpApiLayer = HttpApiBuilder.group( · 94 unmodified lines
9595 Effect.fn("environment.orchestration.dispatch")(function* (args) {
9696 yield* annotateEnvironmentRequest(args.endpoint.name);
9797 yield* requireEnvironmentScope(AuthOrchestrationOperateScope);
98− const normalizedCommand = yield* normalizeDispatchCommand(args.payload).pipe(
99− Effect.catch(() => failEnvironmentInvalidRequest("invalid_command")),
100− );
10198 return yield* serverSelfUpdate
102− .withCommandAdmission(orchestrationEngine.dispatch(normalizedCommand))
99+ .withCommandAdmission(
100+ Effect.gen(function* () {
101+ const normalizedCommand = yield* normalizeDispatchCommand(args.payload).pipe(
102+ Effect.catch(() => failEnvironmentInvalidRequest("invalid_command")),
103+ );
104+ return yield* orchestrationEngine
105+ .dispatch(normalizedCommand)
106+ .pipe(
107+ Effect.catch((cause) =>
108+ failEnvironmentInternal("orchestration_dispatch_failed", cause),
109+ ),
110+ );
111+ }),
112+ )
103113 .pipe(
104− Effect.catch((cause) =>
114+ Effect.catchTag("ServerSelfUpdateError", (cause) =>
105115 failEnvironmentInternal("orchestration_dispatch_failed", cause),
106116 ),
107117 );
apps/web/src/components/ChatView.tsx+6−2
@@ -2130,8 +2130,11 @@ function ChatViewContent(props: ChatViewProps) { · 2129 unmodified lines
21302130 if (
21312131 serverUpdateEnvironmentId &&
21322132 !reconnectingThroughVersionSkew &&
2133− (serverUpdateState.status !== "idle" ||
2134− (showVersionMismatchBanner && versionMismatch && versionMismatchDismissKey))
2133+ (serverUpdateState.status === "running" ||
2134+ serverUpdateState.status === "failed" ||
2135+ (showVersionMismatchBanner &&
2136+ (serverUpdateState.status === "pending" ||
2137+ (versionMismatch && versionMismatchDismissKey))))
21352138 ) {
21362139 const updateInProgress = serverUpdateState.status === "running";
21372140 const updatePending = serverUpdateState.status === "pending";
@@ -2181,6 +2184,7 @@ function ChatViewContent(props: ChatViewProps) { · 43 unmodified lines
21812184 // slot would only repeat it.
21822185 actions:
21832186 updateInProgress ||
2187+ updatePending ||
21842188 !versionMismatch ||
21852189 versionMismatchSelfUpdate === "desktop-managed" ? undefined : (
21862190 <ServerUpdateAction
apps/web/src/components/settings/ConnectionsSettings.tsx+5−2
@@ -1395,10 +1395,13 @@ function AutomaticServerUpdateControl({ · 1394 unmodified lines
13951395
13961396 if (compact) {
13971397 return (
1398− <div className="flex items-center gap-2 pt-1 text-xs text-muted-foreground">
1398+ <label
1399+ className="flex w-fit cursor-pointer items-center gap-2 pt-1 text-xs text-muted-foreground"
1400+ onClick={(event) => event.stopPropagation()}
1401+ >
13991402 {controlWithExplanation}
14001403 <span>Auto-update when idle</span>
1401− </div>
1404+ </label>
14021405 );
14031406 }
14041407

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t