mirror of
https://github.com/zap-proto/zip.git
synced 2026-10-19 15:38:57 +00:00
Running=false means "deployed but down" and Disabled separates a maintenance stop
from a crash. Neither can say WHY, and for a LAZY plugin that was the only trace
there was.
An eager plugin that will not boot fails at Load, in front of the caller. A lazy
one cannot: its mount succeeds with no process behind it — that is what Lazy means
— so the start happens on the first request, inside the child, long after anyone
was watching. The host logged one line and kept nothing, and the route answered
503 "no instance running" for that request and every later one.
So Running=false could not be read: a lazy plugin nobody has asked for yet reports
exactly what one whose every start dies reports. A host could answer
{"status":"ok"} with nine of its surfaces permanently 503, which is what
api.hanzo.ai did.
Status.Error is that missing fact. It is DERIVED against cur rather than cleared
on success — an instance becomes current in three places, and a clear that must be
remembered at all three is a clear that will be forgotten at one. A plugin that has
never been asked still reports nothing, because cold is not an error and must not
read as one.
Co-authored-by: Hanzo Dev <[email protected]>
150 lines
5.4 KiB
Go
150 lines
5.4 KiB
Go
package zip
|
|
|
|
import (
|
|
"sort"
|
|
"time"
|
|
)
|
|
|
|
// What a host is running. A fleet view needs one thing this package did not
|
|
// expose: the ability to ask a live process what it actually loaded, at which
|
|
// version, and whether it is still up. Reading it from deployment config
|
|
// answers what was INTENDED; only the process knows what is TRUE — which is the
|
|
// difference that matters during a rolling upgrade, when the two disagree by
|
|
// design.
|
|
|
|
// Status is one loaded plugin as the host currently sees it.
|
|
type Status struct {
|
|
Name string `json:"name"`
|
|
|
|
// Prefix is the FIRST subtree this plugin answers — the one a log line
|
|
// names it by. Prefixes is every subtree, and a plugin may own several.
|
|
// Reporting only the first would understate the blast radius of taking
|
|
// this plugin down, which is the question a fleet view exists to answer.
|
|
Prefix string `json:"prefix"`
|
|
Prefixes []string `json:"prefixes,omitempty"`
|
|
|
|
// Source is where the binary came from: "embedded", "path", "url", or
|
|
// "remote" for an instance this host did not start.
|
|
Source string `json:"source"`
|
|
|
|
// Version is the artifact's SHA-256 when it was installed from a URL —
|
|
// the only version identifier that cannot drift from the bits actually
|
|
// running, since it IS the bits. Empty for the other sources.
|
|
Version string `json:"version,omitempty"`
|
|
|
|
// Addr is the socket or address serving it.
|
|
Addr string `json:"addr,omitempty"`
|
|
|
|
// PID is the child process, or 0 when this host did not start it.
|
|
PID int `json:"pid,omitempty"`
|
|
|
|
// Running is false after Unload, or after a child exited and no Reload has
|
|
// replaced it. Its routes stay registered and answer 503, so a false here
|
|
// is the difference between "not deployed" and "deployed but down".
|
|
Running bool `json:"running"`
|
|
|
|
// Disabled is true when Unload stopped it deliberately, as opposed to it
|
|
// having crashed. Both answer 503, so without this an operator cannot tell
|
|
// a maintenance window from an outage — and would page for the former.
|
|
Disabled bool `json:"disabled,omitempty"`
|
|
|
|
// Since is when the CURRENT instance started — it resets on Reload, so it
|
|
// reports the age of what is running, not of the mount.
|
|
Since time.Time `json:"since,omitzero"`
|
|
|
|
// Reloads counts successful swaps since Load. A climbing number on one
|
|
// host and not its peers is the signal that a rollout is uneven.
|
|
Reloads int `json:"reloads"`
|
|
|
|
// Restarts counts times the supervisor brought this plugin back after it
|
|
// died on its own. Distinct from Reloads, which are deliberate: a nonzero
|
|
// Restarts is a plugin crashing, and a climbing one is a crash loop.
|
|
Restarts int `json:"restarts"`
|
|
|
|
// Error is why the last start attempt failed, empty when none has. It is the
|
|
// answer Running=false cannot give: a lazy plugin that has never been asked and
|
|
// one whose every start dies both report Running=false, and only this separates
|
|
// them. Cleared when an instance comes up.
|
|
Error string `json:"error,omitempty"`
|
|
|
|
// Usage is what this plugin costs right now, read from the kernel.
|
|
Usage Usage `json:"usage,omitzero"`
|
|
}
|
|
|
|
// Usage is a plugin's resource cost, measured rather than self-reported.
|
|
//
|
|
// This exists because a plugin is a process. A linked-in subsystem cannot be
|
|
// measured this way at any price — one process has one RSS and one CPU clock,
|
|
// and nothing inside it can be attributed to a subsystem. Running a service as
|
|
// its own process makes "which app is eating the memory" a question with an
|
|
// exact answer instead of an argument.
|
|
//
|
|
// Zero means not measured (no /proc, or nothing running), not measured-as-zero.
|
|
type Usage struct {
|
|
// CPU is total user+system time this instance has consumed since it
|
|
// started. It only ever climbs, so a rate is the interesting derivative.
|
|
CPU time.Duration `json:"cpuNs,omitzero"`
|
|
// RSS is resident memory in bytes — the honest number for "what does this
|
|
// plugin cost", as opposed to virtual size.
|
|
RSS int64 `json:"rssBytes,omitzero"`
|
|
// Threads and FDs are the two limits a busy service hits first, and both
|
|
// are leaks when they climb without bound.
|
|
Threads int `json:"threads,omitzero"`
|
|
FDs int `json:"fds,omitzero"`
|
|
}
|
|
|
|
// Plugins reports every plugin this host has loaded, ordered by name so a diff
|
|
// between two hosts is stable. Safe to call while requests are in flight.
|
|
func (a *App) Plugins() []Status {
|
|
var ps []*plugin
|
|
for _, h := range a.hosts() {
|
|
h.plugMu.Lock()
|
|
for _, p := range h.plugins {
|
|
ps = append(ps, p)
|
|
}
|
|
h.plugMu.Unlock()
|
|
}
|
|
|
|
out := make([]Status, 0, len(ps))
|
|
for _, p := range ps {
|
|
s := Status{
|
|
Name: p.name,
|
|
Prefix: p.prefix,
|
|
Prefixes: append([]string(nil), p.prefixes...),
|
|
Source: p.source(),
|
|
Version: p.spec.Sum,
|
|
Disabled: p.disabled.Load(),
|
|
Reloads: int(p.reloads.Load()),
|
|
Restarts: int(p.restarts.Load()),
|
|
Error: p.startError(),
|
|
}
|
|
if in := p.cur.Load(); in != nil {
|
|
s.Running = true
|
|
s.Addr = in.sock
|
|
s.Since = in.started
|
|
if in.cmd != nil && in.cmd.Process != nil {
|
|
s.PID = in.cmd.Process.Pid
|
|
s.Usage = readUsage(s.PID)
|
|
}
|
|
}
|
|
out = append(out, s)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
|
return out
|
|
}
|
|
|
|
// source names where this plugin's binary came from.
|
|
func (p *plugin) source() string {
|
|
switch {
|
|
case p.spec.Addr != "":
|
|
return "remote"
|
|
case p.spec.URL != "":
|
|
return "url"
|
|
case len(p.spec.Bin) > 0:
|
|
return "embedded"
|
|
case p.spec.Path != "":
|
|
return "path"
|
|
}
|
|
return "unknown"
|
|
}
|