vxx2 / vlib / x / async / pool_test.v
739 lines · 684 sloc · 18.11 KB · 0b1ee86f742567f2fcb3c534b0a49a6dddba36ab
Raw
1import context
2import time
3import x.async as xasync
4
5fn test_pool_rejects_invalid_config() {
6 xasync.new_pool(workers: 0, queue_size: 1) or {
7 assert err.msg() == 'async: pool worker count must be positive'
8 return
9 }
10 assert false
11}
12
13fn test_pool_rejects_invalid_queue_size() {
14 xasync.new_pool(workers: 1, queue_size: 0) or {
15 assert err.msg() == 'async: pool queue size must be positive'
16 return
17 }
18 assert false
19}
20
21fn test_pool_respects_worker_count() {
22 mut pool := xasync.new_pool(workers: 2, queue_size: 4)!
23 started := chan bool{cap: 4}
24 release := chan bool{cap: 4}
25 for _ in 0 .. 4 {
26 pool.try_submit(fn [started, release] (mut ctx context.Context) ! {
27 _ = ctx
28 started <- true
29 _ := <-release
30 })!
31 }
32
33 wait_for_bool_signal(started, 'first pool job did not start')
34 wait_for_bool_signal(started, 'second pool job did not start')
35 select {
36 _ := <-started {
37 assert false, 'pool started more jobs than worker count before release'
38 }
39 100 * time.millisecond {}
40 }
41
42 for _ in 0 .. 4 {
43 release <- true
44 }
45 pool.close()!
46}
47
48fn test_pool_queue_full_returns_backpressure_error() {
49 mut pool := xasync.new_pool(workers: 1, queue_size: 1)!
50 started := chan bool{cap: 1}
51 release := chan bool{cap: 2}
52 blocking_job := fn [started, release] (mut ctx context.Context) ! {
53 _ = ctx
54 started <- true
55 _ := <-release
56 }
57
58 pool.try_submit(blocking_job)!
59 wait_for_bool_signal(started, 'blocking pool job did not start')
60 pool.try_submit(blocking_job)!
61 pool.try_submit(blocking_job) or {
62 assert err.msg() == 'async: pool queue is full'
63 release <- true
64 release <- true
65 pool.close()!
66 return
67 }
68 assert false
69}
70
71fn test_pool_submit_with_context_waits_until_capacity_is_available() {
72 mut pool := xasync.new_pool(workers: 1, queue_size: 1)!
73 started := chan bool{cap: 2}
74 release := chan bool{cap: 2}
75 attempting := chan bool{cap: 1}
76 accepted := chan bool{cap: 1}
77 ran := chan bool{cap: 1}
78 blocking_job := fn [started, release] (mut ctx context.Context) ! {
79 _ = ctx
80 started <- true
81 _ := <-release
82 }
83
84 pool.try_submit(blocking_job)!
85 wait_for_bool_signal(started, 'first blocking pool job did not start')
86 pool.try_submit(blocking_job)!
87
88 submit_thread := spawn fn [mut pool, attempting, accepted, ran] () {
89 attempting <- true
90 pool.submit_with_context(context.background(), fn [ran] (mut ctx context.Context) ! {
91 _ = ctx
92 ran <- true
93 }) or {
94 accepted <- false
95 return
96 }
97 accepted <- true
98 }()
99 wait_for_bool_signal(attempting, 'bounded submit thread did not start')
100 select {
101 _ := <-accepted {
102 assert false, 'bounded submit returned while pool was full'
103 }
104 50 * time.millisecond {}
105 }
106
107 release <- true
108 wait_for_bool_signal(accepted, 'bounded submit did not accept after capacity opened')
109 release <- true
110 wait_for_bool_signal(ran, 'accepted bounded submit job did not run')
111 pool.close()!
112 submit_thread.wait()
113}
114
115fn test_pool_submit_with_timeout_returns_timeout_without_leaking_acceptance() {
116 mut pool := xasync.new_pool(workers: 1, queue_size: 1)!
117 started := chan bool{cap: 2}
118 release := chan bool{cap: 2}
119 finished := chan bool{cap: 2}
120 blocking_job := fn [started, release, finished] (mut ctx context.Context) ! {
121 _ = ctx
122 started <- true
123 _ := <-release
124 finished <- true
125 }
126
127 pool.try_submit(blocking_job)!
128 wait_for_bool_signal(started, 'first blocking pool job did not start')
129 pool.try_submit(blocking_job)!
130
131 pool.submit_with_timeout(20 * time.millisecond, fn (mut ctx context.Context) ! {
132 _ = ctx
133 }) or {
134 assert err.msg() == 'async: timeout'
135 release <- true
136 wait_for_bool_signal(finished, 'first blocking pool job did not finish')
137 pool.submit_with_timeout(1 * time.second, fn (mut ctx context.Context) ! {
138 _ = ctx
139 })!
140 release <- true
141 pool.close()!
142 return
143 }
144 assert false
145}
146
147fn test_pool_submit_with_context_parent_cancel_does_not_accept_job() {
148 mut pool := xasync.new_pool(workers: 1, queue_size: 1)!
149 started := chan bool{cap: 1}
150 release := chan bool{cap: 2}
151 attempting := chan bool{cap: 1}
152 would_run := chan bool{cap: 1}
153 result := chan string{cap: 1}
154 parent_ctx, cancel := xasync.with_cancel()
155 blocking_job := fn [started, release] (mut ctx context.Context) ! {
156 _ = ctx
157 started <- true
158 _ := <-release
159 }
160
161 pool.try_submit(blocking_job)!
162 wait_for_bool_signal(started, 'blocking pool job did not start')
163 pool.try_submit(blocking_job)!
164
165 submit_thread := spawn fn [mut pool, parent_ctx, attempting, would_run, result] () {
166 attempting <- true
167 pool.submit_with_context(parent_ctx, fn [would_run] (mut ctx context.Context) ! {
168 _ = ctx
169 would_run <- true
170 }) or {
171 result <- err.msg()
172 return
173 }
174 result <- 'accepted'
175 }()
176 wait_for_bool_signal(attempting, 'bounded submit thread did not start')
177 select {
178 msg := <-result {
179 assert false, 'bounded submit returned before parent cancellation: ${msg}'
180 }
181 50 * time.millisecond {}
182 }
183
184 cancel()
185 select {
186 msg := <-result {
187 assert msg == 'context canceled'
188 }
189 1 * time.second {
190 assert false, 'bounded submit did not return after parent cancellation'
191 }
192 }
193 release <- true
194 release <- true
195 pool.close()!
196 assert_no_bool_signal(would_run, 'job was accepted after parent cancellation')
197 submit_thread.wait()
198}
199
200fn test_pool_close_wakes_waiting_submitter_without_accepting_job() {
201 mut pool := xasync.new_pool(workers: 1, queue_size: 1)!
202 started := chan bool{cap: 1}
203 release := chan bool{cap: 2}
204 attempting := chan bool{cap: 1}
205 would_run := chan bool{cap: 1}
206 result := chan string{cap: 1}
207 closed := chan bool{cap: 1}
208 blocking_job := fn [started, release] (mut ctx context.Context) ! {
209 _ = ctx
210 started <- true
211 _ := <-release
212 }
213
214 pool.try_submit(blocking_job)!
215 wait_for_bool_signal(started, 'blocking pool job did not start')
216 pool.try_submit(blocking_job)!
217
218 submit_thread := spawn fn [mut pool, attempting, would_run, result] () {
219 attempting <- true
220 pool.submit_with_context(context.background(), fn [would_run] (mut ctx context.Context) ! {
221 _ = ctx
222 would_run <- true
223 }) or {
224 result <- err.msg()
225 return
226 }
227 result <- 'accepted'
228 }()
229 wait_for_bool_signal(attempting, 'bounded submit thread did not start')
230 select {
231 msg := <-result {
232 assert false, 'bounded submit returned before close: ${msg}'
233 }
234 50 * time.millisecond {}
235 }
236
237 close_thread := spawn fn [mut pool, closed] () {
238 pool.close() or {
239 closed <- false
240 return
241 }
242 closed <- true
243 }()
244 select {
245 msg := <-result {
246 assert msg == 'async: pool is closed'
247 }
248 1 * time.second {
249 assert false, 'bounded submit did not return after pool close started'
250 }
251 }
252 release <- true
253 release <- true
254 wait_for_bool_signal(closed, 'pool close did not finish after accepted jobs were released')
255 assert_no_bool_signal(would_run, 'job was accepted after pool close started')
256 submit_thread.wait()
257 close_thread.wait()
258}
259
260fn test_pool_bounded_submit_rejects_nil_job() {
261 mut pool := xasync.new_pool(workers: 1, queue_size: 1)!
262 nil_job := unsafe { xasync.JobFn(nil) }
263 pool.submit_with_context(context.background(), nil_job) or {
264 assert err.msg() == 'async: job function is nil'
265 pool.submit_with_timeout(20 * time.millisecond, nil_job) or {
266 assert err.msg() == 'async: job function is nil'
267 pool.close()!
268 return
269 }
270 assert false
271 }
272 assert false
273}
274
275fn test_pool_submit_after_close_is_refused() {
276 mut pool := xasync.new_pool(workers: 1, queue_size: 1)!
277 pool.close()!
278 pool.try_submit(fn (mut ctx context.Context) ! {
279 _ = ctx
280 }) or {
281 assert err.msg() == 'async: pool is closed'
282 return
283 }
284 assert false
285}
286
287fn test_pool_wait_is_one_shot() {
288 mut pool := xasync.new_pool(workers: 1, queue_size: 1)!
289 pool.wait()!
290 pool.wait() or {
291 assert err.msg() == 'async: pool wait was already called'
292 return
293 }
294 assert false
295}
296
297fn test_pool_refuses_nil_job() {
298 mut pool := xasync.new_pool(workers: 1, queue_size: 1)!
299 nil_job := unsafe { xasync.JobFn(nil) }
300 pool.try_submit(nil_job) or {
301 assert err.msg() == 'async: job function is nil'
302 pool.close()!
303 return
304 }
305 assert false
306}
307
308fn test_pool_close_waits_for_accepted_jobs() {
309 mut pool := xasync.new_pool(workers: 1, queue_size: 1)!
310 release := chan bool{cap: 1}
311 closed := chan bool{cap: 1}
312 pool.try_submit(fn [release] (mut ctx context.Context) ! {
313 _ = ctx
314 _ := <-release
315 })!
316
317 close_thread := spawn fn [mut pool, closed] () {
318 pool.close() or {
319 closed <- false
320 return
321 }
322 closed <- true
323 }()
324 select {
325 _ := <-closed {
326 assert false, 'pool close returned before accepted job completed'
327 }
328 100 * time.millisecond {}
329 }
330 release <- true
331 select {
332 ok := <-closed {
333 assert ok
334 }
335 1 * time.second {
336 assert false, 'pool close did not return after accepted job completed'
337 }
338 }
339 close_thread.wait()
340}
341
342fn test_pool_first_error_is_propagated() {
343 mut pool := xasync.new_pool(workers: 1, queue_size: 2)!
344 started := chan bool{cap: 1}
345 release := chan bool{cap: 1}
346 pool.try_submit(fn [started, release] (mut ctx context.Context) ! {
347 _ = ctx
348 started <- true
349 _ := <-release
350 return error('first pool failure')
351 })!
352 wait_for_bool_signal(started, 'first pool failure job did not start')
353 pool.try_submit(fn (mut ctx context.Context) ! {
354 _ = ctx
355 return error('second pool failure')
356 })!
357 release <- true
358 pool.close() or {
359 assert err.msg() == 'first pool failure'
360 return
361 }
362 assert false
363}
364
365fn test_pool_error_does_not_drop_accepted_jobs() {
366 mut pool := xasync.new_pool(workers: 1, queue_size: 1)!
367 accepted_job_ran := chan bool{cap: 1}
368 pool.try_submit(fn (mut ctx context.Context) ! {
369 _ = ctx
370 return error('first pool failure')
371 })!
372 pool.try_submit(fn [accepted_job_ran] (mut ctx context.Context) ! {
373 _ = ctx
374 accepted_job_ran <- true
375 })!
376 pool.close() or {
377 assert err.msg() == 'first pool failure'
378 select {
379 did_run := <-accepted_job_ran {
380 assert did_run
381 }
382 1 * time.second {
383 assert false, 'accepted pool job did not run after earlier error'
384 }
385 }
386 return
387 }
388 assert false
389}
390
391fn test_pool_concurrent_errors_return_one_error_and_drain_accepted_jobs() {
392 error_jobs := 4
393 ok_jobs := 8
394 mut pool := xasync.new_pool(workers: error_jobs, queue_size: ok_jobs)!
395 started := chan bool{cap: error_jobs}
396 release := chan bool{cap: error_jobs}
397 completed_ok := chan bool{cap: ok_jobs}
398 for i in 0 .. error_jobs {
399 pool.try_submit(fn [started, release, i] (mut ctx context.Context) ! {
400 _ = ctx
401 started <- true
402 _ := <-release
403 return error('pool concurrent failure ${i}')
404 })!
405 }
406 for _ in 0 .. ok_jobs {
407 pool.try_submit(fn [completed_ok] (mut ctx context.Context) ! {
408 _ = ctx
409 completed_ok <- true
410 })!
411 }
412 for _ in 0 .. error_jobs {
413 wait_for_bool_signal(started, 'pool error job did not start')
414 }
415 for _ in 0 .. error_jobs {
416 release <- true
417 }
418 pool.close() or {
419 assert err.msg().starts_with('pool concurrent failure ')
420 for _ in 0 .. ok_jobs {
421 wait_for_bool_signal(completed_ok, 'accepted ok pool job did not complete')
422 }
423 return
424 }
425 assert false
426}
427
428fn test_pool_close_drains_many_accepted_jobs_while_finishing() {
429 jobs := 12
430 workers := 3
431 mut pool := xasync.new_pool(workers: workers, queue_size: jobs - workers)!
432 started := chan bool{cap: jobs}
433 release := chan bool{cap: jobs}
434 finished := chan bool{cap: jobs}
435 closed := chan bool{cap: 1}
436 for _ in 0 .. jobs {
437 pool.try_submit(fn [started, release, finished] (mut ctx context.Context) ! {
438 _ = ctx
439 started <- true
440 _ := <-release
441 finished <- true
442 })!
443 }
444 for _ in 0 .. workers {
445 wait_for_bool_signal(started, 'initial pool job did not start')
446 }
447 close_thread := spawn fn [mut pool, closed] () {
448 pool.close() or {
449 closed <- false
450 return
451 }
452 closed <- true
453 }()
454 wait_until_pool_rejects_as_closed(mut pool)
455 assert_no_bool_signal(closed, 'pool close returned while accepted jobs were still blocked')
456
457 for _ in 0 .. jobs - 1 {
458 release <- true
459 }
460 for _ in 0 .. jobs - 1 {
461 wait_for_bool_signal(finished, 'accepted pool job did not finish')
462 }
463 assert_no_bool_signal(closed, 'pool close returned before the last accepted job finished')
464
465 release <- true
466 wait_for_bool_signal(finished, 'last accepted pool job did not finish')
467 wait_for_bool_signal(closed, 'pool close did not drain accepted jobs')
468 close_thread.wait()
469}
470
471fn test_pool_parent_cancellation_is_observed_by_cooperative_job() {
472 parent_ctx, cancel := xasync.with_cancel()
473 mut pool := xasync.new_pool_with_context(parent_ctx, workers: 1, queue_size: 1)!
474 started := chan bool{cap: 1}
475 pool.try_submit(fn [started] (mut ctx context.Context) ! {
476 started <- true
477 done := ctx.done()
478 select {
479 _ := <-done {
480 return ctx.err()
481 }
482 1 * time.second {
483 return error('pool job did not observe parent cancellation')
484 }
485 }
486 return error('unreachable')
487 })!
488 wait_for_bool_signal(started, 'pool job did not start before parent cancellation')
489 cancel()
490 pool.close() or {
491 assert err.msg() == 'context canceled'
492 return
493 }
494 assert false
495}
496
497fn test_pool_non_cooperative_job_finishes_naturally_after_parent_cancel() {
498 parent_ctx, cancel := xasync.with_cancel()
499 mut pool := xasync.new_pool_with_context(parent_ctx, workers: 1, queue_size: 1)!
500 finished := chan bool{cap: 1}
501 pool.try_submit(fn [finished] (mut ctx context.Context) ! {
502 _ = ctx
503 time.sleep(20 * time.millisecond)
504 finished <- true
505 })!
506 cancel()
507 pool.close()!
508 assert <-finished
509}
510
511fn test_pool_short_stress_many_jobs() {
512 jobs := 100
513 mut pool := xasync.new_pool(workers: 4, queue_size: jobs)!
514 done := chan int{cap: jobs}
515 for i in 0 .. jobs {
516 pool.try_submit(fn [done, i] (mut ctx context.Context) ! {
517 _ = ctx
518 done <- i
519 })!
520 }
521 pool.close()!
522
523 mut seen := []bool{len: jobs}
524 for _ in 0 .. jobs {
525 i := <-done
526 assert i >= 0
527 assert i < jobs
528 assert !seen[i]
529 seen[i] = true
530 }
531 for was_seen in seen {
532 assert was_seen
533 }
534}
535
536fn test_pool_bounded_submit_stress_waiting_submitters_are_released() {
537 workers := 2
538 queue_size := 2
539 initial_jobs := workers + queue_size
540 submitters := 8
541 mut pool := xasync.new_pool(workers: workers, queue_size: queue_size)!
542 started := chan bool{cap: initial_jobs}
543 release := chan bool{cap: initial_jobs}
544 attempting := chan bool{cap: submitters}
545 accepted := chan int{cap: submitters}
546 ran := chan int{cap: submitters}
547 blocking_job := fn [started, release] (mut ctx context.Context) ! {
548 _ = ctx
549 started <- true
550 _ := <-release
551 }
552
553 for _ in 0 .. initial_jobs {
554 pool.try_submit(blocking_job)!
555 }
556 for _ in 0 .. workers {
557 wait_for_bool_signal(started, 'initial pool worker did not start')
558 }
559
560 mut submit_threads := []thread{}
561 for i in 0 .. submitters {
562 submit_threads << spawn fn [mut pool, attempting, accepted, ran, i] () {
563 attempting <- true
564 pool.submit_with_context(context.background(), fn [ran, i] (mut ctx context.Context) ! {
565 _ = ctx
566 ran <- i
567 }) or {
568 failed_index := -1 - i
569 accepted <- failed_index
570 return
571 }
572 accepted <- i
573 }()
574 }
575 for _ in 0 .. submitters {
576 wait_for_bool_signal(attempting, 'bounded submitter did not start')
577 }
578
579 for _ in 0 .. initial_jobs {
580 release <- true
581 }
582
583 mut seen_accepted := []bool{len: submitters}
584 for _ in 0 .. submitters {
585 i := wait_for_int_signal(accepted, 'bounded submitter was not accepted')
586 assert i >= 0
587 assert i < submitters
588 assert !seen_accepted[i]
589 seen_accepted[i] = true
590 }
591 mut seen_ran := []bool{len: submitters}
592 for _ in 0 .. submitters {
593 i := wait_for_int_signal(ran, 'accepted bounded submitter job did not run')
594 assert i >= 0
595 assert i < submitters
596 assert !seen_ran[i]
597 seen_ran[i] = true
598 }
599
600 pool.close()!
601 for t in submit_threads {
602 t.wait()
603 }
604}
605
606fn test_pool_bounded_submit_stress_waiting_submitters_rejected_on_close() {
607 workers := 2
608 queue_size := 2
609 initial_jobs := workers + queue_size
610 submitters := 8
611 mut pool := xasync.new_pool(workers: workers, queue_size: queue_size)!
612 started := chan bool{cap: initial_jobs}
613 release := chan bool{cap: initial_jobs}
614 attempting := chan bool{cap: submitters}
615 result := chan string{cap: submitters}
616 ran := chan bool{cap: submitters}
617 closed := chan bool{cap: 1}
618 blocking_job := fn [started, release] (mut ctx context.Context) ! {
619 _ = ctx
620 started <- true
621 _ := <-release
622 }
623
624 for _ in 0 .. initial_jobs {
625 pool.try_submit(blocking_job)!
626 }
627 for _ in 0 .. workers {
628 wait_for_bool_signal(started, 'initial pool worker did not start')
629 }
630
631 mut submit_threads := []thread{}
632 for i in 0 .. submitters {
633 submit_threads << spawn fn [mut pool, attempting, result, ran, i] () {
634 attempting <- true
635 pool.submit_with_context(context.background(), fn [ran] (mut ctx context.Context) ! {
636 _ = ctx
637 ran <- true
638 }) or {
639 result <- err.msg()
640 return
641 }
642 result <- 'accepted ${i}'
643 }()
644 }
645 for _ in 0 .. submitters {
646 wait_for_bool_signal(attempting, 'bounded submitter did not start')
647 }
648
649 close_thread := spawn fn [mut pool, closed] () {
650 pool.close() or {
651 closed <- false
652 return
653 }
654 closed <- true
655 }()
656 for _ in 0 .. submitters {
657 msg := wait_for_string_signal(result, 'bounded submitter did not return after close')
658 assert msg == 'async: pool is closed'
659 }
660 assert_no_bool_signal(ran, 'bounded submitter job ran after close started')
661
662 for _ in 0 .. initial_jobs {
663 release <- true
664 }
665 wait_for_bool_signal(closed, 'pool close did not finish after releasing accepted jobs')
666 for t in submit_threads {
667 t.wait()
668 }
669 close_thread.wait()
670}
671
672fn wait_for_bool_signal(signal chan bool, message string) {
673 select {
674 ok := <-signal {
675 assert ok
676 }
677 pool_test_signal_timeout() {
678 assert false, message
679 }
680 }
681}
682
683fn wait_for_int_signal(signal chan int, message string) int {
684 select {
685 value := <-signal {
686 return value
687 }
688 pool_test_signal_timeout() {
689 assert false, message
690 }
691 }
692 return 0
693}
694
695fn wait_for_string_signal(signal chan string, message string) string {
696 select {
697 value := <-signal {
698 return value
699 }
700 pool_test_signal_timeout() {
701 assert false, message
702 }
703 }
704 return ''
705}
706
707fn pool_test_signal_timeout() time.Duration {
708 $if windows {
709 return 5 * time.second
710 }
711 return 1 * time.second
712}
713
714fn assert_no_bool_signal(signal chan bool, message string) {
715 select {
716 _ := <-signal {
717 assert false, message
718 }
719 else {}
720 }
721}
722
723fn wait_until_pool_rejects_as_closed(mut pool xasync.Pool) {
724 probe := fn (mut ctx context.Context) ! {
725 _ = ctx
726 }
727 for _ in 0 .. 100 {
728 pool.try_submit(probe) or {
729 if err.msg() == 'async: pool is closed' {
730 return
731 }
732 assert err.msg() == 'async: pool queue is full'
733 time.sleep(1 * time.millisecond)
734 continue
735 }
736 assert false, 'pool accepted probe while backlog should be full'
737 }
738 assert false, 'pool close did not start'
739}
740