842782b3dd
Support a random daily run window (random_window_start/end) where the next auto-run time is derived from a stable per-job hash, avoiding thundering-herd starts while keeping one run per day. Honors the last auto-run on initial scheduling so restarts don't double-fire. Manual triggers can opt into dedupe (dedupe_manual_triggers): an advisory-locked path collapses concurrent requests onto an existing pending/running job instead of queueing duplicates. Scheduler error messages are localized to Chinese. Register the media_supply_cache_sync job (dry-run aware) and add the ops migration that seeds it.
132 lines
4.1 KiB
Go
132 lines
4.1 KiB
Go
package scheduler
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestSchedulerLoopSleepCapsManualPollWithoutForcingAutoRun(t *testing.T) {
|
|
now := time.Date(2026, 5, 20, 10, 0, 0, 0, time.UTC)
|
|
nextAutoAt := now.Add(time.Minute)
|
|
|
|
got := schedulerLoopSleep(now, nextAutoAt)
|
|
if got != schedulerManualPollInterval {
|
|
t.Fatalf("schedulerLoopSleep() = %s, want %s", got, schedulerManualPollInterval)
|
|
}
|
|
}
|
|
|
|
func TestNextIntervalAutoAtUsesLastRunOnInitialStart(t *testing.T) {
|
|
now := time.Date(2026, 5, 20, 10, 0, 0, 0, time.UTC)
|
|
last := now.Add(-time.Minute)
|
|
|
|
got := nextIntervalAutoAt(now, 5*time.Minute, true, &last)
|
|
want := last.Add(5 * time.Minute)
|
|
if !got.Equal(want) {
|
|
t.Fatalf("nextIntervalAutoAt() = %s, want %s", got, want)
|
|
}
|
|
}
|
|
|
|
func TestNextIntervalAutoAtRunsImmediatelyWhenOverdue(t *testing.T) {
|
|
now := time.Date(2026, 5, 20, 10, 0, 0, 0, time.UTC)
|
|
last := now.Add(-10 * time.Minute)
|
|
|
|
got := nextIntervalAutoAt(now, 5*time.Minute, true, &last)
|
|
if !got.Equal(now) {
|
|
t.Fatalf("nextIntervalAutoAt() = %s, want %s", got, now)
|
|
}
|
|
}
|
|
|
|
func TestNextIntervalAutoAtSchedulesAfterCurrentCompletion(t *testing.T) {
|
|
now := time.Date(2026, 5, 20, 10, 0, 0, 0, time.UTC)
|
|
|
|
got := nextIntervalAutoAt(now, 5*time.Minute, false, nil)
|
|
want := now.Add(5 * time.Minute)
|
|
if !got.Equal(want) {
|
|
t.Fatalf("nextIntervalAutoAt() = %s, want %s", got, want)
|
|
}
|
|
}
|
|
|
|
func TestNextRandomWindowAutoAtIsStableInsideWindow(t *testing.T) {
|
|
loc := time.FixedZone("CST", 8*60*60)
|
|
window, ok := parseRandomRunWindowLocal(map[string]any{
|
|
"random_window_start": "01:00",
|
|
"random_window_end": "04:00",
|
|
})
|
|
if !ok {
|
|
t.Fatal("expected random window config to parse")
|
|
}
|
|
now := time.Date(2026, 6, 2, 0, 30, 0, 0, loc)
|
|
|
|
got := nextRandomWindowAutoAt("media_supply_cache_sync", now, loc, window, true, nil)
|
|
gotAgain := nextRandomWindowAutoAt("media_supply_cache_sync", now.Add(10*time.Minute), loc, window, true, nil)
|
|
start := time.Date(2026, 6, 2, 1, 0, 0, 0, loc)
|
|
end := time.Date(2026, 6, 2, 4, 0, 0, 0, loc)
|
|
if got.Before(start) || !got.Before(end) {
|
|
t.Fatalf("nextRandomWindowAutoAt() = %s, want inside [%s, %s)", got, start, end)
|
|
}
|
|
if !got.Equal(gotAgain) {
|
|
t.Fatalf("expected stable candidate, got %s and %s", got, gotAgain)
|
|
}
|
|
}
|
|
|
|
func TestNextRandomWindowAutoAtSkipsTodayAfterAutoRun(t *testing.T) {
|
|
loc := time.FixedZone("CST", 8*60*60)
|
|
window, _ := parseRandomRunWindowLocal(map[string]any{
|
|
"random_window_start": "01:00",
|
|
"random_window_end": "04:00",
|
|
})
|
|
now := time.Date(2026, 6, 2, 2, 0, 0, 0, loc)
|
|
last := time.Date(2026, 6, 2, 1, 30, 0, 0, loc)
|
|
|
|
got := nextRandomWindowAutoAt("media_supply_cache_sync", now, loc, window, true, &last)
|
|
wantStart := time.Date(2026, 6, 3, 1, 0, 0, 0, loc)
|
|
wantEnd := time.Date(2026, 6, 3, 4, 0, 0, 0, loc)
|
|
if got.Before(wantStart) || !got.Before(wantEnd) {
|
|
t.Fatalf("nextRandomWindowAutoAt() = %s, want inside tomorrow window [%s, %s)", got, wantStart, wantEnd)
|
|
}
|
|
}
|
|
|
|
func TestIsEmptySuccessRunRequiresConfiguredZeroMetrics(t *testing.T) {
|
|
stats := map[string]any{
|
|
"hydrated_count": 0,
|
|
"claimed_count": 0,
|
|
}
|
|
config := map[string]any{
|
|
"suppress_empty_success_runs": true,
|
|
"empty_success_metric_keys": []any{"hydrated_count", "claimed_count"},
|
|
}
|
|
|
|
if !isEmptySuccessRun(stats, config) {
|
|
t.Fatal("expected empty success run to be suppressed")
|
|
}
|
|
}
|
|
|
|
func TestIsEmptySuccessRunKeepsRunsWithOutput(t *testing.T) {
|
|
stats := map[string]any{
|
|
"recoverable_count": 1,
|
|
"republished_count": 0,
|
|
}
|
|
config := map[string]any{
|
|
"suppress_empty_success_runs": true,
|
|
"empty_success_metric_keys": []any{"recoverable_count", "republished_count"},
|
|
}
|
|
|
|
if isEmptySuccessRun(stats, config) {
|
|
t.Fatal("expected non-empty success run to be retained")
|
|
}
|
|
}
|
|
|
|
func TestShouldSuppressEmptySuccessRunOnlyForAutoNonDryRun(t *testing.T) {
|
|
config := map[string]any{"suppress_empty_success_runs": true}
|
|
|
|
if !shouldSuppressEmptySuccessRun("auto", false, config) {
|
|
t.Fatal("expected auto non-dry-run to allow suppression")
|
|
}
|
|
if shouldSuppressEmptySuccessRun("manual", false, config) {
|
|
t.Fatal("manual runs must not be suppressed")
|
|
}
|
|
if shouldSuppressEmptySuccessRun("auto", true, config) {
|
|
t.Fatal("dry runs must not be suppressed")
|
|
}
|
|
}
|