-
Notifications
You must be signed in to change notification settings - Fork 382
Description
Long Story Short
We cannot return both a value and an error for idempotent calls. I propose we use the gRPC error details as was discussed previously. I still want to know if an idempotent result was idempotent. Short of that, we have to drop the errors completely.
This is a P0 in my opinion. We need to nail down the expected behavior prior to v0.1.0.
Long Story Long
As @cpuguy83 has pointed out, returning a non-nil result with a non-nil error is not idiomatic Go. Turns out, Go gRPC doesn't even give you a choice. While the gRPC spec appears to support the idea of returning both data and errors, the Go implementation of gRPC does not. Using CreateVolume
as an example, this problem occurs in two places:
The Generated Client Code (link)
func (c *controllerClient) CreateVolume(ctx context.Context, in *CreateVolumeRequest, opts ...grpc.CallOption) (*CreateVolumeResponse, error) {
out := new(CreateVolumeResponse)
err := grpc.Invoke(ctx, "/csi.Controller/CreateVolume", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
Note that if the error is not nil, a nil response is always returned. The thing is, I thought I could side-step this by creating my own client:
type controllerClient struct {
csi.ControllerClient
cc *grpc.ClientConn
}
// NewControllerClient returns a new client for the CSI controller service.
func NewControllerClient(cc *grpc.ClientConn) csi.ControllerClient {
return &controllerClient{
ControllerClient: csi.NewControllerClient(cc),
cc: cc,
}
}
func (c *controllerClient) CreateVolume(
ctx context.Context,
in *csi.CreateVolumeRequest,
opts ...grpc.CallOption) (*csi.CreateVolumeResponse, error) {
out := new(csi.CreateVolumeResponse)
return out, grpc.Invoke(
ctx, "/csi.Controller/CreateVolume", in, out, c.cc, opts...)
}
This is the weird part -- it still doesn't work. So I did some more digging.
The gRPC Package Code (link)
err = sendRequest(ctx, cc.dopts, cc.dopts.cp, c, callHdr, stream, t, args, topts)
if err != nil {
if done != nil {
done(balancer.DoneInfo{
Err: err,
BytesSent: true,
BytesReceived: stream.BytesReceived(),
})
}
// Retry a non-failfast RPC when
// i) the server started to drain before this RPC was initiated.
// ii) the server refused the stream.
if !c.failFast && stream.Unprocessed() {
// In this case, the server did not receive the data, but we still
// created wire traffic, so we should not retry indefinitely.
if firstAyttempt {
// TODO: Add a field to header for grpc-transparent-retry-attempts
firstAttempt = false
continue
}
// Otherwise, give up and return an error anyway.
}
return toRPCErr(err)
}
err = recvResponse(ctx, cc.dopts, t, c, stream, reply)
If the invoker receives an error then the response isn't even received and marshalled.