1diff --git a/cmd/server/main.go b/cmd/server/main.go
2index 473bed9c8f84206f027b3c505fb0260e04d5e627..0fa5fea9d42fe4a4c8ea1c67444e034b938f68e9 100644
3--- a/cmd/server/main.go
4+++ b/cmd/server/main.go
5@@ -135,7 +135,6 @@ ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
6 defer stop()
7
8 pool.Start(ctx)
9- pool.Wait()
10 }
11
12 func OpenDatabase(dbType string, dbConn string) (gorm.Dialector, error) {
13diff --git a/pkg/coroutine/coroutine.go b/pkg/coroutine/coroutine.go
14new file mode 100644
15index 0000000000000000000000000000000000000000..96d149ea9851318b4ff9c0c7a0f07a1a6505eb83
16--- /dev/null
17+++ b/pkg/coroutine/coroutine.go
18@@ -0,0 +1,33 @@
19+package coroutine
20+
21+import (
22+ "context"
23+)
24+
25+// WrapProcess wraps process into a go routine and make it cancelable through context
26+func WrapProcess[V any](ctx context.Context, fun func() (V, error)) (V, error) {
27+ c := make(chan V)
28+ e := make(chan error)
29+ go func() {
30+ defer close(c)
31+ defer close(e)
32+
33+ v, err := fun()
34+ if err != nil {
35+ e <- err
36+ } else {
37+ c <- v
38+ }
39+ }()
40+
41+ select {
42+ case <-ctx.Done():
43+ var zero V
44+ return zero, ctx.Err()
45+ case m := <-c:
46+ return m, nil
47+ case err := <-e:
48+ var zero V
49+ return zero, err
50+ }
51+}
52diff --git a/pkg/coroutine/coroutine_test.go b/pkg/coroutine/coroutine_test.go
53new file mode 100644
54index 0000000000000000000000000000000000000000..e876ec3bd69b256426e2dad9fb6c2f1b17ab7348
55--- /dev/null
56+++ b/pkg/coroutine/coroutine_test.go
57@@ -0,0 +1,63 @@
58+//go:build unit
59+
60+package coroutine
61+
62+import (
63+ "context"
64+ "errors"
65+ "sync"
66+ "testing"
67+ "time"
68+
69+ "git.sr.ht/~gabrielgio/img/pkg/testkit"
70+)
71+
72+var rError = errors.New("This is a error")
73+
74+func imediatReturn() (string, error) {
75+ return "A string", nil
76+}
77+
78+func imediatErrorReturn() (string, error) {
79+ return "", rError
80+}
81+
82+func haltedReturn() (string, error) {
83+ time.Sleep(time.Hour)
84+ return "", nil
85+}
86+
87+func TestImediatReturn(t *testing.T) {
88+ ctx := context.Background()
89+ v, err := WrapProcess(ctx, imediatReturn)
90+ testkit.TestError(t, "WrapProcess", nil, err)
91+ testkit.TestValue(t, "WrapProcess", "A string", v)
92+}
93+
94+func TestImediatErrorReturn(t *testing.T) {
95+ ctx := context.Background()
96+ v, err := WrapProcess(ctx, imediatErrorReturn)
97+ testkit.TestError(t, "WrapProcess", rError, err)
98+ testkit.TestValue(t, "WrapProcess", "", v)
99+}
100+
101+func TestHaltedReturn(t *testing.T) {
102+ ctx := context.Background()
103+ ctx, cancel := context.WithCancel(ctx)
104+
105+ var (
106+ err error
107+ wg sync.WaitGroup
108+ )
109+
110+ wg.Add(1)
111+ go func(err *error) {
112+ defer wg.Done()
113+ _, *err = WrapProcess(ctx, haltedReturn)
114+ }(&err)
115+
116+ cancel()
117+ wg.Wait()
118+
119+ testkit.TestError(t, "WrapProcess", context.Canceled, err)
120+}
121diff --git a/pkg/coroutines/coroutines.go b/pkg/coroutines/coroutines.go
122deleted file mode 100644
123index c0f7247cdf3d00e2b1fd4e019f5ec334fa32ea96..0000000000000000000000000000000000000000
124--- a/pkg/coroutines/coroutines.go
125+++ /dev/null
126@@ -1 +0,0 @@
127-package coroutines
128diff --git a/pkg/worker/exif_scanner.go b/pkg/worker/exif_scanner.go
129index 4aa247d832bb5261d0373c4fbb8bb8d90c73af02..91eed1270268368fa3f43216e087648984296d15 100644
130--- a/pkg/worker/exif_scanner.go
131+++ b/pkg/worker/exif_scanner.go
132@@ -4,6 +4,7 @@ import (
133 "context"
134
135 "git.sr.ht/~gabrielgio/img/pkg/components/media"
136+ "git.sr.ht/~gabrielgio/img/pkg/coroutine"
137 "git.sr.ht/~gabrielgio/img/pkg/fileop"
138 )
139
140@@ -33,36 +34,11 @@
141 return medias, nil
142 }
143
144-func wrapReadExif(ctx context.Context, path string) (*media.MediaEXIF, error) {
145- c := make(chan *media.MediaEXIF)
146- e := make(chan error)
147- go func() {
148- defer close(c)
149- defer close(e)
150-
151- newExif, err := fileop.ReadExif(path)
152- if err != nil {
153- e <- err
154- } else {
155- c <- newExif
156- }
157- }()
158-
159- select {
160- case <-ctx.Done():
161- return nil, ctx.Err()
162- case m := <-c:
163- return m, nil
164- case err := <-e:
165- return nil, err
166- }
167-}
168-
169 func (e *EXIFScanner) Process(ctx context.Context, m *media.Media) error {
170- newExif, err := wrapReadExif(ctx, m.Path)
171+ exif, err := coroutine.WrapProcess(ctx, func() (*media.MediaEXIF, error) { return fileop.ReadExif(m.Path) })
172 if err != nil {
173 return err
174 }
175
176- return e.repository.CreateEXIF(ctx, m.ID, newExif)
177+ return e.repository.CreateEXIF(ctx, m.ID, exif)
178 }
179diff --git a/pkg/worker/list_processor_test.go b/pkg/worker/list_processor_test.go
180index 1e4ed2d7793632a996cb6b828fd21677dcedbb55..35672f3b1e9b807fde0698ea752a7763777b7cfa 100644
181--- a/pkg/worker/list_processor_test.go
182+++ b/pkg/worker/list_processor_test.go
183@@ -10,6 +10,7 @@ "sync"
184 "testing"
185
186 "git.sr.ht/~gabrielgio/img/pkg/testkit"
187+ "github.com/sirupsen/logrus"
188 )
189
190 type (
191@@ -24,10 +25,13 @@ }
192 )
193
194 func TestListProcessorLimit(t *testing.T) {
195- mock := &mockCounterListProcessor{
196- countTo: 10000,
197- }
198- worker := NewWorkerFromListProcessor[int](mock, nil)
199+ var (
200+ log = logrus.New()
201+ scheduler = NewScheduler(1)
202+ mock = &mockCounterListProcessor{countTo: 10000}
203+ )
204+
205+ worker := NewWorkerFromBatchProcessor[int](mock, scheduler, log.WithField("context", "testing"))
206
207 err := worker.Start(context.Background())
208 testkit.TestFatalError(t, "Start", err)
209@@ -36,8 +40,13 @@ testkit.TestValue(t, "Start", mock.countTo, mock.counter)
210 }
211
212 func TestListProcessorContextCancelQuery(t *testing.T) {
213- mock := &mockContextListProcessor{}
214- worker := NewWorkerFromListProcessor[int](mock, nil)
215+ var (
216+ log = logrus.New()
217+ scheduler = NewScheduler(1)
218+ mock = &mockContextListProcessor{}
219+ )
220+
221+ worker := NewWorkerFromBatchProcessor[int](mock, scheduler, log.WithField("context", "testing"))
222
223 ctx, cancel := context.WithCancel(context.Background())
224 var wg sync.WaitGroup
225diff --git a/pkg/worker/worker.go b/pkg/worker/worker.go
226index 18cc0e25c15a57d778a806275f6fa354b637d8c0..359384a260713696d42fbbf02b6be54bcda9f6a6 100644
227--- a/pkg/worker/worker.go
228+++ b/pkg/worker/worker.go
229@@ -20,7 +20,6 @@ }
230
231 WorkerPool struct {
232 workers []*Work
233- wg sync.WaitGroup
234 }
235 )
236
237@@ -36,10 +35,13 @@ })
238 }
239
240 func (self *WorkerPool) Start(ctx context.Context) {
241- self.wg.Add(len(self.workers))
242+ var wg sync.WaitGroup
243+
244+ wg.Add(len(self.workers))
245+
246 for _, w := range self.workers {
247 go func(w *Work) {
248- defer self.wg.Done()
249+ defer wg.Done()
250 if err := w.Worker.Start(ctx); err != nil && !errors.Is(err, context.Canceled) {
251 fmt.Println("Processes finished, error", w.Name, err.Error())
252 } else {
253@@ -47,8 +49,6 @@ fmt.Println(w.Name, "done")
254 }
255 }(w)
256 }
257-}
258
259-func (self *WorkerPool) Wait() {
260- self.wg.Wait()
261+ wg.Wait()
262 }