github.com/bazelbuild/remote-apis-sdks@v0.0.0-20240425170053-8a36686a6350/go/pkg/client/retries_test.go (about)

     1  package client_test
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"net"
     8  	"os"
     9  	"path/filepath"
    10  	"reflect"
    11  	"strings"
    12  	"sync"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/client"
    17  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/digest"
    18  	"github.com/google/go-cmp/cmp"
    19  	"github.com/google/go-cmp/cmp/cmpopts"
    20  	"github.com/klauspost/compress/zstd"
    21  	"google.golang.org/grpc"
    22  	"google.golang.org/grpc/codes"
    23  	"google.golang.org/grpc/status"
    24  
    25  	// Redundant imports are required for the google3 mirror. Aliases should not be changed.
    26  	regrpc "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2"
    27  	repb "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2"
    28  	bsgrpc "google.golang.org/genproto/googleapis/bytestream"
    29  	bspb "google.golang.org/genproto/googleapis/bytestream"
    30  	opgrpc "google.golang.org/genproto/googleapis/longrunning"
    31  	oppb "google.golang.org/genproto/googleapis/longrunning"
    32  	anypb "google.golang.org/protobuf/types/known/anypb"
    33  	emptypb "google.golang.org/protobuf/types/known/emptypb"
    34  )
    35  
    36  var zstdEncoder, _ = zstd.NewWriter(nil, zstd.WithZeroFrames(true))
    37  
    38  type flakyServer struct {
    39  	// TODO(jsharpe): This is a hack to work around WaitOperation not existing in some versions of
    40  	// the long running operations API that we need to support.
    41  	opgrpc.OperationsServer
    42  	mu               sync.RWMutex     // Protects numCalls.
    43  	numCalls         map[string]int   // A counter of calls the server encountered thus far, by method.
    44  	muOffset         sync.RWMutex     // Protects initialOffsets.
    45  	initialOffsets   map[string]int64 // Stores an initial offset to verify if retries are started over from a correct offset.
    46  	retriableForever bool             // Set to true to make the flaky server return a retriable error forever, rather than eventually a non-retriable error.
    47  	sleepDelay       time.Duration    // How long to sleep on each RPC.
    48  	useBSCompression bool             // Whether to use/expect compression on ByteStream calls.
    49  }
    50  
    51  func (f *flakyServer) incNumCalls(method string) int {
    52  	f.mu.Lock()
    53  	defer f.mu.Unlock()
    54  	f.numCalls[method]++
    55  	return f.numCalls[method]
    56  }
    57  
    58  func (f *flakyServer) setInitialOffset(name string, offset int64) {
    59  	f.muOffset.Lock()
    60  	defer f.muOffset.Unlock()
    61  	f.initialOffsets[name] = offset
    62  }
    63  
    64  func (f *flakyServer) fetchInitialOffset(name string) int64 {
    65  	f.muOffset.Lock()
    66  	defer f.muOffset.Unlock()
    67  	offset, ok := f.initialOffsets[name]
    68  	if !ok {
    69  		return 0
    70  	}
    71  	return offset
    72  }
    73  
    74  func (f *flakyServer) Write(stream bsgrpc.ByteStream_WriteServer) error {
    75  	numCalls := f.incNumCalls("Write")
    76  	if numCalls < 3 {
    77  		time.Sleep(f.sleepDelay)
    78  		return status.Error(codes.Canceled, "transient error!")
    79  	}
    80  
    81  	req, err := stream.Recv()
    82  	if err != nil {
    83  		return err
    84  	}
    85  	// Verify that the client sends the first chunk, because they should retry from scratch.
    86  	initialOffset := f.fetchInitialOffset(req.ResourceName)
    87  	if req.WriteOffset != initialOffset || req.FinishWrite {
    88  		return status.Error(codes.FailedPrecondition, fmt.Sprintf("expected first chunk, got %v", req))
    89  	}
    90  	if numCalls < 5 {
    91  		return status.Error(codes.Internal, "another transient error!")
    92  	}
    93  	return stream.SendAndClose(&bspb.WriteResponse{CommittedSize: 4})
    94  }
    95  
    96  func (f *flakyServer) Read(req *bspb.ReadRequest, stream bsgrpc.ByteStream_ReadServer) error {
    97  	numCalls := f.incNumCalls("Read")
    98  	if numCalls < 3 {
    99  		time.Sleep(f.sleepDelay)
   100  		return status.Error(codes.Canceled, "transient error!")
   101  	}
   102  	if numCalls < 4 {
   103  		b := []byte("bl")
   104  		if f.useBSCompression {
   105  			b = zstdEncoder.EncodeAll(b, nil)
   106  		}
   107  		// We send the 4 byte test blob in two chunks.
   108  		if err := stream.Send(&bspb.ReadResponse{Data: b}); err != nil {
   109  			return err
   110  		}
   111  		return status.Error(codes.Internal, "another transient error!")
   112  	}
   113  	// Client now will only ask for the remaining two bytes.
   114  	if numCalls < 5 {
   115  		time.Sleep(f.sleepDelay)
   116  		return status.Error(codes.Aborted, "yet another transient error!")
   117  	}
   118  	b := []byte("ob")
   119  	if f.useBSCompression {
   120  		b = zstdEncoder.EncodeAll(b, nil)
   121  	}
   122  	return stream.Send(&bspb.ReadResponse{Data: b})
   123  }
   124  
   125  func (f *flakyServer) flakeAndFail(method string) error {
   126  	numCalls := f.incNumCalls(method)
   127  	if numCalls == 1 {
   128  		if f.sleepDelay != 0 {
   129  			time.Sleep(f.sleepDelay)
   130  			// The error we return here should not matter; the deadline should have passed by now and the
   131  			// retrier should retry DeadlineExceeded.
   132  			return status.Error(codes.InvalidArgument, "non retriable error")
   133  		}
   134  		return status.Error(codes.DeadlineExceeded, "transient error!")
   135  	}
   136  	if f.retriableForever || numCalls < 4 {
   137  		return status.Error(codes.Canceled, "transient error!")
   138  	}
   139  	return status.Error(codes.Unimplemented, "a non retriable error")
   140  }
   141  
   142  func (f *flakyServer) QueryWriteStatus(context.Context, *bspb.QueryWriteStatusRequest) (*bspb.QueryWriteStatusResponse, error) {
   143  	return nil, f.flakeAndFail("QueryWriteStatus")
   144  }
   145  
   146  func (f *flakyServer) GetActionResult(ctx context.Context, req *repb.GetActionResultRequest) (*repb.ActionResult, error) {
   147  	return nil, f.flakeAndFail("GetActionResult")
   148  }
   149  
   150  func (f *flakyServer) UpdateActionResult(ctx context.Context, req *repb.UpdateActionResultRequest) (*repb.ActionResult, error) {
   151  	return nil, f.flakeAndFail("UpdateActionResult")
   152  }
   153  
   154  func (f *flakyServer) FindMissingBlobs(ctx context.Context, req *repb.FindMissingBlobsRequest) (*repb.FindMissingBlobsResponse, error) {
   155  	return nil, f.flakeAndFail("FindMissingBlobs")
   156  }
   157  
   158  func (f *flakyServer) BatchUpdateBlobs(ctx context.Context, req *repb.BatchUpdateBlobsRequest) (*repb.BatchUpdateBlobsResponse, error) {
   159  	return nil, f.flakeAndFail("BatchUpdateBlobs")
   160  }
   161  
   162  func (f *flakyServer) BatchReadBlobs(ctx context.Context, req *repb.BatchReadBlobsRequest) (*repb.BatchReadBlobsResponse, error) {
   163  	return nil, f.flakeAndFail("BatchReadBlobs")
   164  }
   165  
   166  func (f *flakyServer) GetTree(req *repb.GetTreeRequest, stream regrpc.ContentAddressableStorage_GetTreeServer) error {
   167  	numCalls := f.incNumCalls("GetTree")
   168  	if numCalls < 3 {
   169  		return status.Error(codes.Canceled, "transient error!")
   170  	}
   171  	if numCalls < 4 {
   172  		// Send one directory then cut the stream.
   173  		resp := &repb.GetTreeResponse{
   174  			Directories:   []*repb.Directory{{Files: []*repb.FileNode{{Name: "I'm a file!"}}}},
   175  			NextPageToken: "I should be a base64-encoded token, but I'm not",
   176  		}
   177  		if err := stream.Send(resp); err != nil {
   178  			return err
   179  		}
   180  		return status.Error(codes.Internal, "another transient error!")
   181  	}
   182  	// Client now will only ask for the remaining directories.
   183  	if numCalls < 5 {
   184  		return status.Error(codes.Aborted, "yet another transient error!")
   185  	}
   186  	resp := &repb.GetTreeResponse{
   187  		Directories: []*repb.Directory{{Files: []*repb.FileNode{{Name: "I, too, am a file."}}}},
   188  	}
   189  	return stream.Send(resp)
   190  }
   191  
   192  func (f *flakyServer) Execute(req *repb.ExecuteRequest, stream regrpc.Execution_ExecuteServer) error {
   193  	numCalls := f.incNumCalls("Execute")
   194  	if numCalls < 2 {
   195  		return status.Error(codes.Canceled, "transient error!")
   196  	}
   197  	stream.Send(&oppb.Operation{Done: false, Name: "dummy"})
   198  	// After this error, retries should to go the WaitExecution method.
   199  	return status.Error(codes.Internal, "another transient error!")
   200  }
   201  
   202  func (f *flakyServer) WaitExecution(req *repb.WaitExecutionRequest, stream regrpc.Execution_WaitExecutionServer) error {
   203  	numCalls := f.incNumCalls("WaitExecution")
   204  	if numCalls < 2 {
   205  		return status.Error(codes.Canceled, "transient error!")
   206  	}
   207  	if numCalls < 4 {
   208  		stream.Send(&oppb.Operation{Done: false, Name: "dummy"})
   209  		return status.Error(codes.Internal, "another transient error!")
   210  	}
   211  	// Execute (above) will fail twice (and be retried twice) before ExecuteAndWait() switches to
   212  	// WaitExecution. WaitExecution will fail 4 more times more before succeeding, for a total of 6 retries.
   213  	execResp := &repb.ExecuteResponse{Status: status.New(codes.Aborted, "transient operation failure!").Proto()}
   214  	any, e := anypb.New(execResp)
   215  	if e != nil {
   216  		return e
   217  	}
   218  	return stream.Send(&oppb.Operation{Name: "op", Done: true, Result: &oppb.Operation_Response{Response: any}})
   219  }
   220  
   221  func (f *flakyServer) GetOperation(ctx context.Context, req *oppb.GetOperationRequest) (*oppb.Operation, error) {
   222  	return nil, f.flakeAndFail("GetOperation")
   223  }
   224  
   225  func (f *flakyServer) ListOperations(ctx context.Context, req *oppb.ListOperationsRequest) (*oppb.ListOperationsResponse, error) {
   226  	return nil, f.flakeAndFail("ListOperations")
   227  }
   228  
   229  func (f *flakyServer) CancelOperation(ctx context.Context, req *oppb.CancelOperationRequest) (*emptypb.Empty, error) {
   230  	return nil, f.flakeAndFail("CancelOperation")
   231  }
   232  
   233  func (f *flakyServer) DeleteOperation(ctx context.Context, req *oppb.DeleteOperationRequest) (*emptypb.Empty, error) {
   234  	return nil, f.flakeAndFail("DeleteOperation")
   235  }
   236  
   237  type flakyFixture struct {
   238  	client   *client.Client
   239  	listener net.Listener
   240  	server   *grpc.Server
   241  	fake     *flakyServer
   242  	ctx      context.Context
   243  }
   244  
   245  func setup(t *testing.T) *flakyFixture {
   246  	f := &flakyFixture{ctx: context.Background()}
   247  	var err error
   248  	f.listener, err = net.Listen("tcp", ":0")
   249  	if err != nil {
   250  		t.Fatalf("Cannot listen: %v", err)
   251  	}
   252  	f.server = grpc.NewServer()
   253  	f.fake = &flakyServer{numCalls: make(map[string]int), initialOffsets: make(map[string]int64)}
   254  	bsgrpc.RegisterByteStreamServer(f.server, f.fake)
   255  	regrpc.RegisterActionCacheServer(f.server, f.fake)
   256  	regrpc.RegisterContentAddressableStorageServer(f.server, f.fake)
   257  	regrpc.RegisterExecutionServer(f.server, f.fake)
   258  	opgrpc.RegisterOperationsServer(f.server, f.fake)
   259  	go f.server.Serve(f.listener)
   260  	f.client, err = client.NewClient(f.ctx, instance, client.DialParams{
   261  		Service:    f.listener.Addr().String(),
   262  		NoSecurity: true,
   263  	}, client.StartupCapabilities(false), client.ChunkMaxSize(2))
   264  	if err != nil {
   265  		t.Fatalf("Error connecting to server: %v", err)
   266  	}
   267  	return f
   268  }
   269  
   270  func (f *flakyFixture) shutDown() {
   271  	f.client.Close()
   272  	f.listener.Close()
   273  	f.server.Stop()
   274  }
   275  
   276  func compressionBoolToValue(use bool) client.CompressedBytestreamThreshold {
   277  	if use {
   278  		return client.CompressedBytestreamThreshold(0)
   279  	}
   280  	return client.CompressedBytestreamThreshold(-1)
   281  }
   282  
   283  func TestWriteRetries(t *testing.T) {
   284  	t.Parallel()
   285  	for _, sleep := range []bool{false, true} {
   286  		sleep := sleep
   287  		t.Run(fmt.Sprintf("sleep=%t", sleep), func(t *testing.T) {
   288  			t.Parallel()
   289  			f := setup(t)
   290  			defer f.shutDown()
   291  			if sleep {
   292  				f.fake.sleepDelay = time.Second
   293  				client.RPCTimeouts(map[string]time.Duration{"default": 500 * time.Millisecond}).Apply(f.client)
   294  			}
   295  
   296  			blob := []byte("blob")
   297  			gotDg, err := f.client.WriteBlob(f.ctx, blob)
   298  			if err != nil {
   299  				t.Errorf("client.WriteBlob(ctx, blob) gave error %s, wanted nil", err)
   300  			}
   301  			if diff := cmp.Diff(digest.NewFromBlob(blob), gotDg); diff != "" {
   302  				t.Errorf("client.WriteBlob(ctx, blob) had diff on digest returned (want -> got):\n%s", diff)
   303  			}
   304  		})
   305  	}
   306  }
   307  
   308  func TestRetryWriteBytesAtRemoteOffset(t *testing.T) {
   309  	tests := []struct {
   310  		description   string
   311  		initialOffset int64
   312  	}{
   313  		{
   314  			description:   "offset 0",
   315  			initialOffset: 0,
   316  		},
   317  		{
   318  			description:   "offset 3",
   319  			initialOffset: 3,
   320  		},
   321  	}
   322  
   323  	for _, doNotFinalize := range []bool{true, false} {
   324  		for _, test := range tests {
   325  			t.Run(fmt.Sprintf("%s and doNotFinalize %t", test.description, doNotFinalize), func(t *testing.T) {
   326  				f := setup(t)
   327  				defer f.shutDown()
   328  				name := test.description
   329  				data := []byte("Hello World!")
   330  				if test.initialOffset > 0 {
   331  					f.fake.setInitialOffset(name, test.initialOffset)
   332  				}
   333  
   334  				writtenBytes, err := f.client.WriteBytesAtRemoteOffset(f.ctx, name, data[test.initialOffset:], doNotFinalize, test.initialOffset)
   335  				if err != nil {
   336  					t.Errorf("client.WriteBytesAtRemoteOffset(ctx, name, %s, %t, %d) gave error %s, want nil", string(data), doNotFinalize, test.initialOffset, err)
   337  				}
   338  				if int64(len(data))-test.initialOffset != writtenBytes {
   339  					t.Errorf("client.WriteBytesAtRemoteOffset(ctx, name, %s,  %t, %d) gave %d byte(s), want %d", string(data), doNotFinalize, test.initialOffset, writtenBytes, int64(len(data))-test.initialOffset)
   340  				}
   341  			})
   342  		}
   343  	}
   344  }
   345  
   346  func TestReadRetries(t *testing.T) {
   347  	t.Parallel()
   348  	for _, sleep := range []bool{false, true} {
   349  		for _, comp := range []bool{false, true} {
   350  			sleep := sleep
   351  			comp := comp
   352  			t.Run(fmt.Sprintf("sleep=%t,comp=%t", sleep, comp), func(t *testing.T) {
   353  				t.Parallel()
   354  				f := setup(t)
   355  				defer f.shutDown()
   356  				f.fake.useBSCompression = comp
   357  				compOpt := compressionBoolToValue(comp)
   358  				compOpt.Apply(f.client)
   359  				if sleep {
   360  					f.fake.sleepDelay = time.Second
   361  					client.RPCTimeouts(map[string]time.Duration{"default": 500 * time.Millisecond}).Apply(f.client)
   362  				}
   363  
   364  				blob := []byte("blob")
   365  				got, _, err := f.client.ReadBlob(f.ctx, digest.NewFromBlob(blob))
   366  				if err != nil {
   367  					t.Errorf("client.ReadBlob(ctx, digest) gave error %s, want nil", err)
   368  				}
   369  				if diff := cmp.Diff(blob, got, cmpopts.EquateEmpty()); diff != "" {
   370  					t.Errorf("client.ReadBlob(ctx, digest) gave diff (-want, +got):\n%s", diff)
   371  				}
   372  			})
   373  		}
   374  	}
   375  }
   376  
   377  func TestReadToFileRetries(t *testing.T) {
   378  	t.Parallel()
   379  	for _, sleep := range []bool{false, true} {
   380  		for _, comp := range []bool{false, true} {
   381  			sleep := sleep
   382  			comp := comp
   383  			t.Run(fmt.Sprintf("sleep=%t", sleep), func(t *testing.T) {
   384  				t.Parallel()
   385  				f := setup(t)
   386  				defer f.shutDown()
   387  				f.fake.useBSCompression = comp
   388  				compOpt := compressionBoolToValue(comp)
   389  				compOpt.Apply(f.client)
   390  
   391  				if sleep {
   392  					f.fake.sleepDelay = time.Second
   393  					client.RPCTimeouts(map[string]time.Duration{"default": 500 * time.Millisecond}).Apply(f.client)
   394  				}
   395  
   396  				blob := []byte("blob")
   397  				path := filepath.Join(t.TempDir(), strings.Replace(t.Name(), "/", "_", -1))
   398  				stats, err := f.client.ReadBlobToFile(f.ctx, digest.NewFromBlob(blob), path)
   399  				if err != nil {
   400  					t.Errorf("client.ReadBlobToFile(ctx, digest) gave error %s, want nil", err)
   401  				}
   402  				if stats.LogicalMoved != int64(len(blob)) {
   403  					t.Errorf("client.ReadBlobToFile(ctx, digest) returned %d read bytes, wanted %d", stats.LogicalMoved, len(blob))
   404  				}
   405  				if comp && stats.LogicalMoved == stats.RealMoved {
   406  					t.Errorf("client.ReadBlobToFile(ctx, digest) = %v - compression on but same real and logical bytes", stats)
   407  				}
   408  
   409  				contents, err := os.ReadFile(path)
   410  				if err != nil {
   411  					t.Errorf("error reading from %s: %v", path, err)
   412  				}
   413  				if !bytes.Equal(contents, blob) {
   414  					t.Errorf("expected %s to contain %v, got %v", path, blob, contents)
   415  				}
   416  			})
   417  		}
   418  	}
   419  }
   420  
   421  // Verify for one arbitrary method that when retries are exhausted, we get the retriable error code
   422  // back.
   423  func TestBatchWriteBlobsRpcRetriesExhausted(t *testing.T) {
   424  	t.Parallel()
   425  	f := setup(t)
   426  	f.fake.retriableForever = true
   427  	defer f.shutDown()
   428  
   429  	blobs := map[digest.Digest][]byte{
   430  		digest.TestNew("a", 1): []byte{1},
   431  		digest.TestNew("b", 1): []byte{2},
   432  	}
   433  	err := f.client.BatchWriteBlobs(f.ctx, blobs)
   434  	if err == nil {
   435  		t.Error("BatchWriteBlobs(ctx, {}) = nil; expected Canceled error got nil")
   436  	} else if s, ok := status.FromError(err); ok && s.Code() != codes.Canceled {
   437  		t.Errorf("BatchWriteBlobs(ctx, {}) = %v; expected Canceled error, got %v", err, s.Code())
   438  	} else if !ok {
   439  		t.Errorf("BatchWriteBlobs(ctx, {}) = %v; expected Canceled error (status.FromError failed)", err)
   440  	}
   441  }
   442  
   443  func TestGetTreeRetries(t *testing.T) {
   444  	t.Parallel()
   445  	f := setup(t)
   446  	defer f.shutDown()
   447  
   448  	blob := []byte("blob")
   449  	got, err := f.client.GetDirectoryTree(f.ctx, digest.NewFromBlob(blob).ToProto())
   450  	if err != nil {
   451  		t.Errorf("client.GetDirectoryTree(ctx, digest) gave err %s, want nil", err)
   452  	}
   453  	if len(got) != 2 {
   454  		t.Errorf("client.GetDirectoryTree(ctx, digest) gave %d directories, want 2", len(got))
   455  	}
   456  }
   457  
   458  func TestExecuteAndWaitRetries(t *testing.T) {
   459  	t.Parallel()
   460  	f := setup(t)
   461  	defer f.shutDown()
   462  
   463  	op, err := f.client.ExecuteAndWait(f.ctx, &repb.ExecuteRequest{})
   464  	if err != nil {
   465  		t.Fatalf("client.WaitExecution(ctx, {}) = %v", err)
   466  	}
   467  	st := client.OperationStatus(op)
   468  	if st == nil {
   469  		t.Errorf("client.WaitExecution(ctx, {}) returned no status, expected Aborted")
   470  	}
   471  	if st != nil && st.Code() != codes.Aborted {
   472  		t.Errorf("client.WaitExecution(ctx, {}) returned unexpected status code %s", st.Code())
   473  	}
   474  	// 2 separate transient Execute errors.
   475  	if f.fake.numCalls["Execute"] != 2 {
   476  		t.Errorf("Expected 2 Execute calls, got %v", f.fake.numCalls["Execute"])
   477  	}
   478  	// 3 separate transient WaitExecution errors + the final successful call.
   479  	if f.fake.numCalls["WaitExecution"] != 4 {
   480  		t.Errorf("Expected 4 WaitExecution calls, got %v", f.fake.numCalls["WaitExecution"])
   481  	}
   482  }
   483  
   484  func TestNonStreamingRpcRetries(t *testing.T) {
   485  	t.Parallel()
   486  	testcases := []struct {
   487  		name string
   488  		rpc  func(*flakyFixture) (interface{}, error)
   489  	}{
   490  		{
   491  			name: "QueryWriteStatus",
   492  			rpc: func(f *flakyFixture) (interface{}, error) {
   493  				return f.client.QueryWriteStatus(f.ctx, &bspb.QueryWriteStatusRequest{})
   494  			},
   495  		},
   496  		{
   497  			name: "GetActionResult",
   498  			rpc: func(f *flakyFixture) (interface{}, error) {
   499  				return f.client.GetActionResult(f.ctx, &repb.GetActionResultRequest{})
   500  			},
   501  		},
   502  		{
   503  			name: "UpdateActionResult",
   504  			rpc: func(f *flakyFixture) (interface{}, error) {
   505  				return f.client.UpdateActionResult(f.ctx, &repb.UpdateActionResultRequest{})
   506  			},
   507  		},
   508  		{
   509  			name: "FindMissingBlobs",
   510  			rpc: func(f *flakyFixture) (interface{}, error) {
   511  				return f.client.FindMissingBlobs(f.ctx, &repb.FindMissingBlobsRequest{})
   512  			},
   513  		},
   514  		{
   515  			name: "BatchUpdateBlobs",
   516  			rpc: func(f *flakyFixture) (interface{}, error) {
   517  				return f.client.BatchUpdateBlobs(f.ctx, &repb.BatchUpdateBlobsRequest{})
   518  			},
   519  		},
   520  		{
   521  			name: "GetOperation",
   522  			rpc: func(f *flakyFixture) (interface{}, error) {
   523  				return f.client.GetOperation(f.ctx, &oppb.GetOperationRequest{})
   524  			},
   525  		},
   526  		{
   527  			name: "ListOperations",
   528  			rpc: func(f *flakyFixture) (interface{}, error) {
   529  				return f.client.ListOperations(f.ctx, &oppb.ListOperationsRequest{})
   530  			},
   531  		},
   532  		{
   533  			name: "CancelOperation",
   534  			rpc: func(f *flakyFixture) (interface{}, error) {
   535  				return f.client.CancelOperation(f.ctx, &oppb.CancelOperationRequest{})
   536  			},
   537  		},
   538  		{
   539  			name: "DeleteOperation",
   540  			rpc: func(f *flakyFixture) (interface{}, error) {
   541  				return f.client.DeleteOperation(f.ctx, &oppb.DeleteOperationRequest{})
   542  			},
   543  		},
   544  	}
   545  	for _, tc := range testcases {
   546  		tc := tc
   547  		t.Run(tc.name, func(t *testing.T) {
   548  			t.Parallel()
   549  			f := setup(t)
   550  			defer f.shutDown()
   551  
   552  			got, err := tc.rpc(f)
   553  			if !reflect.ValueOf(got).IsNil() {
   554  				t.Errorf("%s(ctx, {}) gave result %s, want nil", tc.name, got)
   555  			}
   556  			if err == nil {
   557  				t.Errorf("%s(ctx, {}) = nil; expected Unimplemented error got nil", tc.name)
   558  			} else if s, ok := status.FromError(err); ok && s.Code() != codes.Unimplemented {
   559  				t.Errorf("%s(ctx, {}) = %v; expected Unimplemented error, got %v", tc.name, err, s.Code())
   560  			} else if !ok {
   561  				t.Errorf("%s(ctx, {}) = %v; expected Unimplemented error (status.FromError failed)", tc.name, err)
   562  			}
   563  		})
   564  	}
   565  }
   566  
   567  func TestNonStreamingRpcRetriesSleep(t *testing.T) {
   568  	t.Parallel()
   569  	f := setup(t)
   570  	defer f.shutDown()
   571  	f.fake.sleepDelay = time.Second
   572  	client.RPCTimeouts(map[string]time.Duration{"QueryWriteStatus": 500 * time.Millisecond}).Apply(f.client)
   573  
   574  	got, err := f.client.QueryWriteStatus(f.ctx, &bspb.QueryWriteStatusRequest{})
   575  	if got != nil {
   576  		t.Errorf("client.QueryWriteStatus(ctx, digest) gave result %s, want nil", got)
   577  	}
   578  	if err == nil {
   579  		t.Error("QueryWriteStatus(ctx, {}) = nil; expected Unimplemented error got nil")
   580  	} else if s, ok := status.FromError(err); ok && s.Code() != codes.Unimplemented {
   581  		t.Errorf("QueryWriteStatus(ctx, {}) = %v; expected Unimplemented error, got %v", err, s.Code())
   582  	} else if !ok {
   583  		t.Errorf("QueryWriteStatus(ctx, {}) = %v; expected Unimplemented error (status.FromError failed)", err)
   584  	}
   585  }