github.com/SaurabhDubey-Groww/go-cloud@v0.0.0-20221124105541-b26c29285fd8/pubsub/gcppubsub/gcppubsub.go (about)

     1  // Copyright 2018 The Go Cloud Development Kit Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     https://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package gcppubsub provides a pubsub implementation that uses GCP
    16  // PubSub. Use OpenTopic to construct a *pubsub.Topic, and/or OpenSubscription
    17  // to construct a *pubsub.Subscription.
    18  //
    19  // # URLs
    20  //
    21  // For pubsub.OpenTopic and pubsub.OpenSubscription, gcppubsub registers
    22  // for the scheme "gcppubsub".
    23  // The default URL opener will creating a connection using use default
    24  // credentials from the environment, as described in
    25  // https://cloud.google.com/docs/authentication/production.
    26  // To customize the URL opener, or for more details on the URL format,
    27  // see URLOpener.
    28  // See https://gocloud.dev/concepts/urls/ for background information.
    29  //
    30  // GCP Pub/Sub emulator is supported as per https://cloud.google.com/pubsub/docs/emulator
    31  // So, when environment variable 'PUBSUB_EMULATOR_HOST' is set
    32  // driver connects to the specified emulator host by default.
    33  //
    34  // # Message Delivery Semantics
    35  //
    36  // GCP Pub/Sub supports at-least-once semantics; applications must
    37  // call Message.Ack after processing a message, or it will be redelivered.
    38  // See https://godoc.org/gocloud.dev/pubsub#hdr-At_most_once_and_At_least_once_Delivery
    39  // for more background.
    40  //
    41  // # As
    42  //
    43  // gcppubsub exposes the following types for As:
    44  //   - Topic: *raw.PublisherClient
    45  //   - Subscription: *raw.SubscriberClient
    46  //   - Message.BeforeSend: *pb.PubsubMessage
    47  //   - Message.AfterSend: *string for the pb.PublishResponse.MessageIds entry corresponding to the message.
    48  //   - Message: *pb.PubsubMessage, *pb.ReceivedMessage
    49  //   - Error: *google.golang.org/grpc/status.Status
    50  package gcppubsub // import "gocloud.dev/pubsub/gcppubsub"
    51  
    52  import (
    53  	"context"
    54  	"fmt"
    55  	"net/url"
    56  	"os"
    57  	"path"
    58  	"regexp"
    59  	"strconv"
    60  	"strings"
    61  	"sync"
    62  	"time"
    63  
    64  	raw "cloud.google.com/go/pubsub/apiv1"
    65  	"github.com/google/wire"
    66  	"gocloud.dev/gcerrors"
    67  	"gocloud.dev/gcp"
    68  	"gocloud.dev/internal/gcerr"
    69  	"gocloud.dev/internal/useragent"
    70  	"gocloud.dev/pubsub"
    71  	"gocloud.dev/pubsub/batcher"
    72  	"gocloud.dev/pubsub/driver"
    73  	"google.golang.org/api/option"
    74  	pb "google.golang.org/genproto/googleapis/pubsub/v1"
    75  	"google.golang.org/grpc"
    76  	"google.golang.org/grpc/credentials"
    77  	"google.golang.org/grpc/credentials/oauth"
    78  	"google.golang.org/grpc/status"
    79  )
    80  
    81  var endPoint = "pubsub.googleapis.com:443"
    82  
    83  var sendBatcherOpts = &batcher.Options{
    84  	MaxBatchSize: 1000, // The PubSub service limits the number of messages in a single Publish RPC
    85  	MaxHandlers:  2,
    86  	// The PubSub service limits the size of the request body in a single Publish RPC.
    87  	// The limit is currently documented as "10MB (total size)" and "10MB (data field)" per message.
    88  	// We are enforcing 9MiB to give ourselves some headroom for message attributes since those
    89  	// are currently not considered when computing the byte size of a message.
    90  	MaxBatchByteSize: 9 * 1024 * 1024,
    91  }
    92  
    93  var defaultRecvBatcherOpts = &batcher.Options{
    94  	// GCP Pub/Sub returns at most 1000 messages per RPC.
    95  	MaxBatchSize: 1000,
    96  	MaxHandlers:  10,
    97  }
    98  
    99  var ackBatcherOpts = &batcher.Options{
   100  	// The PubSub service limits the size of Acknowledge/ModifyAckDeadline RPCs.
   101  	// (E.g., "Request payload size exceeds the limit: 524288 bytes.").
   102  	MaxBatchSize: 1000,
   103  	MaxHandlers:  2,
   104  }
   105  
   106  func init() {
   107  	o := new(lazyCredsOpener)
   108  	pubsub.DefaultURLMux().RegisterTopic(Scheme, o)
   109  	pubsub.DefaultURLMux().RegisterSubscription(Scheme, o)
   110  }
   111  
   112  // Set holds Wire providers for this package.
   113  var Set = wire.NewSet(
   114  	Dial,
   115  	PublisherClient,
   116  	SubscriberClient,
   117  	wire.Struct(new(SubscriptionOptions)),
   118  	wire.Struct(new(TopicOptions)),
   119  	wire.Struct(new(URLOpener), "Conn", "TopicOptions", "SubscriptionOptions"),
   120  )
   121  
   122  // lazyCredsOpener obtains Application Default Credentials on the first call
   123  // to OpenTopicURL/OpenSubscriptionURL.
   124  type lazyCredsOpener struct {
   125  	init   sync.Once
   126  	opener *URLOpener
   127  	err    error
   128  }
   129  
   130  func (o *lazyCredsOpener) defaultConn(ctx context.Context) (*URLOpener, error) {
   131  	o.init.Do(func() {
   132  		var conn *grpc.ClientConn
   133  		var err error
   134  		if e := os.Getenv("PUBSUB_EMULATOR_HOST"); e != "" {
   135  			// Connect to the GCP pubsub emulator by overriding the default endpoint
   136  			// if the 'PUBSUB_EMULATOR_HOST' environment variable is set.
   137  			// Check https://cloud.google.com/pubsub/docs/emulator for more info.
   138  			endPoint = e
   139  			conn, err = dialEmulator(ctx, e)
   140  			if err != nil {
   141  				o.err = err
   142  				return
   143  			}
   144  		} else {
   145  			creds, err := gcp.DefaultCredentials(ctx)
   146  			if err != nil {
   147  				o.err = err
   148  				return
   149  			}
   150  
   151  			conn, _, err = Dial(ctx, creds.TokenSource)
   152  			if err != nil {
   153  				o.err = err
   154  				return
   155  			}
   156  		}
   157  		o.opener = &URLOpener{Conn: conn}
   158  	})
   159  	return o.opener, o.err
   160  }
   161  
   162  func (o *lazyCredsOpener) OpenTopicURL(ctx context.Context, u *url.URL) (*pubsub.Topic, error) {
   163  	opener, err := o.defaultConn(ctx)
   164  	if err != nil {
   165  		return nil, fmt.Errorf("open topic %v: failed to open default connection: %v", u, err)
   166  	}
   167  	return opener.OpenTopicURL(ctx, u)
   168  }
   169  
   170  func (o *lazyCredsOpener) OpenSubscriptionURL(ctx context.Context, u *url.URL) (*pubsub.Subscription, error) {
   171  	opener, err := o.defaultConn(ctx)
   172  	if err != nil {
   173  		return nil, fmt.Errorf("open subscription %v: failed to open default connection: %v", u, err)
   174  	}
   175  	return opener.OpenSubscriptionURL(ctx, u)
   176  }
   177  
   178  // Scheme is the URL scheme gcppubsub registers its URLOpeners under on pubsub.DefaultMux.
   179  const Scheme = "gcppubsub"
   180  
   181  // URLOpener opens GCP Pub/Sub URLs like "gcppubsub://projects/myproject/topics/mytopic" for
   182  // topics or "gcppubsub://projects/myproject/subscriptions/mysub" for subscriptions.
   183  //
   184  // The shortened forms "gcppubsub://myproject/mytopic" for topics or
   185  // "gcppubsub://myproject/mysub" for subscriptions are also supported.
   186  //
   187  // The following query parameters are supported:
   188  //
   189  //   - max_recv_batch_size: sets SubscriptionOptions.MaxBatchSize
   190  //
   191  // Currently their use is limited to subscribers.
   192  type URLOpener struct {
   193  	// Conn must be set to a non-nil ClientConn authenticated with
   194  	// Cloud Pub/Sub scope or equivalent.
   195  	Conn *grpc.ClientConn
   196  
   197  	// TopicOptions specifies the options to pass to OpenTopic.
   198  	TopicOptions TopicOptions
   199  	// SubscriptionOptions specifies the options to pass to OpenSubscription.
   200  	SubscriptionOptions SubscriptionOptions
   201  }
   202  
   203  // OpenTopicURL opens a pubsub.Topic based on u.
   204  func (o *URLOpener) OpenTopicURL(ctx context.Context, u *url.URL) (*pubsub.Topic, error) {
   205  	for param := range u.Query() {
   206  		return nil, fmt.Errorf("open topic %v: invalid query parameter %q", u, param)
   207  	}
   208  	pc, err := PublisherClient(ctx, o.Conn)
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  	topicPath := path.Join(u.Host, u.Path)
   213  	if topicPathRE.MatchString(topicPath) {
   214  		return OpenTopicByPath(pc, topicPath, &o.TopicOptions)
   215  	}
   216  	// Shortened form?
   217  	topicName := strings.TrimPrefix(u.Path, "/")
   218  	return OpenTopic(pc, gcp.ProjectID(u.Host), topicName, &o.TopicOptions), nil
   219  }
   220  
   221  // OpenSubscriptionURL opens a pubsub.Subscription based on u.
   222  func (o *URLOpener) OpenSubscriptionURL(ctx context.Context, u *url.URL) (*pubsub.Subscription, error) {
   223  	// Set subscription options to use defaults
   224  	opts := o.SubscriptionOptions
   225  
   226  	for param, value := range u.Query() {
   227  		switch param {
   228  		case "max_recv_batch_size":
   229  			maxBatchSize, err := queryParameterInt(value)
   230  			if err != nil {
   231  				return nil, fmt.Errorf("open subscription %v: invalid query parameter %q: %v", u, param, err)
   232  			}
   233  
   234  			if maxBatchSize <= 0 || maxBatchSize > 1000 {
   235  				return nil, fmt.Errorf("open subscription %v: invalid query parameter %q: must be between 1 and 1000", u, param)
   236  			}
   237  
   238  			opts.MaxBatchSize = maxBatchSize
   239  		default:
   240  			return nil, fmt.Errorf("open subscription %v: invalid query parameter %q", u, param)
   241  		}
   242  	}
   243  	sc, err := SubscriberClient(ctx, o.Conn)
   244  	if err != nil {
   245  		return nil, err
   246  	}
   247  	subPath := path.Join(u.Host, u.Path)
   248  	if subscriptionPathRE.MatchString(subPath) {
   249  		return OpenSubscriptionByPath(sc, subPath, &opts)
   250  	}
   251  	// Shortened form?
   252  	subName := strings.TrimPrefix(u.Path, "/")
   253  	return OpenSubscription(sc, gcp.ProjectID(u.Host), subName, &opts), nil
   254  }
   255  
   256  type topic struct {
   257  	path   string
   258  	client *raw.PublisherClient
   259  }
   260  
   261  // Dial opens a gRPC connection to the GCP Pub Sub API.
   262  //
   263  // The second return value is a function that can be called to clean up
   264  // the connection opened by Dial.
   265  func Dial(ctx context.Context, ts gcp.TokenSource) (*grpc.ClientConn, func(), error) {
   266  	conn, err := grpc.DialContext(ctx, endPoint,
   267  		grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, "")),
   268  		grpc.WithPerRPCCredentials(oauth.TokenSource{TokenSource: ts}),
   269  		// The default message size limit for gRPC is 4MB, while GCP
   270  		// PubSub supports messages up to 10MB. Aside from the message itself
   271  		// there is also other data in the gRPC response, bringing the maximum
   272  		// response size above 10MB. Tell gRPC to support up to 11MB.
   273  		// https://github.com/googleapis/google-cloud-node/issues/1991
   274  		grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(1024*1024*11)),
   275  		useragent.GRPCDialOption("pubsub"),
   276  	)
   277  
   278  	if err != nil {
   279  		return nil, nil, err
   280  	}
   281  	return conn, func() { conn.Close() }, nil
   282  }
   283  
   284  // dialEmulator opens a gRPC connection to the GCP Pub Sub API.
   285  func dialEmulator(ctx context.Context, e string) (*grpc.ClientConn, error) {
   286  	conn, err := grpc.DialContext(ctx, e, grpc.WithInsecure(), useragent.GRPCDialOption("pubsub"))
   287  	if err != nil {
   288  		return nil, err
   289  	}
   290  	return conn, nil
   291  }
   292  
   293  // PublisherClient returns a *raw.PublisherClient that can be used in OpenTopic.
   294  func PublisherClient(ctx context.Context, conn *grpc.ClientConn) (*raw.PublisherClient, error) {
   295  	return raw.NewPublisherClient(ctx, option.WithGRPCConn(conn))
   296  }
   297  
   298  // SubscriberClient returns a *raw.SubscriberClient that can be used in OpenSubscription.
   299  func SubscriberClient(ctx context.Context, conn *grpc.ClientConn) (*raw.SubscriberClient, error) {
   300  	return raw.NewSubscriberClient(ctx, option.WithGRPCConn(conn))
   301  }
   302  
   303  // TopicOptions will contain configuration for topics.
   304  type TopicOptions struct {
   305  	// BatcherOptions adds constraints to the default batching done for sends.
   306  	BatcherOptions batcher.Options
   307  }
   308  
   309  // OpenTopic returns a *pubsub.Topic backed by an existing GCP PubSub topic
   310  // in the given projectID. topicName is the last part of the full topic
   311  // path, e.g., "foo" from "projects/<projectID>/topic/foo".
   312  // See the package documentation for an example.
   313  func OpenTopic(client *raw.PublisherClient, projectID gcp.ProjectID, topicName string, opts *TopicOptions) *pubsub.Topic {
   314  	topicPath := fmt.Sprintf("projects/%s/topics/%s", projectID, topicName)
   315  	if opts == nil {
   316  		opts = &TopicOptions{}
   317  	}
   318  	bo := sendBatcherOpts.NewMergedOptions(&opts.BatcherOptions)
   319  	return pubsub.NewTopic(openTopic(client, topicPath), bo)
   320  }
   321  
   322  var topicPathRE = regexp.MustCompile("^projects/.+/topics/.+$")
   323  
   324  // OpenTopicByPath returns a *pubsub.Topic backed by an existing GCP PubSub
   325  // topic. topicPath must be of the form "projects/<projectID>/topic/<topic>".
   326  // See the package documentation for an example.
   327  func OpenTopicByPath(client *raw.PublisherClient, topicPath string, opts *TopicOptions) (*pubsub.Topic, error) {
   328  	if !topicPathRE.MatchString(topicPath) {
   329  		return nil, fmt.Errorf("invalid topicPath %q; must match %v", topicPath, topicPathRE)
   330  	}
   331  	if opts == nil {
   332  		opts = &TopicOptions{}
   333  	}
   334  	bo := sendBatcherOpts.NewMergedOptions(&opts.BatcherOptions)
   335  	return pubsub.NewTopic(openTopic(client, topicPath), bo), nil
   336  }
   337  
   338  // openTopic returns the driver for OpenTopic. This function exists so the test
   339  // harness can get the driver interface implementation if it needs to.
   340  func openTopic(client *raw.PublisherClient, topicPath string) driver.Topic {
   341  	return &topic{topicPath, client}
   342  }
   343  
   344  // SendBatch implements driver.Topic.SendBatch.
   345  func (t *topic) SendBatch(ctx context.Context, dms []*driver.Message) error {
   346  	var ms []*pb.PubsubMessage
   347  	for _, dm := range dms {
   348  		psm := &pb.PubsubMessage{Data: dm.Body, Attributes: dm.Metadata}
   349  		if dm.BeforeSend != nil {
   350  			asFunc := func(i interface{}) bool {
   351  				if p, ok := i.(**pb.PubsubMessage); ok {
   352  					*p = psm
   353  					return true
   354  				}
   355  				return false
   356  			}
   357  			if err := dm.BeforeSend(asFunc); err != nil {
   358  				return err
   359  			}
   360  		}
   361  		ms = append(ms, psm)
   362  	}
   363  	req := &pb.PublishRequest{Topic: t.path, Messages: ms}
   364  	pr, err := t.client.Publish(ctx, req)
   365  	if err != nil {
   366  		return err
   367  	}
   368  	if len(pr.MessageIds) == len(dms) {
   369  		for n, dm := range dms {
   370  			if dm.AfterSend != nil {
   371  				asFunc := func(i interface{}) bool {
   372  					if p, ok := i.(*string); ok {
   373  						*p = pr.MessageIds[n]
   374  						return true
   375  					}
   376  					return false
   377  				}
   378  				if err := dm.AfterSend(asFunc); err != nil {
   379  					return err
   380  				}
   381  			}
   382  		}
   383  	}
   384  	return nil
   385  }
   386  
   387  // IsRetryable implements driver.Topic.IsRetryable.
   388  func (t *topic) IsRetryable(error) bool {
   389  	// The client handles retries.
   390  	return false
   391  }
   392  
   393  // As implements driver.Topic.As.
   394  func (t *topic) As(i interface{}) bool {
   395  	c, ok := i.(**raw.PublisherClient)
   396  	if !ok {
   397  		return false
   398  	}
   399  	*c = t.client
   400  	return true
   401  }
   402  
   403  // ErrorAs implements driver.Topic.ErrorAs
   404  func (*topic) ErrorAs(err error, i interface{}) bool {
   405  	return errorAs(err, i)
   406  }
   407  
   408  func errorAs(err error, i interface{}) bool {
   409  	s, ok := status.FromError(err)
   410  	if !ok {
   411  		return false
   412  	}
   413  	p, ok := i.(**status.Status)
   414  	if !ok {
   415  		return false
   416  	}
   417  	*p = s
   418  	return true
   419  }
   420  
   421  func (*topic) ErrorCode(err error) gcerrors.ErrorCode {
   422  	return gcerr.GRPCCode(err)
   423  }
   424  
   425  // Close implements driver.Topic.Close.
   426  func (*topic) Close() error { return nil }
   427  
   428  type subscription struct {
   429  	client  *raw.SubscriberClient
   430  	path    string
   431  	options *SubscriptionOptions
   432  }
   433  
   434  // SubscriptionOptions will contain configuration for subscriptions.
   435  type SubscriptionOptions struct {
   436  	// MaxBatchSize caps the maximum batch size used when retrieving messages. It defaults to 1000.
   437  	MaxBatchSize int
   438  
   439  	// ReceiveBatcherOptions adds constraints to the default batching done for receives.
   440  	ReceiveBatcherOptions batcher.Options
   441  
   442  	// AckBatcherOptions adds constraints to the default batching done for acks.
   443  	AckBatcherOptions batcher.Options
   444  }
   445  
   446  // OpenSubscription returns a *pubsub.Subscription backed by an existing GCP
   447  // PubSub subscription subscriptionName in the given projectID. See the package
   448  // documentation for an example.
   449  func OpenSubscription(client *raw.SubscriberClient, projectID gcp.ProjectID, subscriptionName string, opts *SubscriptionOptions) *pubsub.Subscription {
   450  	path := fmt.Sprintf("projects/%s/subscriptions/%s", projectID, subscriptionName)
   451  
   452  	dsub := openSubscription(client, path, opts)
   453  	recvOpts := *defaultRecvBatcherOpts
   454  	recvOpts.MaxBatchSize = dsub.options.MaxBatchSize
   455  	rbo := recvOpts.NewMergedOptions(&dsub.options.ReceiveBatcherOptions)
   456  	abo := ackBatcherOpts.NewMergedOptions(&dsub.options.AckBatcherOptions)
   457  	return pubsub.NewSubscription(dsub, rbo, abo)
   458  }
   459  
   460  var subscriptionPathRE = regexp.MustCompile("^projects/.+/subscriptions/.+$")
   461  
   462  // OpenSubscriptionByPath returns a *pubsub.Subscription backed by an existing
   463  // GCP PubSub subscription. subscriptionPath must be of the form
   464  // "projects/<projectID>/subscriptions/<subscription>".
   465  // See the package documentation for an example.
   466  func OpenSubscriptionByPath(client *raw.SubscriberClient, subscriptionPath string, opts *SubscriptionOptions) (*pubsub.Subscription, error) {
   467  	if !subscriptionPathRE.MatchString(subscriptionPath) {
   468  		return nil, fmt.Errorf("invalid subscriptionPath %q; must match %v", subscriptionPath, subscriptionPathRE)
   469  	}
   470  
   471  	dsub := openSubscription(client, subscriptionPath, opts)
   472  	recvOpts := *defaultRecvBatcherOpts
   473  	recvOpts.MaxBatchSize = dsub.options.MaxBatchSize
   474  	rbo := recvOpts.NewMergedOptions(&dsub.options.ReceiveBatcherOptions)
   475  	abo := ackBatcherOpts.NewMergedOptions(&dsub.options.AckBatcherOptions)
   476  	return pubsub.NewSubscription(dsub, rbo, abo), nil
   477  }
   478  
   479  // openSubscription returns a driver.Subscription.
   480  func openSubscription(client *raw.SubscriberClient, subscriptionPath string, opts *SubscriptionOptions) *subscription {
   481  	if opts == nil {
   482  		opts = &SubscriptionOptions{}
   483  	}
   484  	if opts.MaxBatchSize == 0 {
   485  		opts.MaxBatchSize = defaultRecvBatcherOpts.MaxBatchSize
   486  	}
   487  	return &subscription{client, subscriptionPath, opts}
   488  }
   489  
   490  // ReceiveBatch implements driver.Subscription.ReceiveBatch.
   491  func (s *subscription) ReceiveBatch(ctx context.Context, maxMessages int) ([]*driver.Message, error) {
   492  	// Whether to ask Pull to return immediately, or wait for some messages to
   493  	// arrive. If we're making multiple RPCs, we don't want any of them to wait;
   494  	// we might have gotten messages from one of the other RPCs.
   495  	// maxMessages will only be high enough to set this to true in high-throughput
   496  	// situations, so the likelihood of getting 0 messages is small anyway.
   497  	returnImmediately := maxMessages == s.options.MaxBatchSize
   498  
   499  	req := &pb.PullRequest{
   500  		Subscription:      s.path,
   501  		ReturnImmediately: returnImmediately,
   502  		MaxMessages:       int32(maxMessages),
   503  	}
   504  	resp, err := s.client.Pull(ctx, req)
   505  	if err != nil {
   506  		return nil, err
   507  	}
   508  	if len(resp.ReceivedMessages) == 0 {
   509  		// If we did happen to get 0 messages, and we didn't ask the server to wait
   510  		// for messages, sleep a bit to avoid spinning.
   511  		if returnImmediately {
   512  			time.Sleep(100 * time.Millisecond)
   513  		}
   514  		return nil, nil
   515  	}
   516  
   517  	ms := make([]*driver.Message, 0, len(resp.ReceivedMessages))
   518  	for _, rm := range resp.ReceivedMessages {
   519  		rm := rm
   520  		rmm := rm.Message
   521  		m := &driver.Message{
   522  			LoggableID: rmm.MessageId,
   523  			Body:       rmm.Data,
   524  			Metadata:   rmm.Attributes,
   525  			AckID:      rm.AckId,
   526  			AsFunc:     messageAsFunc(rmm, rm),
   527  		}
   528  		ms = append(ms, m)
   529  	}
   530  	return ms, nil
   531  }
   532  
   533  func messageAsFunc(pm *pb.PubsubMessage, rm *pb.ReceivedMessage) func(interface{}) bool {
   534  	return func(i interface{}) bool {
   535  		ip, ok := i.(**pb.PubsubMessage)
   536  		if ok {
   537  			*ip = pm
   538  			return true
   539  		}
   540  		rp, ok := i.(**pb.ReceivedMessage)
   541  		if ok {
   542  			*rp = rm
   543  			return true
   544  		}
   545  		return false
   546  	}
   547  }
   548  
   549  // SendAcks implements driver.Subscription.SendAcks.
   550  func (s *subscription) SendAcks(ctx context.Context, ids []driver.AckID) error {
   551  	ids2 := make([]string, 0, len(ids))
   552  	for _, id := range ids {
   553  		ids2 = append(ids2, id.(string))
   554  	}
   555  	return s.client.Acknowledge(ctx, &pb.AcknowledgeRequest{Subscription: s.path, AckIds: ids2})
   556  }
   557  
   558  // CanNack implements driver.CanNack.
   559  func (s *subscription) CanNack() bool { return true }
   560  
   561  // SendNacks implements driver.Subscription.SendNacks.
   562  func (s *subscription) SendNacks(ctx context.Context, ids []driver.AckID) error {
   563  	ids2 := make([]string, 0, len(ids))
   564  	for _, id := range ids {
   565  		ids2 = append(ids2, id.(string))
   566  	}
   567  	return s.client.ModifyAckDeadline(ctx, &pb.ModifyAckDeadlineRequest{
   568  		Subscription:       s.path,
   569  		AckIds:             ids2,
   570  		AckDeadlineSeconds: 0,
   571  	})
   572  }
   573  
   574  // IsRetryable implements driver.Subscription.IsRetryable.
   575  func (s *subscription) IsRetryable(err error) bool {
   576  	// The client mostly handles retries, but does not
   577  	// include DeadlineExceeded for some reason.
   578  	if s.ErrorCode(err) == gcerrors.DeadlineExceeded {
   579  		return true
   580  	}
   581  	return false
   582  }
   583  
   584  // As implements driver.Subscription.As.
   585  func (s *subscription) As(i interface{}) bool {
   586  	c, ok := i.(**raw.SubscriberClient)
   587  	if !ok {
   588  		return false
   589  	}
   590  	*c = s.client
   591  	return true
   592  }
   593  
   594  // ErrorAs implements driver.Subscription.ErrorAs
   595  func (*subscription) ErrorAs(err error, i interface{}) bool {
   596  	return errorAs(err, i)
   597  }
   598  
   599  func (*subscription) ErrorCode(err error) gcerrors.ErrorCode {
   600  	return gcerr.GRPCCode(err)
   601  }
   602  
   603  // Close implements driver.Subscription.Close.
   604  func (*subscription) Close() error { return nil }
   605  
   606  func queryParameterInt(value []string) (int, error) {
   607  	if len(value) > 1 {
   608  		return 0, fmt.Errorf("expected only one parameter value, got: %v", len(value))
   609  	}
   610  
   611  	return strconv.Atoi(value[0])
   612  }