pool.go
⎇
Raw
1package worker
2
3import (
4 "context"
5 "errors"
6 "log/slog"
7 "sync"
8 "time"
9
10 "vidarchive/internal/models"
11 "vidarchive/internal/service"
12)
13
14type Pool struct {
15 downloadSvc *service.DownloadService
16 workers int
17 queue chan *models.Download
18 ctx context.Context
19 cancel context.CancelFunc
20 wg sync.WaitGroup
21}
22
23func New(downloadSvc *service.DownloadService, workers int) *Pool {
24 ctx, cancel := context.WithCancel(context.Background())
25 return &Pool{
26 downloadSvc: downloadSvc,
27 workers: workers,
28 queue: make(chan *models.Download, 100),
29 ctx: ctx,
30 cancel: cancel,
31 }
32}
33
34func (p *Pool) Start() {
35 // A previous run's temp dirs go too, or the re-run imports a duplicate.
36 if err := p.downloadSvc.ResetStalledDownloads(); err != nil {
37 slog.Error("failed to reset stalled downloads", "err", err)
38 }
39
40 for i := 0; i < p.workers; i++ {
41 p.wg.Add(1)
42 go p.worker(i)
43 }
44
45 p.wg.Add(1)
46 go p.queueChecker()
47}
48
49// Stop waits for the workers, so the process does not exit while a yt-dlp child
50// is still writing to the temp dir.
51func (p *Pool) Stop() {
52 p.cancel()
53 p.downloadSvc.CancelAll()
54 p.wg.Wait()
55}
56
57// Submit never blocks: on a full buffer the row stays "queued" for the queue
58// checker, so a burst can't stall the HTTP handler that made it.
59func (p *Pool) Submit(d *models.Download) {
60 if p.ctx.Err() != nil {
61 return
62 }
63
64 select {
65 case p.queue <- d:
66 slog.Debug("download handed to a worker", "download_id", d.ID)
67 default:
68 slog.Info("download left in database queue, worker buffer full", "download_id", d.ID)
69 }
70}
71
72func (p *Pool) worker(id int) {
73 defer p.wg.Done()
74 slog.Debug("worker started", "worker", id)
75 for {
76 select {
77 case d := <-p.queue:
78 slog.Info("processing download", "worker", id, "download_id", d.ID)
79 processed, err := p.downloadSvc.ExecuteDownload(p.ctx, d)
80 switch {
81 case errors.Is(err, service.ErrCancelled):
82 slog.Info("download cancelled", "worker", id, "download_id", d.ID)
83 case err != nil:
84 slog.Error("download failed", "worker", id, "download_id", d.ID, "err", err)
85 case processed:
86 slog.Info("download completed", "worker", id, "download_id", d.ID)
87 default:
88 slog.Debug("download already claimed by another worker", "worker", id, "download_id", d.ID)
89 }
90 case <-p.ctx.Done():
91 slog.Debug("worker stopped", "worker", id)
92 return
93 }
94 }
95}
96
97func (p *Pool) queueChecker() {
98 defer p.wg.Done()
99 ticker := time.NewTicker(2 * time.Second)
100 defer ticker.Stop()
101
102 for {
103 select {
104 case <-ticker.C:
105 p.checkQueue()
106 case <-p.ctx.Done():
107 return
108 }
109 }
110}
111
112func (p *Pool) checkQueue() {
113 // At most a channel's worth per tick; the rest waits for a later one.
114 // Duplicates are harmless because MarkStarted claims atomically.
115 downloads, err := p.downloadSvc.GetQueued(cap(p.queue))
116 if err != nil {
117 slog.Error("queue check failed", "err", err)
118 return
119 }
120
121 for _, d := range downloads {
122 select {
123 case p.queue <- d:
124 default:
125 return
126 }
127 }
128}
129