diff --git a/clientutil/backend.go b/clientutil/backend.go index cdde33b45ffe954a37fcf9d53dfd1601e8430876..3f4d31379bb8a70d01eb4680a6572c517a40e9e6 100644 --- a/clientutil/backend.go +++ b/clientutil/backend.go @@ -2,6 +2,7 @@ package clientutil import ( "context" + "net/http" ) // BackendConfig specifies the configuration of a service backend. @@ -39,6 +40,13 @@ type Backend interface { // *without* a shard ID on a sharded service is an error. Call(context.Context, string, string, interface{}, interface{}) error + // Make a simple HTTP GET request to the remote backend, + // without parsing the response as JSON. + // + // Useful for streaming large responses, where the JSON + // encoding overhead is undesirable. + Get(context.Context, string, string) (*http.Response, error) + // Close all resources associated with the backend. Close() } diff --git a/clientutil/balancer.go b/clientutil/balancer.go index 5c37d6bfcb7e97c0c249d21c236d90df7dcc1637..b70602c8bea87bfb2d53c048163f0711f9ee4154 100644 --- a/clientutil/balancer.go +++ b/clientutil/balancer.go @@ -168,6 +168,44 @@ func (b *balancedBackend) Call(ctx context.Context, shard, path string, req, res }, backoff.WithContext(newExponentialBackOff(), ctx)) } +// Makes a generic HTTP GET request to the backend uri. +func (b *balancedBackend) Get(ctx context.Context, shard, path string) (*http.Response, error) { + // Create the target sequence for this call. If there are multiple + // targets, reduce the timeout on each individual call accordingly to + // accomodate eventual failover. + seq, err := b.makeSequence(shard) + if err != nil { + return nil, err + } + innerTimeout := 1 * time.Hour + if deadline, ok := ctx.Deadline(); ok { + innerTimeout = time.Until(deadline) / time.Duration(seq.Len()) + } + if b.requestMaxTimeout > 0 && innerTimeout > b.requestMaxTimeout { + innerTimeout = b.requestMaxTimeout + } + + req, err := http.NewRequest("GET", b.getURIForRequest(shard, path), nil) + if err != nil { + return nil, err + } + + // Call the backends in the sequence until one succeeds, with an + // exponential backoff policy controlled by the outer Context. + var resp *http.Response + err = backoff.Retry(func() error { + innerCtx, cancel := context.WithTimeout(ctx, innerTimeout) + defer cancel() + + // When do() returns successfully, we already know that the + // response had an HTTP status of 200. + var rerr error + resp, rerr = b.do(innerCtx, seq, req) + return rerr + }, backoff.WithContext(newExponentialBackOff(), ctx)) + return resp, err +} + // Initialize a new target sequence. func (b *balancedBackend) makeSequence(shard string) (*sequence, error) { var tg targetGenerator = b.backendTracker