R-1288: a restore of a set-aside unit sets the replaced install's unit aside at once, with no alarm. R-1282: the later media restart time is stored and resumed at start, once. Red-proved. Commit only.
gates / gates (push) Successful in 1m26s
gates / gates (push) Successful in 1m26s
This commit is contained in:
@@ -250,6 +250,53 @@ func nextMediaLater(now time.Time) time.Time {
|
||||
return t
|
||||
}
|
||||
|
||||
// mediaLaterRestart runs the „later" restart (tests replace it).
|
||||
var mediaLaterRestart = func(s *Server, why string) { s.restartMediaApps(why) }
|
||||
|
||||
// scheduleMediaLater arms the one „later" timer for at (a time already past fires at once); when it fires, the stored
|
||||
// time is cleared, so it runs once.
|
||||
func (s *Server) scheduleMediaLater(at time.Time) {
|
||||
s.mediaMu.Lock()
|
||||
defer s.mediaMu.Unlock()
|
||||
if s.mediaLaterTmr != nil {
|
||||
s.mediaLaterTmr.Stop()
|
||||
}
|
||||
s.mediaLaterAt = at
|
||||
d := time.Until(at)
|
||||
if d < 0 {
|
||||
d = 0
|
||||
}
|
||||
s.mediaLaterTmr = time.AfterFunc(d, func() {
|
||||
mediaLaterRestart(s, "the household's „later\"")
|
||||
s.mediaMu.Lock()
|
||||
s.mediaLaterAt = time.Time{}
|
||||
s.mediaMu.Unlock()
|
||||
if s.settings != nil {
|
||||
if err := s.settings.SetMediaRestartLater(time.Time{}); err != nil {
|
||||
s.logger.Printf("[WARN] [media] clearing the stored „later\" media restart: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ResumeMediaLater (R-1282, main calls it at start) re-arms a „later" media restart the household asked for before
|
||||
// this controller started: at its time, or at once when that time passed while the controller was down. It runs once.
|
||||
func (s *Server) ResumeMediaLater() {
|
||||
if s.settings == nil {
|
||||
return
|
||||
}
|
||||
at := s.settings.GetMediaRestartLater()
|
||||
if at.IsZero() {
|
||||
return
|
||||
}
|
||||
if time.Until(at) <= 0 {
|
||||
s.logger.Printf("[INFO] [media] the household's „later\" media restart (%s) passed while the controller was down — it runs now (R-1282)", at.Format(time.RFC3339))
|
||||
} else {
|
||||
s.logger.Printf("[INFO] [media] the household's „later\" media restart is kept from before the restart: %s (R-1282)", at.Format(time.RFC3339))
|
||||
}
|
||||
s.scheduleMediaLater(at)
|
||||
}
|
||||
|
||||
// handleMediaRestart: POST /api/storage/media/restart {"when":"now"|"later","force":bool}.
|
||||
func (s *Server) handleMediaRestart(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
@@ -262,19 +309,13 @@ func (s *Server) handleMediaRestart(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
if req.When == "later" {
|
||||
at := nextMediaLater(time.Now())
|
||||
s.mediaMu.Lock()
|
||||
if s.mediaLaterTmr != nil {
|
||||
s.mediaLaterTmr.Stop()
|
||||
if s.settings != nil {
|
||||
if err := s.settings.SetMediaRestartLater(at); err != nil {
|
||||
s.logger.Printf("[WARN] [media] storing the „later\" media restart time: %v — a controller restart before then forgets it", err)
|
||||
}
|
||||
}
|
||||
s.mediaLaterAt = at
|
||||
s.mediaLaterTmr = time.AfterFunc(time.Until(at), func() {
|
||||
s.restartMediaApps("the household's „later\"")
|
||||
s.mediaMu.Lock()
|
||||
s.mediaLaterAt = time.Time{}
|
||||
s.mediaMu.Unlock()
|
||||
})
|
||||
s.mediaMu.Unlock()
|
||||
s.logger.Printf("[INFO] [media] media restart scheduled for %s (the household's „later\"; a controller restart forgets it, the notice stays)", at.Format(time.RFC3339))
|
||||
s.scheduleMediaLater(at)
|
||||
s.logger.Printf("[INFO] [media] media restart scheduled for %s (the household's „later\"; stored — a controller restart keeps it, R-1282)", at.Format(time.RFC3339))
|
||||
writeDiskJSON(w, http.StatusOK, true, "", map[string]string{"later_at": at.Format("2006-01-02 15:04")})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
||||
)
|
||||
|
||||
// R-1282: the household's „later" media restart (03:30) was an in-memory timer — a controller restart before then
|
||||
// (the night's self-update, a power cut) forgot it and the media apps never saw the new drive. The time is now stored;
|
||||
// at start it is re-armed, a time that passed while the controller was down fires at once, and it runs ONCE.
|
||||
// RED-PROOF: make ResumeMediaLater a no-op → the passed case never restarts the apps.
|
||||
func r1282Server(t *testing.T) (*Server, *settings.Settings, *int32) {
|
||||
t.Helper()
|
||||
lg := log.New(io.Discard, "", 0)
|
||||
sett, err := settings.Load(filepath.Join(t.TempDir(), "settings.json"), lg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var runs int32
|
||||
old := mediaLaterRestart
|
||||
mediaLaterRestart = func(*Server, string) { atomic.AddInt32(&runs, 1) }
|
||||
t.Cleanup(func() { mediaLaterRestart = old })
|
||||
return &Server{settings: sett, logger: lg}, sett, &runs
|
||||
}
|
||||
|
||||
func TestR1282_ALaterRestartThatPassedWhileDownRunsOnceAtStart(t *testing.T) {
|
||||
s, sett, runs := r1282Server(t)
|
||||
if err := sett.SetMediaRestartLater(time.Now().Add(-2 * time.Hour)); err != nil { // 03:30 passed while down
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.ResumeMediaLater()
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for atomic.LoadInt32(runs) == 0 && time.Now().Before(deadline) {
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
if n := atomic.LoadInt32(runs); n != 1 {
|
||||
t.Fatalf("the passed restart ran %d time(s) at start, want 1", n)
|
||||
}
|
||||
for !sett.GetMediaRestartLater().IsZero() && time.Now().Before(deadline) {
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
if !sett.GetMediaRestartLater().IsZero() {
|
||||
t.Fatal("the stored time must be cleared once it ran — else the next start runs it again")
|
||||
}
|
||||
// A second start finds nothing: it ran once.
|
||||
s.ResumeMediaLater()
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
if n := atomic.LoadInt32(runs); n != 1 {
|
||||
t.Fatalf("a second start ran it again (%d)", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestR1282_AFutureLaterRestartIsKeptOverAStart(t *testing.T) {
|
||||
s, sett, runs := r1282Server(t)
|
||||
at := time.Now().Add(1 * time.Hour)
|
||||
if err := sett.SetMediaRestartLater(at); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.ResumeMediaLater()
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
if n := atomic.LoadInt32(runs); n != 0 {
|
||||
t.Fatalf("a restart due in an hour ran now (%d)", n)
|
||||
}
|
||||
s.mediaMu.Lock()
|
||||
got, armed := s.mediaLaterAt, s.mediaLaterTmr != nil
|
||||
s.mediaMu.Unlock()
|
||||
if !armed || !got.Equal(at.UTC().Truncate(time.Second)) && got.Sub(at).Abs() > time.Second {
|
||||
t.Fatalf("the timer must be armed for %v, got %v (armed %v)", at, got, armed)
|
||||
}
|
||||
if s.mediaLaterTmr != nil {
|
||||
s.mediaLaterTmr.Stop()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user