emitter icon indicating copy to clipboard operation
emitter copied to clipboard

Cancellation example in readme has a race condition that could panic.

Open gaboose opened this issue 5 years ago • 3 comments

Correct me if I'm wrong, but it seems that the below example that's currently on the README.md has a small chance of panicking.

done := e.Emit("broadcast", "the", "event", "with", "timeout")

select {
case <-done:
	// so the sending is done
case <-time.After(timeout):
	// time is out, let's discard emitting
	close(done)
}

If timeout happens at the same time when Emit closes the done channel, the done channel can be closed twice, which will cause a panic. To illustrate the point, the below code causes a panic reproducibly.

e := emitter.Emitter{}
ch := e.On("broadcast")

done := e.Emit("broadcast", "the", "event", "with", "timeout")

go func() {
	time.Sleep(200 * time.Millisecond)
	fmt.Println("this happens second")
	<-ch
}()

select {
case <-done:
case <-time.After(100 * time.Millisecond):
	fmt.Println("this happens first")
	time.Sleep(200 * time.Millisecond)
	fmt.Println("this happens third")
	close(done)
}

gaboose avatar Feb 04 '20 19:02 gaboose

Hey @Gaboose, good catch! Would you like to send a PR? 🙂

olebedev avatar Feb 04 '20 19:02 olebedev

Hi @olebedev! Maybe if I find a free minute or two :) but it's not straight forward.

I think fixing this would require for Emit to return something other than a chan struct{}, because closing a channel on the receiver's side will always have this race condition, I think. But Emit could return a struct with two channels.

type EmitOp struct {
    done chan struct{}
    cancelled chan struct{}
}

func (op EmitOp) Done() <-chan struct{} { return op.done }
func (op EmitOp) Cancel() { close(op.cancelled) )

So it would be an API breaking change. What do you think? Maybe there's some other better solution?

gaboose avatar Feb 05 '20 01:02 gaboose

An alternative for this could be an EmitCtx type api

e := emitter.Emitter{}
ch := e.On("broadcast")

ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
done := e.EmitCtx(ctx, "broadcast", "the", "event", "with", "timeout")

select {
case <-done:
    fmt.Println("Event sent")
case <-ctx.Done():
    fmt.Println("Event send timed out")
}

mattnathan avatar Jul 16 '21 07:07 mattnathan