Files
Pulse/internal/updates/queue.go
rcourtman 01f7d81d38 style: fix gofmt formatting inconsistencies
Run gofmt -w to fix tab/space inconsistencies across 33 files.
2025-11-26 23:44:36 +00:00

199 lines
4.8 KiB
Go

package updates
import (
"context"
"sync"
"time"
"github.com/rs/zerolog/log"
)
// JobState represents the state of an update job
type JobState string
const (
JobStateIdle JobState = "idle"
JobStateQueued JobState = "queued"
JobStateRunning JobState = "running"
JobStateCompleted JobState = "completed"
JobStateFailed JobState = "failed"
JobStateCancelled JobState = "cancelled"
)
// UpdateJob represents a single update job
type UpdateJob struct {
ID string
DownloadURL string
State JobState
StartedAt time.Time
CompletedAt time.Time
Error error
Context context.Context
Cancel context.CancelFunc
}
// UpdateQueue manages the update job queue ensuring only one update runs at a time
type UpdateQueue struct {
mu sync.RWMutex
currentJob *UpdateJob
jobHistory []*UpdateJob
maxHistory int
}
// NewUpdateQueue creates a new update queue
func NewUpdateQueue() *UpdateQueue {
return &UpdateQueue{
maxHistory: 10,
jobHistory: make([]*UpdateJob, 0, 10),
}
}
// Enqueue adds a new update job to the queue
// Returns the job ID and a boolean indicating if the job was accepted
func (q *UpdateQueue) Enqueue(downloadURL string) (*UpdateJob, bool) {
q.mu.Lock()
defer q.mu.Unlock()
// Check if there's already a job running
if q.currentJob != nil && (q.currentJob.State == JobStateQueued || q.currentJob.State == JobStateRunning) {
log.Warn().
Str("current_job_id", q.currentJob.ID).
Str("current_state", string(q.currentJob.State)).
Msg("Update job rejected: another job is already running")
return nil, false
}
// Create new job
ctx, cancel := context.WithCancel(context.Background())
job := &UpdateJob{
ID: generateJobID(),
DownloadURL: downloadURL,
State: JobStateQueued,
StartedAt: time.Now(),
Context: ctx,
Cancel: cancel,
}
q.currentJob = job
log.Info().
Str("job_id", job.ID).
Str("download_url", downloadURL).
Msg("Update job enqueued")
return job, true
}
// MarkRunning marks the current job as running
func (q *UpdateQueue) MarkRunning(jobID string) bool {
q.mu.Lock()
defer q.mu.Unlock()
if q.currentJob == nil || q.currentJob.ID != jobID {
return false
}
q.currentJob.State = JobStateRunning
log.Info().Str("job_id", jobID).Msg("Update job started")
return true
}
// MarkCompleted marks the current job as completed
func (q *UpdateQueue) MarkCompleted(jobID string, err error) {
q.mu.Lock()
defer q.mu.Unlock()
if q.currentJob == nil || q.currentJob.ID != jobID {
return
}
q.currentJob.CompletedAt = time.Now()
if err != nil {
q.currentJob.State = JobStateFailed
q.currentJob.Error = err
log.Error().
Err(err).
Str("job_id", jobID).
Dur("duration", q.currentJob.CompletedAt.Sub(q.currentJob.StartedAt)).
Msg("Update job failed")
} else {
q.currentJob.State = JobStateCompleted
log.Info().
Str("job_id", jobID).
Dur("duration", q.currentJob.CompletedAt.Sub(q.currentJob.StartedAt)).
Msg("Update job completed")
}
// Add to history
q.addToHistory(q.currentJob)
// Clear current job after a short delay (allow status polling to see completion)
go func() {
time.Sleep(30 * time.Second)
q.mu.Lock()
if q.currentJob != nil && q.currentJob.ID == jobID {
q.currentJob = nil
}
q.mu.Unlock()
}()
}
// Cancel cancels the current job if it matches the given ID
func (q *UpdateQueue) Cancel(jobID string) bool {
q.mu.Lock()
defer q.mu.Unlock()
if q.currentJob == nil || q.currentJob.ID != jobID {
return false
}
if q.currentJob.State == JobStateQueued || q.currentJob.State == JobStateRunning {
q.currentJob.Cancel()
q.currentJob.State = JobStateCancelled
q.currentJob.CompletedAt = time.Now()
log.Info().Str("job_id", jobID).Msg("Update job cancelled")
return true
}
return false
}
// GetCurrentJob returns the current job if any
func (q *UpdateQueue) GetCurrentJob() *UpdateJob {
q.mu.RLock()
defer q.mu.RUnlock()
return q.currentJob
}
// IsRunning returns true if there's a job currently running or queued
func (q *UpdateQueue) IsRunning() bool {
q.mu.RLock()
defer q.mu.RUnlock()
return q.currentJob != nil && (q.currentJob.State == JobStateQueued || q.currentJob.State == JobStateRunning)
}
// GetHistory returns the job history
func (q *UpdateQueue) GetHistory() []*UpdateJob {
q.mu.RLock()
defer q.mu.RUnlock()
// Return a copy to avoid concurrent access issues
history := make([]*UpdateJob, len(q.jobHistory))
copy(history, q.jobHistory)
return history
}
// addToHistory adds a job to the history (must be called with lock held)
func (q *UpdateQueue) addToHistory(job *UpdateJob) {
q.jobHistory = append(q.jobHistory, job)
// Keep only the last N jobs
if len(q.jobHistory) > q.maxHistory {
q.jobHistory = q.jobHistory[len(q.jobHistory)-q.maxHistory:]
}
}
// generateJobID generates a unique job ID
func generateJobID() string {
return time.Now().Format("20060102-150405.000")
}