github.com/SaurabhDubey-Groww/go-cloud@v0.0.0-20221124105541-b26c29285fd8/pubsub/azuresb/azuresb.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 azuresb provides an implementation of pubsub using Azure Service
    16  // Bus Topic and Subscription.
    17  // See https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-messaging-overview for an overview.
    18  //
    19  // # URLs
    20  //
    21  // For pubsub.OpenTopic and pubsub.OpenSubscription, azuresb registers
    22  // for the scheme "azuresb".
    23  // The default URL opener will use a Service Bus Connection String based on
    24  // the environment variable "SERVICEBUS_CONNECTION_STRING".
    25  // To customize the URL opener, or for more details on the URL format,
    26  // see URLOpener.
    27  // See https://gocloud.dev/concepts/urls/ for background information.
    28  //
    29  // # Message Delivery Semantics
    30  //
    31  // Azure ServiceBus supports at-least-once semantics in the default Peek-Lock
    32  // mode; messages will be redelivered if they are not Acked, or if they are
    33  // explicitly Nacked.
    34  //
    35  // ServiceBus also supports a Receive-Delete mode, which essentially auto-acks a
    36  // message when it is delivered, resulting in at-most-once semantics. Set
    37  // SubscriberOptions.ReceiveAndDelete to true to tell azuresb.Subscription that
    38  // you've enabled Receive-Delete mode. When enabled, pubsub.Message.Ack is a
    39  // no-op, pubsub.Message.Nackable will return false, and pubsub.Message.Nack
    40  // will panic.
    41  //
    42  // See https://godoc.org/gocloud.dev/pubsub#hdr-At_most_once_and_At_least_once_Delivery
    43  // for more background.
    44  //
    45  // # As
    46  //
    47  // azuresb exposes the following types for As:
    48  //   - Topic: *servicebus.Topic
    49  //   - Subscription: *servicebus.Subscription
    50  //   - Message.BeforeSend: *servicebus.Message
    51  //   - Message.AfterSend: None
    52  //   - Message: *servicebus.Message
    53  //   - Error: common.Retryable, *amqp.Error, *amqp.DetachError
    54  package azuresb // import "gocloud.dev/pubsub/azuresb"
    55  
    56  import (
    57  	"context"
    58  	"errors"
    59  	"fmt"
    60  	"net/url"
    61  	"os"
    62  	"path"
    63  	"strings"
    64  	"sync"
    65  	"time"
    66  
    67  	common "github.com/Azure/azure-amqp-common-go/v3"
    68  	servicebus "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus"
    69  	"github.com/Azure/go-amqp"
    70  	"gocloud.dev/gcerrors"
    71  	"gocloud.dev/pubsub"
    72  	"gocloud.dev/pubsub/batcher"
    73  	"gocloud.dev/pubsub/driver"
    74  )
    75  
    76  const (
    77  	listenerTimeout = 2 * time.Second
    78  )
    79  
    80  var sendBatcherOpts = &batcher.Options{
    81  	MaxBatchSize: 1,   // SendBatch only supports one message at a time
    82  	MaxHandlers:  100, // max concurrency for sends
    83  }
    84  
    85  var recvBatcherOpts = &batcher.Options{
    86  	MaxBatchSize: 50,
    87  	MaxHandlers:  100, // max concurrency for reads
    88  }
    89  
    90  var ackBatcherOpts = &batcher.Options{
    91  	MaxBatchSize: 1,
    92  	MaxHandlers:  100, // max concurrency for acks
    93  }
    94  
    95  func init() {
    96  	o := new(defaultOpener)
    97  	pubsub.DefaultURLMux().RegisterTopic(Scheme, o)
    98  	pubsub.DefaultURLMux().RegisterSubscription(Scheme, o)
    99  }
   100  
   101  // defaultURLOpener creates an URLOpener with ConnectionString initialized from
   102  // the environment variable SERVICEBUS_CONNECTION_STRING.
   103  type defaultOpener struct {
   104  	init   sync.Once
   105  	opener *URLOpener
   106  	err    error
   107  }
   108  
   109  func (o *defaultOpener) defaultOpener() (*URLOpener, error) {
   110  	o.init.Do(func() {
   111  		cs := os.Getenv("SERVICEBUS_CONNECTION_STRING")
   112  		if cs == "" {
   113  			o.err = errors.New("SERVICEBUS_CONNECTION_STRING environment variable not set")
   114  			return
   115  		}
   116  		o.opener = &URLOpener{ConnectionString: cs}
   117  	})
   118  	return o.opener, o.err
   119  }
   120  
   121  func (o *defaultOpener) OpenTopicURL(ctx context.Context, u *url.URL) (*pubsub.Topic, error) {
   122  	opener, err := o.defaultOpener()
   123  	if err != nil {
   124  		return nil, fmt.Errorf("open topic %v: %v", u, err)
   125  	}
   126  	return opener.OpenTopicURL(ctx, u)
   127  }
   128  
   129  func (o *defaultOpener) OpenSubscriptionURL(ctx context.Context, u *url.URL) (*pubsub.Subscription, error) {
   130  	opener, err := o.defaultOpener()
   131  	if err != nil {
   132  		return nil, fmt.Errorf("open subscription %v: %v", u, err)
   133  	}
   134  	return opener.OpenSubscriptionURL(ctx, u)
   135  }
   136  
   137  // Scheme is the URL scheme azuresb registers its URLOpeners under on pubsub.DefaultMux.
   138  const Scheme = "azuresb"
   139  
   140  // URLOpener opens Azure Service Bus URLs like "azuresb://mytopic" for
   141  // topics or "azuresb://mytopic?subscription=mysubscription" for subscriptions.
   142  //
   143  //   - The URL's host+path is used as the topic name.
   144  //   - For subscriptions, the subscription name must be provided in the
   145  //     "subscription" query parameter.
   146  //
   147  // No other query parameters are supported.
   148  type URLOpener struct {
   149  	// ConnectionString is the Service Bus connection string (required).
   150  	// https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-dotnet-get-started-with-queues
   151  	ConnectionString string
   152  
   153  	// ClientOptions are options when creating the Client.
   154  	ServiceBusClientOptions *servicebus.ClientOptions
   155  
   156  	// Options passed when creating the ServiceBus Topic/Subscription.
   157  	ServiceBusSenderOptions   *servicebus.NewSenderOptions
   158  	ServiceBusReceiverOptions *servicebus.ReceiverOptions
   159  
   160  	// TopicOptions specifies the options to pass to OpenTopic.
   161  	TopicOptions TopicOptions
   162  	// SubscriptionOptions specifies the options to pass to OpenSubscription.
   163  	SubscriptionOptions SubscriptionOptions
   164  }
   165  
   166  func (o *URLOpener) sbClient(kind string, u *url.URL) (*servicebus.Client, error) {
   167  	if o.ConnectionString == "" {
   168  		return nil, fmt.Errorf("open %s %v: ConnectionString is required", kind, u)
   169  	}
   170  	client, err := NewClientFromConnectionString(o.ConnectionString, o.ServiceBusClientOptions)
   171  	if err != nil {
   172  		return nil, fmt.Errorf("open %s %v: invalid connection string %q: %v", kind, u, o.ConnectionString, err)
   173  	}
   174  	return client, nil
   175  }
   176  
   177  // OpenTopicURL opens a pubsub.Topic based on u.
   178  func (o *URLOpener) OpenTopicURL(ctx context.Context, u *url.URL) (*pubsub.Topic, error) {
   179  	sbClient, err := o.sbClient("topic", u)
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  	for param := range u.Query() {
   184  		return nil, fmt.Errorf("open topic %v: invalid query parameter %q", u, param)
   185  	}
   186  	topicName := path.Join(u.Host, u.Path)
   187  	sbSender, err := NewSender(sbClient, topicName, o.ServiceBusSenderOptions)
   188  	if err != nil {
   189  		return nil, fmt.Errorf("open topic %v: couldn't open topic %q: %v", u, topicName, err)
   190  	}
   191  	return OpenTopic(ctx, sbSender, &o.TopicOptions)
   192  }
   193  
   194  // OpenSubscriptionURL opens a pubsub.Subscription based on u.
   195  func (o *URLOpener) OpenSubscriptionURL(ctx context.Context, u *url.URL) (*pubsub.Subscription, error) {
   196  	sbClient, err := o.sbClient("subscription", u)
   197  	if err != nil {
   198  		return nil, err
   199  	}
   200  	topicName := path.Join(u.Host, u.Path)
   201  	q := u.Query()
   202  	subName := q.Get("subscription")
   203  	q.Del("subscription")
   204  	if subName == "" {
   205  		return nil, fmt.Errorf("open subscription %v: missing required query parameter subscription", u)
   206  	}
   207  	for param := range q {
   208  		return nil, fmt.Errorf("open subscription %v: invalid query parameter %q", u, param)
   209  	}
   210  	sbReceiver, err := NewReceiver(sbClient, topicName, subName, o.ServiceBusReceiverOptions)
   211  	if err != nil {
   212  		return nil, fmt.Errorf("open subscription %v: couldn't open subscription %q: %v", u, subName, err)
   213  	}
   214  	return OpenSubscription(ctx, sbClient, sbReceiver, &o.SubscriptionOptions)
   215  }
   216  
   217  type topic struct {
   218  	sbSender *servicebus.Sender
   219  }
   220  
   221  // TopicOptions provides configuration options for an Azure SB Topic.
   222  type TopicOptions struct {
   223  	// BatcherOptions adds constraints to the default batching done for sends.
   224  	BatcherOptions batcher.Options
   225  }
   226  
   227  // NewClientFromConnectionString returns a *servicebus.Client from a Service Bus connection string.
   228  // https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-dotnet-get-started-with-queues
   229  func NewClientFromConnectionString(connectionString string, opts *servicebus.ClientOptions) (*servicebus.Client, error) {
   230  	return servicebus.NewClientFromConnectionString(connectionString, opts)
   231  }
   232  
   233  // NewSender returns a *servicebus.Sender associated with a Service Bus Client.
   234  func NewSender(sbClient *servicebus.Client, topicName string, opts *servicebus.NewSenderOptions) (*servicebus.Sender, error) {
   235  	return sbClient.NewSender(topicName, opts)
   236  }
   237  
   238  // NewReceiver returns a *servicebus.Receiver associated with a Service Bus Topic.
   239  func NewReceiver(sbClient *servicebus.Client, topicName, subscriptionName string, opts *servicebus.ReceiverOptions) (*servicebus.Receiver, error) {
   240  	return sbClient.NewReceiverForSubscription(topicName, subscriptionName, opts)
   241  }
   242  
   243  // OpenTopic initializes a pubsub Topic on a given Service Bus Sender.
   244  func OpenTopic(ctx context.Context, sbSender *servicebus.Sender, opts *TopicOptions) (*pubsub.Topic, error) {
   245  	t, err := openTopic(ctx, sbSender, opts)
   246  	if err != nil {
   247  		return nil, err
   248  	}
   249  	if opts == nil {
   250  		opts = &TopicOptions{}
   251  	}
   252  	bo := sendBatcherOpts.NewMergedOptions(&opts.BatcherOptions)
   253  	return pubsub.NewTopic(t, bo), nil
   254  }
   255  
   256  // openTopic returns the driver for OpenTopic. This function exists so the test
   257  // harness can get the driver interface implementation if it needs to.
   258  func openTopic(ctx context.Context, sbSender *servicebus.Sender, _ *TopicOptions) (driver.Topic, error) {
   259  	if sbSender == nil {
   260  		return nil, errors.New("azuresb: OpenTopic requires a Service Bus Sender")
   261  	}
   262  	return &topic{sbSender: sbSender}, nil
   263  }
   264  
   265  // SendBatch implements driver.Topic.SendBatch.
   266  func (t *topic) SendBatch(ctx context.Context, dms []*driver.Message) error {
   267  	if len(dms) != 1 {
   268  		panic("azuresb.SendBatch should only get one message at a time")
   269  	}
   270  	dm := dms[0]
   271  	sbms := &servicebus.Message{Body: dm.Body}
   272  	if len(dm.Metadata) > 0 {
   273  		sbms.ApplicationProperties = map[string]interface{}{}
   274  		for k, v := range dm.Metadata {
   275  			sbms.ApplicationProperties[k] = v
   276  		}
   277  	}
   278  	if dm.BeforeSend != nil {
   279  		asFunc := func(i interface{}) bool {
   280  			if p, ok := i.(**servicebus.Message); ok {
   281  				*p = sbms
   282  				return true
   283  			}
   284  			return false
   285  		}
   286  		if err := dm.BeforeSend(asFunc); err != nil {
   287  			return err
   288  		}
   289  	}
   290  	err := t.sbSender.SendMessage(ctx, sbms, nil)
   291  	if err != nil {
   292  		return err
   293  	}
   294  	if dm.AfterSend != nil {
   295  		asFunc := func(i interface{}) bool { return false }
   296  		if err := dm.AfterSend(asFunc); err != nil {
   297  			return err
   298  		}
   299  	}
   300  	return nil
   301  }
   302  
   303  func (t *topic) IsRetryable(err error) bool {
   304  	_, retryable := errorCode(err)
   305  	return retryable
   306  }
   307  
   308  func (t *topic) As(i interface{}) bool {
   309  	p, ok := i.(**servicebus.Sender)
   310  	if !ok {
   311  		return false
   312  	}
   313  	*p = t.sbSender
   314  	return true
   315  }
   316  
   317  // ErrorAs implements driver.Topic.ErrorAs
   318  func (*topic) ErrorAs(err error, i interface{}) bool {
   319  	return errorAs(err, i)
   320  }
   321  
   322  func errorAs(err error, i interface{}) bool {
   323  	switch v := err.(type) {
   324  	case *amqp.DetachError:
   325  		if p, ok := i.(**amqp.DetachError); ok {
   326  			*p = v
   327  			return true
   328  		}
   329  	case *amqp.Error:
   330  		if p, ok := i.(**amqp.Error); ok {
   331  			*p = v
   332  			return true
   333  		}
   334  	case common.Retryable:
   335  		if p, ok := i.(*common.Retryable); ok {
   336  			*p = v
   337  			return true
   338  		}
   339  	}
   340  	return false
   341  }
   342  
   343  func (*topic) ErrorCode(err error) gcerrors.ErrorCode {
   344  	code, _ := errorCode(err)
   345  	return code
   346  }
   347  
   348  // Close implements driver.Topic.Close.
   349  func (*topic) Close() error { return nil }
   350  
   351  type subscription struct {
   352  	sbReceiver *servicebus.Receiver
   353  	opts       *SubscriptionOptions
   354  }
   355  
   356  // SubscriptionOptions will contain configuration for subscriptions.
   357  type SubscriptionOptions struct {
   358  	// If false, the serviceBus.Subscription MUST be in the default Peek-Lock mode.
   359  	// If true, the serviceBus.Subscription MUST be in Receive-and-Delete mode.
   360  	// When true: pubsub.Message.Ack will be a no-op, pubsub.Message.Nackable
   361  	// will return true, and pubsub.Message.Nack will panic.
   362  	ReceiveAndDelete bool
   363  
   364  	// ReceiveBatcherOptions adds constraints to the default batching done for receives.
   365  	ReceiveBatcherOptions batcher.Options
   366  
   367  	// AckBatcherOptions adds constraints to the default batching done for acks.
   368  	// Only used when ReceiveAndDelete is false.
   369  	AckBatcherOptions batcher.Options
   370  }
   371  
   372  // OpenSubscription initializes a pubsub Subscription on a given Service Bus Subscription and its parent Service Bus Topic.
   373  func OpenSubscription(ctx context.Context, sbClient *servicebus.Client, sbReceiver *servicebus.Receiver, opts *SubscriptionOptions) (*pubsub.Subscription, error) {
   374  	ds, err := openSubscription(ctx, sbClient, sbReceiver, opts)
   375  	if err != nil {
   376  		return nil, err
   377  	}
   378  	if opts == nil {
   379  		opts = &SubscriptionOptions{}
   380  	}
   381  	rbo := recvBatcherOpts.NewMergedOptions(&opts.ReceiveBatcherOptions)
   382  	abo := ackBatcherOpts.NewMergedOptions(&opts.AckBatcherOptions)
   383  	return pubsub.NewSubscription(ds, rbo, abo), nil
   384  }
   385  
   386  // openSubscription returns a driver.Subscription.
   387  func openSubscription(ctx context.Context, sbClient *servicebus.Client, sbReceiver *servicebus.Receiver, opts *SubscriptionOptions) (driver.Subscription, error) {
   388  	if sbClient == nil {
   389  		return nil, errors.New("azuresb: OpenSubscription requires a Service Bus Client")
   390  	}
   391  	if sbReceiver == nil {
   392  		return nil, errors.New("azuresb: OpenSubscription requires a Service Bus Receiver")
   393  	}
   394  	if opts == nil {
   395  		opts = &SubscriptionOptions{}
   396  	}
   397  	return &subscription{sbReceiver: sbReceiver, opts: opts}, nil
   398  }
   399  
   400  // IsRetryable implements driver.Subscription.IsRetryable.
   401  func (s *subscription) IsRetryable(err error) bool {
   402  	_, retryable := errorCode(err)
   403  	return retryable
   404  }
   405  
   406  // As implements driver.Subscription.As.
   407  func (s *subscription) As(i interface{}) bool {
   408  	p, ok := i.(**servicebus.Receiver)
   409  	if !ok {
   410  		return false
   411  	}
   412  	*p = s.sbReceiver
   413  	return true
   414  }
   415  
   416  // ErrorAs implements driver.Subscription.ErrorAs
   417  func (s *subscription) ErrorAs(err error, i interface{}) bool {
   418  	return errorAs(err, i)
   419  }
   420  
   421  func (s *subscription) ErrorCode(err error) gcerrors.ErrorCode {
   422  	code, _ := errorCode(err)
   423  	return code
   424  }
   425  
   426  // ReceiveBatch implements driver.Subscription.ReceiveBatch.
   427  func (s *subscription) ReceiveBatch(ctx context.Context, maxMessages int) ([]*driver.Message, error) {
   428  	// ReceiveMessages will block until rctx is Done; we want to return after
   429  	// a reasonably short delay even if there are no messages. So, create a
   430  	// sub context for the RPC.
   431  	rctx, cancel := context.WithTimeout(ctx, listenerTimeout)
   432  	defer cancel()
   433  
   434  	var messages []*driver.Message
   435  	sbmsgs, err := s.sbReceiver.ReceiveMessages(rctx, maxMessages, nil)
   436  	for _, sbmsg := range sbmsgs {
   437  		metadata := map[string]string{}
   438  		for key, value := range sbmsg.ApplicationProperties {
   439  			if strVal, ok := value.(string); ok {
   440  				metadata[key] = strVal
   441  			}
   442  		}
   443  		messages = append(messages, &driver.Message{
   444  			LoggableID: sbmsg.MessageID,
   445  			Body:       sbmsg.Body,
   446  			Metadata:   metadata,
   447  			AckID:      sbmsg,
   448  			AsFunc:     messageAsFunc(sbmsg),
   449  		})
   450  	}
   451  	// Mask rctx timeouts, they are expected if no messages are available.
   452  	if err == rctx.Err() {
   453  		err = nil
   454  	}
   455  	return messages, err
   456  }
   457  
   458  func messageAsFunc(sbmsg *servicebus.ReceivedMessage) func(interface{}) bool {
   459  	return func(i interface{}) bool {
   460  		p, ok := i.(**servicebus.ReceivedMessage)
   461  		if !ok {
   462  			return false
   463  		}
   464  		*p = sbmsg
   465  		return true
   466  	}
   467  }
   468  
   469  // SendAcks implements driver.Subscription.SendAcks.
   470  func (s *subscription) SendAcks(ctx context.Context, ids []driver.AckID) error {
   471  	if s.opts.ReceiveAndDelete {
   472  		// Ack is a no-op in Receive-and-Delete mode.
   473  		return nil
   474  	}
   475  	var err error
   476  	for _, id := range ids {
   477  		oneErr := s.sbReceiver.CompleteMessage(ctx, id.(*servicebus.ReceivedMessage), nil)
   478  		if oneErr != nil {
   479  			err = oneErr
   480  		}
   481  	}
   482  	return err
   483  }
   484  
   485  // CanNack implements driver.CanNack.
   486  func (s *subscription) CanNack() bool {
   487  	if s == nil {
   488  		return false
   489  	}
   490  	return !s.opts.ReceiveAndDelete
   491  }
   492  
   493  // SendNacks implements driver.Subscription.SendNacks.
   494  func (s *subscription) SendNacks(ctx context.Context, ids []driver.AckID) error {
   495  	if !s.CanNack() {
   496  		panic("unreachable")
   497  	}
   498  	var err error
   499  	for _, id := range ids {
   500  		oneErr := s.sbReceiver.AbandonMessage(ctx, id.(*servicebus.ReceivedMessage), nil)
   501  		if oneErr != nil {
   502  			err = oneErr
   503  		}
   504  	}
   505  	return err
   506  }
   507  
   508  // errorCode returns an error code and whether err is retryable.
   509  func errorCode(err error) (gcerrors.ErrorCode, bool) {
   510  	// Unfortunately Azure sometimes returns common.Retryable or even
   511  	// errors.errorString, which don't expose anything other than the error
   512  	// string :-(.
   513  	if strings.Contains(err.Error(), "status code 404") {
   514  		return gcerrors.NotFound, false
   515  	}
   516  	var cond amqp.ErrorCondition
   517  	var aderr *amqp.DetachError
   518  	var aerr *amqp.Error
   519  	if errors.As(err, &aderr) {
   520  		if aderr.RemoteError == nil {
   521  			return gcerrors.NotFound, false
   522  		}
   523  		cond = aderr.RemoteError.Condition
   524  	} else if errors.As(err, &aerr) {
   525  		cond = aerr.Condition
   526  	}
   527  	switch cond {
   528  	case amqp.ErrorNotFound:
   529  		return gcerrors.NotFound, false
   530  
   531  	case amqp.ErrorPreconditionFailed:
   532  		return gcerrors.FailedPrecondition, false
   533  
   534  	case amqp.ErrorInternalError:
   535  		return gcerrors.Internal, true
   536  
   537  	case amqp.ErrorNotImplemented:
   538  		return gcerrors.Unimplemented, false
   539  
   540  	case amqp.ErrorUnauthorizedAccess, amqp.ErrorNotAllowed:
   541  		return gcerrors.PermissionDenied, false
   542  
   543  	case amqp.ErrorResourceLimitExceeded:
   544  		return gcerrors.ResourceExhausted, true
   545  
   546  	case amqp.ErrorInvalidField:
   547  		return gcerrors.InvalidArgument, false
   548  	}
   549  	return gcerrors.Unknown, true
   550  }
   551  
   552  // Close implements driver.Subscription.Close.
   553  func (*subscription) Close() error { return nil }