// Package scheduler wires together the 15D detection engine, 15E orchestration // engine, 15F capacity service, 15A probe store, and 15G notifier into three // leader-elected goroutine loops (DetectLoop / OrchestrateLoop / CapacityLoop), // plus an optional unguarded 15C third-party prober goroutine. // // This file declares the narrow dependency interfaces used by Scheduler. Real // implementations satisfy these interfaces; test mocks do too. package scheduler import ( "context" "log/slog" "github.com/wangjia/pangolin/server/internal/scheduler/probe" ) // ───────────────────────────────────────────────────────────────────────────── // 15D – detection engine // ───────────────────────────────────────────────────────────────────────────── // DetectEngine is the 15D interface. *detect.Engine satisfies it. // Tick is called by DetectLoop on every 5-minute cycle. type DetectEngine interface { Tick(ctx context.Context) error } // ───────────────────────────────────────────────────────────────────────────── // 15E – orchestration engine // ───────────────────────────────────────────────────────────────────────────── // OrchestrateReplacer is the 15E replacement-orchestration interface. // *orchestrate.Replacer satisfies it. // Tick is called by OrchestrateLoop every 30 seconds. type OrchestrateReplacer interface { Tick(ctx context.Context) error } // OrchestrateGrayscale is the 15E grayscale warm-up interface. // *orchestrate.Grayscale satisfies it. // Advance is called by CapacityLoop every 1–2 minutes. type OrchestrateGrayscale interface { Advance(ctx context.Context) error } // ───────────────────────────────────────────────────────────────────────────── // 15F – capacity service (stub until task 15F is delivered) // ───────────────────────────────────────────────────────────────────────────── // CapacityService is the 15F interface for watermark/quota checking and // circuit-breaker time-decay. Replace StubCapacityService with the real // 15F implementation once that task is complete. type CapacityService interface { // Check verifies that the node-pool watermark and per-region quotas are // within acceptable bounds. Non-fatal: errors are logged, not returned. Check(ctx context.Context) error // Decay applies time-based decay to circuit-breaker event counters so that // temporary bursts do not permanently block replacements. Decay(ctx context.Context) error } // StubCapacityService is a no-op CapacityService used until task 15F is ready. type StubCapacityService struct{} // Check implements CapacityService (always succeeds). func (StubCapacityService) Check(_ context.Context) error { return nil } // Decay implements CapacityService (always succeeds). func (StubCapacityService) Decay(_ context.Context) error { return nil } // ───────────────────────────────────────────────────────────────────────────── // 15A – probe state reader // ───────────────────────────────────────────────────────────────────────────── // ProbeStateReader reads the heartbeat liveness of probe agents from the 15A // Redis store. *probe.Store satisfies it (via AliveProbes). type ProbeStateReader interface { // AliveProbes returns the IDs of probe agents that have sent a heartbeat // within the last heartbeatTTL window (15 min). An empty result means // "no recent heartbeats", not "all probes are down". AliveProbes(ctx context.Context) ([]string, error) } // ───────────────────────────────────────────────────────────────────────────── // 15C – third-party prober // ───────────────────────────────────────────────────────────────────────────── // ThirdPartyProber is the 15C interface. // *probe.AliyunSyntheticAgent satisfies it. type ThirdPartyProber interface { // RunOnce issues a full multi-ISP probe cycle for all targets and writes // results to Redis via the 15A probe store. RunOnce(ctx context.Context, targets []probe.ProbeTarget) error } // TargetProvider supplies the current list of active nodes to be probed via // the 15C third-party service. Typically backed by a DB query; a static // implementation is available as StaticTargetProvider. type TargetProvider interface { ListProbeTargets(ctx context.Context) ([]probe.ProbeTarget, error) } // StaticTargetProvider returns a fixed, pre-configured list of probe targets. // Useful for initial deployment and integration tests before a DB-backed // provider is available. type StaticTargetProvider struct { Targets []probe.ProbeTarget } // ListProbeTargets implements TargetProvider. func (p StaticTargetProvider) ListProbeTargets(_ context.Context) ([]probe.ProbeTarget, error) { return p.Targets, nil } // ───────────────────────────────────────────────────────────────────────────── // 15G – notifier // ───────────────────────────────────────────────────────────────────────────── // Notifier is the 15G alerting interface. The real implementation sends a // Telegram (or similar) alert; LogNotifier is the stub. type Notifier interface { NotifyFault(ctx context.Context, nodeID, reason string) error } // LogNotifier is a Notifier stub that writes to slog. // Used when no real 15G notifier is wired. type LogNotifier struct{} // NotifyFault implements Notifier. func (LogNotifier) NotifyFault(_ context.Context, nodeID, reason string) error { slog.Warn("scheduler: node fault — manual review required", "node_id", nodeID, "reason", reason, ) return nil }