I have an idle timeout timer being select
ed on in a goroutine, if I see activity I want to cancel the timer.
I had a look at the documentation and I'm not positive I'm clear on what it says.
func (t *Timer) Stop() bool
Stop prevents the Timer from firing. It returns true if the call stops the timer, false if the timer has already expired or been stopped. Stop does not close the channel, to prevent a read from the channel succeeding incorrectly.To prevent a timer created with NewTimer from firing after a call to Stop, check the return value and drain the channel. For example, assuming the program has not received from t.C already:
if !t.Stop() { <-t.C }
This cannot be done concurrent to other receives from the Timer's channel.
I'm trying to understand when I have to drain the channel manually.
I'll list my understanding and if I'm wrong please correct me.
If Stop
returned false
this means either:
In my case getting a superfluous event from the timer is no big deal, does that inform what I should do here?
The reason that you might need to drain the channel is because of how goroutines are scheduled.
Problem
Imagine this case:
t.C
t.Stop()
is called.In this case there is a value on the channel t.C
, and t.Stop()
returns false because "the timer already expired" (i.e. when it sent the value on t.C
).
The reason that the docs say "this cannot be done concurrent to other receives" is because there's not guarantee of ordering between the if !t.Stop {
and the <-t.C
. The stop command could return false, entering the if body. And then another goroutine could be scheduled and read the value from t.C
that the body of the if statement was trying to drain. This would cause a datarace and result in blocking inside the if statement. (as you pointed out in your question!)
Solution
It depends on what the behaviour of the thing listening to the timer is.
If you are just in a simple select:
select {
case result <- doWork():
case <-t.C
}
Something like above. One of a few things could happen:
doWork
, sending the result, all good.doWork()
completing.t.Stop()
, but it's too late because the value has been sent, causing the timeout, breaking the select.As long as you are OK with case 4, you do not need to interact / drain the channel after calling Stop.
If you are not OK with case 4. You still cannot drain the t.C
channel because there's another goroutine listening to it. This could block in the if statement. Instead you must find another way of laying out the code, or ensuring that your goroutine in the select is not still listening on the channel.