be066a4b04
internal/update applies a new build of Maven to the box she runs on and
undoes it when the new build does not come up. cmd/mavupdate is the only
trigger: a CLI the owner runs on the host.
Apply is health-check the running daemon, snapshot the deployed artifacts,
make build, make test, install, restart, health-check — and restore the
snapshot on any failure. The order is load-bearing:
- The preflight health check refuses to update a daemon that is already
not answering. Without a working baseline, a failed update and a box
that was already broken are indistinguishable, and the rollback has
nothing to prove itself against.
- The snapshot is taken BEFORE the build, because make build writes its
binaries into the working tree and on the docker deployment the tree
is the install dir — snapshotting afterwards would snapshot the new
artifacts and leave nothing to roll back to.
- Verification is make build plus make test, before anything is
deployed, so a broken tree costs time and nothing else. A failed
verify also puts the tree's artifacts back, so a later restart by
hand cannot deploy code that failed its own tests.
- The rollback depends on nothing that just changed: byte-for-byte
copies out of the snapshot dir, sha256-verified on the way in, and
the same restart command. No build, no migration, no cooperation from
the code being replaced. It also runs on an uncancellable context —
a rollback interrupted halfway is worse than the failure that caused
it. When the restore itself fails it says so and names the directory
to copy back by hand rather than reporting a tidy rollback.
Off unless configured, and the refusals are code, not documentation. The
daemon does not import this package: there is no IPC method, no web route,
no timer and no act that can start an update, so nothing Maven says or
routes reaches it. Nothing fetches code — the new version is whatever the
owner pulled into the tree. The plan's release checker, auto-update
channel and in-process crash-loop supervisor are deliberately absent; a
process cannot reliably notice that it keeps dying, and restart-on-crash
belongs to compose or systemd. The database is never snapshotted or rolled
back; schema compatibility stays store.Migrate's job.
The config is refused at load without a health socket, since an update
that cannot check its own result cannot roll back, and refused when the
snapshot dir is inside the install dir, since a restore must not read from
what the install writes.
Vikunja #249
62 lines
1.8 KiB
Go
62 lines
1.8 KiB
Go
package update
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/kami/maven/internal/ipc"
|
|
)
|
|
|
|
// The health check is the whole basis for rolling back, so it has to mean
|
|
// something. "The process is running" does not: mavend can be up with a dead
|
|
// store, a socket it never bound, or a config it failed to parse. What is
|
|
// checked instead is that she answers a real read over the real IPC socket —
|
|
// which exercises the socket, the dispatch table and the store in one call.
|
|
//
|
|
// Presence is the method used because it is read-only (safe to retry), needs no
|
|
// arguments, and touches the store. It cannot write anything, so a health check
|
|
// never leaves a trace in her memory.
|
|
|
|
// DialHealth connects to the mavend socket and performs one read.
|
|
func DialHealth(ctx context.Context, socket string) error {
|
|
c, err := ipc.Dial(socket)
|
|
if err != nil {
|
|
return fmt.Errorf("update: health dial: %w", err)
|
|
}
|
|
defer c.Close()
|
|
if _, err := c.Presence(ctx); err != nil {
|
|
return fmt.Errorf("update: health read: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// waitHealthy retries the health check until it passes or the timeout elapses.
|
|
// A restart is not instantaneous — she loads a 1.7B on boot — so the first few
|
|
// failures are expected and are not a reason to roll back.
|
|
func (u *Updater) waitHealthy(ctx context.Context, timeout time.Duration) error {
|
|
deadline := u.now().Add(timeout)
|
|
delay := 500 * time.Millisecond
|
|
var last error
|
|
for {
|
|
attemptCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
|
err := u.health(attemptCtx, u.cfg.HealthSocket)
|
|
cancel()
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
last = err
|
|
if u.now().After(deadline) {
|
|
return fmt.Errorf("update: not healthy after %s: %w", timeout, last)
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-time.After(delay):
|
|
}
|
|
if delay < 5*time.Second {
|
|
delay *= 2
|
|
}
|
|
}
|
|
}
|