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 }