Admin UI add maintenance menu (#6944)
* add ui for maintenance * valid config loading. fix workers page. * refactor * grpc between admin and workers * add a long-running bidirectional grpc call between admin and worker * use the grpc call to heartbeat * use the grpc call to communicate * worker can remove the http client * admin uses http port + 10000 as its default grpc port * one task one package * handles connection failures gracefully with exponential backoff * grpc with insecure tls * grpc with optional tls * fix detecting tls * change time config from nano seconds to seconds * add tasks with 3 interfaces * compiles reducing hard coded * remove a couple of tasks * remove hard coded references * reduce hard coded values * remove hard coded values * remove hard coded from templ * refactor maintenance package * fix import cycle * simplify * simplify * auto register * auto register factory * auto register task types * self register types * refactor * simplify * remove one task * register ui * lazy init executor factories * use registered task types * DefaultWorkerConfig remove hard coded task types * remove more hard coded * implement get maintenance task * dynamic task configuration * "System Settings" should only have system level settings * adjust menu for tasks * ensure menu not collapsed * render job configuration well * use templ for ui of task configuration * fix ordering * fix bugs * saving duration in seconds * use value and unit for duration * Delete WORKER_REFACTORING_PLAN.md * Delete maintenance.json * Delete custom_worker_example.go * remove address from workers * remove old code from ec task * remove creating collection button * reconnect with exponential backoff * worker use security.toml * start admin server with tls info from security.toml * fix "weed admin" cli description
This commit is contained in:
82
weed/worker/tasks/balance/balance.go
Normal file
82
weed/worker/tasks/balance/balance.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package balance
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/tasks"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/types"
|
||||
)
|
||||
|
||||
// Task implements balance operation to redistribute volumes across volume servers
|
||||
type Task struct {
|
||||
*tasks.BaseTask
|
||||
server string
|
||||
volumeID uint32
|
||||
collection string
|
||||
}
|
||||
|
||||
// NewTask creates a new balance task instance
|
||||
func NewTask(server string, volumeID uint32, collection string) *Task {
|
||||
task := &Task{
|
||||
BaseTask: tasks.NewBaseTask(types.TaskTypeBalance),
|
||||
server: server,
|
||||
volumeID: volumeID,
|
||||
collection: collection,
|
||||
}
|
||||
return task
|
||||
}
|
||||
|
||||
// Execute executes the balance task
|
||||
func (t *Task) Execute(params types.TaskParams) error {
|
||||
glog.Infof("Starting balance task for volume %d on server %s (collection: %s)", t.volumeID, t.server, t.collection)
|
||||
|
||||
// Simulate balance operation with progress updates
|
||||
steps := []struct {
|
||||
name string
|
||||
duration time.Duration
|
||||
progress float64
|
||||
}{
|
||||
{"Analyzing cluster state", 2 * time.Second, 15},
|
||||
{"Identifying optimal placement", 3 * time.Second, 35},
|
||||
{"Moving volume data", 6 * time.Second, 75},
|
||||
{"Updating cluster metadata", 2 * time.Second, 95},
|
||||
{"Verifying balance", 1 * time.Second, 100},
|
||||
}
|
||||
|
||||
for _, step := range steps {
|
||||
if t.IsCancelled() {
|
||||
return fmt.Errorf("balance task cancelled")
|
||||
}
|
||||
|
||||
glog.V(1).Infof("Balance task step: %s", step.name)
|
||||
t.SetProgress(step.progress)
|
||||
|
||||
// Simulate work
|
||||
time.Sleep(step.duration)
|
||||
}
|
||||
|
||||
glog.Infof("Balance task completed for volume %d on server %s", t.volumeID, t.server)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate validates the task parameters
|
||||
func (t *Task) Validate(params types.TaskParams) error {
|
||||
if params.VolumeID == 0 {
|
||||
return fmt.Errorf("volume_id is required")
|
||||
}
|
||||
if params.Server == "" {
|
||||
return fmt.Errorf("server is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EstimateTime estimates the time needed for the task
|
||||
func (t *Task) EstimateTime(params types.TaskParams) time.Duration {
|
||||
// Base time for balance operation
|
||||
baseTime := 35 * time.Second
|
||||
|
||||
// Could adjust based on volume size or cluster state
|
||||
return baseTime
|
||||
}
|
||||
171
weed/worker/tasks/balance/balance_detector.go
Normal file
171
weed/worker/tasks/balance/balance_detector.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package balance
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/types"
|
||||
)
|
||||
|
||||
// BalanceDetector implements TaskDetector for balance tasks
|
||||
type BalanceDetector struct {
|
||||
enabled bool
|
||||
threshold float64 // Imbalance threshold (0.1 = 10%)
|
||||
minCheckInterval time.Duration
|
||||
minVolumeCount int
|
||||
lastCheck time.Time
|
||||
}
|
||||
|
||||
// Compile-time interface assertions
|
||||
var (
|
||||
_ types.TaskDetector = (*BalanceDetector)(nil)
|
||||
)
|
||||
|
||||
// NewBalanceDetector creates a new balance detector
|
||||
func NewBalanceDetector() *BalanceDetector {
|
||||
return &BalanceDetector{
|
||||
enabled: true,
|
||||
threshold: 0.1, // 10% imbalance threshold
|
||||
minCheckInterval: 1 * time.Hour,
|
||||
minVolumeCount: 10, // Don't balance small clusters
|
||||
lastCheck: time.Time{},
|
||||
}
|
||||
}
|
||||
|
||||
// GetTaskType returns the task type
|
||||
func (d *BalanceDetector) GetTaskType() types.TaskType {
|
||||
return types.TaskTypeBalance
|
||||
}
|
||||
|
||||
// ScanForTasks checks if cluster balance is needed
|
||||
func (d *BalanceDetector) ScanForTasks(volumeMetrics []*types.VolumeHealthMetrics, clusterInfo *types.ClusterInfo) ([]*types.TaskDetectionResult, error) {
|
||||
if !d.enabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
glog.V(2).Infof("Scanning for balance tasks...")
|
||||
|
||||
// Don't check too frequently
|
||||
if time.Since(d.lastCheck) < d.minCheckInterval {
|
||||
return nil, nil
|
||||
}
|
||||
d.lastCheck = time.Now()
|
||||
|
||||
// Skip if cluster is too small
|
||||
if len(volumeMetrics) < d.minVolumeCount {
|
||||
glog.V(2).Infof("Cluster too small for balance (%d volumes < %d minimum)", len(volumeMetrics), d.minVolumeCount)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Analyze volume distribution across servers
|
||||
serverVolumeCounts := make(map[string]int)
|
||||
for _, metric := range volumeMetrics {
|
||||
serverVolumeCounts[metric.Server]++
|
||||
}
|
||||
|
||||
if len(serverVolumeCounts) < 2 {
|
||||
glog.V(2).Infof("Not enough servers for balance (%d servers)", len(serverVolumeCounts))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Calculate balance metrics
|
||||
totalVolumes := len(volumeMetrics)
|
||||
avgVolumesPerServer := float64(totalVolumes) / float64(len(serverVolumeCounts))
|
||||
|
||||
maxVolumes := 0
|
||||
minVolumes := totalVolumes
|
||||
maxServer := ""
|
||||
minServer := ""
|
||||
|
||||
for server, count := range serverVolumeCounts {
|
||||
if count > maxVolumes {
|
||||
maxVolumes = count
|
||||
maxServer = server
|
||||
}
|
||||
if count < minVolumes {
|
||||
minVolumes = count
|
||||
minServer = server
|
||||
}
|
||||
}
|
||||
|
||||
// Check if imbalance exceeds threshold
|
||||
imbalanceRatio := float64(maxVolumes-minVolumes) / avgVolumesPerServer
|
||||
if imbalanceRatio <= d.threshold {
|
||||
glog.V(2).Infof("Cluster is balanced (imbalance ratio: %.2f <= %.2f)", imbalanceRatio, d.threshold)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Create balance task
|
||||
reason := fmt.Sprintf("Cluster imbalance detected: %.1f%% (max: %d on %s, min: %d on %s, avg: %.1f)",
|
||||
imbalanceRatio*100, maxVolumes, maxServer, minVolumes, minServer, avgVolumesPerServer)
|
||||
|
||||
task := &types.TaskDetectionResult{
|
||||
TaskType: types.TaskTypeBalance,
|
||||
Priority: types.TaskPriorityNormal,
|
||||
Reason: reason,
|
||||
ScheduleAt: time.Now(),
|
||||
Parameters: map[string]interface{}{
|
||||
"imbalance_ratio": imbalanceRatio,
|
||||
"threshold": d.threshold,
|
||||
"max_volumes": maxVolumes,
|
||||
"min_volumes": minVolumes,
|
||||
"avg_volumes_per_server": avgVolumesPerServer,
|
||||
"max_server": maxServer,
|
||||
"min_server": minServer,
|
||||
"total_servers": len(serverVolumeCounts),
|
||||
},
|
||||
}
|
||||
|
||||
glog.V(1).Infof("🔄 Found balance task: %s", reason)
|
||||
return []*types.TaskDetectionResult{task}, nil
|
||||
}
|
||||
|
||||
// ScanInterval returns how often to scan
|
||||
func (d *BalanceDetector) ScanInterval() time.Duration {
|
||||
return d.minCheckInterval
|
||||
}
|
||||
|
||||
// IsEnabled returns whether the detector is enabled
|
||||
func (d *BalanceDetector) IsEnabled() bool {
|
||||
return d.enabled
|
||||
}
|
||||
|
||||
// SetEnabled sets whether the detector is enabled
|
||||
func (d *BalanceDetector) SetEnabled(enabled bool) {
|
||||
d.enabled = enabled
|
||||
glog.V(1).Infof("🔄 Balance detector enabled: %v", enabled)
|
||||
}
|
||||
|
||||
// SetThreshold sets the imbalance threshold
|
||||
func (d *BalanceDetector) SetThreshold(threshold float64) {
|
||||
d.threshold = threshold
|
||||
glog.V(1).Infof("🔄 Balance threshold set to: %.1f%%", threshold*100)
|
||||
}
|
||||
|
||||
// SetMinCheckInterval sets the minimum time between balance checks
|
||||
func (d *BalanceDetector) SetMinCheckInterval(interval time.Duration) {
|
||||
d.minCheckInterval = interval
|
||||
glog.V(1).Infof("🔄 Balance check interval set to: %v", interval)
|
||||
}
|
||||
|
||||
// SetMinVolumeCount sets the minimum volume count for balance operations
|
||||
func (d *BalanceDetector) SetMinVolumeCount(count int) {
|
||||
d.minVolumeCount = count
|
||||
glog.V(1).Infof("🔄 Balance minimum volume count set to: %d", count)
|
||||
}
|
||||
|
||||
// GetThreshold returns the current imbalance threshold
|
||||
func (d *BalanceDetector) GetThreshold() float64 {
|
||||
return d.threshold
|
||||
}
|
||||
|
||||
// GetMinCheckInterval returns the minimum check interval
|
||||
func (d *BalanceDetector) GetMinCheckInterval() time.Duration {
|
||||
return d.minCheckInterval
|
||||
}
|
||||
|
||||
// GetMinVolumeCount returns the minimum volume count
|
||||
func (d *BalanceDetector) GetMinVolumeCount() int {
|
||||
return d.minVolumeCount
|
||||
}
|
||||
81
weed/worker/tasks/balance/balance_register.go
Normal file
81
weed/worker/tasks/balance/balance_register.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package balance
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/tasks"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/types"
|
||||
)
|
||||
|
||||
// Factory creates balance task instances
|
||||
type Factory struct {
|
||||
*tasks.BaseTaskFactory
|
||||
}
|
||||
|
||||
// NewFactory creates a new balance task factory
|
||||
func NewFactory() *Factory {
|
||||
return &Factory{
|
||||
BaseTaskFactory: tasks.NewBaseTaskFactory(
|
||||
types.TaskTypeBalance,
|
||||
[]string{"balance", "storage", "optimization"},
|
||||
"Balance data across volume servers for optimal performance",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Create creates a new balance task instance
|
||||
func (f *Factory) Create(params types.TaskParams) (types.TaskInterface, error) {
|
||||
// Validate parameters
|
||||
if params.VolumeID == 0 {
|
||||
return nil, fmt.Errorf("volume_id is required")
|
||||
}
|
||||
if params.Server == "" {
|
||||
return nil, fmt.Errorf("server is required")
|
||||
}
|
||||
|
||||
task := NewTask(params.Server, params.VolumeID, params.Collection)
|
||||
task.SetEstimatedDuration(task.EstimateTime(params))
|
||||
|
||||
return task, nil
|
||||
}
|
||||
|
||||
// Shared detector and scheduler instances
|
||||
var (
|
||||
sharedDetector *BalanceDetector
|
||||
sharedScheduler *BalanceScheduler
|
||||
)
|
||||
|
||||
// getSharedInstances returns the shared detector and scheduler instances
|
||||
func getSharedInstances() (*BalanceDetector, *BalanceScheduler) {
|
||||
if sharedDetector == nil {
|
||||
sharedDetector = NewBalanceDetector()
|
||||
}
|
||||
if sharedScheduler == nil {
|
||||
sharedScheduler = NewBalanceScheduler()
|
||||
}
|
||||
return sharedDetector, sharedScheduler
|
||||
}
|
||||
|
||||
// GetSharedInstances returns the shared detector and scheduler instances (public access)
|
||||
func GetSharedInstances() (*BalanceDetector, *BalanceScheduler) {
|
||||
return getSharedInstances()
|
||||
}
|
||||
|
||||
// Auto-register this task when the package is imported
|
||||
func init() {
|
||||
factory := NewFactory()
|
||||
tasks.AutoRegister(types.TaskTypeBalance, factory)
|
||||
|
||||
// Get shared instances for all registrations
|
||||
detector, scheduler := getSharedInstances()
|
||||
|
||||
// Register with types registry
|
||||
tasks.AutoRegisterTypes(func(registry *types.TaskRegistry) {
|
||||
registry.RegisterTask(detector, scheduler)
|
||||
})
|
||||
|
||||
// Register with UI registry using the same instances
|
||||
tasks.AutoRegisterUI(func(uiRegistry *types.UIRegistry) {
|
||||
RegisterUI(uiRegistry, detector, scheduler)
|
||||
})
|
||||
}
|
||||
197
weed/worker/tasks/balance/balance_scheduler.go
Normal file
197
weed/worker/tasks/balance/balance_scheduler.go
Normal file
@@ -0,0 +1,197 @@
|
||||
package balance
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/types"
|
||||
)
|
||||
|
||||
// BalanceScheduler implements TaskScheduler for balance tasks
|
||||
type BalanceScheduler struct {
|
||||
enabled bool
|
||||
maxConcurrent int
|
||||
minInterval time.Duration
|
||||
lastScheduled map[string]time.Time // track when we last scheduled a balance for each task type
|
||||
minServerCount int
|
||||
moveDuringOffHours bool
|
||||
offHoursStart string
|
||||
offHoursEnd string
|
||||
}
|
||||
|
||||
// Compile-time interface assertions
|
||||
var (
|
||||
_ types.TaskScheduler = (*BalanceScheduler)(nil)
|
||||
)
|
||||
|
||||
// NewBalanceScheduler creates a new balance scheduler
|
||||
func NewBalanceScheduler() *BalanceScheduler {
|
||||
return &BalanceScheduler{
|
||||
enabled: true,
|
||||
maxConcurrent: 1, // Only run one balance at a time
|
||||
minInterval: 6 * time.Hour,
|
||||
lastScheduled: make(map[string]time.Time),
|
||||
minServerCount: 3,
|
||||
moveDuringOffHours: true,
|
||||
offHoursStart: "23:00",
|
||||
offHoursEnd: "06:00",
|
||||
}
|
||||
}
|
||||
|
||||
// GetTaskType returns the task type
|
||||
func (s *BalanceScheduler) GetTaskType() types.TaskType {
|
||||
return types.TaskTypeBalance
|
||||
}
|
||||
|
||||
// CanScheduleNow determines if a balance task can be scheduled
|
||||
func (s *BalanceScheduler) CanScheduleNow(task *types.Task, runningTasks []*types.Task, availableWorkers []*types.Worker) bool {
|
||||
if !s.enabled {
|
||||
return false
|
||||
}
|
||||
|
||||
// Count running balance tasks
|
||||
runningBalanceCount := 0
|
||||
for _, runningTask := range runningTasks {
|
||||
if runningTask.Type == types.TaskTypeBalance {
|
||||
runningBalanceCount++
|
||||
}
|
||||
}
|
||||
|
||||
// Check concurrency limit
|
||||
if runningBalanceCount >= s.maxConcurrent {
|
||||
glog.V(3).Infof("⏸️ Balance task blocked: too many running (%d >= %d)", runningBalanceCount, s.maxConcurrent)
|
||||
return false
|
||||
}
|
||||
|
||||
// Check minimum interval between balance operations
|
||||
if lastTime, exists := s.lastScheduled["balance"]; exists {
|
||||
if time.Since(lastTime) < s.minInterval {
|
||||
timeLeft := s.minInterval - time.Since(lastTime)
|
||||
glog.V(3).Infof("⏸️ Balance task blocked: too soon (wait %v)", timeLeft)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we have available workers
|
||||
availableWorkerCount := 0
|
||||
for _, worker := range availableWorkers {
|
||||
for _, capability := range worker.Capabilities {
|
||||
if capability == types.TaskTypeBalance {
|
||||
availableWorkerCount++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if availableWorkerCount == 0 {
|
||||
glog.V(3).Infof("⏸️ Balance task blocked: no available workers")
|
||||
return false
|
||||
}
|
||||
|
||||
// All checks passed - can schedule
|
||||
s.lastScheduled["balance"] = time.Now()
|
||||
glog.V(2).Infof("✅ Balance task can be scheduled (running: %d/%d, workers: %d)",
|
||||
runningBalanceCount, s.maxConcurrent, availableWorkerCount)
|
||||
return true
|
||||
}
|
||||
|
||||
// GetPriority returns the priority for balance tasks
|
||||
func (s *BalanceScheduler) GetPriority(task *types.Task) types.TaskPriority {
|
||||
// Balance is typically normal priority - not urgent but important for optimization
|
||||
return types.TaskPriorityNormal
|
||||
}
|
||||
|
||||
// GetMaxConcurrent returns the maximum concurrent balance tasks
|
||||
func (s *BalanceScheduler) GetMaxConcurrent() int {
|
||||
return s.maxConcurrent
|
||||
}
|
||||
|
||||
// GetDefaultRepeatInterval returns the default interval to wait before repeating balance tasks
|
||||
func (s *BalanceScheduler) GetDefaultRepeatInterval() time.Duration {
|
||||
return s.minInterval
|
||||
}
|
||||
|
||||
// IsEnabled returns whether the scheduler is enabled
|
||||
func (s *BalanceScheduler) IsEnabled() bool {
|
||||
return s.enabled
|
||||
}
|
||||
|
||||
// SetEnabled sets whether the scheduler is enabled
|
||||
func (s *BalanceScheduler) SetEnabled(enabled bool) {
|
||||
s.enabled = enabled
|
||||
glog.V(1).Infof("🔄 Balance scheduler enabled: %v", enabled)
|
||||
}
|
||||
|
||||
// SetMaxConcurrent sets the maximum concurrent balance tasks
|
||||
func (s *BalanceScheduler) SetMaxConcurrent(max int) {
|
||||
s.maxConcurrent = max
|
||||
glog.V(1).Infof("🔄 Balance max concurrent set to: %d", max)
|
||||
}
|
||||
|
||||
// SetMinInterval sets the minimum interval between balance operations
|
||||
func (s *BalanceScheduler) SetMinInterval(interval time.Duration) {
|
||||
s.minInterval = interval
|
||||
glog.V(1).Infof("🔄 Balance minimum interval set to: %v", interval)
|
||||
}
|
||||
|
||||
// GetLastScheduled returns when we last scheduled this task type
|
||||
func (s *BalanceScheduler) GetLastScheduled(taskKey string) time.Time {
|
||||
if lastTime, exists := s.lastScheduled[taskKey]; exists {
|
||||
return lastTime
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// SetLastScheduled updates when we last scheduled this task type
|
||||
func (s *BalanceScheduler) SetLastScheduled(taskKey string, when time.Time) {
|
||||
s.lastScheduled[taskKey] = when
|
||||
}
|
||||
|
||||
// GetMinServerCount returns the minimum server count
|
||||
func (s *BalanceScheduler) GetMinServerCount() int {
|
||||
return s.minServerCount
|
||||
}
|
||||
|
||||
// SetMinServerCount sets the minimum server count
|
||||
func (s *BalanceScheduler) SetMinServerCount(count int) {
|
||||
s.minServerCount = count
|
||||
glog.V(1).Infof("🔄 Balance minimum server count set to: %d", count)
|
||||
}
|
||||
|
||||
// GetMoveDuringOffHours returns whether to move only during off-hours
|
||||
func (s *BalanceScheduler) GetMoveDuringOffHours() bool {
|
||||
return s.moveDuringOffHours
|
||||
}
|
||||
|
||||
// SetMoveDuringOffHours sets whether to move only during off-hours
|
||||
func (s *BalanceScheduler) SetMoveDuringOffHours(enabled bool) {
|
||||
s.moveDuringOffHours = enabled
|
||||
glog.V(1).Infof("🔄 Balance move during off-hours: %v", enabled)
|
||||
}
|
||||
|
||||
// GetOffHoursStart returns the off-hours start time
|
||||
func (s *BalanceScheduler) GetOffHoursStart() string {
|
||||
return s.offHoursStart
|
||||
}
|
||||
|
||||
// SetOffHoursStart sets the off-hours start time
|
||||
func (s *BalanceScheduler) SetOffHoursStart(start string) {
|
||||
s.offHoursStart = start
|
||||
glog.V(1).Infof("🔄 Balance off-hours start time set to: %s", start)
|
||||
}
|
||||
|
||||
// GetOffHoursEnd returns the off-hours end time
|
||||
func (s *BalanceScheduler) GetOffHoursEnd() string {
|
||||
return s.offHoursEnd
|
||||
}
|
||||
|
||||
// SetOffHoursEnd sets the off-hours end time
|
||||
func (s *BalanceScheduler) SetOffHoursEnd(end string) {
|
||||
s.offHoursEnd = end
|
||||
glog.V(1).Infof("🔄 Balance off-hours end time set to: %s", end)
|
||||
}
|
||||
|
||||
// GetMinInterval returns the minimum interval
|
||||
func (s *BalanceScheduler) GetMinInterval() time.Duration {
|
||||
return s.minInterval
|
||||
}
|
||||
361
weed/worker/tasks/balance/ui.go
Normal file
361
weed/worker/tasks/balance/ui.go
Normal file
@@ -0,0 +1,361 @@
|
||||
package balance
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/types"
|
||||
)
|
||||
|
||||
// UIProvider provides the UI for balance task configuration
|
||||
type UIProvider struct {
|
||||
detector *BalanceDetector
|
||||
scheduler *BalanceScheduler
|
||||
}
|
||||
|
||||
// NewUIProvider creates a new balance UI provider
|
||||
func NewUIProvider(detector *BalanceDetector, scheduler *BalanceScheduler) *UIProvider {
|
||||
return &UIProvider{
|
||||
detector: detector,
|
||||
scheduler: scheduler,
|
||||
}
|
||||
}
|
||||
|
||||
// GetTaskType returns the task type
|
||||
func (ui *UIProvider) GetTaskType() types.TaskType {
|
||||
return types.TaskTypeBalance
|
||||
}
|
||||
|
||||
// GetDisplayName returns the human-readable name
|
||||
func (ui *UIProvider) GetDisplayName() string {
|
||||
return "Volume Balance"
|
||||
}
|
||||
|
||||
// GetDescription returns a description of what this task does
|
||||
func (ui *UIProvider) GetDescription() string {
|
||||
return "Redistributes volumes across volume servers to optimize storage utilization and performance"
|
||||
}
|
||||
|
||||
// GetIcon returns the icon CSS class for this task type
|
||||
func (ui *UIProvider) GetIcon() string {
|
||||
return "fas fa-balance-scale text-secondary"
|
||||
}
|
||||
|
||||
// BalanceConfig represents the balance configuration
|
||||
type BalanceConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
ImbalanceThreshold float64 `json:"imbalance_threshold"`
|
||||
ScanIntervalSeconds int `json:"scan_interval_seconds"`
|
||||
MaxConcurrent int `json:"max_concurrent"`
|
||||
MinServerCount int `json:"min_server_count"`
|
||||
MoveDuringOffHours bool `json:"move_during_off_hours"`
|
||||
OffHoursStart string `json:"off_hours_start"`
|
||||
OffHoursEnd string `json:"off_hours_end"`
|
||||
MinIntervalSeconds int `json:"min_interval_seconds"`
|
||||
}
|
||||
|
||||
// Helper functions for duration conversion
|
||||
func secondsToDuration(seconds int) time.Duration {
|
||||
return time.Duration(seconds) * time.Second
|
||||
}
|
||||
|
||||
func durationToSeconds(d time.Duration) int {
|
||||
return int(d.Seconds())
|
||||
}
|
||||
|
||||
// formatDurationForUser formats seconds as a user-friendly duration string
|
||||
func formatDurationForUser(seconds int) string {
|
||||
d := secondsToDuration(seconds)
|
||||
if d < time.Minute {
|
||||
return fmt.Sprintf("%ds", seconds)
|
||||
}
|
||||
if d < time.Hour {
|
||||
return fmt.Sprintf("%.0fm", d.Minutes())
|
||||
}
|
||||
if d < 24*time.Hour {
|
||||
return fmt.Sprintf("%.1fh", d.Hours())
|
||||
}
|
||||
return fmt.Sprintf("%.1fd", d.Hours()/24)
|
||||
}
|
||||
|
||||
// RenderConfigForm renders the configuration form HTML
|
||||
func (ui *UIProvider) RenderConfigForm(currentConfig interface{}) (template.HTML, error) {
|
||||
config := ui.getCurrentBalanceConfig()
|
||||
|
||||
// Build form using the FormBuilder helper
|
||||
form := types.NewFormBuilder()
|
||||
|
||||
// Detection Settings
|
||||
form.AddCheckboxField(
|
||||
"enabled",
|
||||
"Enable Balance Tasks",
|
||||
"Whether balance tasks should be automatically created",
|
||||
config.Enabled,
|
||||
)
|
||||
|
||||
form.AddNumberField(
|
||||
"imbalance_threshold",
|
||||
"Imbalance Threshold (%)",
|
||||
"Trigger balance when storage imbalance exceeds this percentage (0.0-1.0)",
|
||||
config.ImbalanceThreshold,
|
||||
true,
|
||||
)
|
||||
|
||||
form.AddDurationField("scan_interval", "Scan Interval", "How often to scan for imbalanced volumes", secondsToDuration(config.ScanIntervalSeconds), true)
|
||||
|
||||
// Scheduling Settings
|
||||
form.AddNumberField(
|
||||
"max_concurrent",
|
||||
"Max Concurrent Tasks",
|
||||
"Maximum number of balance tasks that can run simultaneously",
|
||||
float64(config.MaxConcurrent),
|
||||
true,
|
||||
)
|
||||
|
||||
form.AddNumberField(
|
||||
"min_server_count",
|
||||
"Minimum Server Count",
|
||||
"Only balance when at least this many servers are available",
|
||||
float64(config.MinServerCount),
|
||||
true,
|
||||
)
|
||||
|
||||
// Timing Settings
|
||||
form.AddCheckboxField(
|
||||
"move_during_off_hours",
|
||||
"Restrict to Off-Hours",
|
||||
"Only perform balance operations during off-peak hours",
|
||||
config.MoveDuringOffHours,
|
||||
)
|
||||
|
||||
form.AddTextField(
|
||||
"off_hours_start",
|
||||
"Off-Hours Start Time",
|
||||
"Start time for off-hours window (e.g., 23:00)",
|
||||
config.OffHoursStart,
|
||||
false,
|
||||
)
|
||||
|
||||
form.AddTextField(
|
||||
"off_hours_end",
|
||||
"Off-Hours End Time",
|
||||
"End time for off-hours window (e.g., 06:00)",
|
||||
config.OffHoursEnd,
|
||||
false,
|
||||
)
|
||||
|
||||
// Timing constraints
|
||||
form.AddDurationField("min_interval", "Min Interval", "Minimum time between balance operations", secondsToDuration(config.MinIntervalSeconds), true)
|
||||
|
||||
// Generate organized form sections using Bootstrap components
|
||||
html := `
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-balance-scale me-2"></i>
|
||||
Balance Configuration
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
` + string(form.Build()) + `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
Performance Considerations
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<h6 class="alert-heading">Important Considerations:</h6>
|
||||
<p class="mb-2"><strong>Performance:</strong> Volume balancing involves data movement and can impact cluster performance.</p>
|
||||
<p class="mb-2"><strong>Recommendation:</strong> Enable off-hours restriction to minimize impact on production workloads.</p>
|
||||
<p class="mb-0"><strong>Safety:</strong> Requires at least ` + fmt.Sprintf("%d", config.MinServerCount) + ` servers to ensure data safety during moves.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
return template.HTML(html), nil
|
||||
}
|
||||
|
||||
// ParseConfigForm parses form data into configuration
|
||||
func (ui *UIProvider) ParseConfigForm(formData map[string][]string) (interface{}, error) {
|
||||
config := &BalanceConfig{}
|
||||
|
||||
// Parse enabled
|
||||
config.Enabled = len(formData["enabled"]) > 0
|
||||
|
||||
// Parse imbalance threshold
|
||||
if values, ok := formData["imbalance_threshold"]; ok && len(values) > 0 {
|
||||
threshold, err := strconv.ParseFloat(values[0], 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid imbalance threshold: %v", err)
|
||||
}
|
||||
if threshold < 0 || threshold > 1 {
|
||||
return nil, fmt.Errorf("imbalance threshold must be between 0.0 and 1.0")
|
||||
}
|
||||
config.ImbalanceThreshold = threshold
|
||||
}
|
||||
|
||||
// Parse scan interval
|
||||
if values, ok := formData["scan_interval"]; ok && len(values) > 0 {
|
||||
duration, err := time.ParseDuration(values[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid scan interval: %v", err)
|
||||
}
|
||||
config.ScanIntervalSeconds = int(duration.Seconds())
|
||||
}
|
||||
|
||||
// Parse max concurrent
|
||||
if values, ok := formData["max_concurrent"]; ok && len(values) > 0 {
|
||||
maxConcurrent, err := strconv.Atoi(values[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid max concurrent: %v", err)
|
||||
}
|
||||
if maxConcurrent < 1 {
|
||||
return nil, fmt.Errorf("max concurrent must be at least 1")
|
||||
}
|
||||
config.MaxConcurrent = maxConcurrent
|
||||
}
|
||||
|
||||
// Parse min server count
|
||||
if values, ok := formData["min_server_count"]; ok && len(values) > 0 {
|
||||
minServerCount, err := strconv.Atoi(values[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid min server count: %v", err)
|
||||
}
|
||||
if minServerCount < 2 {
|
||||
return nil, fmt.Errorf("min server count must be at least 2")
|
||||
}
|
||||
config.MinServerCount = minServerCount
|
||||
}
|
||||
|
||||
// Parse off-hours settings
|
||||
config.MoveDuringOffHours = len(formData["move_during_off_hours"]) > 0
|
||||
|
||||
if values, ok := formData["off_hours_start"]; ok && len(values) > 0 {
|
||||
config.OffHoursStart = values[0]
|
||||
}
|
||||
|
||||
if values, ok := formData["off_hours_end"]; ok && len(values) > 0 {
|
||||
config.OffHoursEnd = values[0]
|
||||
}
|
||||
|
||||
// Parse min interval
|
||||
if values, ok := formData["min_interval"]; ok && len(values) > 0 {
|
||||
duration, err := time.ParseDuration(values[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid min interval: %v", err)
|
||||
}
|
||||
config.MinIntervalSeconds = int(duration.Seconds())
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// GetCurrentConfig returns the current configuration
|
||||
func (ui *UIProvider) GetCurrentConfig() interface{} {
|
||||
return ui.getCurrentBalanceConfig()
|
||||
}
|
||||
|
||||
// ApplyConfig applies the new configuration
|
||||
func (ui *UIProvider) ApplyConfig(config interface{}) error {
|
||||
balanceConfig, ok := config.(*BalanceConfig)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid config type, expected *BalanceConfig")
|
||||
}
|
||||
|
||||
// Apply to detector
|
||||
if ui.detector != nil {
|
||||
ui.detector.SetEnabled(balanceConfig.Enabled)
|
||||
ui.detector.SetThreshold(balanceConfig.ImbalanceThreshold)
|
||||
ui.detector.SetMinCheckInterval(secondsToDuration(balanceConfig.ScanIntervalSeconds))
|
||||
}
|
||||
|
||||
// Apply to scheduler
|
||||
if ui.scheduler != nil {
|
||||
ui.scheduler.SetEnabled(balanceConfig.Enabled)
|
||||
ui.scheduler.SetMaxConcurrent(balanceConfig.MaxConcurrent)
|
||||
ui.scheduler.SetMinServerCount(balanceConfig.MinServerCount)
|
||||
ui.scheduler.SetMoveDuringOffHours(balanceConfig.MoveDuringOffHours)
|
||||
ui.scheduler.SetOffHoursStart(balanceConfig.OffHoursStart)
|
||||
ui.scheduler.SetOffHoursEnd(balanceConfig.OffHoursEnd)
|
||||
}
|
||||
|
||||
glog.V(1).Infof("Applied balance configuration: enabled=%v, threshold=%.1f%%, max_concurrent=%d, min_servers=%d, off_hours=%v",
|
||||
balanceConfig.Enabled, balanceConfig.ImbalanceThreshold*100, balanceConfig.MaxConcurrent,
|
||||
balanceConfig.MinServerCount, balanceConfig.MoveDuringOffHours)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getCurrentBalanceConfig gets the current configuration from detector and scheduler
|
||||
func (ui *UIProvider) getCurrentBalanceConfig() *BalanceConfig {
|
||||
config := &BalanceConfig{
|
||||
// Default values (fallback if detectors/schedulers are nil)
|
||||
Enabled: true,
|
||||
ImbalanceThreshold: 0.1, // 10% imbalance
|
||||
ScanIntervalSeconds: durationToSeconds(4 * time.Hour),
|
||||
MaxConcurrent: 1,
|
||||
MinServerCount: 3,
|
||||
MoveDuringOffHours: true,
|
||||
OffHoursStart: "23:00",
|
||||
OffHoursEnd: "06:00",
|
||||
MinIntervalSeconds: durationToSeconds(1 * time.Hour),
|
||||
}
|
||||
|
||||
// Get current values from detector
|
||||
if ui.detector != nil {
|
||||
config.Enabled = ui.detector.IsEnabled()
|
||||
config.ImbalanceThreshold = ui.detector.GetThreshold()
|
||||
config.ScanIntervalSeconds = int(ui.detector.ScanInterval().Seconds())
|
||||
}
|
||||
|
||||
// Get current values from scheduler
|
||||
if ui.scheduler != nil {
|
||||
config.MaxConcurrent = ui.scheduler.GetMaxConcurrent()
|
||||
config.MinServerCount = ui.scheduler.GetMinServerCount()
|
||||
config.MoveDuringOffHours = ui.scheduler.GetMoveDuringOffHours()
|
||||
config.OffHoursStart = ui.scheduler.GetOffHoursStart()
|
||||
config.OffHoursEnd = ui.scheduler.GetOffHoursEnd()
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// RegisterUI registers the balance UI provider with the UI registry
|
||||
func RegisterUI(uiRegistry *types.UIRegistry, detector *BalanceDetector, scheduler *BalanceScheduler) {
|
||||
uiProvider := NewUIProvider(detector, scheduler)
|
||||
uiRegistry.RegisterUI(uiProvider)
|
||||
|
||||
glog.V(1).Infof("✅ Registered balance task UI provider")
|
||||
}
|
||||
|
||||
// DefaultBalanceConfig returns default balance configuration
|
||||
func DefaultBalanceConfig() *BalanceConfig {
|
||||
return &BalanceConfig{
|
||||
Enabled: false,
|
||||
ImbalanceThreshold: 0.3,
|
||||
ScanIntervalSeconds: durationToSeconds(4 * time.Hour),
|
||||
MaxConcurrent: 1,
|
||||
MinServerCount: 3,
|
||||
MoveDuringOffHours: false,
|
||||
OffHoursStart: "22:00",
|
||||
OffHoursEnd: "06:00",
|
||||
MinIntervalSeconds: durationToSeconds(1 * time.Hour),
|
||||
}
|
||||
}
|
||||
369
weed/worker/tasks/balance/ui_templ.go
Normal file
369
weed/worker/tasks/balance/ui_templ.go
Normal file
@@ -0,0 +1,369 @@
|
||||
package balance
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/view/components"
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/types"
|
||||
)
|
||||
|
||||
// Helper function to format seconds as duration string
|
||||
func formatDurationFromSeconds(seconds int) string {
|
||||
d := time.Duration(seconds) * time.Second
|
||||
return d.String()
|
||||
}
|
||||
|
||||
// Helper functions to convert between seconds and value+unit format
|
||||
func secondsToValueAndUnit(seconds int) (float64, string) {
|
||||
if seconds == 0 {
|
||||
return 0, "minutes"
|
||||
}
|
||||
|
||||
// Try days first
|
||||
if seconds%(24*3600) == 0 && seconds >= 24*3600 {
|
||||
return float64(seconds / (24 * 3600)), "days"
|
||||
}
|
||||
|
||||
// Try hours
|
||||
if seconds%3600 == 0 && seconds >= 3600 {
|
||||
return float64(seconds / 3600), "hours"
|
||||
}
|
||||
|
||||
// Default to minutes
|
||||
return float64(seconds / 60), "minutes"
|
||||
}
|
||||
|
||||
func valueAndUnitToSeconds(value float64, unit string) int {
|
||||
switch unit {
|
||||
case "days":
|
||||
return int(value * 24 * 3600)
|
||||
case "hours":
|
||||
return int(value * 3600)
|
||||
case "minutes":
|
||||
return int(value * 60)
|
||||
default:
|
||||
return int(value * 60) // Default to minutes
|
||||
}
|
||||
}
|
||||
|
||||
// UITemplProvider provides the templ-based UI for balance task configuration
|
||||
type UITemplProvider struct {
|
||||
detector *BalanceDetector
|
||||
scheduler *BalanceScheduler
|
||||
}
|
||||
|
||||
// NewUITemplProvider creates a new balance templ UI provider
|
||||
func NewUITemplProvider(detector *BalanceDetector, scheduler *BalanceScheduler) *UITemplProvider {
|
||||
return &UITemplProvider{
|
||||
detector: detector,
|
||||
scheduler: scheduler,
|
||||
}
|
||||
}
|
||||
|
||||
// GetTaskType returns the task type
|
||||
func (ui *UITemplProvider) GetTaskType() types.TaskType {
|
||||
return types.TaskTypeBalance
|
||||
}
|
||||
|
||||
// GetDisplayName returns the human-readable name
|
||||
func (ui *UITemplProvider) GetDisplayName() string {
|
||||
return "Volume Balance"
|
||||
}
|
||||
|
||||
// GetDescription returns a description of what this task does
|
||||
func (ui *UITemplProvider) GetDescription() string {
|
||||
return "Redistributes volumes across volume servers to optimize storage utilization and performance"
|
||||
}
|
||||
|
||||
// GetIcon returns the icon CSS class for this task type
|
||||
func (ui *UITemplProvider) GetIcon() string {
|
||||
return "fas fa-balance-scale text-secondary"
|
||||
}
|
||||
|
||||
// RenderConfigSections renders the configuration as templ section data
|
||||
func (ui *UITemplProvider) RenderConfigSections(currentConfig interface{}) ([]components.ConfigSectionData, error) {
|
||||
config := ui.getCurrentBalanceConfig()
|
||||
|
||||
// Detection settings section
|
||||
detectionSection := components.ConfigSectionData{
|
||||
Title: "Detection Settings",
|
||||
Icon: "fas fa-search",
|
||||
Description: "Configure when balance tasks should be triggered",
|
||||
Fields: []interface{}{
|
||||
components.CheckboxFieldData{
|
||||
FormFieldData: components.FormFieldData{
|
||||
Name: "enabled",
|
||||
Label: "Enable Balance Tasks",
|
||||
Description: "Whether balance tasks should be automatically created",
|
||||
},
|
||||
Checked: config.Enabled,
|
||||
},
|
||||
components.NumberFieldData{
|
||||
FormFieldData: components.FormFieldData{
|
||||
Name: "imbalance_threshold",
|
||||
Label: "Imbalance Threshold",
|
||||
Description: "Trigger balance when storage imbalance exceeds this percentage (0.0-1.0)",
|
||||
Required: true,
|
||||
},
|
||||
Value: config.ImbalanceThreshold,
|
||||
Step: "0.01",
|
||||
Min: floatPtr(0.0),
|
||||
Max: floatPtr(1.0),
|
||||
},
|
||||
components.DurationInputFieldData{
|
||||
FormFieldData: components.FormFieldData{
|
||||
Name: "scan_interval",
|
||||
Label: "Scan Interval",
|
||||
Description: "How often to scan for imbalanced volumes",
|
||||
Required: true,
|
||||
},
|
||||
Seconds: config.ScanIntervalSeconds,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Scheduling settings section
|
||||
schedulingSection := components.ConfigSectionData{
|
||||
Title: "Scheduling Settings",
|
||||
Icon: "fas fa-clock",
|
||||
Description: "Configure task scheduling and concurrency",
|
||||
Fields: []interface{}{
|
||||
components.NumberFieldData{
|
||||
FormFieldData: components.FormFieldData{
|
||||
Name: "max_concurrent",
|
||||
Label: "Max Concurrent Tasks",
|
||||
Description: "Maximum number of balance tasks that can run simultaneously",
|
||||
Required: true,
|
||||
},
|
||||
Value: float64(config.MaxConcurrent),
|
||||
Step: "1",
|
||||
Min: floatPtr(1),
|
||||
},
|
||||
components.NumberFieldData{
|
||||
FormFieldData: components.FormFieldData{
|
||||
Name: "min_server_count",
|
||||
Label: "Minimum Server Count",
|
||||
Description: "Only balance when at least this many servers are available",
|
||||
Required: true,
|
||||
},
|
||||
Value: float64(config.MinServerCount),
|
||||
Step: "1",
|
||||
Min: floatPtr(1),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Timing constraints section
|
||||
timingSection := components.ConfigSectionData{
|
||||
Title: "Timing Constraints",
|
||||
Icon: "fas fa-calendar-clock",
|
||||
Description: "Configure when balance operations are allowed",
|
||||
Fields: []interface{}{
|
||||
components.CheckboxFieldData{
|
||||
FormFieldData: components.FormFieldData{
|
||||
Name: "move_during_off_hours",
|
||||
Label: "Restrict to Off-Hours",
|
||||
Description: "Only perform balance operations during off-peak hours",
|
||||
},
|
||||
Checked: config.MoveDuringOffHours,
|
||||
},
|
||||
components.TextFieldData{
|
||||
FormFieldData: components.FormFieldData{
|
||||
Name: "off_hours_start",
|
||||
Label: "Off-Hours Start Time",
|
||||
Description: "Start time for off-hours window (e.g., 23:00)",
|
||||
},
|
||||
Value: config.OffHoursStart,
|
||||
},
|
||||
components.TextFieldData{
|
||||
FormFieldData: components.FormFieldData{
|
||||
Name: "off_hours_end",
|
||||
Label: "Off-Hours End Time",
|
||||
Description: "End time for off-hours window (e.g., 06:00)",
|
||||
},
|
||||
Value: config.OffHoursEnd,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Performance impact info section
|
||||
performanceSection := components.ConfigSectionData{
|
||||
Title: "Performance Considerations",
|
||||
Icon: "fas fa-exclamation-triangle",
|
||||
Description: "Important information about balance operations",
|
||||
Fields: []interface{}{
|
||||
components.TextFieldData{
|
||||
FormFieldData: components.FormFieldData{
|
||||
Name: "performance_info",
|
||||
Label: "Performance Impact",
|
||||
Description: "Volume balancing involves data movement and can impact cluster performance",
|
||||
},
|
||||
Value: "Enable off-hours restriction to minimize impact on production workloads",
|
||||
},
|
||||
components.TextFieldData{
|
||||
FormFieldData: components.FormFieldData{
|
||||
Name: "safety_info",
|
||||
Label: "Safety Requirements",
|
||||
Description: fmt.Sprintf("Requires at least %d servers to ensure data safety during moves", config.MinServerCount),
|
||||
},
|
||||
Value: "Maintains data safety during volume moves between servers",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return []components.ConfigSectionData{detectionSection, schedulingSection, timingSection, performanceSection}, nil
|
||||
}
|
||||
|
||||
// ParseConfigForm parses form data into configuration
|
||||
func (ui *UITemplProvider) ParseConfigForm(formData map[string][]string) (interface{}, error) {
|
||||
config := &BalanceConfig{}
|
||||
|
||||
// Parse enabled checkbox
|
||||
config.Enabled = len(formData["enabled"]) > 0 && formData["enabled"][0] == "on"
|
||||
|
||||
// Parse imbalance threshold
|
||||
if thresholdStr := formData["imbalance_threshold"]; len(thresholdStr) > 0 {
|
||||
if threshold, err := strconv.ParseFloat(thresholdStr[0], 64); err != nil {
|
||||
return nil, fmt.Errorf("invalid imbalance threshold: %v", err)
|
||||
} else if threshold < 0 || threshold > 1 {
|
||||
return nil, fmt.Errorf("imbalance threshold must be between 0.0 and 1.0")
|
||||
} else {
|
||||
config.ImbalanceThreshold = threshold
|
||||
}
|
||||
}
|
||||
|
||||
// Parse scan interval
|
||||
if valueStr := formData["scan_interval"]; len(valueStr) > 0 {
|
||||
if value, err := strconv.ParseFloat(valueStr[0], 64); err != nil {
|
||||
return nil, fmt.Errorf("invalid scan interval value: %v", err)
|
||||
} else {
|
||||
unit := "minutes" // default
|
||||
if unitStr := formData["scan_interval_unit"]; len(unitStr) > 0 {
|
||||
unit = unitStr[0]
|
||||
}
|
||||
config.ScanIntervalSeconds = valueAndUnitToSeconds(value, unit)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse max concurrent
|
||||
if concurrentStr := formData["max_concurrent"]; len(concurrentStr) > 0 {
|
||||
if concurrent, err := strconv.Atoi(concurrentStr[0]); err != nil {
|
||||
return nil, fmt.Errorf("invalid max concurrent: %v", err)
|
||||
} else if concurrent < 1 {
|
||||
return nil, fmt.Errorf("max concurrent must be at least 1")
|
||||
} else {
|
||||
config.MaxConcurrent = concurrent
|
||||
}
|
||||
}
|
||||
|
||||
// Parse min server count
|
||||
if serverCountStr := formData["min_server_count"]; len(serverCountStr) > 0 {
|
||||
if serverCount, err := strconv.Atoi(serverCountStr[0]); err != nil {
|
||||
return nil, fmt.Errorf("invalid min server count: %v", err)
|
||||
} else if serverCount < 1 {
|
||||
return nil, fmt.Errorf("min server count must be at least 1")
|
||||
} else {
|
||||
config.MinServerCount = serverCount
|
||||
}
|
||||
}
|
||||
|
||||
// Parse move during off hours
|
||||
config.MoveDuringOffHours = len(formData["move_during_off_hours"]) > 0 && formData["move_during_off_hours"][0] == "on"
|
||||
|
||||
// Parse off hours start time
|
||||
if startStr := formData["off_hours_start"]; len(startStr) > 0 {
|
||||
config.OffHoursStart = startStr[0]
|
||||
}
|
||||
|
||||
// Parse off hours end time
|
||||
if endStr := formData["off_hours_end"]; len(endStr) > 0 {
|
||||
config.OffHoursEnd = endStr[0]
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// GetCurrentConfig returns the current configuration
|
||||
func (ui *UITemplProvider) GetCurrentConfig() interface{} {
|
||||
return ui.getCurrentBalanceConfig()
|
||||
}
|
||||
|
||||
// ApplyConfig applies the new configuration
|
||||
func (ui *UITemplProvider) ApplyConfig(config interface{}) error {
|
||||
balanceConfig, ok := config.(*BalanceConfig)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid config type, expected *BalanceConfig")
|
||||
}
|
||||
|
||||
// Apply to detector
|
||||
if ui.detector != nil {
|
||||
ui.detector.SetEnabled(balanceConfig.Enabled)
|
||||
ui.detector.SetThreshold(balanceConfig.ImbalanceThreshold)
|
||||
ui.detector.SetMinCheckInterval(time.Duration(balanceConfig.ScanIntervalSeconds) * time.Second)
|
||||
}
|
||||
|
||||
// Apply to scheduler
|
||||
if ui.scheduler != nil {
|
||||
ui.scheduler.SetEnabled(balanceConfig.Enabled)
|
||||
ui.scheduler.SetMaxConcurrent(balanceConfig.MaxConcurrent)
|
||||
ui.scheduler.SetMinServerCount(balanceConfig.MinServerCount)
|
||||
ui.scheduler.SetMoveDuringOffHours(balanceConfig.MoveDuringOffHours)
|
||||
ui.scheduler.SetOffHoursStart(balanceConfig.OffHoursStart)
|
||||
ui.scheduler.SetOffHoursEnd(balanceConfig.OffHoursEnd)
|
||||
}
|
||||
|
||||
glog.V(1).Infof("Applied balance configuration: enabled=%v, threshold=%.1f%%, max_concurrent=%d, min_servers=%d, off_hours=%v",
|
||||
balanceConfig.Enabled, balanceConfig.ImbalanceThreshold*100, balanceConfig.MaxConcurrent,
|
||||
balanceConfig.MinServerCount, balanceConfig.MoveDuringOffHours)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getCurrentBalanceConfig gets the current configuration from detector and scheduler
|
||||
func (ui *UITemplProvider) getCurrentBalanceConfig() *BalanceConfig {
|
||||
config := &BalanceConfig{
|
||||
// Default values (fallback if detectors/schedulers are nil)
|
||||
Enabled: true,
|
||||
ImbalanceThreshold: 0.1, // 10% imbalance
|
||||
ScanIntervalSeconds: int((4 * time.Hour).Seconds()),
|
||||
MaxConcurrent: 1,
|
||||
MinServerCount: 3,
|
||||
MoveDuringOffHours: true,
|
||||
OffHoursStart: "23:00",
|
||||
OffHoursEnd: "06:00",
|
||||
}
|
||||
|
||||
// Get current values from detector
|
||||
if ui.detector != nil {
|
||||
config.Enabled = ui.detector.IsEnabled()
|
||||
config.ImbalanceThreshold = ui.detector.GetThreshold()
|
||||
config.ScanIntervalSeconds = int(ui.detector.ScanInterval().Seconds())
|
||||
}
|
||||
|
||||
// Get current values from scheduler
|
||||
if ui.scheduler != nil {
|
||||
config.MaxConcurrent = ui.scheduler.GetMaxConcurrent()
|
||||
config.MinServerCount = ui.scheduler.GetMinServerCount()
|
||||
config.MoveDuringOffHours = ui.scheduler.GetMoveDuringOffHours()
|
||||
config.OffHoursStart = ui.scheduler.GetOffHoursStart()
|
||||
config.OffHoursEnd = ui.scheduler.GetOffHoursEnd()
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// floatPtr is a helper function to create float64 pointers
|
||||
func floatPtr(f float64) *float64 {
|
||||
return &f
|
||||
}
|
||||
|
||||
// RegisterUITempl registers the balance templ UI provider with the UI registry
|
||||
func RegisterUITempl(uiRegistry *types.UITemplRegistry, detector *BalanceDetector, scheduler *BalanceScheduler) {
|
||||
uiProvider := NewUITemplProvider(detector, scheduler)
|
||||
uiRegistry.RegisterUI(uiProvider)
|
||||
|
||||
glog.V(1).Infof("✅ Registered balance task templ UI provider")
|
||||
}
|
||||
Reference in New Issue
Block a user