mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-29 08:20:42 +01:00
admin: configure maintenance tasks via admin.toml (#9926)
* admin: configure maintenance tasks via admin.toml Maintenance task settings could only be edited in the admin UI and live under <dataDir>/conf, so they silently reverted to defaults whenever the data directory was recreated. An optional admin.toml now declares vacuum, balance, and erasure coding settings; keys set there are written through to the persisted task configs at every startup, overriding UI edits, so the configuration stays declarative. Generate an example with "weed scaffold -config=admin". * vacuum: round min volume age up to whole hours MinVolumeAgeSeconds was truncated by integer division when converted to the hour-granular protobuf field, so a sub-hour setting silently became 0 and disabled the age guard. * admin: split and normalize preferred_tags from admin.toml A comma-separated string, as set via environment variable, came through viper as a single slice element. Split on commas and reuse util.NormalizeTagList, matching the plugin config path. * scaffold: clarify admin.toml wording
This commit is contained in:
1 parent
42030381ae
commit
37962e2445
9 files changed
+312
-6
No files matched your search
@@ -706,7 +706,7 @@ func buildPolicyFromTaskConfigs() *worker_pb.MaintenancePolicy {
|
||||
TaskConfig: &worker_pb.TaskPolicy_VacuumConfig{
|
||||
VacuumConfig: &worker_pb.VacuumTaskConfig{
|
||||
GarbageThreshold: float64(vacuumConfig.GarbageThreshold),
|
||||
MinVolumeAgeHours: int32(vacuumConfig.MinVolumeAgeSeconds / 3600), // Convert seconds to hours
|
||||
MinVolumeAgeHours: int32((vacuumConfig.MinVolumeAgeSeconds + 3599) / 3600), // round up so sub-hour values don't become 0
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
package dash
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/tasks/balance"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/tasks/base"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/tasks/erasure_coding"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/tasks/vacuum"
|
||||
)
|
||||
|
||||
// TomlConfig is the subset of viper used to read admin.toml values.
|
||||
type TomlConfig interface {
|
||||
IsSet(key string) bool
|
||||
GetBool(key string) bool
|
||||
GetInt(key string) int
|
||||
GetFloat64(key string) float64
|
||||
GetString(key string) string
|
||||
GetStringSlice(key string) []string
|
||||
}
|
||||
|
||||
// ApplyMaintenanceConfigFromToml writes maintenance task settings declared in
|
||||
// admin.toml through to the persisted task configs, so they survive data
|
||||
// directory loss and override admin UI edits on restart. Absent keys keep
|
||||
// their persisted values.
|
||||
func (cp *ConfigPersistence) ApplyMaintenanceConfigFromToml(v TomlConfig) error {
|
||||
vacuumConf := vacuum.LoadConfigFromPersistence(cp)
|
||||
vacuumChanged := applyBaseConfigFromToml(v, "maintenance.vacuum.", &vacuumConf.BaseConfig)
|
||||
if k := "maintenance.vacuum.garbage_threshold"; v.IsSet(k) {
|
||||
vacuumConf.GarbageThreshold = v.GetFloat64(k)
|
||||
vacuumChanged = true
|
||||
}
|
||||
if k := "maintenance.vacuum.min_volume_age_seconds"; v.IsSet(k) {
|
||||
vacuumConf.MinVolumeAgeSeconds = v.GetInt(k)
|
||||
vacuumChanged = true
|
||||
}
|
||||
|
||||
balanceConf := balance.LoadConfigFromPersistence(cp)
|
||||
balanceChanged := applyBaseConfigFromToml(v, "maintenance.balance.", &balanceConf.BaseConfig)
|
||||
if k := "maintenance.balance.imbalance_threshold"; v.IsSet(k) {
|
||||
balanceConf.ImbalanceThreshold = v.GetFloat64(k)
|
||||
balanceChanged = true
|
||||
}
|
||||
if k := "maintenance.balance.min_server_count"; v.IsSet(k) {
|
||||
balanceConf.MinServerCount = v.GetInt(k)
|
||||
balanceChanged = true
|
||||
}
|
||||
|
||||
ecConf := erasure_coding.LoadConfigFromPersistence(cp)
|
||||
ecChanged := applyBaseConfigFromToml(v, "maintenance.erasure_coding.", &ecConf.BaseConfig)
|
||||
if k := "maintenance.erasure_coding.fullness_ratio"; v.IsSet(k) {
|
||||
ecConf.FullnessRatio = v.GetFloat64(k)
|
||||
ecChanged = true
|
||||
}
|
||||
if k := "maintenance.erasure_coding.quiet_for_seconds"; v.IsSet(k) {
|
||||
ecConf.QuietForSeconds = v.GetInt(k)
|
||||
ecChanged = true
|
||||
}
|
||||
if k := "maintenance.erasure_coding.min_size_mb"; v.IsSet(k) {
|
||||
ecConf.MinSizeMB = v.GetInt(k)
|
||||
ecChanged = true
|
||||
}
|
||||
if k := "maintenance.erasure_coding.collection_filter"; v.IsSet(k) {
|
||||
ecConf.CollectionFilter = v.GetString(k)
|
||||
ecChanged = true
|
||||
}
|
||||
if k := "maintenance.erasure_coding.preferred_tags"; v.IsSet(k) {
|
||||
// viper does not split comma-separated values from env vars
|
||||
var tags []string
|
||||
for _, tag := range v.GetStringSlice(k) {
|
||||
tags = append(tags, strings.Split(tag, ",")...)
|
||||
}
|
||||
ecConf.PreferredTags = util.NormalizeTagList(tags)
|
||||
ecChanged = true
|
||||
}
|
||||
if k := "maintenance.erasure_coding.replica_placement"; v.IsSet(k) {
|
||||
ecConf.ReplicaPlacement = v.GetString(k)
|
||||
ecChanged = true
|
||||
}
|
||||
|
||||
if !vacuumChanged && !balanceChanged && !ecChanged {
|
||||
return nil
|
||||
}
|
||||
if !cp.IsConfigured() {
|
||||
return fmt.Errorf("admin.toml maintenance settings require -dataDir to persist")
|
||||
}
|
||||
|
||||
if vacuumChanged {
|
||||
if err := cp.SaveVacuumTaskPolicy(vacuumConf.ToTaskPolicy()); err != nil {
|
||||
return fmt.Errorf("save vacuum task config: %w", err)
|
||||
}
|
||||
glog.V(0).Infof("Applied [maintenance.vacuum] settings from admin.toml")
|
||||
}
|
||||
if balanceChanged {
|
||||
if err := cp.SaveBalanceTaskPolicy(balanceConf.ToTaskPolicy()); err != nil {
|
||||
return fmt.Errorf("save balance task config: %w", err)
|
||||
}
|
||||
glog.V(0).Infof("Applied [maintenance.balance] settings from admin.toml")
|
||||
}
|
||||
if ecChanged {
|
||||
if err := cp.SaveErasureCodingTaskPolicy(ecConf.ToTaskPolicy()); err != nil {
|
||||
return fmt.Errorf("save erasure coding task config: %w", err)
|
||||
}
|
||||
glog.V(0).Infof("Applied [maintenance.erasure_coding] settings from admin.toml")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyBaseConfigFromToml(v TomlConfig, prefix string, c *base.BaseConfig) bool {
|
||||
changed := false
|
||||
if k := prefix + "enabled"; v.IsSet(k) {
|
||||
c.Enabled = v.GetBool(k)
|
||||
changed = true
|
||||
}
|
||||
if k := prefix + "scan_interval_seconds"; v.IsSet(k) {
|
||||
c.ScanIntervalSeconds = v.GetInt(k)
|
||||
changed = true
|
||||
}
|
||||
if k := prefix + "max_concurrent"; v.IsSet(k) {
|
||||
c.MaxConcurrent = v.GetInt(k)
|
||||
changed = true
|
||||
}
|
||||
return changed
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package dash
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/tasks/balance"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/tasks/erasure_coding"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/tasks/vacuum"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func tomlConfig(t *testing.T, content string) *viper.Viper {
|
||||
t.Helper()
|
||||
v := viper.New()
|
||||
v.SetConfigType("toml")
|
||||
if err := v.ReadConfig(strings.NewReader(content)); err != nil {
|
||||
t.Fatalf("read toml: %v", err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func TestApplyMaintenanceConfigFromToml(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cp := NewConfigPersistence(dir)
|
||||
|
||||
// simulate an earlier UI edit
|
||||
saved := vacuum.NewDefaultConfig()
|
||||
saved.MaxConcurrent = 5
|
||||
saved.GarbageThreshold = 0.5
|
||||
if err := cp.SaveVacuumTaskPolicy(saved.ToTaskPolicy()); err != nil {
|
||||
t.Fatalf("save vacuum policy: %v", err)
|
||||
}
|
||||
|
||||
balanceBefore := balance.LoadConfigFromPersistence(cp)
|
||||
|
||||
v := tomlConfig(t, `
|
||||
[maintenance.vacuum]
|
||||
garbage_threshold = 0.03
|
||||
min_volume_age_seconds = 1800
|
||||
|
||||
[maintenance.erasure_coding]
|
||||
enabled = false
|
||||
fullness_ratio = 0.8
|
||||
preferred_tags = "Fast, ssd"
|
||||
`)
|
||||
if err := cp.ApplyMaintenanceConfigFromToml(v); err != nil {
|
||||
t.Fatalf("apply: %v", err)
|
||||
}
|
||||
|
||||
vacuumConf := vacuum.LoadConfigFromPersistence(cp)
|
||||
if vacuumConf.GarbageThreshold != 0.03 {
|
||||
t.Errorf("garbage threshold = %v, want 0.03", vacuumConf.GarbageThreshold)
|
||||
}
|
||||
// sub-hour values round up to a whole hour instead of truncating to 0
|
||||
if vacuumConf.MinVolumeAgeSeconds != 3600 {
|
||||
t.Errorf("min volume age = %v, want 3600", vacuumConf.MinVolumeAgeSeconds)
|
||||
}
|
||||
if vacuumConf.MaxConcurrent != 5 {
|
||||
t.Errorf("max concurrent = %v, want UI-saved 5 preserved", vacuumConf.MaxConcurrent)
|
||||
}
|
||||
|
||||
ecConf := erasure_coding.LoadConfigFromPersistence(cp)
|
||||
if ecConf.Enabled {
|
||||
t.Errorf("ec enabled = true, want false")
|
||||
}
|
||||
if ecConf.FullnessRatio != 0.8 {
|
||||
t.Errorf("fullness ratio = %v, want 0.8", ecConf.FullnessRatio)
|
||||
}
|
||||
if !reflect.DeepEqual(ecConf.PreferredTags, []string{"fast", "ssd"}) {
|
||||
t.Errorf("preferred tags = %v, want [fast ssd]", ecConf.PreferredTags)
|
||||
}
|
||||
if ecConf.QuietForSeconds != 3600 {
|
||||
t.Errorf("quiet for = %v, want default 3600 preserved", ecConf.QuietForSeconds)
|
||||
}
|
||||
|
||||
// balance section absent: nothing written
|
||||
if _, err := os.Stat(filepath.Join(dir, ConfigSubdir, BalanceTaskConfigFile)); !os.IsNotExist(err) {
|
||||
t.Errorf("balance config written without [maintenance.balance] section")
|
||||
}
|
||||
if got := balance.LoadConfigFromPersistence(cp); !reflect.DeepEqual(got, balanceBefore) {
|
||||
t.Errorf("balance config changed without [maintenance.balance] section")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyMaintenanceConfigFromTomlNoKeys(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cp := NewConfigPersistence(dir)
|
||||
if err := cp.ApplyMaintenanceConfigFromToml(viper.New()); err != nil {
|
||||
t.Fatalf("apply: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, ConfigSubdir)); !os.IsNotExist(err) {
|
||||
t.Errorf("conf dir created without any maintenance keys set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyMaintenanceConfigFromTomlRequiresDataDir(t *testing.T) {
|
||||
cp := NewConfigPersistence("")
|
||||
v := tomlConfig(t, `
|
||||
[maintenance.vacuum]
|
||||
garbage_threshold = 0.03
|
||||
`)
|
||||
if err := cp.ApplyMaintenanceConfigFromToml(v); err == nil {
|
||||
t.Errorf("expected error when maintenance keys are set without a data dir")
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ func buildPolicyFromTaskConfigs() *worker_pb.MaintenancePolicy {
|
||||
TaskConfig: &worker_pb.TaskPolicy_VacuumConfig{
|
||||
VacuumConfig: &worker_pb.VacuumTaskConfig{
|
||||
GarbageThreshold: float64(vacuumConfig.GarbageThreshold),
|
||||
MinVolumeAgeHours: int32(vacuumConfig.MinVolumeAgeSeconds / 3600), // Convert seconds to hours
|
||||
MinVolumeAgeHours: int32((vacuumConfig.MinVolumeAgeSeconds + 3599) / 3600), // round up so sub-hour values don't become 0
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user