I have a concurrency utility which is essentially a wrapper around unbuffered channels- providing Send
and Receive
functions with optional timeouts via context.Context
. Hence, the inside of the function has a select
that looks roughly like so (simplified for exposition):
func Send(ctx context.Context, ch chan interface{}, item interface{}) error {
select {
case <-ctx.Done():
return ctx.Err()
case ch <- item:
return nil
}
}
This code is tested and working, but there is an undesirable behaviour where if the context has been cancelled and the channel can be written to, the send will succeed or fail non-deterministically. This is intended behaviour of select
, so I added a non-blocking send before the main select
, with the expected behaviour that this first select
should always succeed if the chan is available.
func Send(ctx context.Context, ch chan interface{}, item interface{}) error {
select {
case ch <- item:
return nil
default:
}
select {
case <-ctx.Done():
return ctx.Err()
case ch <- item:
return nil
}
}
I am reasonably confident that this is a correct implementation, but I would like to write a test case that attempts to exercise the event ordering. However, due to the non-determinism inherent in dealing with multiple goroutines, I cannot work out how to sequence events properly.
Here is what I would like to test:
func TestReceiveThenSend(t *testing.T) {
ch := make(chan interface{})
go Receive(context.Background(), ch)
// Somehow, wait until the above `Receive` is blocked reading from `ch`.
// Immediately expired context.
ctx, cancel := context.WithTimeout(context.Background(), 0)
defer cancel()
if err := Send(ctx, ch, "foo"); err != nil {
t.Fatal("expected Send to succeed because Receive is already called.")
}
}
Obviously, I can't expect the above test to work since there is no ordering between the go Receive
and Send
. I've tried various hacks such as forcing runtime.Gosched
, time.Sleep
, or implementing a custom Context
that knows when its Done
member is used. None of these can force the ordering that I would like.
I'm stuck, I'm looking for any way, pretty or ugly, to force this behaviour. Does anyone have any tricks to solve this?