v0.335.0: no app stop for a tier the agent will refuse because its host is busy (host_busy pre-check, R-921)
gates / gates (push) Successful in 53s
gates / gates (push) Successful in 53s
This commit is contained in:
@@ -132,7 +132,7 @@ func (c *Client) BackupStatusFor(ctx context.Context, target string) (StatusResp
|
||||
// tier is the agent's first reason to refuse POST /backup (HTTP 409), so the controller asks this BEFORE it
|
||||
// stops any app. It reads only GET /backup/tiers and GET /backup/status?target= (both agent >= v0.97.0); a
|
||||
// pre-R-82 agent (no /backup/tiers) answers nil, nil. Any other error is returned — the caller fails toward
|
||||
// backing up. It does NOT see the agent's host-wide heavy-operation gate: no endpoint serves it.
|
||||
// backing up. It does NOT see the agent's host-wide heavy-operation gate; HostBusy does (newer agents).
|
||||
func (c *Client) BackupJobsInFlight(ctx context.Context) ([]string, error) {
|
||||
tiers, err := c.BackupTiers(ctx)
|
||||
if errors.Is(err, ErrTiersUnsupported) {
|
||||
@@ -154,6 +154,21 @@ func (c *Client) BackupJobsInFlight(ctx context.Context) ([]string, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// HostBusy (R-921) reads GET /backup/status's host_busy: whether the agent's host-wide heavy-operation gate is held
|
||||
// right now (another guest's backup, the restore test, a night OS step …) — the agent's second reason to refuse
|
||||
// POST /backup. known=false: the agent does not serve the field (older than the agent release that adds it) — no
|
||||
// information, never „free". An error is returned; the caller fails toward backing up.
|
||||
func (c *Client) HostBusy(ctx context.Context) (busy bool, what string, known bool, err error) {
|
||||
st, err := c.BackupStatusFor(ctx, "")
|
||||
if err != nil {
|
||||
return false, "", false, err
|
||||
}
|
||||
if st.HostBusy == nil {
|
||||
return false, "", false, nil
|
||||
}
|
||||
return st.HostBusy.Busy, st.HostBusy.What, true, nil
|
||||
}
|
||||
|
||||
// SetBackupTargetResponse mirrors POST /backup/target (agent >= v0.113.0).
|
||||
type SetBackupTargetResponse struct {
|
||||
Target string `json:"target"`
|
||||
|
||||
@@ -202,6 +202,15 @@ type StatusResponse struct {
|
||||
// Tiers (agent >= v0.131.0, R-517) — per tier: newest success, last attempt, storage presence.
|
||||
// Absent on an older agent; the page then falls back to the single latest record.
|
||||
Tiers []TierBackupState `json:"tiers,omitempty"`
|
||||
// HostBusy (R-921, agent Unreleased 2026-10-19) — whether the agent's host-wide heavy-operation gate is held
|
||||
// right now (then POST /backup answers 409). nil = an older agent that does not serve it.
|
||||
HostBusy *HostBusyState `json:"host_busy,omitempty"`
|
||||
}
|
||||
|
||||
// HostBusyState mirrors the agent's localapi.HostBusyState.
|
||||
type HostBusyState struct {
|
||||
Busy bool `json:"busy"`
|
||||
What string `json:"what,omitempty"`
|
||||
}
|
||||
|
||||
// TierBackupState mirrors the agent's localapi.TierBackupState.
|
||||
|
||||
@@ -73,3 +73,35 @@ func TestBackupJobsInFlight_StatusErrorIsReturned(t *testing.T) {
|
||||
t.Fatal("a failed status read was swallowed — the caller could not tell it from 'nothing in flight'")
|
||||
}
|
||||
}
|
||||
|
||||
// R-921 part 2: HostBusy reads the agent's real wire shape (untargeted GET /backup/status, `host_busy`); an agent
|
||||
// without the field is „not known" — never „free".
|
||||
func TestHostBusy_ReadsTheAgentsGate(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name, body string
|
||||
busy, known bool
|
||||
what string
|
||||
}{
|
||||
{"held", `{"ok":true,"data":{"vmid":9201,"phase":"idle","host_busy":{"busy":true,"what":"os-pve-step"}}}`, true, true, "os-pve-step"},
|
||||
{"free", `{"ok":true,"data":{"vmid":9201,"phase":"idle","host_busy":{"busy":false}}}`, false, true, ""},
|
||||
{"older agent", `{"ok":true,"data":{"vmid":9201,"phase":"idle"}}`, false, false, ""},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /backup/status", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.RawQuery != "" {
|
||||
http.Error(w, "want the untargeted status", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
_, _ = w.Write([]byte(tc.body))
|
||||
})
|
||||
s := httptest.NewTLSServer(mux)
|
||||
defer s.Close()
|
||||
c := clientFor(t, s, strings.TrimPrefix(s.URL, "https://"))
|
||||
busy, what, known, err := c.HostBusy(context.Background())
|
||||
if err != nil || busy != tc.busy || known != tc.known || what != tc.what {
|
||||
t.Fatalf("HostBusy = %v %q known=%v err=%v, want %v %q known=%v", busy, what, known, err, tc.busy, tc.what, tc.known)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user