Files
seaweedfs/weed/admin/maintenance/maintenance_policy_wiring_test.go
T

210 lines
7.9 KiB
Go

package maintenance
import (
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/worker_pb"
"github.com/seaweedfs/seaweedfs/weed/worker/tasks"
"github.com/seaweedfs/seaweedfs/weed/worker/types"
)
// The detectors and schedulers live in a process-global registry, so these tests restore
// whatever they change. Otherwise a test that disables a task leaks that state into every
// later test in the package.
func snapshotDetectorState(t *testing.T) {
t.Helper()
registry := tasks.GetGlobalTypesRegistry()
enabled := make(map[types.TaskType]bool)
maxConcurrent := make(map[types.TaskType]int)
for taskType, detector := range registry.GetAllDetectors() {
enabled[taskType] = detector.IsEnabled()
}
for taskType, scheduler := range registry.GetAllSchedulers() {
maxConcurrent[taskType] = scheduler.GetMaxConcurrent()
}
t.Cleanup(func() {
for taskType, detector := range registry.GetAllDetectors() {
if setter, ok := detector.(interface{ SetEnabled(bool) }); ok {
setter.SetEnabled(enabled[taskType])
}
}
for taskType, scheduler := range registry.GetAllSchedulers() {
if setter, ok := scheduler.(interface{ SetMaxConcurrent(int) }); ok {
setter.SetMaxConcurrent(maxConcurrent[taskType])
}
}
})
}
// TestPolicyReachesRegisteredDetectors is the regression test for the half of the scan-flood bug
// that a corrected policy alone did not fix: MaintenanceIntegration pushes the policy into
// detectors and schedulers through interface{ SetEnabled(bool) } and
// interface{ SetMaxConcurrent(int) } type assertions, but every task is backed by
// base.GenericDetector/base.GenericScheduler, which implemented neither. The assertions
// failed silently for every task on every startup, so the policy never reached
// detector.IsEnabled() - which is what ScanWithTaskDetectors gates scanning on.
func TestPolicyReachesRegisteredDetectors(t *testing.T) {
snapshotDetectorState(t)
registry := tasks.GetGlobalTypesRegistry()
if len(registry.GetAllDetectors()) == 0 {
t.Fatal("no detectors registered, the test cannot prove anything")
}
// Enable everything first so the disabling below cannot pass by accident.
policy := policyWithAllTasks(t, true)
NewMaintenanceIntegration(NewMaintenanceQueue(policy), policy)
for taskType, detector := range registry.GetAllDetectors() {
if !detector.IsEnabled() {
t.Fatalf("detector %s is disabled after an all-enabled policy was applied", taskType)
}
}
// Now disable everything through the policy and check it lands on the detectors.
policy = policyWithAllTasks(t, false)
NewMaintenanceIntegration(NewMaintenanceQueue(policy), policy)
for taskType, detector := range registry.GetAllDetectors() {
if detector.IsEnabled() {
t.Errorf("detector %s still reports enabled after the policy disabled it; "+
"the policy is not reaching the flag ScanWithTaskDetectors gates on", taskType)
}
}
for taskType, scheduler := range registry.GetAllSchedulers() {
if scheduler.IsEnabled() {
t.Errorf("scheduler %s still reports enabled after the policy disabled it", taskType)
}
}
}
// TestPolicyMaxConcurrentReachesSchedulers covers the SetMaxConcurrent half of the same
// wiring. GetMaxConcurrent is what MaintenanceQueue.getMaxConcurrentForTaskType asks before
// starting another task of a type.
func TestPolicyMaxConcurrentReachesSchedulers(t *testing.T) {
snapshotDetectorState(t)
const wantMaxConcurrent = 7
policy := policyWithAllTasks(t, true)
for _, taskPolicy := range policy.TaskPolicies {
taskPolicy.MaxConcurrent = wantMaxConcurrent
}
NewMaintenanceIntegration(NewMaintenanceQueue(policy), policy)
registry := tasks.GetGlobalTypesRegistry()
for taskType, scheduler := range registry.GetAllSchedulers() {
if _, covered := policy.TaskPolicies[string(taskType)]; !covered {
continue
}
if got := scheduler.GetMaxConcurrent(); got != wantMaxConcurrent {
t.Errorf("scheduler %s max concurrent = %d, want %d from the policy", taskType, got, wantMaxConcurrent)
}
}
}
// TestPolicyWithoutEntryLeavesTaskAlone guards the direction that would have been a silent
// outage: IsTaskEnabled reports false for a task type the policy does not list, so applying
// it unconditionally would disable every task the policy has no entry for.
func TestPolicyWithoutEntryLeavesTaskAlone(t *testing.T) {
snapshotDetectorState(t)
registry := tasks.GetGlobalTypesRegistry()
// Start from a policy that enables everything.
enabling := policyWithAllTasks(t, true)
NewMaintenanceIntegration(NewMaintenanceQueue(enabling), enabling)
// An empty policy has an entry for nothing at all.
empty := &MaintenancePolicy{TaskPolicies: make(map[string]*worker_pb.TaskPolicy)}
NewMaintenanceIntegration(NewMaintenanceQueue(empty), empty)
for taskType, detector := range registry.GetAllDetectors() {
if !detector.IsEnabled() {
t.Errorf("detector %s was disabled by a policy that has no entry for it; "+
"a missing entry means no opinion, not disabled", taskType)
}
}
}
// TestBuildPolicyCoversEveryRegisteredTask keeps BuildPolicyFromTaskConfigs and the task
// registry in step. A registered task with no policy entry has no enabled flag and no
// concurrency limit of its own, which is the state ec_balance was in.
func TestBuildPolicyCoversEveryRegisteredTask(t *testing.T) {
policy := BuildPolicyFromTaskConfigs(nil)
for taskType := range tasks.GetGlobalTypesRegistry().GetAllDetectors() {
if _, ok := policy.TaskPolicies[string(taskType)]; !ok {
t.Errorf("task %s is registered as a detector but BuildPolicyFromTaskConfigs "+
"builds no policy entry for it, so IsTaskEnabled reports false for it", taskType)
}
}
}
// TestBuildPolicyIncludesEcBalance pins the specific gap above.
func TestBuildPolicyIncludesEcBalance(t *testing.T) {
policy := BuildPolicyFromTaskConfigs(nil)
ecBalance := policy.TaskPolicies[string(types.TaskTypeECBalance)]
if ecBalance == nil {
t.Fatal("no ec_balance entry in the built maintenance policy")
}
if !IsTaskEnabled(policy, MaintenanceTaskType(types.TaskTypeECBalance)) {
t.Error("ec_balance reports disabled with no persisted config, want the compiled-in default of enabled")
}
if ecBalance.GetEcBalanceConfig() == nil {
t.Error("ec_balance policy entry carries no EcBalanceConfig")
}
}
// policyWithAllTasks builds a policy that has an entry for every registered task type,
// all sharing the same enabled flag.
func policyWithAllTasks(t *testing.T, enabled bool) *MaintenancePolicy {
t.Helper()
policy := &MaintenancePolicy{
GlobalMaxConcurrent: 4,
TaskPolicies: make(map[string]*worker_pb.TaskPolicy),
}
for taskType := range tasks.GetGlobalTypesRegistry().GetAllDetectors() {
policy.TaskPolicies[string(taskType)] = &worker_pb.TaskPolicy{
Enabled: enabled,
MaxConcurrent: 1,
RepeatIntervalSeconds: 3600,
}
}
return policy
}
// TestPolicyHelpersTolerateNilPolicy pins the nil handling in the exported policy helpers.
// MaintenanceConfig.Policy is nil until something builds one, and UpdateConfig accepts a
// config that has none, so these are reachable with a nil policy - GetTaskPolicy used to
// dereference it and take the admin process down.
func TestPolicyHelpersTolerateNilPolicy(t *testing.T) {
const taskType = MaintenanceTaskType("balance")
if got := GetTaskPolicy(nil, taskType); got != nil {
t.Errorf("GetTaskPolicy(nil) = %v, want nil", got)
}
if IsTaskEnabled(nil, taskType) {
t.Error("IsTaskEnabled(nil) = true, want false")
}
if got := GetMaxConcurrent(nil, taskType); got != 1 {
t.Errorf("GetMaxConcurrent(nil) = %d, want the safe default 1", got)
}
if got := GetRepeatInterval(nil, taskType); got != 0 {
t.Errorf("GetRepeatInterval(nil) = %d, want 0 so callers fall back to their own default", got)
}
// A policy that exists but lists nothing must behave the same way.
empty := &MaintenancePolicy{}
if got := GetTaskPolicy(empty, taskType); got != nil {
t.Errorf("GetTaskPolicy(empty) = %v, want nil", got)
}
if IsTaskEnabled(empty, taskType) {
t.Error("IsTaskEnabled(empty) = true, want false")
}
}