I've been fiddling around with writing variations on task group-like structures lately, and I've somehow created two very similar implementations where one implementation fails some tests that the other succeeds at.
The implementations are tokio-worker-map
, for mapping values through an asynchronous function on multiple worker tasks, and tokio-worker-nursery
, which spawns futures on worker tasks and streams the results. Both implementations work by providing handles for sending input values — function arguments in worker-map
's case, futures (boxed internally) in worker-nursery
's case — over an mpmc channel from the async-channel
crate to be received by pre-spawned worker tasks, which await the operations and send the results over an unbounded Tokio mpsc channel to be received by a stream handle.
Because I thought it would be a neat idea, I gave both implementations methods for "shutting down," closing the input channels and preventing any queued inputs from being processed. The tests of shutdown behavior work fine for worker-map
, but for worker-nursery
, they're failing because no outputs are making it to the output stream, seemingly because the workers don't receive the shutdown notification before starting to await the first round of inputs.
Aside from the external API, the primary differences between the two implementations are:
-
worker-map
uses a boundedasync-channel
for the input channel, whileworker-nursery
uses an unbounded channel. -
worker-map
usesSender::send()
(an async method) to send values to the workers, whileworker-nursery
usesSender::try_send()
(synchronous).
Even if I adjust worker-nursery
to match worker-map
on these points, the shutdown tests still fail.
I suspect the failure has something to do with nondeterminism around AtomicBool
, but then why are the results so consistent on both my Intel MacBook Pro and GitHub Actions' Ubuntu, macOS, and Windows runners? Is there a simple way to guarantee the tests succeed for both implementations?