diff --git a/call_option.go b/call_option.go index 7b621643..0a172011 100644 --- a/call_option.go +++ b/call_option.go @@ -30,37 +30,21 @@ package gax import ( - "math/rand" - "time" - + v2 "github.com/googleapis/gax-go/v2" "google.golang.org/grpc" "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" ) // CallOption is an option used by Invoke to control behaviors of RPC calls. // CallOption works by modifying relevant fields of CallSettings. -type CallOption interface { - // Resolve applies the option by modifying cs. - Resolve(cs *CallSettings) -} +type CallOption = v2.CallOption // Retryer is used by Invoke to determine retry behavior. -type Retryer interface { - // Retry reports whether a request should be retriedand how long to pause before retrying - // if the previous attempt returned with err. Invoke never calls Retry with nil error. - Retry(err error) (pause time.Duration, shouldRetry bool) -} - -type retryerOption func() Retryer - -func (o retryerOption) Resolve(s *CallSettings) { - s.Retry = o -} +type Retryer = v2.Retryer // WithRetry sets CallSettings.Retry to fn. func WithRetry(fn func() Retryer) CallOption { - return retryerOption(fn) + return v2.WithRetry(fn) } // OnCodes returns a Retryer that retries if and only if @@ -69,89 +53,17 @@ func WithRetry(fn func() Retryer) CallOption { // // bo is only used for its parameters; each Retryer has its own copy. func OnCodes(cc []codes.Code, bo Backoff) Retryer { - return &boRetryer{ - backoff: bo, - codes: append([]codes.Code(nil), cc...), - } -} - -type boRetryer struct { - backoff Backoff - codes []codes.Code -} - -func (r *boRetryer) Retry(err error) (time.Duration, bool) { - st, ok := status.FromError(err) - if !ok { - return 0, false - } - c := st.Code() - for _, rc := range r.codes { - if c == rc { - return r.backoff.Pause(), true - } - } - return 0, false + return v2.OnCodes(cc, bo) } // Backoff implements exponential backoff. // The wait time between retries is a random value between 0 and the "retry envelope". // The envelope starts at Initial and increases by the factor of Multiplier every retry, // but is capped at Max. -type Backoff struct { - // Initial is the initial value of the retry envelope, defaults to 1 second. - Initial time.Duration - - // Max is the maximum value of the retry envelope, defaults to 30 seconds. - Max time.Duration - - // Multiplier is the factor by which the retry envelope increases. - // It should be greater than 1 and defaults to 2. - Multiplier float64 - - // cur is the current retry envelope - cur time.Duration -} - -func (bo *Backoff) Pause() time.Duration { - if bo.Initial == 0 { - bo.Initial = time.Second - } - if bo.cur == 0 { - bo.cur = bo.Initial - } - if bo.Max == 0 { - bo.Max = 30 * time.Second - } - if bo.Multiplier < 1 { - bo.Multiplier = 2 - } - // Select a duration between zero and the current max. It might seem counterintuitive to - // have so much jitter, but https://www.awsarchitectureblog.com/2015/03/backoff.html - // argues that that is the best strategy. - d := time.Duration(rand.Int63n(int64(bo.cur))) - bo.cur = time.Duration(float64(bo.cur) * bo.Multiplier) - if bo.cur > bo.Max { - bo.cur = bo.Max - } - return d -} - -type grpcOpt []grpc.CallOption - -func (o grpcOpt) Resolve(s *CallSettings) { - s.GRPC = o -} +type Backoff = v2.Backoff func WithGRPCOptions(opt ...grpc.CallOption) CallOption { - return grpcOpt(append([]grpc.CallOption(nil), opt...)) + return v2.WithGRPCOptions(opt...) } -type CallSettings struct { - // Retry returns a Retryer to be used to control retry logic of a method call. - // If Retry is nil or the returned Retryer is nil, the call will not be retried. - Retry func() Retryer - - // CallOptions to be forwarded to GRPC. - GRPC []grpc.CallOption -} +type CallSettings = v2.CallSettings diff --git a/call_option_test.go b/call_option_test.go deleted file mode 100644 index 6f81305f..00000000 --- a/call_option_test.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2016, Google Inc. -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package gax - -import ( - "testing" - "time" - - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" -) - -var _ Retryer = &boRetryer{} - -func TestBackofDefault(t *testing.T) { - backoff := Backoff{} - - max := []time.Duration{1, 2, 4, 8, 16, 30, 30, 30, 30, 30} - for i, m := range max { - max[i] = m * time.Second - } - - for i, w := range max { - if d := backoff.Pause(); d > w { - t.Errorf("Backoff duration should be at most %s, got %s", w, d) - } else if i < len(max)-1 && backoff.cur != max[i+1] { - t.Errorf("current envelope is %s, want %s", backoff.cur, max[i+1]) - } - } -} - -func TestBackoffExponential(t *testing.T) { - backoff := Backoff{Initial: 1, Max: 20, Multiplier: 2} - want := []time.Duration{1, 2, 4, 8, 16, 20, 20, 20, 20, 20} - for _, w := range want { - if d := backoff.Pause(); d > w { - t.Errorf("Backoff duration should be at most %s, got %s", w, d) - } - } -} - -func TestOnCodes(t *testing.T) { - // Lint errors grpc.Errorf in 1.6. It mistakenly expects the first arg to Errorf to be a string. - errf := status.Errorf - apiErr := errf(codes.Unavailable, "") - tests := []struct { - c []codes.Code - retry bool - }{ - {nil, false}, - {[]codes.Code{codes.DeadlineExceeded}, false}, - {[]codes.Code{codes.DeadlineExceeded, codes.Unavailable}, true}, - {[]codes.Code{codes.Unavailable}, true}, - } - for _, tst := range tests { - b := OnCodes(tst.c, Backoff{}) - if _, retry := b.Retry(apiErr); retry != tst.retry { - t.Errorf("retriable codes: %v, error: %s, retry: %t, want %t", tst.c, apiErr, retry, tst.retry) - } - } -} diff --git a/gax.go b/gax.go index 8b2900e7..4613b430 100644 --- a/gax.go +++ b/gax.go @@ -35,4 +35,4 @@ // to simplify code generation and to provide more convenient and idiomatic API surfaces. package gax -const Version = "2.0.0" +const Version = "1.0.1" diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..dc69a07c --- /dev/null +++ b/go.mod @@ -0,0 +1,6 @@ +module github.com/googleapis/gax-go + +require ( + github.com/googleapis/gax-go/v2 v2.0.2 + google.golang.org/grpc v1.16.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..2fb5cc0a --- /dev/null +++ b/go.sum @@ -0,0 +1,28 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/googleapis/gax-go/v2 v2.0.2 h1:/rNgUniLy2vDXiK2xyJOcirGpC3G99dtK1NWx26WZ8Y= +github.com/googleapis/gax-go/v2 v2.0.2/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d h1:g9qWBGx4puODJTMVyoPrpoxPFgVGd+z1DZwjfRu4d0I= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522 h1:Ve1ORMCxvRmSXBwJK+t3Oy+V2vRW2OetUQBq4rJIkZE= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/grpc v1.16.0 h1:dz5IJGuC2BB7qXR5AyHNwAUBhZscK2xVez7mznh72sY= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/header.go b/header.go index d81455ec..2d434c84 100644 --- a/header.go +++ b/header.go @@ -1,24 +1,11 @@ package gax -import "bytes" +import v2 "github.com/googleapis/gax-go/v2" // XGoogHeader is for use by the Google Cloud Libraries only. // // XGoogHeader formats key-value pairs. // The resulting string is suitable for x-goog-api-client header. func XGoogHeader(keyval ...string) string { - if len(keyval) == 0 { - return "" - } - if len(keyval)%2 != 0 { - panic("gax.Header: odd argument count") - } - var buf bytes.Buffer - for i := 0; i < len(keyval); i += 2 { - buf.WriteByte(' ') - buf.WriteString(keyval[i]) - buf.WriteByte('/') - buf.WriteString(keyval[i+1]) - } - return buf.String()[1:] + return v2.XGoogHeader(keyval...) } diff --git a/header_test.go b/header_test.go deleted file mode 100644 index 05d8de6f..00000000 --- a/header_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package gax - -import "testing" - -func TestXGoogHeader(t *testing.T) { - for _, tst := range []struct { - kv []string - want string - }{ - {nil, ""}, - {[]string{"abc", "def"}, "abc/def"}, - {[]string{"abc", "def", "xyz", "123", "foo", ""}, "abc/def xyz/123 foo/"}, - } { - got := XGoogHeader(tst.kv...) - if got != tst.want { - t.Errorf("Header(%q) = %q, want %q", tst.kv, got, tst.want) - } - } -} diff --git a/invoke.go b/invoke.go index cb5cd2a9..c6582c66 100644 --- a/invoke.go +++ b/invoke.go @@ -32,58 +32,21 @@ package gax import ( "context" "time" + + v2 "github.com/googleapis/gax-go/v2" ) // A user defined call stub. -type APICall func(context.Context, CallSettings) error +type APICall = v2.APICall // Invoke calls the given APICall, // performing retries as specified by opts, if any. func Invoke(ctx context.Context, call APICall, opts ...CallOption) error { - var settings CallSettings - for _, opt := range opts { - opt.Resolve(&settings) - } - return invoke(ctx, call, settings, Sleep) + return v2.Invoke(ctx, call, opts...) } // Sleep is similar to time.Sleep, but it can be interrupted by ctx.Done() closing. // If interrupted, Sleep returns ctx.Err(). func Sleep(ctx context.Context, d time.Duration) error { - t := time.NewTimer(d) - select { - case <-ctx.Done(): - t.Stop() - return ctx.Err() - case <-t.C: - return nil - } -} - -type sleeper func(ctx context.Context, d time.Duration) error - -// invoke implements Invoke, taking an additional sleeper argument for testing. -func invoke(ctx context.Context, call APICall, settings CallSettings, sp sleeper) error { - var retryer Retryer - for { - err := call(ctx, settings) - if err == nil { - return nil - } - if settings.Retry == nil { - return err - } - if retryer == nil { - if r := settings.Retry(); r != nil { - retryer = r - } else { - return err - } - } - if d, ok := retryer.Retry(err); !ok { - return err - } else if err = sp(ctx, d); err != nil { - return err - } - } + return v2.Sleep(ctx, d) } diff --git a/invoke_test.go b/invoke_test.go deleted file mode 100644 index d476d14e..00000000 --- a/invoke_test.go +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright 2016, Google Inc. -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package gax - -import ( - "context" - "errors" - "testing" - "time" -) - -var canceledContext context.Context - -func init() { - ctx, cancel := context.WithCancel(context.Background()) - cancel() - canceledContext = ctx -} - -// recordSleeper is a test implementation of sleeper. -type recordSleeper int - -func (s *recordSleeper) sleep(ctx context.Context, _ time.Duration) error { - *s++ - return ctx.Err() -} - -type boolRetryer bool - -func (r boolRetryer) Retry(err error) (time.Duration, bool) { return 0, bool(r) } - -func TestInvokeSuccess(t *testing.T) { - apiCall := func(context.Context, CallSettings) error { return nil } - var sp recordSleeper - err := invoke(context.Background(), apiCall, CallSettings{}, sp.sleep) - - if err != nil { - t.Errorf("found error %s, want nil", err) - } - if sp != 0 { - t.Errorf("slept %d times, should not have slept since the call succeeded", int(sp)) - } -} - -func TestInvokeNoRetry(t *testing.T) { - apiErr := errors.New("foo error") - apiCall := func(context.Context, CallSettings) error { return apiErr } - var sp recordSleeper - err := invoke(context.Background(), apiCall, CallSettings{}, sp.sleep) - - if err != apiErr { - t.Errorf("found error %s, want %s", err, apiErr) - } - if sp != 0 { - t.Errorf("slept %d times, should not have slept since retry is not specified", int(sp)) - } -} - -func TestInvokeNilRetry(t *testing.T) { - apiErr := errors.New("foo error") - apiCall := func(context.Context, CallSettings) error { return apiErr } - var settings CallSettings - WithRetry(func() Retryer { return nil }).Resolve(&settings) - var sp recordSleeper - err := invoke(context.Background(), apiCall, settings, sp.sleep) - - if err != apiErr { - t.Errorf("found error %s, want %s", err, apiErr) - } - if sp != 0 { - t.Errorf("slept %d times, should not have slept since retry is not specified", int(sp)) - } -} - -func TestInvokeNeverRetry(t *testing.T) { - apiErr := errors.New("foo error") - apiCall := func(context.Context, CallSettings) error { return apiErr } - var settings CallSettings - WithRetry(func() Retryer { return boolRetryer(false) }).Resolve(&settings) - var sp recordSleeper - err := invoke(context.Background(), apiCall, settings, sp.sleep) - - if err != apiErr { - t.Errorf("found error %s, want %s", err, apiErr) - } - if sp != 0 { - t.Errorf("slept %d times, should not have slept since retry is not specified", int(sp)) - } -} - -func TestInvokeRetry(t *testing.T) { - const target = 3 - - retryNum := 0 - apiErr := errors.New("foo error") - apiCall := func(context.Context, CallSettings) error { - retryNum++ - if retryNum < target { - return apiErr - } - return nil - } - var settings CallSettings - WithRetry(func() Retryer { return boolRetryer(true) }).Resolve(&settings) - var sp recordSleeper - err := invoke(context.Background(), apiCall, settings, sp.sleep) - - if err != nil { - t.Errorf("found error %s, want nil, call should have succeeded after %d tries", err, target) - } - if sp != target-1 { - t.Errorf("retried %d times, want %d", int(sp), int(target-1)) - } -} - -func TestInvokeRetryTimeout(t *testing.T) { - apiErr := errors.New("foo error") - apiCall := func(context.Context, CallSettings) error { return apiErr } - var settings CallSettings - WithRetry(func() Retryer { return boolRetryer(true) }).Resolve(&settings) - var sp recordSleeper - - err := invoke(canceledContext, apiCall, settings, sp.sleep) - - if err != context.Canceled { - t.Errorf("found error %s, want %s", err, context.Canceled) - } -} diff --git a/v2/gax.go b/v2/gax.go index 8b2900e7..31915862 100644 --- a/v2/gax.go +++ b/v2/gax.go @@ -35,4 +35,4 @@ // to simplify code generation and to provide more convenient and idiomatic API surfaces. package gax -const Version = "2.0.0" +const Version = "2.0.1"