package scheduler // wiring.go — construction helpers for starting the scheduler in the server // monolith before tasks #5 (LifecycleService) and #14 (ProvisionService) are // fully implemented. // // BuildStubConfig returns a ready-to-use Config whose lifecycle and provision // dependencies are replaced with no-op stubs. The scheduler's three loops // still run and hold leader leases, but detect/orchestrate ticks are no-ops. // Replace the stubs with real implementations once #5/#14 are ready. import ( "context" "database/sql" "github.com/redis/go-redis/v9" "github.com/wangjia/pangolin/server/internal/nodes" "github.com/wangjia/pangolin/server/internal/scheduler/detect" "github.com/wangjia/pangolin/server/internal/scheduler/orchestrate" "github.com/wangjia/pangolin/server/internal/scheduler/probe" ) // BuildRealConfig wires the scheduler against the real lifecycle (SQL-backed) // and provision (#14) services, replacing the no-op stubs of BuildStubConfig. // // - detect/orchestrate lifecycle → SQLLifecycle over the nodes table + LoadCache. // - provision → provisionAdapter over the real provision.Service. // // Without vendor credentials the provision service's CreateNode fails (a pending // replacement simply stays pending), but all lifecycle reads/transitions operate // on real node rows. Capacity monitoring is left disabled (nil) as in the stub. // // e2e behaviour (auto-detect → drain → replace → provision) is verifiable only // against a real node fleet with vendor provisioning configured. func BuildRealConfig( rdb *redis.Client, probeStore *probe.Store, db *sql.DB, loads *nodes.LoadCache, provSvc provisionSvc, provStore provisionResolver, ) Config { sqlLC := NewSQLLifecycle(db, loads) detectLC := NewDetectLifecycle(sqlLC) orchLC := NewOrchestrateLifecycle(sqlLC) prov := NewProvisionAdapter(provSvc, provStore) streaks := detect.NewStreakStore(rdb) engine := detect.NewEngine(probeStore, detectLC, streaks, rdb, nil, nil) replacer := orchestrate.NewReplacer(orchestrate.Config{ RDB: rdb, Prov: prov, LC: orchLC, Snaps: probeStore, Breaker: nil, // StubBreaker default until 15F policy tuned Notifier: nil, // LogNotifier default Clock: nil, }) grayscale := orchestrate.NewGrayscale(rdb, orchLC, nil) return Config{ RDB: rdb, Engine: engine, Replacer: replacer, Grayscale: grayscale, } } // BuildStubConfig constructs a scheduler Config wired entirely with in-process // stubs. Useful for: // - Starting the scheduler before tasks #5 / #14 are delivered (no-op ticks). // - Integration tests that need a scheduler goroutine to be running. // // To swap in real implementations once they exist: // // cfg := scheduler.BuildStubConfig(rdb, probeStore) // cfg.Engine = detect.NewEngine(realLC, ...) // cfg.Replacer = orchestrate.NewReplacer(orchestrate.Config{LC: realLC, Prov: realProv, ...}) // cfg.Grayscale = orchestrate.NewGrayscale(rdb, realLC, nil) // sched := scheduler.New(cfg) func BuildStubConfig(rdb *redis.Client, probeStore *probe.Store) Config { // Stub lifecycle for 15D — no nodes registered, so Tick is a safe no-op. stubDetectLC := detect.NewMockLifecycle(nil) // Stub lifecycle and provision for 15E. stubOrchLC := &stubOrchLC{} stubProv := &stubProvision{} streaks := detect.NewStreakStore(rdb) engine := detect.NewEngine( probeStore, // 15A probe snapshot reader stubDetectLC, // 15D lifecycle (stub until #5) streaks, rdb, nil, // notifier — LogNotifier used by default nil, // config — DefaultConfig used ) replacer := orchestrate.NewReplacer(orchestrate.Config{ RDB: rdb, Prov: stubProv, // provision stub until #14 LC: stubOrchLC, // lifecycle stub until #5 Snaps: probeStore, Breaker: nil, // StubBreaker used by default Notifier: nil, // LogNotifier used by default Clock: nil, // RealClock used by default }) grayscale := orchestrate.NewGrayscale(rdb, stubOrchLC, nil) return Config{ RDB: rdb, Engine: engine, Replacer: replacer, Grayscale: grayscale, // Capacity, Notifier, ProbeStore, Prober, Targets: nil → stubs/disabled } } // ───────────────────────────────────────────────────────────────────────────── // Stub implementations for orchestrate.LifecycleService and ProvisionService // // These stubs satisfy the interface contracts while doing nothing harmful. // Replace with the real services (#5 / #14) when those tasks are complete. // ───────────────────────────────────────────────────────────────────────────── // stubOrchLC implements orchestrate.LifecycleService as a no-op. // GetNode always returns nil (node not found); all mutating calls succeed // silently. This prevents any orchestration work from proceeding until a real // lifecycle service is wired in. type stubOrchLC struct{} func (stubOrchLC) GetNode(_ context.Context, _ string) (*orchestrate.NodeInfo, error) { return nil, nil // "node not found" → orchestrate skips this replacement } func (stubOrchLC) TransitionStatus(_ context.Context, _, _, _ string, _ map[string]any) (int, error) { return 1, nil } func (stubOrchLC) SetWeight(_ context.Context, _ string, _ int) error { return nil } func (stubOrchLC) BumpVersion(_ context.Context) error { return nil } func (stubOrchLC) WriteAuditLog(_ context.Context, _, _, _, _ string) error { return nil } // stubProvision implements orchestrate.ProvisionService as a no-op. // CreateNode always returns an error so pending replacements stay pending // rather than silently creating phantom resources. type stubProvision struct{} func (stubProvision) CreateNode(_ context.Context, _ orchestrate.NodeSpec, _ string) (string, error) { return "", context.DeadlineExceeded // signal "not ready" without alarming } func (stubProvision) DestroyNode(_ context.Context, _ string) error { return nil } func (stubProvision) RotateIP(_ context.Context, _ string) (string, error) { return "", nil } func (stubProvision) ListProviders(_ context.Context, _, _ string) ([]orchestrate.ProviderInfo, error) { return nil, nil }