↪️

Google Cloud Client Libraries for Go の Retry 処理のコードを追ってみる

2022/09/11に公開

Google Cloud Client Libraries for Go を使って Google Cloud APIs を呼び出すとき(ネットワークの問題などで)エラーになった場合、デフォルトでリトライします。

裏で何が起こっているか知りたいと思い、コードを追ってみようと思いました。

Retryの処理までコードリーディング

PubSub パッケージの Publish メソッドから追ってみようと思います。

https://github.com/googleapis/google-cloud-go/blob/pubsub/v1.24.0/pubsub/topic.go#L531

topic.go
func (t *Topic) Publish(ctx context.Context, msg *Message) *PublishResult {
  // (略)
  t.initBundler()
  // (略)
}
topic.go
func (t *Topic) initBundler() {
  // (略)
  t.publishMessageBundle(ctx, bundle.([]*bundledMessage))
  // (略)
}
topic.go
func (t *Topic) publishMessageBundle(ctx context.Context, bms []*bundledMessage) {
  // (略)
		// Apply custom publish retryer on top of user specified retryer and
		// default retryer.
		opts := t.c.pubc.CallOptions.Publish
		var settings gax.CallSettings
		for _, opt := range opts {
			opt.Resolve(&settings)
		}
		r := &publishRetryer{defaultRetryer: settings.Retry()}
		res, err = t.c.pubc.Publish(ctx, &pb.PublishRequest{
			Topic:    t.name,
			Messages: pbMsgs,
		}, gax.WithGRPCOptions(grpc.MaxCallSendMsgSize(maxSendRecvBytes)),
			gax.WithRetry(func() gax.Retryer { return r }))
  // (略)
}

Retryerの項目が出てきました。
デフォルトでは次のような設定となっています。

https://github.com/googleapis/google-cloud-go/blob/pubsub/v1.24.0/pubsub/apiv1/publisher_client.go#L93-L109

apiv1/publisher_client.go
func (c *PublisherClient) Publish(ctx context.Context, req *pubsubpb.PublishRequest, opts ...gax.CallOption) (*pubsubpb.PublishResponse, error) {
	return c.internalClient.Publish(ctx, req, opts...)
}
apiv1/publisher_client.go
func (c *publisherGRPCClient) Publish(ctx context.Context, req *pubsubpb.PublishRequest, opts ...gax.CallOption) (*pubsubpb.PublishResponse, error) {
	if _, ok := ctx.Deadline(); !ok && !c.disableDeadlines {
		cctx, cancel := context.WithTimeout(ctx, 60000*time.Millisecond)
		defer cancel()
		ctx = cctx
	}
	md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "topic", url.QueryEscape(req.GetTopic())))

	ctx = insertMetadata(ctx, c.xGoogMetadata, md)
	opts = append((*c.CallOptions).Publish[0:len((*c.CallOptions).Publish):len((*c.CallOptions).Publish)], opts...)
	var resp *pubsubpb.PublishResponse
	err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
		var err error
		resp, err = c.publisherClient.Publish(ctx, req, settings.GRPC...)
		return err
	}, opts...)
	if err != nil {
		return nil, err
	}
	return resp, nil
}

gax.Invoke() の中で c.publisherClient.Publish が呼ばれています。

https://github.com/googleapis/gax-go/blob/v2.4.0/v2/invoke.go#L45

invoke.go
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)
}
invoke.go
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
		}
		// Never retry permanent certificate errors. (e.x. if ca-certificates
		// are not installed). We should only make very few, targeted
		// exceptions: many (other) status=Unavailable should be retried, such
		// as if there's a network hiccup, or the internet goes out for a
		// minute. This is also why here we are doing string parsing instead of
		// simply making Unavailable a non-retried code elsewhere.
		if strings.Contains(err.Error(), "x509: certificate signed by unknown authority") {
			return err
		}
		if apierr, ok := apierror.FromError(err); ok {
			err = apierr
		}
		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
		}
	}
}

こちらの for 文内で設定に応じて、リトライが実行されてそうです。

Retryの設定変更

https://github.com/googleapis/google-cloud-go/blob/pubsub/v1.24.0/pubsub/pubsub.go#L122

NewClientWithConfigの引数にClientConfigがあります。

pubsub.go
// ClientConfig has configurations for the client.
type ClientConfig struct {
	PublisherCallOptions  *vkit.PublisherCallOptions
	SubscriberCallOptions *vkit.SubscriberCallOptions
}

*vkit.PublisherCallOptions がこちらになるので、変えれば変更できそうです。

https://github.com/googleapis/google-cloud-go/blob/pubsub/v1.24.0/pubsub/apiv1/publisher_client.go#L42-L55

終わりに

クライアントライブラリの公式ドキュメント

  • Cloud APIs を簡単かつ直感的に使用できるように、各言語で慣用的なコードを提供します。
  • 複数の Cloud サービスでの作業を簡素化するため、クライアント ライブラリ間で一貫したスタイルを指定します。
  • Google での認証など、サーバーとの通信に関する下位レベルのすべての詳細を処理します。
  • npm や pip など、使い慣れたパッケージ管理ツールを使用してインストールできます。
  • 場合によっては、gRPC を使用してパフォーマンス上のメリットが得られます。詳細については、gRPC API をご覧ください。

リトライのことは利点としては書いてなさそうですが、利用側のプログラムでリトライ処理を書かなくてもデフォルトで良しなに行ってくれるのは非常にありがたいです。

Discussion