// send sends a request, kills it after passed timeout and writes the result to passed channel func send(httpClient *http.Client, tr *http.Transport, req request, timeout time.Duration, resp chan request) { if *debug { fmt.Println("Sending a request to", req.httpReq.URL) } // Send the HTTP request in it's own goroutine using a HTTP client to be able to abort the request if reached timeout go func() { start := time.Now() resp, err := httpClient.Do(req.httpReq) if err != nil { req.err = err } else { req.StatusCode = resp.StatusCode req.Duration = time.Since(start) resp.Body.Close() } // Indicate the request is finished close(req.done) }() // Either the request finished, or the timeout triggers select { case <-req.done: // Request is done, please continue case <-time.After(timeout): req.Duration = timeout // Manually cancel the request in flight tr.CancelRequest(req.httpReq) req.err = errTimeout } // Send back on the response channel resp <- req }
func Fanout(transport *http.Transport, request *http.Request, endpoints []*Endpoint) (*http.Response, error) { body, err := ioutil.ReadAll(request.Body) if err != nil { return nil, err } responses := make(chan *http.Response, len(endpoints)) errs := make(chan error, len(endpoints)) for _, e := range endpoints { url := *request.URL url.Scheme = "http" url.Host = e.Addr req := *request req.Body = ioutil.NopCloser(bytes.NewBuffer(body)) req.URL = &url req.Close = true request := &req go func() { response, err := transport.RoundTrip(request) if err != nil { errs <- err } else { responses <- response } }() defer transport.CancelRequest(request) } var fanoutErr error var response *http.Response for i := 0; i < len(endpoints); i++ { select { case fanoutErr = <-errs: case response = <-responses: if response.StatusCode < 400 { return response, nil } } } if response != nil { return response, nil } return nil, fanoutErr }
// HTTPDo runs the HTTP request in a goroutine and passes the response to f in // that same goroutine. After the goroutine finishes, returns the result of f. // If ctx.Done() receives before f returns, calls transport.CancelRequest(req) before // before returning. Even though HTTPDo executes f in another goroutine, you // can treat it as a synchronous call to f. Also, since HTTPDo uses client to run // requests and transport to cancel them, client.Transport should be transport // in most cases. // // Example Usage: // type Resp struct { Num int `json:"num"` } // var resp *Resp // err := HttpDo(ctx, client, transport, req, func(resp *http.Response, err error) error { // if err != nil { return err } // defer resp.Body.Close() // // if err := json.NewDecoder(resp.Body).Decode(resp); err != nil { // return err // } // return nil // }) // if err != nil { return err } // // do something with resp... // // This func was stolen/adapted from https://blog.golang.org/context func HTTPDo(ctx context.Context, client *http.Client, transport *http.Transport, req *http.Request, f func(*http.Response, error) error) error { // Run the HTTP request in a goroutine and pass the response to f. c := make(chan error, 1) // see if the ctx was already cancelled/timed out select { case <-ctx.Done(): return ErrCancelled default: } go func() { c <- f(client.Do(req)) }() select { case <-ctx.Done(): transport.CancelRequest(req) <-c // Wait for f to return. return ctx.Err() case err := <-c: return err } }