* Fix nil pointer panic in maintenance worker when receiving empty task assignment When a worker requests a task and none are available, the admin server sends an empty TaskAssignment message. The worker was attempting to log the task details without checking if the TaskId was empty, causing a nil pointer dereference when accessing taskAssign.Params.VolumeId. This fix adds a check for empty TaskId before processing the assignment, preventing worker crashes and improving stability in production environments. * Add EC integration test for admin-worker maintenance system Adds comprehensive integration test that verifies the end-to-end flow of erasure coding maintenance tasks: - Admin server detects volumes needing EC encoding - Workers register and receive task assignments - EC encoding is executed and verified in master topology - File read-back validation confirms data integrity The test uses unique absolute working directories for each worker to prevent ID conflicts and ensure stable worker registration. Includes proper cleanup and process management for reliable test execution. * Improve maintenance system stability and task deduplication - Add cross-type task deduplication to prevent concurrent maintenance operations on the same volume (EC, balance, vacuum) - Implement HasAnyTask check in ActiveTopology for better coordination - Increase RequestTask timeout from 5s to 30s to prevent unnecessary worker reconnections - Add TaskTypeNone sentinel for generic task checks - Update all task detectors to use HasAnyTask for conflict prevention - Improve config persistence and schema handling * Add GitHub Actions workflow for EC integration tests Adds CI workflow that runs EC integration tests on push and pull requests to master branch. The workflow: - Triggers on changes to admin, worker, or test files - Builds the weed binary - Runs the EC integration test suite - Uploads test logs as artifacts on failure for debugging This ensures the maintenance system remains stable and worker-admin integration is validated in CI. * go version 1.24 * address comments * Update maintenance_integration.go * support seconds * ec prioritize over balancing in tests
452 lines
10 KiB
Plaintext
452 lines
10 KiB
Plaintext
package components
|
|
|
|
import "fmt"
|
|
|
|
// FormFieldData represents common form field data
|
|
type FormFieldData struct {
|
|
Name string
|
|
Label string
|
|
Description string
|
|
Required bool
|
|
}
|
|
|
|
// TextFieldData represents text input field data
|
|
type TextFieldData struct {
|
|
FormFieldData
|
|
Value string
|
|
Placeholder string
|
|
}
|
|
|
|
// NumberFieldData represents number input field data
|
|
type NumberFieldData struct {
|
|
FormFieldData
|
|
Value float64
|
|
Step string
|
|
Min *float64
|
|
Max *float64
|
|
}
|
|
|
|
// CheckboxFieldData represents checkbox field data
|
|
type CheckboxFieldData struct {
|
|
FormFieldData
|
|
Checked bool
|
|
}
|
|
|
|
// SelectFieldData represents select field data
|
|
type SelectFieldData struct {
|
|
FormFieldData
|
|
Value string
|
|
Options []SelectOption
|
|
}
|
|
|
|
type SelectOption struct {
|
|
Value string
|
|
Label string
|
|
}
|
|
|
|
// DurationFieldData represents duration input field data
|
|
type DurationFieldData struct {
|
|
FormFieldData
|
|
Value string
|
|
Placeholder string
|
|
}
|
|
|
|
// DurationInputFieldData represents duration input with number + unit dropdown
|
|
type DurationInputFieldData struct {
|
|
FormFieldData
|
|
Seconds int // The duration value in seconds
|
|
}
|
|
|
|
// TextField renders a Bootstrap text input field
|
|
templ TextField(data TextFieldData) {
|
|
<div class="mb-3">
|
|
<label for={ data.Name } class="form-label">
|
|
{ data.Label }
|
|
if data.Required {
|
|
<span class="text-danger">*</span>
|
|
}
|
|
</label>
|
|
<input
|
|
type="text"
|
|
class="form-control"
|
|
id={ data.Name }
|
|
name={ data.Name }
|
|
value={ data.Value }
|
|
if data.Placeholder != "" {
|
|
placeholder={ data.Placeholder }
|
|
}
|
|
if data.Required {
|
|
required
|
|
}
|
|
/>
|
|
if data.Description != "" {
|
|
<div class="form-text text-muted">{ data.Description }</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
// NumberField renders a Bootstrap number input field
|
|
templ NumberField(data NumberFieldData) {
|
|
<div class="mb-3">
|
|
<label for={ data.Name } class="form-label">
|
|
{ data.Label }
|
|
if data.Required {
|
|
<span class="text-danger">*</span>
|
|
}
|
|
</label>
|
|
<input
|
|
type="number"
|
|
class="form-control"
|
|
id={ data.Name }
|
|
name={ data.Name }
|
|
value={ fmt.Sprintf("%.6g", data.Value) }
|
|
if data.Step != "" {
|
|
step={ data.Step }
|
|
} else {
|
|
step="any"
|
|
}
|
|
if data.Min != nil {
|
|
min={ fmt.Sprintf("%.6g", *data.Min) }
|
|
}
|
|
if data.Max != nil {
|
|
max={ fmt.Sprintf("%.6g", *data.Max) }
|
|
}
|
|
if data.Required {
|
|
required
|
|
}
|
|
/>
|
|
if data.Description != "" {
|
|
<div class="form-text text-muted">{ data.Description }</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
// CheckboxField renders a Bootstrap checkbox field
|
|
templ CheckboxField(data CheckboxFieldData) {
|
|
<div class="mb-3">
|
|
<div class="form-check">
|
|
<input
|
|
type="checkbox"
|
|
class="form-check-input"
|
|
id={ data.Name }
|
|
name={ data.Name }
|
|
if data.Checked {
|
|
checked
|
|
}
|
|
/>
|
|
<label class="form-check-label" for={ data.Name }>
|
|
{ data.Label }
|
|
</label>
|
|
</div>
|
|
if data.Description != "" {
|
|
<div class="form-text text-muted">{ data.Description }</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
// SelectField renders a Bootstrap select field
|
|
templ SelectField(data SelectFieldData) {
|
|
<div class="mb-3">
|
|
<label for={ data.Name } class="form-label">
|
|
{ data.Label }
|
|
if data.Required {
|
|
<span class="text-danger">*</span>
|
|
}
|
|
</label>
|
|
<select
|
|
class="form-select"
|
|
id={ data.Name }
|
|
name={ data.Name }
|
|
if data.Required {
|
|
required
|
|
}
|
|
>
|
|
for _, option := range data.Options {
|
|
<option
|
|
value={ option.Value }
|
|
if option.Value == data.Value {
|
|
selected
|
|
}
|
|
>
|
|
{ option.Label }
|
|
</option>
|
|
}
|
|
</select>
|
|
if data.Description != "" {
|
|
<div class="form-text text-muted">{ data.Description }</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
// DurationField renders a Bootstrap duration input field
|
|
templ DurationField(data DurationFieldData) {
|
|
<div class="mb-3">
|
|
<label for={ data.Name } class="form-label">
|
|
{ data.Label }
|
|
if data.Required {
|
|
<span class="text-danger">*</span>
|
|
}
|
|
</label>
|
|
<input
|
|
type="text"
|
|
class="form-control"
|
|
id={ data.Name }
|
|
name={ data.Name }
|
|
value={ data.Value }
|
|
if data.Placeholder != "" {
|
|
placeholder={ data.Placeholder }
|
|
} else {
|
|
placeholder="e.g., 30m, 2h, 24h"
|
|
}
|
|
if data.Required {
|
|
required
|
|
}
|
|
/>
|
|
if data.Description != "" {
|
|
<div class="form-text text-muted">{ data.Description }</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
// DurationInputField renders a Bootstrap duration input with number + unit dropdown
|
|
templ DurationInputField(data DurationInputFieldData) {
|
|
<div class="mb-3">
|
|
<label for={ data.Name } class="form-label">
|
|
{ data.Label }
|
|
if data.Required {
|
|
<span class="text-danger">*</span>
|
|
}
|
|
</label>
|
|
<div class="input-group">
|
|
<input
|
|
type="number"
|
|
class="form-control"
|
|
id={ data.Name }
|
|
name={ data.Name }
|
|
value={ fmt.Sprintf("%.0f", convertSecondsToValue(data.Seconds, convertSecondsToUnit(data.Seconds))) }
|
|
step="1"
|
|
min="1"
|
|
if data.Required {
|
|
required
|
|
}
|
|
/>
|
|
<select
|
|
class="form-select"
|
|
id={ data.Name + "_unit" }
|
|
name={ data.Name + "_unit" }
|
|
style="max-width: 120px;"
|
|
>
|
|
<option
|
|
value="seconds"
|
|
if convertSecondsToUnit(data.Seconds) == "seconds" {
|
|
selected
|
|
}
|
|
>
|
|
Seconds
|
|
</option>
|
|
<option
|
|
value="minutes"
|
|
if convertSecondsToUnit(data.Seconds) == "minutes" {
|
|
selected
|
|
}
|
|
>
|
|
Minutes
|
|
</option>
|
|
<option
|
|
value="hours"
|
|
if convertSecondsToUnit(data.Seconds) == "hours" {
|
|
selected
|
|
}
|
|
>
|
|
Hours
|
|
</option>
|
|
<option
|
|
value="days"
|
|
if convertSecondsToUnit(data.Seconds) == "days" {
|
|
selected
|
|
}
|
|
>
|
|
Days
|
|
</option>
|
|
</select>
|
|
</div>
|
|
if data.Description != "" {
|
|
<div class="form-text text-muted">{ data.Description }</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
// Helper functions for duration conversion (used by DurationInputField)
|
|
|
|
// Typed conversion functions for protobuf int32 (most common) - EXPORTED
|
|
func ConvertInt32SecondsToDisplayValue(seconds int32) float64 {
|
|
return convertIntSecondsToDisplayValue(int(seconds))
|
|
}
|
|
|
|
func GetInt32DisplayUnit(seconds int32) string {
|
|
return getIntDisplayUnit(int(seconds))
|
|
}
|
|
|
|
// Typed conversion functions for regular int
|
|
func convertIntSecondsToDisplayValue(seconds int) float64 {
|
|
if seconds == 0 {
|
|
return 0
|
|
}
|
|
|
|
// Check if it's evenly divisible by days
|
|
if seconds%(24*3600) == 0 {
|
|
return float64(seconds / (24 * 3600))
|
|
}
|
|
|
|
// Check if it's evenly divisible by hours
|
|
if seconds%3600 == 0 {
|
|
return float64(seconds / 3600)
|
|
}
|
|
|
|
// Default to minutes
|
|
return float64(seconds / 60)
|
|
}
|
|
|
|
func getIntDisplayUnit(seconds int) string {
|
|
if seconds == 0 {
|
|
return "minutes"
|
|
}
|
|
|
|
// Preserve seconds when not divisible by minutes
|
|
if seconds < 60 || seconds%60 != 0 {
|
|
return "seconds"
|
|
}
|
|
|
|
// Check if it's evenly divisible by days
|
|
if seconds%(24*3600) == 0 {
|
|
return "days"
|
|
}
|
|
|
|
// Check if it's evenly divisible by hours
|
|
if seconds%3600 == 0 {
|
|
return "hours"
|
|
}
|
|
|
|
// Default to minutes
|
|
return "minutes"
|
|
}
|
|
|
|
func convertSecondsToUnit(seconds int) string {
|
|
if seconds == 0 {
|
|
return "minutes"
|
|
}
|
|
|
|
// Preserve seconds when not divisible by minutes
|
|
if seconds < 60 || seconds%60 != 0 {
|
|
return "seconds"
|
|
}
|
|
|
|
// Try days first
|
|
if seconds%(24*3600) == 0 && seconds >= 24*3600 {
|
|
return "days"
|
|
}
|
|
|
|
// Try hours
|
|
if seconds%3600 == 0 && seconds >= 3600 {
|
|
return "hours"
|
|
}
|
|
|
|
// Default to minutes
|
|
return "minutes"
|
|
}
|
|
|
|
func convertSecondsToValue(seconds int, unit string) float64 {
|
|
if seconds == 0 {
|
|
return 0
|
|
}
|
|
|
|
switch unit {
|
|
case "days":
|
|
return float64(seconds / (24 * 3600))
|
|
case "hours":
|
|
return float64(seconds / 3600)
|
|
case "minutes":
|
|
return float64(seconds / 60)
|
|
case "seconds":
|
|
return float64(seconds)
|
|
default:
|
|
return float64(seconds / 60) // Default to minutes
|
|
}
|
|
}
|
|
|
|
// IntervalFieldData represents interval input field data with separate value and unit
|
|
type IntervalFieldData struct {
|
|
FormFieldData
|
|
Seconds int // The interval value in seconds
|
|
}
|
|
|
|
// IntervalField renders a Bootstrap interval input with number + unit dropdown (like task config)
|
|
templ IntervalField(data IntervalFieldData) {
|
|
<div class="mb-3">
|
|
<label for={ data.Name } class="form-label">
|
|
{ data.Label }
|
|
if data.Required {
|
|
<span class="text-danger">*</span>
|
|
}
|
|
</label>
|
|
<div class="input-group">
|
|
<input
|
|
type="number"
|
|
class="form-control"
|
|
id={ data.Name + "_value" }
|
|
name={ data.Name + "_value" }
|
|
value={ fmt.Sprintf("%.0f", convertSecondsToValue(data.Seconds, convertSecondsToUnit(data.Seconds))) }
|
|
step="1"
|
|
min="1"
|
|
if data.Required {
|
|
required
|
|
}
|
|
/>
|
|
<select
|
|
class="form-select"
|
|
id={ data.Name + "_unit" }
|
|
name={ data.Name + "_unit" }
|
|
style="max-width: 120px;"
|
|
if data.Required {
|
|
required
|
|
}
|
|
>
|
|
<option
|
|
value="seconds"
|
|
if convertSecondsToUnit(data.Seconds) == "seconds" {
|
|
selected
|
|
}
|
|
>
|
|
Seconds
|
|
</option>
|
|
<option
|
|
value="minutes"
|
|
if convertSecondsToUnit(data.Seconds) == "minutes" {
|
|
selected
|
|
}
|
|
>
|
|
Minutes
|
|
</option>
|
|
<option
|
|
value="hours"
|
|
if convertSecondsToUnit(data.Seconds) == "hours" {
|
|
selected
|
|
}
|
|
>
|
|
Hours
|
|
</option>
|
|
<option
|
|
value="days"
|
|
if convertSecondsToUnit(data.Seconds) == "days" {
|
|
selected
|
|
}
|
|
>
|
|
Days
|
|
</option>
|
|
</select>
|
|
</div>
|
|
if data.Description != "" {
|
|
<div class="form-text text-muted">{ data.Description }</div>
|
|
}
|
|
</div>
|
|
} |