I couldn't use <-channel.Close since in my case the goroutine isn't
guaranteed to have something sent, so that would leak goroutines. I added a
cleanup goroutine to scan timed-out channels and close them, which solves
this problem. But now I can use that close as a signal to the receiver than
the a timeout happened, and eliminate the select and the race entirely. The
close can in rare cases race with the sender, but that's easily enough
fixed:

// TrySend tries to send on a possibly closed channel and handles the panic
if necessary.
// Returns true if conn was successfully sent over the channel.
func (waiter *Waiter) TrySend(conn Connection) (sent bool) {
    defer func() {
        r := recover()
        sent = r != nil
    }()
    waiter.channel <- conn
    return
}

So I guess the best thing to do in these cases is don't combine select with
sending unmanaged resources over a channel. It's probably worth warning
about this problem in the docs for select? It's not an obvious gotcha.

On Mon, Jul 8, 2019 at 10:06 PM Ian Lance Taylor <i...@golang.org> wrote:

> On Mon, Jul 8, 2019 at 9:14 PM Daniel Eloff <dan.el...@gmail.com> wrote:
> >
> > If a select statement has multiple channels ready when it runs, then it
> will choose one at a random. So if you fire something across a channel that
> holds a resource, like an open file descriptor - you have no guarantees
> that the other end of the channel receives it. The (possibly full) channel
> will get garbage collected later and the resource will leak in that case.
> >
> > Some code that explains things better than my clumsy prose:
> >
> > Receiver:
> >  // Wait on the channel, or for timeout
> >  select {
> >  case fd := <-channel:
> >      return fd, nil
> >  case <-time.After(queue.timeout):
> >      return nil, ErrTimeoutElapsed
> >  }
> >
> > Sender:
> >  channel <- fd
> >
> > What happens when the timeout races with the channel send? I think it's
> possible the select handles the timeout in that case and leaves the channel
> containing a connection alone.
> >
> > Am I right that this is a problem? How might I fix this code?
>
> There are many approaches.  Here is a simple one:
>
>     select {
>     case fd := <-channel:
>         return fd, nil
>     case <-time.After(queue.timeout):
>         go func() {
>             <-channel.Close()
>         }()
>         return nil, ErrTimeoutElapsed
>     }
>
> Another approach is to use a context.Context on the sending side, and
> cancel the Context if the timeout occurs.  I won't write that out, but
> see https://blog.golang.org/context .
>
> Ian
>

-- 
You received this message because you are subscribed to the Google Groups 
"golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to golang-nuts+unsubscr...@googlegroups.com.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/golang-nuts/CADz32d%3D%2B%3Du81%3DZLxrOvs3%3DS3%3DrP5u3ED0wsr8DFxRg7biAhwqA%40mail.gmail.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to