From 14995ca7a76e2a72a2169def7f55f3b9e2433ee2 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Wed, 8 Apr 2026 10:12:37 +0300 Subject: fix(gui): make GetActiveJobs concurrency-safe without channel access under lock GetActiveJobs previously drained and refilled q.jobs while holding RLock, racing with AddWordWithPrompt and ProcessNextJob on the same channel. Derive queued jobs from q.results (StatusQueued) and processing jobs from q.processing, matching the authoritative state already updated under the mutex elsewhere. Made-with: Cursor --- internal/gui/queue.go | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) (limited to 'internal/gui/queue.go') diff --git a/internal/gui/queue.go b/internal/gui/queue.go index 1646708..37b4b72 100644 --- a/internal/gui/queue.go +++ b/internal/gui/queue.go @@ -153,33 +153,24 @@ func (q *WordQueue) GetQueueStatus() (queued, processing, completed, failed int) return } -// GetActiveJobs returns all jobs that are currently queued or processing +// GetActiveJobs returns all jobs that are currently queued or processing. +// It uses only q.processing and q.results under the read lock; it does not +// touch q.jobs, because channel operations must not run while holding the +// mutex (other goroutines send/receive on q.jobs without the lock). func (q *WordQueue) GetActiveJobs() []*WordJob { q.mu.RLock() defer q.mu.RUnlock() var jobs []*WordJob - - // Add processing jobs for _, job := range q.processing { jobs = append(jobs, job) } - - // Add queued jobs from channel (non-blocking) - queuedJobs := make([]*WordJob, 0) - for { - select { - case job := <-q.jobs: - queuedJobs = append(queuedJobs, job) - default: - // Re-add jobs back to queue - for _, job := range queuedJobs { - q.jobs <- job - } - jobs = append(jobs, queuedJobs...) - return jobs + for _, job := range q.results { + if job.Status == StatusQueued { + jobs = append(jobs, job) } } + return jobs } // GetCompletedJobs returns all completed jobs -- cgit v1.2.3