F6 deeper half: poll agent /disks/format/status on the 15s client timeout (slow USB mkfs runs detached) — agentapi.FormatStatus + awaitAgentFormat; then mount+register. Found on the live leg.

This commit is contained in:
2026-07-17 09:41:50 +02:00
parent 0025a3b090
commit 0515d153db
4 changed files with 142 additions and 3 deletions
+60 -2
View File
@@ -31,6 +31,7 @@ type diskAgent interface {
Disks(ctx context.Context) (agentapi.DisksResponse, error)
ListCandidates(ctx context.Context) (agentapi.CandidatesResult, error)
FormatDisk(ctx context.Context, device, fstype string, confirmed bool, durableID string) (agentapi.FormatResult, error)
FormatStatus(ctx context.Context) (agentapi.FormatStatusResult, error)
AssignDisk(ctx context.Context, uuid, where, fstype, options string) error
EjectDisk(ctx context.Context, where string) (agentapi.EjectResult, error)
Decommission(ctx context.Context, where string) (agentapi.DecommissionResult, error)
@@ -133,11 +134,18 @@ func (s *Server) runStorageInit(ctx context.Context, agent diskAgent, device, fs
}
return res, nil // STOP — no bypass; the UI surfaces Opsign.
}
if err != nil {
if err != nil && !isAgentTimeout(err) {
return storageInitResult{}, fmt.Errorf("formázás sikertelen: %w", err)
}
if !fr.Formatted {
return storageInitResult{}, fmt.Errorf("az eszköz nem lett megformázva (%s)", fr.Reason)
// A slow mkfs (e.g. a large USB) outran the agent client's 15 s timeout — the agent runs the
// mkfs DETACHED and records the job, so we POLL GET /disks/format/status to the terminal
// outcome (F6: "mkfs continues detached; poll the status") and only THEN continue to
// mount+register. A real (non-timeout) format failure surfaced above already.
fr, err = s.awaitAgentFormat(ctx, agent, device)
if err != nil {
return storageInitResult{}, err
}
}
// 2. Resolve the NEW fs UUID. A freshly-formatted RAW device isn't in /disks (not enrolled yet), so
// resolve via the raw-device scan too (Impl-2b) — the device now appears there with its new durable_id.
@@ -196,6 +204,56 @@ func (s *Server) runStorageAttach(ctx context.Context, agent diskAgent, device,
return storageInitResult{Registered: true, Where: stable}, nil
}
// isAgentTimeout reports whether a FormatDisk error is the agent-client's 15 s timeout (the mkfs is
// then running DETACHED agent-side) rather than a real format failure — so we poll the status instead
// of surfacing it as a failure (F6). A confirm/refuse verdict is returned as a typed error and is
// matched BEFORE this, so it never reaches here.
func isAgentTimeout(err error) bool {
if err == nil {
return false
}
if errors.Is(err, context.DeadlineExceeded) {
return true
}
s := err.Error()
return strings.Contains(s, "Client.Timeout") || strings.Contains(s, "deadline exceeded") || strings.Contains(s, "awaiting headers")
}
// awaitAgentFormat follows a mkfs that outran the agent-client timeout: the agent runs it detached and
// records the job, so we poll GET /disks/format/status until it leaves `running` (F6). Returns a
// Formatted result on a `done` job, or an error on `failed` / idle (no job) / a persistent poll error /
// ctx expiry. Transient poll errors are tolerated up to a small budget (the agent may be busy).
func (s *Server) awaitAgentFormat(ctx context.Context, agent diskAgent, device string) (agentapi.FormatResult, error) {
t := time.NewTicker(2 * time.Second)
defer t.Stop()
consecutiveErrs := 0
for {
st, err := agent.FormatStatus(ctx)
if err != nil {
consecutiveErrs++
if consecutiveErrs >= 5 {
return agentapi.FormatResult{}, fmt.Errorf("a formázás állapota nem kérdezhető le: %w", err)
}
} else {
consecutiveErrs = 0
switch st.Phase {
case "done": // the agent's format-job terminal phases (internal/localapi/formatjob.go)
return agentapi.FormatResult{Device: device, Formatted: true}, nil
case "failed":
return agentapi.FormatResult{}, fmt.Errorf("formázás sikertelen: %s", st.Error)
case "idle":
return agentapi.FormatResult{}, fmt.Errorf("a formázás nem indult el az eszközön (%s)", device)
// "running" (or an unexpected phase) → keep polling
}
}
select {
case <-ctx.Done():
return agentapi.FormatResult{}, ctx.Err()
case <-t.C:
}
}
}
// reEnrollClearMarker un-retires a re-plugged decommissioned drive (Change 4): clears the soft marker
// and restores Schedulable so its apps' "missing storage" indicator clears. Returns true if it acted.
func (s *Server) reEnrollClearMarker(where string) (bool, error) {