5f3f4189e5
把 scheduler 的 stub lifecycle 换成 SQL 后端真实实现,并接入 server:
- SQLLifecycle:MySQL 后端,同时满足 detect.LifecycleService 与
orchestrate.LifecycleService(两个 thin adapter 包装共享核心)。
- TransitionStatus:乐观锁 UPDATE nodes SET status WHERE id AND status=from
→ rows affected(1=成功/0=冲突 no-op)+ 写 node_events(按状态映射 enum,
eventForStatus 纯函数已单测)+ bump directory_version,全在一个事务内。
- ListNodes/GetNode/SetWeight/BumpVersion/GetLoad(读 LoadCache)/WriteAuditLog。
- 已知限制:无历史 load 表 → GetLoadHistory 返回当前点(掉量规则安全降级);
destroy 的 cert 撤销留 TODO(provision 已拆 VM,撤销为纵深防御,待机群再接)。
- BuildRealConfig:用真实 lifecycle + provisionAdapter 装配三循环。
- main.go:SCHED_ENABLED 且有 DB 时走 BuildRealConfig(provision 无厂商凭证则
CreateNode 优雅失败、替换保持 pending),否则回退 stub。
- 全量 server 23 包测试通过;e2e(判封→drain→换机→置备)待机群+厂商凭证验证。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
163 lines
6.3 KiB
Go
163 lines
6.3 KiB
Go
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
|
|
}
|