diff --git a/weed/admin/maintenance/maintenance_queue.go b/weed/admin/maintenance/maintenance_queue.go index 733ef2be2..669442993 100644 --- a/weed/admin/maintenance/maintenance_queue.go +++ b/weed/admin/maintenance/maintenance_queue.go @@ -272,7 +272,7 @@ func (mq *MaintenanceQueue) GetNextTask(workerID string, capabilities []Maintena // If no task found, return nil if selectedTask == nil { - glog.V(2).Infof("No suitable tasks available for worker %s (checked %d pending tasks)", workerID, len(mq.pendingTasks)) + glog.V(3).Infof("No suitable tasks available for worker %s (checked %d pending tasks)", workerID, len(mq.pendingTasks)) return nil } diff --git a/weed/admin/view/app/task_config_schema.templ b/weed/admin/view/app/task_config_schema.templ index bc2f29661..7be500a52 100644 --- a/weed/admin/view/app/task_config_schema.templ +++ b/weed/admin/view/app/task_config_schema.templ @@ -6,9 +6,9 @@ import ( "fmt" "reflect" "strings" + "github.com/seaweedfs/seaweedfs/weed/admin/config" "github.com/seaweedfs/seaweedfs/weed/admin/maintenance" "github.com/seaweedfs/seaweedfs/weed/worker/tasks" - "github.com/seaweedfs/seaweedfs/weed/admin/config" "github.com/seaweedfs/seaweedfs/weed/admin/view/components" "github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding" ) @@ -207,7 +207,7 @@ templ TaskConfigField(field *config.Field, config interface{}) { class="form-control" id={ field.JSONName + "_value" } name={ field.JSONName + "_value" } - value={ fmt.Sprintf("%.0f", components.ConvertInt32SecondsToDisplayValue(getTaskConfigInt32Field(config, field.JSONName))) } + value={ fmt.Sprintf("%.0f", components.ConvertInt32SecondsToDisplayValue(getTaskConfigInt32FieldWithDefault(config, field))) } step="1" min="1" if field.Required { @@ -223,30 +223,30 @@ templ TaskConfigField(field *config.Field, config interface{}) { required } > - - - + + + if field.Description != "" { @@ -388,6 +388,26 @@ func getTaskConfigInt32Field(config interface{}, fieldName string) int32 { } } +func getTaskConfigInt32FieldWithDefault(config interface{}, field *config.Field) int32 { + value := getTaskConfigInt32Field(config, field.JSONName) + + // If no value is stored (value is 0), use the schema default + if value == 0 && field.DefaultValue != nil { + switch defaultVal := field.DefaultValue.(type) { + case int: + return int32(defaultVal) + case int32: + return defaultVal + case int64: + return int32(defaultVal) + case float64: + return int32(defaultVal) + } + } + + return value +} + func getTaskConfigFloatField(config interface{}, fieldName string) float64 { if value := getTaskFieldValue(config, fieldName); value != nil { switch v := value.(type) { @@ -429,7 +449,7 @@ func getTaskConfigStringField(config interface{}, fieldName string) string { } func getTaskNumberStep(field *config.Field) string { - if field.Type == config.FieldTypeFloat { + if field.Type == "float" { return "0.01" } return "1" diff --git a/weed/admin/view/app/task_config_schema_templ.go b/weed/admin/view/app/task_config_schema_templ.go index 258542e39..be58be80a 100644 --- a/weed/admin/view/app/task_config_schema_templ.go +++ b/weed/admin/view/app/task_config_schema_templ.go @@ -281,9 +281,9 @@ func TaskConfigField(field *config.Field, config interface{}) templ.Component { return templ_7745c5c3_Err } var templ_7745c5c3_Var14 string - templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f", components.ConvertInt32SecondsToDisplayValue(getTaskConfigInt32Field(config, field.JSONName)))) + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f", components.ConvertInt32SecondsToDisplayValue(getTaskConfigInt32FieldWithDefault(config, field)))) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 210, Col: 142} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 210, Col: 144} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { @@ -339,7 +339,7 @@ func TaskConfigField(field *config.Field, config interface{}) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if components.GetInt32DisplayUnit(getTaskConfigInt32Field(config, field.JSONName)) == "minutes" { + if components.GetInt32DisplayUnit(getTaskConfigInt32FieldWithDefault(config, field)) == "minutes" { templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, " selected") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err @@ -349,7 +349,7 @@ func TaskConfigField(field *config.Field, config interface{}) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if components.GetInt32DisplayUnit(getTaskConfigInt32Field(config, field.JSONName)) == "hours" { + if components.GetInt32DisplayUnit(getTaskConfigInt32FieldWithDefault(config, field)) == "hours" { templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, " selected") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err @@ -359,7 +359,7 @@ func TaskConfigField(field *config.Field, config interface{}) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if components.GetInt32DisplayUnit(getTaskConfigInt32Field(config, field.JSONName)) == "days" { + if components.GetInt32DisplayUnit(getTaskConfigInt32FieldWithDefault(config, field)) == "days" { templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, " selected") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err @@ -849,6 +849,26 @@ func getTaskConfigInt32Field(config interface{}, fieldName string) int32 { } } +func getTaskConfigInt32FieldWithDefault(config interface{}, field *config.Field) int32 { + value := getTaskConfigInt32Field(config, field.JSONName) + + // If no value is stored (value is 0), use the schema default + if value == 0 && field.DefaultValue != nil { + switch defaultVal := field.DefaultValue.(type) { + case int: + return int32(defaultVal) + case int32: + return defaultVal + case int64: + return int32(defaultVal) + case float64: + return int32(defaultVal) + } + } + + return value +} + func getTaskConfigFloatField(config interface{}, fieldName string) float64 { if value := getTaskFieldValue(config, fieldName); value != nil { switch v := value.(type) { @@ -890,7 +910,7 @@ func getTaskConfigStringField(config interface{}, fieldName string) string { } func getTaskNumberStep(field *config.Field) string { - if field.Type == config.FieldTypeFloat { + if field.Type == "float" { return "0.01" } return "1" diff --git a/weed/worker/tasks/ec_vacuum/config.go b/weed/worker/tasks/ec_vacuum/config.go index 093d4d592..60533be41 100644 --- a/weed/worker/tasks/ec_vacuum/config.go +++ b/weed/worker/tasks/ec_vacuum/config.go @@ -12,10 +12,10 @@ import ( // Config extends BaseConfig with EC vacuum specific settings type Config struct { base.BaseConfig - DeletionThreshold float64 `json:"deletion_threshold"` // Minimum deletion ratio to trigger vacuum - MinVolumeAgeHours int `json:"min_volume_age_hours"` // Minimum age before considering vacuum - CollectionFilter string `json:"collection_filter"` // Filter by collection - MinSizeMB int `json:"min_size_mb"` // Minimum original volume size + DeletionThreshold float64 `json:"deletion_threshold"` // Minimum deletion ratio to trigger vacuum + MinVolumeAgeSeconds int `json:"min_volume_age_seconds"` // Minimum age before considering vacuum (in seconds) + CollectionFilter string `json:"collection_filter"` // Filter by collection + MinSizeMB int `json:"min_size_mb"` // Minimum original volume size } // NewDefaultConfig creates a new default EC vacuum configuration @@ -26,10 +26,10 @@ func NewDefaultConfig() *Config { ScanIntervalSeconds: 24 * 60 * 60, // 24 hours MaxConcurrent: 1, }, - DeletionThreshold: 0.3, // 30% deletions trigger vacuum - MinVolumeAgeHours: 72, // 3 days minimum age - CollectionFilter: "", // No filter by default - MinSizeMB: 100, // 100MB minimum size + DeletionThreshold: 0.3, // 30% deletions trigger vacuum + MinVolumeAgeSeconds: 72 * 60 * 60, // 3 days minimum age (72 hours in seconds) + CollectionFilter: "", // No filter by default + MinSizeMB: 100, // 100MB minimum size } } @@ -98,12 +98,12 @@ func GetConfigSpec() base.ConfigSpec { CSSClasses: "form-control", }, { - Name: "min_volume_age_hours", - JSONName: "min_volume_age_hours", + Name: "min_volume_age_seconds", + JSONName: "min_volume_age_seconds", Type: config.FieldTypeInterval, - DefaultValue: 72, - MinValue: 24, - MaxValue: 30 * 24, // 30 days + DefaultValue: 72 * 60 * 60, // 72 hours in seconds + MinValue: 24 * 60 * 60, // 24 hours in seconds + MaxValue: 30 * 24 * 60 * 60, // 30 days in seconds Required: true, DisplayName: "Minimum Volume Age", Description: "Minimum age before considering EC volume for vacuum", diff --git a/weed/worker/tasks/ec_vacuum/detection.go b/weed/worker/tasks/ec_vacuum/detection.go index 4f97e3712..7c7938392 100644 --- a/weed/worker/tasks/ec_vacuum/detection.go +++ b/weed/worker/tasks/ec_vacuum/detection.go @@ -63,9 +63,9 @@ func Detection(metrics []*wtypes.VolumeHealthMetrics, info *wtypes.ClusterInfo, Server: ecInfo.PrimaryNode, Collection: ecInfo.Collection, Priority: wtypes.TaskPriorityLow, // EC vacuum is not urgent - Reason: fmt.Sprintf("EC volume needs vacuum: deletion_ratio=%.1f%% (>%.1f%%), age=%.1fh (>%dh), size=%.1fMB (>%dMB)", + Reason: fmt.Sprintf("EC volume needs vacuum: deletion_ratio=%.1f%% (>%.1f%%), age=%.1fh (>%.1fh), size=%.1fMB (>%dMB)", deletionRatio*100, ecVacuumConfig.DeletionThreshold*100, - ecInfo.Age.Hours(), ecVacuumConfig.MinVolumeAgeHours, + ecInfo.Age.Hours(), (time.Duration(ecVacuumConfig.MinVolumeAgeSeconds) * time.Second).Hours(), float64(ecInfo.Size)/(1024*1024), ecVacuumConfig.MinSizeMB), ScheduleAt: now, } @@ -98,12 +98,18 @@ func Detection(metrics []*wtypes.VolumeHealthMetrics, info *wtypes.ClusterInfo, deletionRatio := calculateDeletionRatio(ecInfo) sizeMB := float64(ecInfo.Size) / (1024 * 1024) deletedMB := deletionRatio * sizeMB - ageRequired := time.Duration(ecVacuumConfig.MinVolumeAgeHours) * time.Hour + ageRequired := time.Duration(ecVacuumConfig.MinVolumeAgeSeconds) * time.Second - glog.Infof("EC VACUUM: Volume %d: deleted=%.1fMB, ratio=%.1f%% (need ≥%.1f%%), age=%s (need ≥%s), size=%.1fMB (need ≥%dMB)", + // Check shard availability + totalShards := 0 + for _, shardBits := range ecInfo.ShardNodes { + totalShards += shardBits.ShardIdCount() + } + + glog.Infof("EC VACUUM: Volume %d: deleted=%.1fMB, ratio=%.1f%% (need ≥%.1f%%), age=%s (need ≥%s), size=%.1fMB (need ≥%dMB), shards=%d (need ≥%d)", volumeID, deletedMB, deletionRatio*100, ecVacuumConfig.DeletionThreshold*100, ecInfo.Age.Truncate(time.Minute), ageRequired.Truncate(time.Minute), - sizeMB, ecVacuumConfig.MinSizeMB) + sizeMB, ecVacuumConfig.MinSizeMB, totalShards, erasure_coding.DataShardsCount) count++ } } @@ -173,9 +179,10 @@ func collectEcVolumeInfo(metrics []*wtypes.VolumeHealthMetrics) map[uint32]*EcVo // shouldVacuumEcVolume determines if an EC volume should be considered for vacuum func shouldVacuumEcVolume(ecInfo *EcVolumeInfo, config *Config, now time.Time) bool { // Check minimum age - if ecInfo.Age < time.Duration(config.MinVolumeAgeHours)*time.Hour { - glog.V(3).Infof("EC volume %d too young: age=%.1fh < %dh", - ecInfo.VolumeID, ecInfo.Age.Hours(), config.MinVolumeAgeHours) + minAge := time.Duration(config.MinVolumeAgeSeconds) * time.Second + if ecInfo.Age < minAge { + glog.V(3).Infof("EC volume %d too young: age=%.1fh < %.1fh", + ecInfo.VolumeID, ecInfo.Age.Hours(), minAge.Hours()) return false }