adding retry button

This commit is contained in:
chrislu
2025-08-11 18:28:02 -07:00
parent 5c93557314
commit 2a61cda2cd
6 changed files with 489 additions and 230 deletions

View File

@@ -167,6 +167,7 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, username,
maintenanceApi.GET("/tasks/:id", h.adminServer.GetMaintenanceTask)
maintenanceApi.GET("/tasks/:id/detail", h.adminServer.GetMaintenanceTaskDetailAPI)
maintenanceApi.POST("/tasks/:id/cancel", h.adminServer.CancelMaintenanceTask)
maintenanceApi.POST("/tasks/:taskId/retry", h.maintenanceHandlers.RetryTask)
maintenanceApi.GET("/workers", h.adminServer.GetMaintenanceWorkersAPI)
maintenanceApi.GET("/workers/:id", h.adminServer.GetMaintenanceWorker)
maintenanceApi.GET("/workers/:id/logs", h.adminServer.GetWorkerLogs)
@@ -293,6 +294,7 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, username,
maintenanceApi.GET("/tasks/:id", h.adminServer.GetMaintenanceTask)
maintenanceApi.GET("/tasks/:id/detail", h.adminServer.GetMaintenanceTaskDetailAPI)
maintenanceApi.POST("/tasks/:id/cancel", h.adminServer.CancelMaintenanceTask)
maintenanceApi.POST("/tasks/:taskId/retry", h.maintenanceHandlers.RetryTask)
maintenanceApi.GET("/workers", h.adminServer.GetMaintenanceWorkersAPI)
maintenanceApi.GET("/workers/:id", h.adminServer.GetMaintenanceWorker)
maintenanceApi.GET("/workers/:id/logs", h.adminServer.GetWorkerLogs)

View File

