Skip to content

Commit d66ea30

Browse files
test(dispatcher): race worker-status offers against an idling runner
One producer offers statuses while the runners it starts drain them, so offers land both mid-batch and as a runner goes idle. Asserts every entry is handled and batches never run in parallel.
1 parent a35b05e commit d66ea30

1 file changed

Lines changed: 79 additions & 0 deletions

File tree

‎internal/services/dispatcher/worker_status_coalescer_test.go‎

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package dispatcher
33
import (
44
"context"
55
"io"
6+
"runtime"
7+
"strconv"
68
"sync"
79
"sync/atomic"
810
"testing"
@@ -150,6 +152,83 @@ func TestWorkerStatusCoalescerRunnerLifecycle(t *testing.T) {
150152
assert.ElementsMatch(t, []statusEntry{{"c", 1, 1, 1}}, entriesOf(c.next()))
151153
}
152154

155+
// TestWorkerStatusCoalescerConcurrentHandoff races one producer, as on the stream loop, against
156+
// the runners it starts, so offers land while a runner is handling a batch and while it is
157+
// going idle. Every offered entry must be handled and batches must never be handled in parallel.
158+
func TestWorkerStatusCoalescerConcurrentHandoff(t *testing.T) {
159+
const offers = 2_000
160+
161+
c := newWorkerStatusCoalescer()
162+
163+
var (
164+
runners sync.WaitGroup
165+
mu sync.Mutex
166+
seen = make(map[statusEntry]struct{}, offers)
167+
handling atomic.Int32
168+
overlaps atomic.Int32
169+
runnerRun atomic.Int32
170+
)
171+
172+
runner := func() {
173+
defer runners.Done()
174+
175+
runnerRun.Add(1)
176+
177+
for {
178+
batch := c.next()
179+
if batch == nil {
180+
return
181+
}
182+
183+
if handling.Add(1) > 1 {
184+
overlaps.Add(1)
185+
}
186+
187+
mu.Lock()
188+
for _, e := range entriesOf(batch) {
189+
seen[e] = struct{}{}
190+
}
191+
mu.Unlock()
192+
193+
runtime.Gosched()
194+
195+
handling.Add(-1)
196+
}
197+
}
198+
199+
want := make([]statusEntry, 0, offers)
200+
201+
for i := range offers {
202+
e := statusEntry{task: strconv.Itoa(i), branch: 1, node: 1, invocation: 1}
203+
want = append(want, e)
204+
205+
if c.offer(statusOf(e)) {
206+
runners.Add(1)
207+
go runner()
208+
}
209+
210+
if i%7 == 0 {
211+
runtime.Gosched()
212+
}
213+
}
214+
215+
runners.Wait()
216+
217+
assert.Zero(t, overlaps.Load(), "batches were handled in parallel")
218+
assert.Positive(t, runnerRun.Load())
219+
220+
mu.Lock()
221+
defer mu.Unlock()
222+
223+
assert.Len(t, seen, offers, "every offered entry is handled")
224+
225+
for _, e := range want {
226+
assert.Contains(t, seen, e)
227+
}
228+
229+
assert.Nil(t, c.next(), "nothing is left pending")
230+
}
231+
153232
func TestWorkerStatusCoalescerCapsBatchesAtWholeTasks(t *testing.T) {
154233
// 5 tasks of 4_000 entries is over the cap in total, and any 3 of them are too, so a batch
155234
// holds exactly 2 whole tasks

0 commit comments

Comments
 (0)