feat(scheduler): random run-window scheduling and deduped manual triggers

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.
This commit is contained in:
2026-06-02 14:50:25 +08:00
parent ba2f117265
commit 842782b3dd
7 changed files with 343 additions and 8 deletions
@@ -46,6 +46,46 @@ func TestNextIntervalAutoAtSchedulesAfterCurrentCompletion(t *testing.T) {
}
}
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,