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 } } }