| /* |
| Copyright 2017 Google LLC |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| package spanner |
| |
| import ( |
| "errors" |
| "fmt" |
| "testing" |
| "time" |
| |
| "github.com/golang/protobuf/proto" |
| "github.com/golang/protobuf/ptypes" |
| "golang.org/x/net/context" |
| edpb "google.golang.org/genproto/googleapis/rpc/errdetails" |
| "google.golang.org/grpc/codes" |
| "google.golang.org/grpc/metadata" |
| "google.golang.org/grpc/status" |
| ) |
| |
| // Test if runRetryable loop deals with various errors correctly. |
| func TestRetry(t *testing.T) { |
| if testing.Short() { |
| t.SkipNow() |
| } |
| responses := []error{ |
| status.Errorf(codes.Internal, "transport is closing"), |
| status.Errorf(codes.Unknown, "unexpected EOF"), |
| status.Errorf(codes.Internal, "stream terminated by RST_STREAM with error code: 2"), |
| status.Errorf(codes.Unavailable, "service is currently unavailable"), |
| errRetry(fmt.Errorf("just retry it")), |
| } |
| err := runRetryable(context.Background(), func(ct context.Context) error { |
| var r error |
| if len(responses) > 0 { |
| r = responses[0] |
| responses = responses[1:] |
| } |
| return r |
| }) |
| if err != nil { |
| t.Errorf("runRetryable should be able to survive all retryable errors, but it returns %v", err) |
| } |
| // Unretryable errors |
| injErr := errors.New("this is unretryable") |
| err = runRetryable(context.Background(), func(ct context.Context) error { |
| return injErr |
| }) |
| if wantErr := toSpannerError(injErr); !testEqual(err, wantErr) { |
| t.Errorf("runRetryable returns error %v, want %v", err, wantErr) |
| } |
| // Timeout |
| ctx, cancel := context.WithTimeout(context.Background(), time.Second) |
| defer cancel() |
| retryErr := errRetry(fmt.Errorf("still retrying")) |
| err = runRetryable(ctx, func(ct context.Context) error { |
| // Expect to trigger timeout in retryable runner after 10 executions. |
| <-time.After(100 * time.Millisecond) |
| // Let retryable runner to retry so that timeout will eventually happen. |
| return retryErr |
| }) |
| // Check error code and error message |
| if wantErrCode, wantErr := codes.DeadlineExceeded, errContextCanceled(ctx, retryErr); ErrCode(err) != wantErrCode || !testEqual(err, wantErr) { |
| t.Errorf("<err code, err>=\n<%v, %v>, want:\n<%v, %v>", ErrCode(err), err, wantErrCode, wantErr) |
| } |
| // Cancellation |
| ctx, cancel = context.WithCancel(context.Background()) |
| retries := 3 |
| retryErr = errRetry(fmt.Errorf("retry before cancel")) |
| err = runRetryable(ctx, func(ct context.Context) error { |
| retries-- |
| if retries == 0 { |
| cancel() |
| } |
| return retryErr |
| }) |
| // Check error code, error message, retry count |
| if wantErrCode, wantErr := codes.Canceled, errContextCanceled(ctx, retryErr); ErrCode(err) != wantErrCode || !testEqual(err, wantErr) || retries != 0 { |
| t.Errorf("<err code, err, retries>=\n<%v, %v, %v>, want:\n<%v, %v, %v>", ErrCode(err), err, retries, wantErrCode, wantErr, 0) |
| } |
| } |
| |
| func TestRetryInfo(t *testing.T) { |
| b, _ := proto.Marshal(&edpb.RetryInfo{ |
| RetryDelay: ptypes.DurationProto(time.Second), |
| }) |
| trailers := map[string]string{ |
| retryInfoKey: string(b), |
| } |
| gotDelay, ok := extractRetryDelay(errRetry(toSpannerErrorWithMetadata(status.Errorf(codes.Aborted, ""), metadata.New(trailers)))) |
| if !ok || !testEqual(time.Second, gotDelay) { |
| t.Errorf("<ok, retryDelay> = <%t, %v>, want <true, %v>", ok, gotDelay, time.Second) |
| } |
| } |