@@ -469,6 +469,32 @@ func (h *MaintenanceHandlers) UpdateMaintenanceConfig(c *gin.Context) {
c.Redirect(http.StatusSeeOther, "/maintenance/config")
}
// RetryTask manually retries a maintenance task
func (h *MaintenanceHandlers) RetryTask(c *gin.Context) {
taskID := c.Param("taskId")
if taskID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Task ID is required"})
return
}
manager := h.adminServer.GetMaintenanceManager()
if manager == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Maintenance manager not available"})
return
}
err := manager.RetryTask(taskID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": fmt.Sprintf("Task %s has been queued for retry", taskID),
})
}
// Helper methods that delegate to AdminServer
func (h *MaintenanceHandlers) getMaintenanceQueueData() (*maintenance.MaintenanceQueueData, error) {

View File

@@ -568,3 +568,8 @@ func (mm *MaintenanceManager) UpdateTaskProgress(taskID string, progress float64
func (mm *MaintenanceManager) UpdateWorkerHeartbeat(workerID string) {
mm.queue.UpdateWorkerHeartbeat(workerID)
}
// RetryTask manually retries a failed or pending task
func (mm *MaintenanceManager) RetryTask(taskID string) error {
return mm.queue.RetryTask(taskID)
}

View File

@@ -639,7 +639,59 @@ func generateTaskID() string {
return fmt.Sprintf("%s-%04d", string(b), timestamp)
}
// CleanupOldTasks removes old completed and failed tasks
// RetryTask manually retries a failed or pending task
func (mq *MaintenanceQueue) RetryTask(taskID string) error {
mq.mutex.Lock()
defer mq.mutex.Unlock()
task, exists := mq.tasks[taskID]
if !exists {
return fmt.Errorf("task %s not found", taskID)
}
// Only allow retry for failed or pending tasks
if task.Status != TaskStatusFailed && task.Status != TaskStatusPending {
return fmt.Errorf("task %s cannot be retried (status: %s)", taskID, task.Status)
}
// Reset task for retry
now := time.Now()
task.Status = TaskStatusPending
task.WorkerID = ""
task.StartedAt = nil
task.CompletedAt = nil
task.Error = ""
task.ScheduledAt = now // Schedule immediately
task.Progress = 0
// Add to assignment history if it was previously assigned
if len(task.AssignmentHistory) > 0 {
lastAssignment := task.AssignmentHistory[len(task.AssignmentHistory)-1]
if lastAssignment.UnassignedAt == nil {
unassignedTime := now
lastAssignment.UnassignedAt = &unassignedTime
lastAssignment.Reason = "Manual retry requested"
}
}
// Remove from current pending list if already there to avoid duplicates
for i, pendingTask := range mq.pendingTasks {
if pendingTask.ID == taskID {
mq.pendingTasks = append(mq.pendingTasks[:i], mq.pendingTasks[i+1:]...)
break
}
}
// Add back to pending queue
mq.pendingTasks = append(mq.pendingTasks, task)
// Save task state
mq.saveTaskState(task)
glog.Infof("Task manually retried: %s (%s) for volume %d", taskID, task.Type, task.VolumeID)
return nil
}
func (mq *MaintenanceQueue) CleanupOldTasks(retention time.Duration) int {
mq.mutex.Lock()
defer mq.mutex.Unlock()

View File

@@ -98,40 +98,47 @@ templ MaintenanceQueue(data *maintenance.MaintenanceQueueData) {
<th>Worker</th>
<th>Duration</th>
<th>Completed</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
for _, task := range data.Tasks {
if string(task.Status) == "completed" || string(task.Status) == "failed" || string(task.Status) == "cancelled" {
if string(task.Status) == "failed" {
<tr class="table-danger clickable-row" data-task-id={task.ID} onclick="navigateToTask(this)" style="cursor: pointer;">
<td>
<tr class="table-danger">
<td onclick="navigateToTask(this)" style="cursor: pointer;" data-task-id={task.ID}>
@TaskTypeIcon(task.Type)
{string(task.Type)}
</td>
<td>@StatusBadge(task.Status)</td>
<td>{fmt.Sprintf("%d", task.VolumeID)}</td>
<td>
<td onclick="navigateToTask(this)" style="cursor: pointer;" data-task-id={task.ID}>@StatusBadge(task.Status)</td>
<td onclick="navigateToTask(this)" style="cursor: pointer;" data-task-id={task.ID}>{fmt.Sprintf("%d", task.VolumeID)}</td>
<td onclick="navigateToTask(this)" style="cursor: pointer;" data-task-id={task.ID}>
if task.WorkerID != "" {
<small>{task.WorkerID}</small>
} else {
<span class="text-muted">-</span>
}
</td>
<td>
<td onclick="navigateToTask(this)" style="cursor: pointer;" data-task-id={task.ID}>
if task.StartedAt != nil && task.CompletedAt != nil {
{formatDuration(task.CompletedAt.Sub(*task.StartedAt))}
} else {
<span class="text-muted">-</span>
}
</td>
<td>
<td onclick="navigateToTask(this)" style="cursor: pointer;" data-task-id={task.ID}>
if task.CompletedAt != nil {
{task.CompletedAt.Format("2006-01-02 15:04")}
} else {
<span class="text-muted">-</span>
}
</td>
<td>
<button type="button" class="btn btn-sm btn-warning" onclick="retryTask('{task.ID}')" title="Retry Failed Task">
<i class="fas fa-redo me-1"></i>
Retry
</button>
</td>
</tr>
} else {
<tr class="clickable-row" data-task-id={task.ID} onclick="navigateToTask(this)" style="cursor: pointer;">
@@ -162,6 +169,9 @@ templ MaintenanceQueue(data *maintenance.MaintenanceQueueData) {
<span class="text-muted">-</span>
}
</td>
<td>
<span class="text-muted">-</span>
</td>
</tr>
}
}
@@ -203,21 +213,28 @@ templ MaintenanceQueue(data *maintenance.MaintenanceQueueData) {
<th>Server</th>
<th>Reason</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
for _, task := range data.Tasks {
if string(task.Status) == "pending" {
<tr class="clickable-row" data-task-id={task.ID} onclick="navigateToTask(this)" style="cursor: pointer;">
<td>
<tr>
<td onclick="navigateToTask(this)" style="cursor: pointer;" data-task-id={task.ID}>
@TaskTypeIcon(task.Type)
{string(task.Type)}
</td>
<td>@PriorityBadge(task.Priority)</td>
<td>{fmt.Sprintf("%d", task.VolumeID)}</td>
<td><small>{task.Server}</small></td>
<td><small>{task.Reason}</small></td>
<td>{task.CreatedAt.Format("2006-01-02 15:04")}</td>
<td onclick="navigateToTask(this)" style="cursor: pointer;" data-task-id={task.ID}>@PriorityBadge(task.Priority)</td>
<td onclick="navigateToTask(this)" style="cursor: pointer;" data-task-id={task.ID}>{fmt.Sprintf("%d", task.VolumeID)}</td>
<td onclick="navigateToTask(this)" style="cursor: pointer;" data-task-id={task.ID}><small>{task.Server}</small></td>
<td onclick="navigateToTask(this)" style="cursor: pointer;" data-task-id={task.ID}><small>{task.Reason}</small></td>
<td onclick="navigateToTask(this)" style="cursor: pointer;" data-task-id={task.ID}>{task.CreatedAt.Format("2006-01-02 15:04")}</td>
<td>
<button type="button" class="btn btn-sm btn-warning" onclick="retryTask('{task.ID}')" title="Retry Task">
<i class="fas fa-redo me-1"></i>
Retry
</button>
</td>
</tr>
}
}
@@ -342,6 +359,33 @@ templ MaintenanceQueue(data *maintenance.MaintenanceQueueData) {
window.location.href = '/maintenance/tasks/' + taskId;
}
};
window.retryTask = function(taskId) {
console.log("retryTask called for task:", taskId);
if (!confirm('Are you sure you want to retry this task?')) {
return;
}
fetch('/api/maintenance/tasks/' + taskId + '/retry', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Task retried successfully: ' + data.message);
setTimeout(() => window.location.reload(), 1000);
} else {
alert('Failed to retry task: ' + (data.error || 'Unknown error'));
}
})
.catch(error => {
alert('Error retrying task: ' + error.message);
});
};
</script>
}

File diff suppressed because it is too large Load Diff