mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-05-23 18:21:28 +00:00
interval field UI component
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
config.ScanIntervalSeconds = int(duration.Seconds())
|
||||
|
||||
unit := "minute" // default
|
||||
if units, ok := formData["scan_interval_unit"]; ok && len(units) > 0 {
|
||||
unit = units[0]
|
||||
}
|
||||
|
||||
// Convert to seconds
|
||||
config.ScanIntervalSeconds = types.IntervalValueUnitToSeconds(value, unit)
|
||||
}
|
||||
|
||||
// Parse max concurrent
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
<div class="alert alert-info" role="alert">
|
||||
<h6 class="alert-heading">Important Notes:</h6>
|
||||
<p class="mb-2"><strong>Performance:</strong> Erasure coding is CPU and I/O intensive. Consider running during off-peak hours.</p>
|
||||
<p class="mb-0"><strong>Durability:</strong> With ` + fmt.Sprintf("%d+%d", config.ShardCount, config.ParityCount) + ` configuration, can tolerate up to ` + fmt.Sprintf("%d", config.ParityCount) + ` shard failures.</p>
|
||||
<p class="mb-2"><strong>Durability:</strong> With 10+4 configuration, can tolerate up to 4 shard failures.</p>
|
||||
<p class="mb-0"><strong>Configuration:</strong> 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%).</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -180,27 +179,41 @@ func (ui *UIProvider) RenderConfigForm(currentConfig interface{}) (template.HTML
|
||||
|
||||
// ParseConfigForm parses form data into configuration
|
||||
func (ui *UIProvider) ParseConfigForm(formData map[string][]string) (interface{}, error) {
|
||||
config := &ErasureCodingConfig{}
|
||||
config := ErasureCodingConfig{}
|
||||
|
||||
// Parse enabled
|
||||
config.Enabled = len(formData["enabled"]) > 0
|
||||
|
||||
// Parse volume age hours
|
||||
if values, ok := formData["volume_age_hours_seconds"]; ok && len(values) > 0 {
|
||||
hours, err := strconv.Atoi(values[0])
|
||||
// Parse quiet for duration
|
||||
if values, ok := formData["quiet_for_seconds_value"]; ok && len(values) > 0 {
|
||||
value, err := strconv.Atoi(values[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid volume age hours: %w", err)
|
||||
return nil, fmt.Errorf("invalid quiet for duration value: %w", err)
|
||||
}
|
||||
config.VolumeAgeHoursSeconds = hours
|
||||
|
||||
unit := "minute" // default
|
||||
if units, ok := formData["quiet_for_seconds_unit"]; ok && len(units) > 0 {
|
||||
unit = units[0]
|
||||
}
|
||||
|
||||
// Convert to seconds using the helper function from types package
|
||||
config.QuietForSeconds = types.IntervalValueUnitToSeconds(value, unit)
|
||||
}
|
||||
|
||||
// Parse scan interval
|
||||
if values, ok := formData["scan_interval_seconds"]; ok && len(values) > 0 {
|
||||
interval, err := strconv.Atoi(values[0])
|
||||
if values, ok := formData["scan_interval_seconds_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)
|
||||
}
|
||||
config.ScanIntervalSeconds = interval
|
||||
|
||||
unit := "minute" // default
|
||||
if units, ok := formData["scan_interval_seconds_unit"]; ok && len(units) > 0 {
|
||||
unit = units[0]
|
||||
}
|
||||
|
||||
// Convert to seconds
|
||||
config.ScanIntervalSeconds = types.IntervalValueUnitToSeconds(value, unit)
|
||||
}
|
||||
|
||||
// Parse max concurrent
|
||||
@@ -215,28 +228,21 @@ func (ui *UIProvider) ParseConfigForm(formData map[string][]string) (interface{}
|
||||
config.MaxConcurrent = maxConcurrent
|
||||
}
|
||||
|
||||
// Parse shard count
|
||||
if values, ok := formData["shard_count"]; ok && len(values) > 0 {
|
||||
shardCount, err := strconv.Atoi(values[0])
|
||||
// Parse fullness ratio
|
||||
if values, ok := formData["fullness_ratio"]; ok && len(values) > 0 {
|
||||
fullnessRatio, err := strconv.ParseFloat(values[0], 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid shard count: %w", err)
|
||||
return nil, fmt.Errorf("invalid fullness ratio: %w", err)
|
||||
}
|
||||
if shardCount < 1 {
|
||||
return nil, fmt.Errorf("shard count must be at least 1")
|
||||
if fullnessRatio < 0 || fullnessRatio > 1 {
|
||||
return nil, fmt.Errorf("fullness ratio must be between 0.0 and 1.0")
|
||||
}
|
||||
config.ShardCount = shardCount
|
||||
config.FullnessRatio = fullnessRatio
|
||||
}
|
||||
|
||||
// Parse parity count
|
||||
if values, ok := formData["parity_count"]; ok && len(values) > 0 {
|
||||
parityCount, err := strconv.Atoi(values[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid parity count: %w", err)
|
||||
}
|
||||
if parityCount < 1 {
|
||||
return nil, fmt.Errorf("parity count must be at least 1")
|
||||
}
|
||||
config.ParityCount = parityCount
|
||||
// Parse collection filter
|
||||
if values, ok := formData["collection_filter"]; ok && len(values) > 0 {
|
||||
config.CollectionFilter = values[0]
|
||||
}
|
||||
|
||||
return config, nil
|
||||
@@ -257,7 +263,9 @@ func (ui *UIProvider) ApplyConfig(config interface{}) error {
|
||||
// Apply to detector
|
||||
if ui.detector != nil {
|
||||
ui.detector.SetEnabled(ecConfig.Enabled)
|
||||
ui.detector.SetVolumeAgeHours(ecConfig.VolumeAgeHoursSeconds)
|
||||
ui.detector.SetQuietForSeconds(ecConfig.QuietForSeconds)
|
||||
ui.detector.SetFullnessRatio(ecConfig.FullnessRatio)
|
||||
ui.detector.SetCollectionFilter(ecConfig.CollectionFilter)
|
||||
ui.detector.SetScanInterval(secondsToDuration(ecConfig.ScanIntervalSeconds))
|
||||
}
|
||||
|
||||
@@ -267,8 +275,8 @@ func (ui *UIProvider) ApplyConfig(config interface{}) error {
|
||||
ui.scheduler.SetMaxConcurrent(ecConfig.MaxConcurrent)
|
||||
}
|
||||
|
||||
glog.V(1).Infof("Applied erasure coding configuration: enabled=%v, age_threshold=%v, max_concurrent=%d, shards=%d+%d",
|
||||
ecConfig.Enabled, ecConfig.VolumeAgeHoursSeconds, ecConfig.MaxConcurrent, ecConfig.ShardCount, ecConfig.ParityCount)
|
||||
glog.V(1).Infof("Applied erasure coding configuration: enabled=%v, quiet_for=%v seconds, max_concurrent=%d, fullness_ratio=%f, collection_filter=%s, shards=10+4",
|
||||
ecConfig.Enabled, ecConfig.QuietForSeconds, ecConfig.MaxConcurrent, ecConfig.FullnessRatio, ecConfig.CollectionFilter)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -277,18 +285,20 @@ func (ui *UIProvider) ApplyConfig(config interface{}) error {
|
||||
func (ui *UIProvider) getCurrentECConfig() ErasureCodingConfig {
|
||||
config := ErasureCodingConfig{
|
||||
// Default values (fallback if detectors/schedulers are nil)
|
||||
Enabled: true,
|
||||
VolumeAgeHoursSeconds: 24 * 3600, // 24 hours in seconds
|
||||
ScanIntervalSeconds: 2 * 3600, // 2 hours in seconds
|
||||
MaxConcurrent: 1,
|
||||
ShardCount: 10,
|
||||
ParityCount: 4,
|
||||
Enabled: true,
|
||||
QuietForSeconds: 24 * 3600, // Default to 24 hours in seconds
|
||||
ScanIntervalSeconds: 2 * 3600, // 2 hours in seconds
|
||||
MaxConcurrent: 1,
|
||||
FullnessRatio: 0.90, // Default fullness ratio
|
||||
CollectionFilter: "",
|
||||
}
|
||||
|
||||
// Get current values from detector
|
||||
if ui.detector != nil {
|
||||
config.Enabled = ui.detector.IsEnabled()
|
||||
config.VolumeAgeHoursSeconds = ui.detector.GetVolumeAgeHours()
|
||||
config.QuietForSeconds = ui.detector.GetQuietForSeconds()
|
||||
config.FullnessRatio = ui.detector.GetFullnessRatio()
|
||||
config.CollectionFilter = ui.detector.GetCollectionFilter()
|
||||
config.ScanIntervalSeconds = durationToSeconds(ui.detector.ScanInterval())
|
||||
}
|
||||
|
||||
|
||||
@@ -101,19 +101,19 @@ func (ui *UIProvider) RenderConfigForm(currentConfig interface{}) (template.HTML
|
||||
true,
|
||||
)
|
||||
|
||||
form.AddDurationField(
|
||||
form.AddIntervalField(
|
||||
"scan_interval",
|
||||
"Scan Interval",
|
||||
"How often to scan for volumes needing vacuum",
|
||||
secondsToDuration(config.ScanIntervalSeconds),
|
||||
config.ScanIntervalSeconds,
|
||||
true,
|
||||
)
|
||||
|
||||
form.AddDurationField(
|
||||
form.AddIntervalField(
|
||||
"min_volume_age",
|
||||
"Minimum Volume Age",
|
||||
"Only vacuum volumes older than this duration",
|
||||
secondsToDuration(config.MinVolumeAgeSeconds),
|
||||
config.MinVolumeAgeSeconds,
|
||||
true,
|
||||
)
|
||||
|
||||
@@ -189,21 +189,35 @@ func (ui *UIProvider) ParseConfigForm(formData map[string][]string) (interface{}
|
||||
}
|
||||
|
||||
// Parse scan interval
|
||||
if intervalStr := formData["scan_interval"]; len(intervalStr) > 0 {
|
||||
if interval, err := time.ParseDuration(intervalStr[0]); err != nil {
|
||||
return nil, fmt.Errorf("invalid scan interval: %w", err)
|
||||
} else {
|
||||
config.ScanIntervalSeconds = durationToSeconds(interval)
|
||||
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 value: %w", err)
|
||||
}
|
||||
|
||||
unit := "minute" // default
|
||||
if units, ok := formData["scan_interval_unit"]; ok && len(units) > 0 {
|
||||
unit = units[0]
|
||||
}
|
||||
|
||||
// Convert to seconds
|
||||
config.ScanIntervalSeconds = types.IntervalValueUnitToSeconds(value, unit)
|
||||
}
|
||||
|
||||
// Parse min volume age
|
||||
if ageStr := formData["min_volume_age"]; len(ageStr) > 0 {
|
||||
if age, err := time.ParseDuration(ageStr[0]); err != nil {
|
||||
return nil, fmt.Errorf("invalid min volume age: %w", err)
|
||||
} else {
|
||||
config.MinVolumeAgeSeconds = durationToSeconds(age)
|
||||
if values, ok := formData["min_volume_age_value"]; ok && len(values) > 0 {
|
||||
value, err := strconv.Atoi(values[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid min volume age value: %w", err)
|
||||
}
|
||||
|
||||
unit := "minute" // default
|
||||
if units, ok := formData["min_volume_age_unit"]; ok && len(units) > 0 {
|
||||
unit = units[0]
|
||||
}
|
||||
|
||||
// Convert to seconds
|
||||
config.MinVolumeAgeSeconds = types.IntervalValueUnitToSeconds(value, unit)
|
||||
}
|
||||
|
||||
// Parse max concurrent
|
||||
|
||||
@@ -6,6 +6,40 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Helper function to convert seconds to the most appropriate interval unit
|
||||
func secondsToIntervalValueUnit(totalSeconds int) (int, string) {
|
||||
if totalSeconds == 0 {
|
||||
return 0, "minute"
|
||||
}
|
||||
|
||||
// Check if it's evenly divisible by days
|
||||
if totalSeconds%(24*3600) == 0 {
|
||||
return totalSeconds / (24 * 3600), "day"
|
||||
}
|
||||
|
||||
// Check if it's evenly divisible by hours
|
||||
if totalSeconds%3600 == 0 {
|
||||
return totalSeconds / 3600, "hour"
|
||||
}
|
||||
|
||||
// Default to minutes
|
||||
return totalSeconds / 60, "minute"
|
||||
}
|
||||
|
||||
// Helper function to convert interval value and unit to seconds
|
||||
func IntervalValueUnitToSeconds(value int, unit string) int {
|
||||
switch unit {
|
||||
case "day":
|
||||
return value * 24 * 3600
|
||||
case "hour":
|
||||
return value * 3600
|
||||
case "minute":
|
||||
return value * 60
|
||||
default:
|
||||
return value * 60 // Default to minutes
|
||||
}
|
||||
}
|
||||
|
||||
// TaskUIProvider defines how tasks provide their configuration UI
|
||||
type TaskUIProvider interface {
|
||||
// GetTaskType returns the task type
|
||||
@@ -100,11 +134,14 @@ type TaskDetailsData struct {
|
||||
type FormField struct {
|
||||
Name string `json:"name"`
|
||||
Label string `json:"label"`
|
||||
Type string `json:"type"` // text, number, checkbox, select, duration
|
||||
Type string `json:"type"` // text, number, checkbox, select, duration, interval
|
||||
Value interface{} `json:"value"`
|
||||
Description string `json:"description"`
|
||||
Required bool `json:"required"`
|
||||
Options []FormOption `json:"options,omitempty"` // For select fields
|
||||
// Interval-specific fields
|
||||
IntervalValue int `json:"interval_value,omitempty"` // Numeric value for interval
|
||||
IntervalUnit string `json:"interval_unit,omitempty"` // Unit for interval (day, hour, minute)
|
||||
}
|
||||
|
||||
type FormOption struct {
|
||||
@@ -190,6 +227,23 @@ func (fb *FormBuilder) AddDurationField(name, label, description string, value t
|
||||
return fb
|
||||
}
|
||||
|
||||
// AddIntervalField adds an interval field with value and unit dropdown
|
||||
func (fb *FormBuilder) AddIntervalField(name, label, description string, totalSeconds int, required bool) *FormBuilder {
|
||||
// Convert seconds to the most appropriate unit
|
||||
value, unit := secondsToIntervalValueUnit(totalSeconds)
|
||||
|
||||
fb.fields = append(fb.fields, FormField{
|
||||
Name: name,
|
||||
Label: label,
|
||||
Type: "interval",
|
||||
Description: description,
|
||||
Required: required,
|
||||
IntervalValue: value,
|
||||
IntervalUnit: unit,
|
||||
})
|
||||
return fb
|
||||
}
|
||||
|
||||
// Build generates the HTML form fields with Bootstrap styling
|
||||
func (fb *FormBuilder) Build() template.HTML {
|
||||
html := ""
|
||||
@@ -269,6 +323,37 @@ func (fb *FormBuilder) renderField(field FormField) string {
|
||||
html += " required"
|
||||
}
|
||||
html += ">\n"
|
||||
|
||||
case "interval":
|
||||
// Create input group with number input and unit select
|
||||
html += " <div class=\"input-group\">\n"
|
||||
html += " <input type=\"number\" class=\"form-control\" id=\"" + field.Name + "_value\" name=\"" + field.Name + "_value\" min=\"0\" value=\"" +
|
||||
fmt.Sprintf("%d", field.IntervalValue) + "\""
|
||||
if field.Required {
|
||||
html += " required"
|
||||
}
|
||||
html += ">\n"
|
||||
html += " <select class=\"form-select\" id=\"" + field.Name + "_unit\" name=\"" + field.Name + "_unit\" style=\"max-width: 120px;\""
|
||||
if field.Required {
|
||||
html += " required"
|
||||
}
|
||||
html += ">\n"
|
||||
|
||||
// Add unit options
|
||||
units := []struct{ Value, Label string }{
|
||||
{"minute", "Minutes"},
|
||||
{"hour", "Hours"},
|
||||
{"day", "Days"},
|
||||
}
|
||||
for _, unit := range units {
|
||||
selected := ""
|
||||
if unit.Value == field.IntervalUnit {
|
||||
selected = " selected"
|
||||
}
|
||||
html += " <option value=\"" + unit.Value + "\"" + selected + ">" + unit.Label + "</option>\n"
|
||||
}
|
||||
html += " </select>\n"
|
||||
html += " </div>\n"
|
||||
}
|
||||
|
||||
// Description for non-checkbox fields
|
||||
|
||||
Reference in New Issue
Block a user