| author | |
| committer | |
| log | ef4ec2ad4b9b08d1aca31d68aea28f0a846ef295 |
| tree | 577179355579f677b4dec2e9d23aa61ab260e6aa |
| parent | 61514c1294ff79e2a9924551b2042fcc0d1c3a34 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>2 files changed, 23 insertions(+), 6 deletions(-)
apps/server/src/cloud/selfUpdate.test.ts+10-2| ... | ... | @@ -545,11 +545,19 @@ it.layer(NodeServices.layer)("ServerSelfUpdate.update", (it) => { |
| 545 | 545 | yield* TestClock.adjust(Duration.seconds(10)); |
| 546 | 546 | assert.deepEqual(context.commands[3], { |
| 547 | 547 | command: "systemctl", |
| 548 | args: ["--user", "restart", "t3code.service"], | |
| 548 | args: ["--user", "restart", "--no-block", "t3code.service"], | |
| 549 | 549 | }); |
| 550 | 550 | assert.lengthOf(context.spawns, 0); |
| 551 | 551 | // systemd replaces the process; the server must not exit itself. |
| 552 | 552 | assert.equal(context.exitCount(), 0); |
| 553 | ||
| 554 | // The queued restart returns while this process is still shutting | |
| 555 | // down; the lock must stay held so a second update cannot rewrite the | |
| 556 | // unit mid-teardown. | |
| 557 | const concurrentError = yield* context.service | |
| 558 | .update({ targetVersion: "0.0.30" }) | |
| 559 | .pipe(Effect.flip); | |
| 560 | assert.include(concurrentError.reason, "already in progress"); | |
| 553 | 561 | }).pipe(Effect.provide(TestClock.layer())), |
| 554 | 562 | ); |
| 555 | 563 | |
| ... | ... | @@ -583,7 +591,7 @@ it.layer(NodeServices.layer)("ServerSelfUpdate.update", (it) => { |
| 583 | 591 | assert.deepEqual( |
| 584 | 592 | context.commands.slice(-2).map((entry) => entry.args), |
| 585 | 593 | [ |
| 586 | ["--user", "restart", BOOT_SERVICE_UNIT_FILE], | |
| 594 | ["--user", "restart", "--no-block", BOOT_SERVICE_UNIT_FILE], | |
| 587 | 595 | ["--user", "daemon-reload"], |
| 588 | 596 | ], |
| 589 | 597 | ); |
apps/server/src/cloud/selfUpdate.ts+13-4| ... | ... | @@ -354,14 +354,19 @@ export const make = Effect.fn("cloud.server_self_update.make")(function* (option |
| 354 | 354 | targetVersion, |
| 355 | 355 | }); |
| 356 | 356 | // Restart after the acknowledgement has had time to cross any relay |
| 357 | // hop. If systemd rejects the handoff, restore the previous unit while | |
| 358 | // this process is still alive and log the failure for diagnostics. | |
| 357 | // hop. --no-block queues the restart job and exits before systemd | |
| 358 | // stops this unit: a blocking restart's SIGTERM reaches the systemctl | |
| 359 | // child (it shares this service's cgroup), which read as a restart | |
| 360 | // failure and rolled the new unit back while the old server finished | |
| 361 | // shutting down. With the handoff race gone, a non-zero exit or spawn | |
| 362 | // error means systemd genuinely rejected the job while this process is | |
| 363 | // still alive, so restoring the previous unit below stays correct. | |
| 359 | 364 | yield* scheduleRestart( |
| 360 | 365 | Effect.gen(function* () { |
| 361 | 366 | const restart = yield* runner |
| 362 | 367 | .run({ |
| 363 | 368 | command: "systemctl", |
| 364 | args: ["--user", "restart", BOOT_SERVICE_UNIT_FILE], | |
| 369 | args: ["--user", "restart", "--no-block", BOOT_SERVICE_UNIT_FILE], | |
| 365 | 370 | }) |
| 366 | 371 | .pipe( |
| 367 | 372 | Effect.mapError((cause) => |
| ... | ... | @@ -389,9 +394,13 @@ export const make = Effect.fn("cloud.server_self_update.make")(function* (option |
| 389 | 394 | Effect.catch((error) => |
| 390 | 395 | Effect.logError("Server self-update could not restart the boot service.").pipe( |
| 391 | 396 | Effect.annotateLogs({ targetVersion, error: error.reason }), |
| 397 | // Permit a retry only after the failed handoff was rolled | |
| 398 | // back. A queued restart returns while this process is still | |
| 399 | // shutting down; releasing the lock then would let a second | |
| 400 | // update rewrite the unit mid-teardown. | |
| 401 | Effect.andThen(Ref.set(inFlight, false)), | |
| 392 | 402 | ), |
| 393 | 403 | ), |
| 394 | Effect.ensuring(Ref.set(inFlight, false)), | |
| 395 | 404 | ), |
| 396 | 405 | ); |
| 397 | 406 | } else { |