Fix scheduler cleanup edge cases
Some checks failed
ci / openapi (push) Failing after 6s
ci / agent (push) Failing after 5s
ci / server (push) Failing after 7s
ci / stack-smoke (push) Has been skipped
ci / web (push) Failing after 6s

This commit is contained in:
Stanislav Rossovskii
2026-06-23 20:04:08 +04:00
parent a94e0deaba
commit 762c48e96b
5 changed files with 69 additions and 20 deletions

View File

@@ -217,7 +217,7 @@ func (c *Config) Validate() error {
return fmt.Errorf("check %q: timeout must be > 0", ch.ID) return fmt.Errorf("check %q: timeout must be > 0", ch.ID)
} }
if ch.Interval.Duration < 0 { if ch.Interval.Duration < 0 {
return fmt.Errorf("check %q: interval must be > 0", ch.ID) return fmt.Errorf("check %q: interval must not be negative", ch.ID)
} }
ch.Cron = strings.TrimSpace(ch.Cron) ch.Cron = strings.TrimSpace(ch.Cron)
hasInterval := ch.Interval.Duration > 0 hasInterval := ch.Interval.Duration > 0

View File

@@ -123,6 +123,29 @@ func TestValidateCheckScheduleChoice(t *testing.T) {
} }
} }
func TestValidateNegativeIntervalMessage(t *testing.T) {
_, err := Load(writeTmp(t, `
agent_id = "host-1"
state_dir = "/tmp/x"
[server]
enabled = true
url = "http://localhost"
[[checks]]
id = "c1"
command = "true"
interval = "-1s"
timeout = "5s"
`))
if err == nil {
t.Fatal("expected negative interval error")
}
if !strings.Contains(err.Error(), "interval must not be negative") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestLoadResourceLimits(t *testing.T) { func TestLoadResourceLimits(t *testing.T) {
c, err := Load(writeTmp(t, ` c, err := Load(writeTmp(t, `
agent_id = "host-1" agent_id = "host-1"

View File

@@ -202,13 +202,24 @@ func (w *checkWorker) Wait() {
} }
func (w *checkWorker) loop(initial config.CheckConfig) { func (w *checkWorker) loop(initial config.CheckConfig) {
cfg := initial
stoppedNotified := false
notifyStopped := func() {
if stoppedNotified {
return
}
stoppedNotified = true
if w.hooks.OnStopped != nil {
w.hooks.OnStopped(cfg.ID)
}
}
defer func() { defer func() {
notifyStopped()
if w.onDone != nil { if w.onDone != nil {
w.onDone(w) w.onDone(w)
} }
w.wg.Done() w.wg.Done()
}() }()
cfg := initial
t := time.NewTimer(time.Hour) t := time.NewTimer(time.Hour)
stopTimer(t) stopTimer(t)
defer t.Stop() defer t.Stop()
@@ -249,9 +260,6 @@ func (w *checkWorker) loop(initial config.CheckConfig) {
} }
if !arm(cfg, true) { if !arm(cfg, true) {
if w.hooks.OnStopped != nil {
w.hooks.OnStopped(cfg.ID)
}
return return
} }
@@ -261,18 +269,12 @@ func (w *checkWorker) loop(initial config.CheckConfig) {
stopping = true stopping = true
ctxDone = nil ctxDone = nil
if !running { if !running {
if w.hooks.OnStopped != nil {
w.hooks.OnStopped(cfg.ID)
}
return return
} }
case <-stopCh: case <-stopCh:
stopping = true stopping = true
stopCh = nil stopCh = nil
if !running { if !running {
if w.hooks.OnStopped != nil {
w.hooks.OnStopped(cfg.ID)
}
return return
} }
case next := <-w.updateCh: case next := <-w.updateCh:
@@ -307,9 +309,6 @@ func (w *checkWorker) loop(initial config.CheckConfig) {
} }
} }
if stopping { if stopping {
if w.hooks.OnStopped != nil {
w.hooks.OnStopped(cfg.ID)
}
return return
} }
if runAfterCurrent { if runAfterCurrent {

View File

@@ -2,6 +2,7 @@ package scheduler
import ( import (
"context" "context"
"errors"
"strings" "strings"
"sync/atomic" "sync/atomic"
"testing" "testing"
@@ -107,6 +108,38 @@ func TestCronEmitsOnDueSlot(t *testing.T) {
m.Wait() m.Wait()
} }
func TestCronArmErrorCallsStoppedHook(t *testing.T) {
old := parseCronSchedule
var calls int32
parseCronSchedule = func(string) (cron.Schedule, error) {
if atomic.AddInt32(&calls, 1) == 1 {
return fakeCronSchedule{delay: 10 * time.Millisecond}, nil
}
return nil, errors.New("boom")
}
t.Cleanup(func() { parseCronSchedule = old })
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
out := make(chan Event, 10)
stopped := make(chan string, 1)
m := NewManager(ctx, "a1", out, Hooks{
OnStopped: func(id string) { stopped <- id },
})
m.Update([]config.CheckConfig{mkCronCheck("cron", "true", 100*time.Millisecond)})
select {
case id := <-stopped:
if id != "cron" {
t.Fatalf("unexpected stopped id: %q", id)
}
case <-ctx.Done():
t.Fatal("stopped hook was not called")
}
m.Stop()
m.Wait()
}
func TestNoOverlapSkips(t *testing.T) { func TestNoOverlapSkips(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 600*time.Millisecond) ctx, cancel := context.WithTimeout(context.Background(), 600*time.Millisecond)
defer cancel() defer cancel()

View File

@@ -13,12 +13,6 @@
--font-mono: var(--font-geist-mono); --font-mono: var(--font-geist-mono);
} }
:root[data-theme="dark"] {
--background: #0a0a0a;
--foreground: #ededed;
color-scheme: dark;
}
body { body {
background: var(--background); background: var(--background);
color: var(--foreground); color: var(--foreground);