package erasure_coding
import (
"fmt"
"html/template"
"strconv"
"time"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/worker/types"
)
// UIProvider provides the UI for erasure coding task configuration
type UIProvider struct {
detector *EcDetector
scheduler *Scheduler
}
// NewUIProvider creates a new erasure coding UI provider
func NewUIProvider(detector *EcDetector, scheduler *Scheduler) *UIProvider {
return &UIProvider{
detector: detector,
scheduler: scheduler,
}
}
// GetTaskType returns the task type
func (ui *UIProvider) GetTaskType() types.TaskType {
return types.TaskTypeErasureCoding
}
// GetDisplayName returns the human-readable name
func (ui *UIProvider) GetDisplayName() string {
return "Erasure Coding"
}
// GetDescription returns a description of what this task does
func (ui *UIProvider) GetDescription() string {
return "Converts volumes to erasure coded format for improved data durability and fault tolerance"
}
// GetIcon returns the icon CSS class for this task type
func (ui *UIProvider) GetIcon() string {
return "fas fa-shield-alt text-info"
}
// 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"`
}
// 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.getCurrentECConfig()
// Build form using the FormBuilder helper
form := types.NewFormBuilder()
// Detection Settings
form.AddCheckboxField(
"enabled",
"Enable Erasure Coding Tasks",
"Whether erasure coding tasks should be automatically created",
config.Enabled,
)
form.AddNumberField(
"volume_age_hours_seconds",
"Volume Age Threshold",
"Only apply erasure coding to volumes older than this duration",
float64(config.VolumeAgeHoursSeconds),
true,
)
form.AddNumberField(
"scan_interval_seconds",
"Scan Interval",
"How often to scan for volumes needing erasure coding",
float64(config.ScanIntervalSeconds),
true,
)
// Scheduling Settings
form.AddNumberField(
"max_concurrent",
"Max Concurrent Tasks",
"Maximum number of erasure coding tasks that can run simultaneously",
float64(config.MaxConcurrent),
true,
)
// Erasure Coding Parameters
form.AddNumberField(
"shard_count",
"Data Shards",
"Number of data shards for erasure coding (recommended: 10)",
float64(config.ShardCount),
true,
)
form.AddNumberField(
"parity_count",
"Parity Shards",
"Number of parity shards for erasure coding (recommended: 4)",
float64(config.ParityCount),
true,
)
// Generate organized form sections using Bootstrap components
html := `
` + string(form.Build()) + `
Important Notes:
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.
`
return template.HTML(html), nil
}
// ParseConfigForm parses form data into configuration
func (ui *UIProvider) ParseConfigForm(formData map[string][]string) (interface{}, error) {
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])
if err != nil {
return nil, fmt.Errorf("invalid volume age hours: %v", err)
}
config.VolumeAgeHoursSeconds = hours
}
// Parse scan interval
if values, ok := formData["scan_interval_seconds"]; ok && len(values) > 0 {
interval, err := strconv.Atoi(values[0])
if err != nil {
return nil, fmt.Errorf("invalid scan interval: %v", err)
}
config.ScanIntervalSeconds = interval
}
// 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 shard count
if values, ok := formData["shard_count"]; ok && len(values) > 0 {
shardCount, err := strconv.Atoi(values[0])
if err != nil {
return nil, fmt.Errorf("invalid shard count: %v", err)
}
if shardCount < 1 {
return nil, fmt.Errorf("shard count must be at least 1")
}
config.ShardCount = shardCount
}
// 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: %v", err)
}
if parityCount < 1 {
return nil, fmt.Errorf("parity count must be at least 1")
}
config.ParityCount = parityCount
}
return config, nil
}
// GetCurrentConfig returns the current configuration
func (ui *UIProvider) GetCurrentConfig() interface{} {
return ui.getCurrentECConfig()
}
// ApplyConfig applies the new configuration
func (ui *UIProvider) ApplyConfig(config interface{}) error {
ecConfig, ok := config.(ErasureCodingConfig)
if !ok {
return fmt.Errorf("invalid config type, expected ErasureCodingConfig")
}
// Apply to detector
if ui.detector != nil {
ui.detector.SetEnabled(ecConfig.Enabled)
ui.detector.SetVolumeAgeHours(ecConfig.VolumeAgeHoursSeconds)
ui.detector.SetScanInterval(secondsToDuration(ecConfig.ScanIntervalSeconds))
}
// Apply to scheduler
if ui.scheduler != nil {
ui.scheduler.SetEnabled(ecConfig.Enabled)
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)
return nil
}
// getCurrentECConfig gets the current configuration from detector and scheduler
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,
}
// Get current values from detector
if ui.detector != nil {
config.Enabled = ui.detector.IsEnabled()
config.VolumeAgeHoursSeconds = ui.detector.GetVolumeAgeHours()
config.ScanIntervalSeconds = durationToSeconds(ui.detector.ScanInterval())
}
// Get current values from scheduler
if ui.scheduler != nil {
config.MaxConcurrent = ui.scheduler.GetMaxConcurrent()
}
return config
}
// RegisterUI registers the erasure coding UI provider with the UI registry
func RegisterUI(uiRegistry *types.UIRegistry, detector *EcDetector, scheduler *Scheduler) {
uiProvider := NewUIProvider(detector, scheduler)
uiRegistry.RegisterUI(uiProvider)
glog.V(1).Infof("✅ Registered erasure coding task UI provider")
}