Safe removal (R-1044), found on Tester 1 G4: the drive gate leaves a safely removed drive alone (no false RETURNED/ABSENT, no mail); presence = backing device, not the agent's state
gates / gates (push) Successful in 46s
gates / gates (push) Successful in 46s
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -266,6 +266,10 @@ func planDriveGates(paths []settings.StoragePath, disks []agentapi.DiskInfo) []g
|
||||
if sp.Decommissioned {
|
||||
continue
|
||||
}
|
||||
// R-1044: a SAFELY REMOVED drive is reconcileEjectedDrives' alone (TestPlanDriveGates_SkipsSafelyRemovedDrive).
|
||||
if sp.EjectedAt != "" {
|
||||
continue
|
||||
}
|
||||
// NAS network storage is NOT a drive — the agent never lists it in /disks, so the drive-absent gate
|
||||
// would falsely see it "absent" (present[sp.Path]==false) and STOP its apps. A NAS blip is
|
||||
// recoverable (the agent's per-share liveness → a warning badge), never the drive stop-cascade.
|
||||
|
||||
@@ -291,3 +291,20 @@ func TestPlanDriveGates(t *testing.T) {
|
||||
t.Errorf("internal SSD/system path must NEVER be gated (only /mnt/felhom-drives/ externals)")
|
||||
}
|
||||
}
|
||||
|
||||
// R-1044 (found on Tester 1, Part G4, 2026-10-15): a SAFELY REMOVED drive belongs to reconcileEjectedDrives only. The
|
||||
// periodic gate saw it still bound during the 32-second removal and called it RETURNED (clearing the removal's
|
||||
// disconnected mark), then ABSENT 30 s later — with a „disconnected" mail for a removal the household asked for.
|
||||
// COMPANION RED-PROOF: drop the EjectedAt skip in planDriveGates → both cases below produce an action. Restored.
|
||||
func TestPlanDriveGates_SkipsSafelyRemovedDrive(t *testing.T) {
|
||||
p := "/mnt/felhom-drives/media_d"
|
||||
removed := settings.StoragePath{Path: p, Disconnected: true, EjectedAt: "2026-10-15T08:10:20Z"}
|
||||
still := []agentapi.DiskInfo{{GuestPath: p, MountPath: "/mnt/media_d", State: "attached", BoundUnderParent: true}}
|
||||
if a := planDriveGates([]settings.StoragePath{removed}, still); len(a) != 0 {
|
||||
t.Fatalf("a safely removed drive still bound during the removal must not be RETURNED by the gate: %+v", a)
|
||||
}
|
||||
removed.Disconnected = false
|
||||
if a := planDriveGates([]settings.StoragePath{removed}, nil); len(a) != 0 {
|
||||
t.Fatalf("a safely removed drive that is gone must not be gated ABSENT (no mail for a safe removal): %+v", a)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,21 +325,13 @@ func (s *Server) reconcileEjectedDrives(ctx context.Context, agent *agentapi.Cli
|
||||
if sp.EjectedAt == "" {
|
||||
continue
|
||||
}
|
||||
var disk *agentapi.DiskInfo
|
||||
for i := range disks {
|
||||
if (sp.EjectDurableID != "" && disks[i].DurableID == sp.EjectDurableID) || (sp.EjectDurableID == "" && disks[i].GuestPath == sp.Path) {
|
||||
disk = &disks[i]
|
||||
}
|
||||
}
|
||||
attached := disk != nil && disk.State == "attached"
|
||||
if !sp.EjectSeenGone {
|
||||
if !attached {
|
||||
_ = s.settings.SetEjectSeenGone(sp.Path)
|
||||
s.logger.Printf("[INFO] [media] safely removed drive %s: its disk has left the box", sp.Path)
|
||||
}
|
||||
step, disk := ejectStep(sp, disks)
|
||||
switch step {
|
||||
case ejectWaitGone, ejectWaitBack:
|
||||
continue
|
||||
}
|
||||
if !attached {
|
||||
case ejectGone:
|
||||
_ = s.settings.SetEjectSeenGone(sp.Path)
|
||||
s.logger.Printf("[INFO] [media] safely removed drive %s: its disk has left the box", sp.Path)
|
||||
continue
|
||||
}
|
||||
if !disk.BoundUnderParent && disk.MountPath != "" {
|
||||
@@ -420,3 +412,33 @@ var plexClaim = func(code string) (int, error) {
|
||||
resp.Body.Close()
|
||||
return resp.StatusCode, nil
|
||||
}
|
||||
|
||||
// The safe-removal states (R-1044).
|
||||
const (
|
||||
ejectWaitGone = iota // removed, the disk is still in the box
|
||||
ejectGone // the disk has just been seen gone
|
||||
ejectWaitBack // gone, not back yet
|
||||
ejectBack // back: re-attach and clear
|
||||
)
|
||||
|
||||
// ejectStep is the PURE decision of reconcileEjectedDrives. A disk is PHYSICALLY present when the agent reports a backing
|
||||
// device: its `state` stays „attached" for a registered disk that is gone (measured on Tester 1, Part G4: Media D pulled,
|
||||
// state attached, backing_device "", total_bytes 0). Pinned by TestEjectStep_G4.
|
||||
func ejectStep(sp settings.StoragePath, disks []agentapi.DiskInfo) (int, *agentapi.DiskInfo) {
|
||||
var disk *agentapi.DiskInfo
|
||||
for i := range disks {
|
||||
if (sp.EjectDurableID != "" && disks[i].DurableID == sp.EjectDurableID) || (sp.EjectDurableID == "" && disks[i].GuestPath == sp.Path) {
|
||||
disk = &disks[i]
|
||||
}
|
||||
}
|
||||
present := disk != nil && disk.BackingDevice != ""
|
||||
switch {
|
||||
case !sp.EjectSeenGone && present:
|
||||
return ejectWaitGone, disk
|
||||
case !sp.EjectSeenGone:
|
||||
return ejectGone, disk
|
||||
case !present:
|
||||
return ejectWaitBack, disk
|
||||
}
|
||||
return ejectBack, disk
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ package web
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/agentapi"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
||||
)
|
||||
|
||||
// „Later" is tonight 03:30 Budapest time — or tomorrow's when it is already past 03:30.
|
||||
@@ -18,3 +21,25 @@ func TestNextMediaLater(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// R-1044, the records measured on Tester 1 (Part G4): a pulled, registered disk keeps state „attached" with an EMPTY
|
||||
// backing device; the decision must read the backing device. COMPANION RED-PROOF: use `disk.State == "attached"` as
|
||||
// presence → the „gone" case fails (the box never notices the disk left — what rc.2 did on Tester 1). Restored.
|
||||
func TestEjectStep_G4(t *testing.T) {
|
||||
sp := settings.StoragePath{Path: "/mnt/felhom-drives/media_d", EjectedAt: "2026-10-15T08:10:20Z", EjectDurableID: "uuid:8a31"}
|
||||
in := agentapi.DiskInfo{DurableID: "uuid:8a31", State: "attached", BackingDevice: "/dev/sdd", MountPath: "/mnt/media_d", TotalBytes: 21474836480}
|
||||
gone := agentapi.DiskInfo{DurableID: "uuid:8a31", State: "attached", BackingDevice: "", MountPath: "/mnt/media_d"}
|
||||
if st, _ := ejectStep(sp, []agentapi.DiskInfo{in}); st != ejectWaitGone {
|
||||
t.Fatalf("still in the box right after the removal: want wait-gone, got %d", st)
|
||||
}
|
||||
if st, _ := ejectStep(sp, []agentapi.DiskInfo{gone}); st != ejectGone {
|
||||
t.Fatalf("pulled (state attached, no backing device): want gone, got %d", st)
|
||||
}
|
||||
sp.EjectSeenGone = true
|
||||
if st, _ := ejectStep(sp, []agentapi.DiskInfo{gone}); st != ejectWaitBack {
|
||||
t.Fatalf("still gone: want wait-back, got %d", st)
|
||||
}
|
||||
if st, d := ejectStep(sp, []agentapi.DiskInfo{in}); st != ejectBack || d == nil {
|
||||
t.Fatalf("plugged back: want back, got %d", st)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user