This caught me out recently; there's definitely a gap in my understanding 
as to how select works. Maybe it's caught other people out too?

For one of my clients, I'd written an application that would try to read 
data from a backend. There were multiple instances of this backend (say one 
in Oregon, and one in Virginia).

In this context, it made sense to try reading from all of the backends 
concurrently, and return the first successful result. All of the slower 
responses could be ignored, as long as the code cleaned up any resources 
(goroutines, file descriptors etc).

Reader, it did not.

The code which demonstrates the failure is at https://github.com/jabley/fptp

The code of interest is in composite.go [2]

The failing version is 
in https://github.com/jabley/fptp/tree/bug/original-version

This is the diff:

--- a/composite.go
+++ b/composite.go
@@ -66,8 +66,7 @@ func (c *compositeSearcher) search(ctx context.Context, s 
Searcher, req *SearchR
        case <-ctx.Done():
                // Another Searcher already returned a successful response. 
Ensure we don't leak things
                res.Close()
-       default:
-               respc <- searchResponse{res, err}
+       case respc <- searchResponse{res, err}:
        }
 }

I've gone through the spec[1]  and documentation, but I haven't been able 
to understand what I'm missing.

So 2 questions:

1. Is the revised version correct? I think it is, and I've got better test 
coverage now. But tests aren't everything; I might still have a gap in my 
understanding.

2. Why is the revised version (more) correct, versus the original that used 
a default CommCase rather than a SendStmt?

[1] https://golang.org/ref/spec#Select_statements
[2] composite.go – https://github.com/jabley/fptp/blob/master/composite.go 
(in-line below)

package fptp

import (
"context"
"errors"
"io"
"sync"
"time"
)

// ErrAllSearchersFailed is only really used when we don't have any 
Searchers at all
var ErrAllSearchersFailed = errors.New("fptp: all searchers failed")

type compositeSearcher struct {
searchers []Searcher
timeout   time.Duration
}

// NewCompositeSearcher returns a Searcher that will try all of the 
provided Searchers
func NewCompositeSearcher(searchers []Searcher, timeout time.Duration) 
Searcher {
return &compositeSearcher{
searchers: searchers,
timeout:   timeout,
}
}

// searchResponse is a nicer way of packaging the result of Searcher.Search 
for
// use with channels.
type searchResponse struct {
closer io.Closer
err    error
}

func (c *compositeSearcher) Search(req *SearchRequest) (io.Closer, error) {
ctx, cancel := context.WithTimeout(context.Background(), c.timeout)
defer cancel()
// We use a WaitGroup to keep track of when we've tried all of the Searcher 
instances
var wg sync.WaitGroup

// Synchronise on the channel
respc := make(chan searchResponse)

// Make all of the requests
for _, s := range c.searchers {
wg.Add(1)
go func(req *SearchRequest, s Searcher) {
defer wg.Done()
c.search(ctx, s, req, respc)
}(req, s)
}

// Housekeeping to track when we've tried all of the Searchers
done := make(chan struct{})
go func() {
defer close(done)
wg.Wait()
}()

return c.waitForSearchCompletion(ctx, cancel, respc, done)
}

func (c *compositeSearcher) search(ctx context.Context, s Searcher, req 
*SearchRequest, respc chan<- searchResponse) {

res, err := s.Search(req)

select {
case <-ctx.Done():
// Another Searcher already returned a successful response. Ensure we don't 
leak things
res.Close()
case respc <- searchResponse{res, err}:
}
}

func (c *compositeSearcher) waitForSearchCompletion(ctx context.Context, 
cancel context.CancelFunc, respc chan searchResponse, done chan struct{}) 
(io.Closer, error) {
var lastErr error

for {
select {
case <-ctx.Done(): // timed out
return nil, ctx.Err() // context.DeadlineExceeded
case r := <-respc: // We got a response from one of the Searchers
if r.err == nil { // success!
return r.closer, r.err
}
lastErr = r.err // failed, keep track of why
case <-done:
// we've tried every Searcher, and none of them returned a successful 
response
if lastErr != nil {
return nil, lastErr
}
return nil, ErrAllSearchersFailed
}
}
}


-- 
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.
For more options, visit https://groups.google.com/d/optout.

Reply via email to