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)
}
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)
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) {
c, err := Load(writeTmp(t, `
agent_id = "host-1"

View File

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

View File

@@ -2,6 +2,7 @@ package scheduler
import (
"context"
"errors"
"strings"
"sync/atomic"
"testing"
@@ -107,6 +108,38 @@ func TestCronEmitsOnDueSlot(t *testing.T) {
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) {
ctx, cancel := context.WithTimeout(context.Background(), 600*time.Millisecond)
defer cancel()

View File

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