Google Cloud Client Library for Goはデフォルトでリトライするので、あまり意識する必要はないと思いますが、場合によってはリトライを細かく制御したくなることはあるかもしれません。この記事では、リトライのために必要そうなオプションをまとめました。
リトライ
クライアントライブラリで実装された一部のAPI、例えばmonitoring.MetricClient.ListTimeSeriesなど、可変長引数でgax.CallOptionを取るものがあります。
import ( "context" gax "github.com/googleapis/gax-go/v2" monitoringpb "google.golang.org/genproto/googleapis/monitoring/v3" ) func (c *MetricClient) ListTimeSeries(ctx context.Context, req *monitoringpb.ListTimeSeriesRequest, opts ...gax.CallOption) *TimeSeriesIterator
これらの関数はgax.CallOptionとしてリトライオプションを持っていて、gax.WithRetryにRetryerを渡すことでオプションを生成します。Functional Optionパターンな実装になっていますね。
package gax import "time" 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 CallOption interface { Resolve(cs *CallSettings) } type Retryer interface { Retry(err error) (pause time.Duration, shouldRetry bool) } func WithRetry(fn func() Retryer) CallOption
Retryerを自分で全部実装するのは意外と面倒なんですが、指数バックオフを行うRetryerはクライアントライブラリが用意してくれているので、これを使うといいでしょう。
import ( "time" gax "github.com/googleapis/gax-go/v2" "google.golang.org/grpc/codes" ) var RetryableCodes = []codes.Code{ codes.Canceled, codes.Unknown, codes.DeadlineExceeded, codes.ResourceExhausted, codes.Aborted, codes.Internal, codes.Unavailable, codes.DataLoss, } func DefaultRetryOption() gax.Retryer { // This configuration performs to retry 3 times; 200ms, 400ms, 800ms return gax.OnCodes(RetryableCodes, gax.Backoff{ Initial: 200 * time.Millisecond, Max: 1 * time.Second, Multiplier: 2.0, }) } c.ListTimeSeries(ctx, &monitoringpb.ListTimeSeriesRequest{...}, gax.WithRetry(DefaultRetryOption))
ところで、上に引用したCallSettingsのコメントでは、Retryがnilの場合はリトライしないと書いていますが、monitoring.MetricClientなどクライアントライブラリで実装されたクライアントは、デフォルトでリトライオプションを持っているので、デフォルトで支障がなければオプションを渡す必要はありません。具体的には、上で例に挙げたListTimeSeriesのデフォルトは以下のような内容です。
import ( "time" gax "github.com/googleapis/gax-go/v2" "google.golang.org/grpc/codes" ) gax.WithRetry(func() gax.Retryer { return gax.OnCodes([]codes.Code{ codes.DeadlineExceeded, codes.Unavailable, }, gax.Backoff{ Initial: 100 * time.Millisecond, Max: 30000 * time.Millisecond, Multiplier: 1.30, }) })
このデフォルト値は、それぞれのクライアント構造体メンバー変数に持っています。以下はmonitoring.MetricClientの例ですが、他のクライアントもだいたい同じ作りになっているようにみえます。
type MetricClient struct { CallOptions *MetricCallOptions } type MetricCallOptions struct { ListMonitoredResourceDescriptors []gax.CallOption GetMonitoredResourceDescriptor []gax.CallOption ListMetricDescriptors []gax.CallOption GetMetricDescriptor []gax.CallOption CreateMetricDescriptor []gax.CallOption DeleteMetricDescriptor []gax.CallOption ListTimeSeries []gax.CallOption CreateTimeSeries []gax.CallOption }
エラーの詳細を取得する
Retryerでリトライしても解決しない場合、エラーの内容によって、メッセージキューに戻すか破棄するか、など判断したいことがあるかもしれません。その場合、クライアントライブラリはほとんどの場合にgRPCのエラーを返すので、status.FromErrorなどを使うとgRPCのステータスコードなど詳細を調べられます。
import "google.golang.org/grpc/status" s, ok := status.FromError(err) // status.Convertでも良い if !ok { return } code := s.Code()
ただし、アカウントの認証に失敗したとか、APIが無効になっているなどgRPCへ到達する前のエラーが発生することもありますが、こういったエラーではstatus.FromErrorを使っても詳細を取得できません。その場合はgoogleapi.Errorを使うと、HTTP/1.1ステータスコードなどの詳細な情報を確認できます。
import ( "errors" "google.golang.org/api/googleapi" ) var e *googleapi.Error if errors.As(err, &e) { return e.Code }
codes.Codeのコメントによると、gRPCのエラーコードはHTTP/1.1のステータスコードに置き換え可能なので、まとめて扱えるようにしておくと便利かもしれませんね。
import ( "errors" "net/http" "google.golang.org/api/googleapi" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) // https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto var codeMappings = map[codes.Code]int{ codes.OK: http.StatusOK, codes.Canceled: 499, // Go 1.16のnet/httpには定数がない codes.Unknown: http.StatusInternalServerError, codes.InvalidArgument: http.StatusBadRequest, codes.DeadlineExceeded: http.StatusGatewayTimeout, codes.NotFound: http.StatusNotFound, codes.AlreadyExists: http.StatusConflict, codes.PermissionDenied: http.StatusForbidden, codes.ResourceExhausted: http.StatusTooManyRequests, codes.FailedPrecondition: http.StatusBadRequest, codes.Aborted: http.StatusConflict, codes.OutOfRange: http.StatusBadRequest, codes.Unimplemented: http.StatusNotImplemented, codes.Internal: http.StatusInternalServerError, codes.Unavailable: http.StatusServiceUnavailable, codes.DataLoss: http.StatusInternalServerError, codes.Unauthenticated: http.StatusUnauthorized, } // Code returns HTTP/1.1 Status Code. func Code(err error) int { if err == nil { return http.StatusOK } if v := errors.Unwrap(err); v != nil { err = v } var e *googleapi.Error if errors.As(err, &e) { return e.Code } s, ok := status.FromError(err) if !ok { return http.StatusInternalServerError } c, ok := codeMappings[s.Code()] if !ok { return http.StatusInternalServerError } return c } // IsPermissionError returns true if err is an error categorised of permission denied. func IsPermissionError(err error) bool { switch Code(err) { case http.StatusUnauthorized, http.StatusForbidden: return true default: return false } }
他にも何か気づいたら追記します。おわり。