diff --git a/weed/admin/handlers/maintenance_handlers.go b/weed/admin/handlers/maintenance_handlers.go index 7c5feacd6..ba5e58ad7 100644 --- a/weed/admin/handlers/maintenance_handlers.go +++ b/weed/admin/handlers/maintenance_handlers.go @@ -8,7 +8,6 @@ import ( "github.com/seaweedfs/seaweedfs/weed/admin/dash" "github.com/seaweedfs/seaweedfs/weed/admin/maintenance" "github.com/seaweedfs/seaweedfs/weed/admin/view/app" - "github.com/seaweedfs/seaweedfs/weed/admin/view/components" "github.com/seaweedfs/seaweedfs/weed/admin/view/layout" "github.com/seaweedfs/seaweedfs/weed/glog" "github.com/seaweedfs/seaweedfs/weed/worker/tasks" @@ -124,75 +123,28 @@ func (h *MaintenanceHandlers) ShowTaskConfig(c *gin.Context) { return } - // Try to get templ UI provider first - temporarily disabled - // templUIProvider := getTemplUIProvider(taskType) - var configSections []components.ConfigSectionData - - // Temporarily disabled templ UI provider - // if templUIProvider != nil { - // // Use the new templ-based UI provider - // currentConfig := templUIProvider.GetCurrentConfig() - // sections, err := templUIProvider.RenderConfigSections(currentConfig) - // if err != nil { - // c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render configuration sections: " + err.Error()}) - // return - // } - // configSections = sections - // } else { - // Fallback to basic configuration for providers that haven't been migrated yet - configSections = []components.ConfigSectionData{ - { - Title: "Configuration Settings", - Icon: "fas fa-cogs", - Description: "Configure task detection and scheduling parameters", - Fields: []interface{}{ - components.CheckboxFieldData{ - FormFieldData: components.FormFieldData{ - Name: "enabled", - Label: "Enable Task", - Description: "Whether this task type should be enabled", - }, - Checked: true, - }, - components.NumberFieldData{ - FormFieldData: components.FormFieldData{ - Name: "max_concurrent", - Label: "Max Concurrent Tasks", - Description: "Maximum number of concurrent tasks", - Required: true, - }, - Value: 2, - Step: "1", - Min: floatPtr(1), - }, - components.DurationFieldData{ - FormFieldData: components.FormFieldData{ - Name: "scan_interval", - Label: "Scan Interval", - Description: "How often to scan for tasks", - Required: true, - }, - Value: "30m", - }, - }, - }, + // Get current configuration and render form using the actual UI provider + currentConfig := provider.GetCurrentConfig() + formHTML, err := provider.RenderConfigForm(currentConfig) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render configuration form: " + err.Error()}) + return } - // } // End of disabled templ UI provider else block - // Create task configuration data using templ components - configData := &app.TaskConfigTemplData{ + // Create task configuration data using the actual form HTML + configData := &maintenance.TaskConfigData{ TaskType: taskType, TaskName: provider.GetDisplayName(), TaskIcon: provider.GetIcon(), Description: provider.GetDescription(), - ConfigSections: configSections, + ConfigFormHTML: formHTML, } - // Render HTML template using templ components + // Render HTML template c.Header("Content-Type", "text/html") - taskConfigComponent := app.TaskConfigTempl(configData) + taskConfigComponent := app.TaskConfig(configData) layoutComponent := layout.Layout(c, taskConfigComponent) - err := layoutComponent.Render(c.Request.Context(), c.Writer) + err = layoutComponent.Render(c.Request.Context(), c.Writer) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()}) return diff --git a/weed/worker/tasks/balance/ui.go b/weed/worker/tasks/balance/ui.go index 2cea20a76..b4d04ee8f 100644 --- a/weed/worker/tasks/balance/ui.go +++ b/weed/worker/tasks/balance/ui.go @@ -104,7 +104,7 @@ func (ui *UIProvider) RenderConfigForm(currentConfig interface{}) (template.HTML true, ) - form.AddDurationField("scan_interval", "Scan Interval", "How often to scan for imbalanced volumes", secondsToDuration(config.ScanIntervalSeconds), true) + form.AddIntervalField("scan_interval", "Scan Interval", "How often to scan for imbalanced volumes", config.ScanIntervalSeconds, true) // Scheduling Settings form.AddNumberField( @@ -212,12 +212,19 @@ func (ui *UIProvider) ParseConfigForm(formData map[string][]string) (interface{} } // Parse scan interval - if values, ok := formData["scan_interval"]; ok && len(values) > 0 { - duration, err := time.ParseDuration(values[0]) + if values, ok := formData["scan_interval_value"]; ok && len(values) > 0 { + value, err := strconv.Atoi(values[0]) if err != nil { - return nil, fmt.Errorf("invalid scan interval: %w", err) + return nil, fmt.Errorf("invalid scan interval value: %w", err) + } + + unit := "minute" // default + if units, ok := formData["scan_interval_unit"]; ok && len(units) > 0 { + unit = units[0] } - config.ScanIntervalSeconds = int(duration.Seconds()) + + // Convert to seconds + config.ScanIntervalSeconds = types.IntervalValueUnitToSeconds(value, unit) } // Parse max concurrent diff --git a/weed/worker/tasks/erasure_coding/ec_detector.go b/weed/worker/tasks/erasure_coding/ec_detector.go index 6bcca4d64..bd303ceb3 100644 --- a/weed/worker/tasks/erasure_coding/ec_detector.go +++ b/weed/worker/tasks/erasure_coding/ec_detector.go @@ -2,6 +2,7 @@ package erasure_coding import ( "fmt" + "strings" "time" "github.com/seaweedfs/seaweedfs/weed/glog" @@ -10,11 +11,12 @@ import ( // EcDetector implements erasure coding task detection type EcDetector struct { - enabled bool - volumeAgeHours int - fullnessRatio float64 - minSizeMB int // Minimum volume size in MB before considering EC - scanInterval time.Duration + enabled bool + quietForSeconds int + fullnessRatio float64 + minSizeMB int // Minimum volume size in MB before considering EC + scanInterval time.Duration + collectionFilter string } // Compile-time interface assertions @@ -26,11 +28,12 @@ var ( // NewEcDetector creates a new erasure coding detector with configurable defaults func NewEcDetector() *EcDetector { return &EcDetector{ - enabled: true, // Enabled for testing - volumeAgeHours: 0, // No age requirement for testing (was 24) - fullnessRatio: 0.8, // 80% full by default - minSizeMB: 50, // Minimum 50MB for testing (was 100MB) - scanInterval: 30 * time.Second, // Faster scanning for testing + enabled: true, // Enabled for testing + quietForSeconds: 0, // No quiet requirement for testing (was 24) + fullnessRatio: 0.90, // 90% full by default + minSizeMB: 50, // Minimum 50MB for testing (was 100MB) + scanInterval: 30 * time.Second, // Faster scanning for testing + collectionFilter: "", // No collection filter by default } } @@ -48,11 +51,11 @@ func (d *EcDetector) ScanForTasks(volumeMetrics []*types.VolumeHealthMetrics, cl var results []*types.TaskDetectionResult now := time.Now() - ageThreshold := time.Duration(d.volumeAgeHours) * time.Hour + quietThreshold := time.Duration(d.quietForSeconds) * time.Second minSizeBytes := uint64(d.minSizeMB) * 1024 * 1024 - glog.V(2).Infof("EC detector scanning %d volumes with thresholds: age=%dh, fullness=%.2f, minSize=%dMB", - len(volumeMetrics), d.volumeAgeHours, d.fullnessRatio, d.minSizeMB) + glog.V(2).Infof("EC detector scanning %d volumes with thresholds: quietFor=%ds, fullness=%.2f, minSize=%dMB", + len(volumeMetrics), d.quietForSeconds, d.fullnessRatio, d.minSizeMB) for _, metric := range volumeMetrics { // Skip if already EC volume @@ -65,8 +68,21 @@ func (d *EcDetector) ScanForTasks(volumeMetrics []*types.VolumeHealthMetrics, cl continue } - // Check age and fullness criteria - if metric.Age >= ageThreshold && metric.FullnessRatio >= d.fullnessRatio { + // Check collection filter if specified + if d.collectionFilter != "" { + // Parse comma-separated collections + allowedCollections := make(map[string]bool) + for _, collection := range strings.Split(d.collectionFilter, ",") { + allowedCollections[strings.TrimSpace(collection)] = true + } + // Skip if volume's collection is not in the allowed list + if !allowedCollections[metric.Collection] { + continue + } + } + + // Check quiet duration and fullness criteria + if metric.Age >= quietThreshold && metric.FullnessRatio >= d.fullnessRatio { // Note: Removed read-only requirement for testing // In production, you might want to enable this: // if !metric.IsReadOnly { @@ -79,11 +95,11 @@ func (d *EcDetector) ScanForTasks(volumeMetrics []*types.VolumeHealthMetrics, cl Server: metric.Server, Collection: metric.Collection, Priority: types.TaskPriorityLow, // EC is not urgent - Reason: fmt.Sprintf("Volume meets EC criteria: age=%.1fh (>%dh), fullness=%.1f%% (>%.1f%%), size=%.1fMB (>%dMB)", - metric.Age.Hours(), d.volumeAgeHours, metric.FullnessRatio*100, d.fullnessRatio*100, + Reason: fmt.Sprintf("Volume meets EC criteria: quiet for %.1fs (>%ds), fullness=%.1f%% (>%.1f%%), size=%.1fMB (>%dMB)", + metric.Age.Seconds(), d.quietForSeconds, metric.FullnessRatio*100, d.fullnessRatio*100, float64(metric.Size)/(1024*1024), d.minSizeMB), Parameters: map[string]interface{}{ - "age_hours": int(metric.Age.Hours()), + "age_seconds": int(metric.Age.Seconds()), "fullness_ratio": metric.FullnessRatio, "size_mb": int(metric.Size / (1024 * 1024)), }, @@ -117,8 +133,8 @@ func (d *EcDetector) Configure(config map[string]interface{}) error { d.enabled = enabled } - if ageHours, ok := config["volume_age_hours"].(float64); ok { - d.volumeAgeHours = int(ageHours) + if ageSeconds, ok := config["quiet_for_seconds"].(float64); ok { + d.quietForSeconds = int(ageSeconds) } if fullnessRatio, ok := config["fullness_ratio"].(float64); ok { @@ -129,8 +145,12 @@ func (d *EcDetector) Configure(config map[string]interface{}) error { d.minSizeMB = int(minSizeMB) } - glog.V(1).Infof("EC detector configured: enabled=%v, age=%dh, fullness=%.2f, minSize=%dMB", - d.enabled, d.volumeAgeHours, d.fullnessRatio, d.minSizeMB) + if collectionFilter, ok := config["collection_filter"].(string); ok { + d.collectionFilter = collectionFilter + } + + glog.V(1).Infof("EC detector configured: enabled=%v, quietFor=%ds, fullness=%.2f, minSize=%dMB, collection_filter='%s'", + d.enabled, d.quietForSeconds, d.fullnessRatio, d.minSizeMB, d.collectionFilter) return nil } @@ -141,14 +161,26 @@ func (d *EcDetector) SetEnabled(enabled bool) { d.enabled = enabled } +func (d *EcDetector) SetVolumeAgeSeconds(seconds int) { + d.quietForSeconds = seconds +} + func (d *EcDetector) SetVolumeAgeHours(hours int) { - d.volumeAgeHours = hours + d.quietForSeconds = hours * 3600 // Convert hours to seconds +} + +func (d *EcDetector) SetQuietForSeconds(seconds int) { + d.quietForSeconds = seconds } func (d *EcDetector) SetFullnessRatio(ratio float64) { d.fullnessRatio = ratio } +func (d *EcDetector) SetCollectionFilter(filter string) { + d.collectionFilter = filter +} + func (d *EcDetector) SetScanInterval(interval time.Duration) { d.scanInterval = interval } @@ -180,9 +212,19 @@ func (d *EcDetector) ConfigureFromPolicy(policy interface{}) { } } -// GetVolumeAgeHours returns the current volume age threshold in hours +// GetVolumeAgeSeconds returns the current volume age threshold in seconds (legacy method) +func (d *EcDetector) GetVolumeAgeSeconds() int { + return d.quietForSeconds +} + +// GetVolumeAgeHours returns the current volume age threshold in hours (legacy method) func (d *EcDetector) GetVolumeAgeHours() int { - return d.volumeAgeHours + return d.quietForSeconds / 3600 // Convert seconds to hours +} + +// GetQuietForSeconds returns the current quiet duration threshold in seconds +func (d *EcDetector) GetQuietForSeconds() int { + return d.quietForSeconds } // GetFullnessRatio returns the current fullness ratio threshold @@ -190,6 +232,11 @@ func (d *EcDetector) GetFullnessRatio() float64 { return d.fullnessRatio } +// GetCollectionFilter returns the current collection filter +func (d *EcDetector) GetCollectionFilter() string { + return d.collectionFilter +} + // GetScanInterval returns the scan interval func (d *EcDetector) GetScanInterval() time.Duration { return d.scanInterval diff --git a/weed/worker/tasks/erasure_coding/ui.go b/weed/worker/tasks/erasure_coding/ui.go index e17cba89a..c61c65628 100644 --- a/weed/worker/tasks/erasure_coding/ui.go +++ b/weed/worker/tasks/erasure_coding/ui.go @@ -46,14 +46,12 @@ func (ui *UIProvider) GetIcon() string { // ErasureCodingConfig represents the erasure coding configuration type ErasureCodingConfig struct { - Enabled bool `json:"enabled"` - VolumeAgeHoursSeconds int `json:"volume_age_hours_seconds"` - FullnessRatio float64 `json:"fullness_ratio"` - ScanIntervalSeconds int `json:"scan_interval_seconds"` - MaxConcurrent int `json:"max_concurrent"` - ShardCount int `json:"shard_count"` - ParityCount int `json:"parity_count"` - CollectionFilter string `json:"collection_filter"` + Enabled bool `json:"enabled"` + QuietForSeconds int `json:"quiet_for_seconds"` + FullnessRatio float64 `json:"fullness_ratio"` + ScanIntervalSeconds int `json:"scan_interval_seconds"` + MaxConcurrent int `json:"max_concurrent"` + CollectionFilter string `json:"collection_filter"` } // Helper functions for duration conversion @@ -95,19 +93,19 @@ func (ui *UIProvider) RenderConfigForm(currentConfig interface{}) (template.HTML config.Enabled, ) - form.AddNumberField( - "volume_age_hours_seconds", - "Volume Age Threshold", - "Only apply erasure coding to volumes older than this duration", - float64(config.VolumeAgeHoursSeconds), + form.AddIntervalField( + "quiet_for_seconds", + "Quiet For Duration", + "Only apply erasure coding to volumes that have not been modified for this duration", + config.QuietForSeconds, true, ) - form.AddNumberField( + form.AddIntervalField( "scan_interval_seconds", "Scan Interval", "How often to scan for volumes needing erasure coding", - float64(config.ScanIntervalSeconds), + config.ScanIntervalSeconds, true, ) @@ -120,21 +118,21 @@ func (ui *UIProvider) RenderConfigForm(currentConfig interface{}) (template.HTML true, ) - // Erasure Coding Parameters + // Detection Parameters form.AddNumberField( - "shard_count", - "Data Shards", - "Number of data shards for erasure coding (recommended: 10)", - float64(config.ShardCount), + "fullness_ratio", + "Fullness Ratio (0.0-1.0)", + "Only apply erasure coding to volumes with fullness ratio above this threshold (e.g., 0.90 for 90%)", + config.FullnessRatio, true, ) - form.AddNumberField( - "parity_count", - "Parity Shards", - "Number of parity shards for erasure coding (recommended: 4)", - float64(config.ParityCount), - true, + form.AddTextField( + "collection_filter", + "Collection Filter", + "Only apply erasure coding to volumes in these collections (comma-separated, leave empty for all collections)", + config.CollectionFilter, + false, ) // Generate organized form sections using Bootstrap components @@ -168,7 +166,8 @@ func (ui *UIProvider) RenderConfigForm(currentConfig interface{}) (template.HTML
Performance: Erasure coding is CPU and I/O intensive. Consider running during off-peak hours.
-Durability: With ` + fmt.Sprintf("%d+%d", config.ShardCount, config.ParityCount) + ` configuration, can tolerate up to ` + fmt.Sprintf("%d", config.ParityCount) + ` shard failures.
+Durability: With 10+4 configuration, can tolerate up to 4 shard failures.
+Configuration: Use the dropdown to select time units (days, hours, minutes). Fullness ratio should be between 0.0 and 1.0 (e.g., 0.90 for 90%).