github.com/alwitt/goutils@v0.6.4/pubsub.go (about) 1 package goutils 2 3 import ( 4 "context" 5 "fmt" 6 "sync" 7 "time" 8 9 "cloud.google.com/go/pubsub" 10 "github.com/apex/log" 11 "google.golang.org/api/iterator" 12 ) 13 14 /* 15 CreateBasicGCPPubSubClient define a basic GCP PubSub client 16 17 @param ctxt context.Context - execution context 18 @param projectID string - GCP project ID 19 @returns new client 20 */ 21 func CreateBasicGCPPubSubClient(ctxt context.Context, projectID string) (*pubsub.Client, error) { 22 return pubsub.NewClient(ctxt, projectID) 23 } 24 25 // ========================================================================================== 26 // Client Interface 27 28 // PubSubMessageHandler callback to trigger when PubSub message received 29 type PubSubMessageHandler func( 30 ctxt context.Context, pubTimestamp time.Time, msg []byte, metadata map[string]string, 31 ) error 32 33 // PubSubClient is a wrapper interface around the PubSub API with some ease-of-use features 34 type PubSubClient interface { 35 /* 36 UpdateLocalTopicCache sync local topic cache with existing topics in project 37 38 @param ctxt context.Context - execution context 39 */ 40 UpdateLocalTopicCache(ctxt context.Context) error 41 42 /* 43 UpdateLocalSubscriptionCache sync local subscription cache with existing subscriptions in project 44 45 @param ctxt context.Context - execution context 46 */ 47 UpdateLocalSubscriptionCache(ctxt context.Context) error 48 49 /* 50 CreateTopic create PubSub topic 51 52 @param ctxt context.Context - execution context 53 @param topic string - topic name 54 @param config *pubsub.TopicConfig - optionally, provide config on the topic 55 */ 56 CreateTopic(ctxt context.Context, topic string, config *pubsub.TopicConfig) error 57 58 /* 59 DeleteTopic delete PubSub topic 60 61 @param ctxt context.Context - execution context 62 @param topic string - topic name 63 */ 64 DeleteTopic(ctxt context.Context, topic string) error 65 66 /* 67 GetTopic get the topic config for a topic 68 69 @param ctxt context.Context - execution context 70 @param topic string - topic name 71 @returns if topic is known, the topic config 72 */ 73 GetTopic(ctxt context.Context, topic string) (pubsub.TopicConfig, error) 74 75 /* 76 UpdateTopic update the topic config 77 78 @param ctxt context.Context - execution context 79 @param topic string - topic name 80 @param newConfig pubsub.TopicConfigToUpdate - the new config 81 */ 82 UpdateTopic(ctxt context.Context, topic string, newConfig pubsub.TopicConfigToUpdate) error 83 84 /* 85 CreateSubscription create PubSub subscription to attach to topic 86 87 @param ctxt context.Context - execution context 88 @param targetTopic string - target topic 89 @param subscription string - subscription name 90 @param config pubsub.SubscriptionConfig - subscription config 91 */ 92 CreateSubscription( 93 ctxt context.Context, targetTopic, subscription string, config pubsub.SubscriptionConfig, 94 ) error 95 96 /* 97 DeleteSubscription delete PubSub subscription 98 99 @param ctxt context.Context - execution context 100 @param subscription string - subscription name 101 */ 102 DeleteSubscription(ctxt context.Context, subscription string) error 103 104 /* 105 GetSubscription get the subscription config for a subscription 106 107 @param ctxt context.Context - execution context 108 @param subscription string - subscription name 109 @returns if subscription is known, the subscription config 110 */ 111 GetSubscription(ctxt context.Context, subscription string) (pubsub.SubscriptionConfig, error) 112 113 /* 114 UpdateSubscription update the subscription config 115 116 @param ctxt context.Context - execution context 117 @param subscription string - subscription name 118 @param newConfig pubsub.SubscriptionConfigToUpdate - the new config 119 */ 120 UpdateSubscription( 121 ctxt context.Context, subscription string, newConfig pubsub.SubscriptionConfigToUpdate, 122 ) error 123 124 /* 125 Publish publish a message to a topic 126 127 @param ctxt context.Context - execution context 128 @param topic string - topic name 129 @param message []byte - message content 130 @param metadata map[string]string - message metadata, which will be sent using attributes 131 @param blocking bool - whether the call is blocking until publish is complete 132 @returns when non-blocking, the async result object to check on publish status 133 */ 134 Publish( 135 ctxt context.Context, topic string, message []byte, metadata map[string]string, blocking bool, 136 ) (*pubsub.PublishResult, error) 137 138 /* 139 Subscribe subscribe for message on a subscription 140 141 THIS CALL IS BLOCKING!! 142 143 @param ctxt context.Context - execution context 144 @param subscription string - subscription name 145 @param handler PubSubMessageHandler - RX message callback 146 */ 147 Subscribe(ctxt context.Context, subscription string, handler PubSubMessageHandler) error 148 149 /* 150 Close close and clean up the client 151 152 @param ctxt context.Context - execution context 153 */ 154 Close(ctxt context.Context) error 155 } 156 157 // pubsubClientImpl implements PubSubClient 158 type pubsubClientImpl struct { 159 Component 160 client *pubsub.Client 161 topicLock sync.RWMutex 162 topics map[string]*pubsub.Topic 163 subLock sync.RWMutex 164 subscriptions map[string]*pubsub.Subscription 165 metricsHelper PubSubMetricHelper 166 } 167 168 /* 169 GetNewPubSubClientInstance get PubSub wrapper client 170 171 @param client *pubsub.Client - core PubSub client 172 @param logTags log.Fields - metadata fields to include in the logs 173 @param metricsHelper PubSubMetricHelper - metric collection helper agent 174 @returns new PubSubClient instance 175 */ 176 func GetNewPubSubClientInstance( 177 client *pubsub.Client, logTags log.Fields, metricsHelper PubSubMetricHelper, 178 ) (PubSubClient, error) { 179 return &pubsubClientImpl{ 180 Component: Component{LogTags: logTags}, 181 client: client, 182 topicLock: sync.RWMutex{}, 183 topics: make(map[string]*pubsub.Topic), 184 subLock: sync.RWMutex{}, 185 subscriptions: make(map[string]*pubsub.Subscription), 186 metricsHelper: metricsHelper, 187 }, nil 188 } 189 190 // ========================================================================================== 191 // Maintenance 192 193 // updateLocalTopicCacheCore independent function for updating local topic cache for reuse 194 func (p *pubsubClientImpl) updateLocalTopicCacheCore(ctxt context.Context) error { 195 logTag := p.GetLogTagsForContext(ctxt) 196 topicItr := p.client.Topics(ctxt) 197 for { 198 topicEntry, err := topicItr.Next() 199 if err == iterator.Done { 200 break 201 } 202 if err != nil { 203 log.WithError(err).WithFields(logTag).Error("Topic iterator query failure") 204 return err 205 } 206 p.topics[topicEntry.ID()] = topicEntry 207 log.WithFields(logTag).Debugf("Found topic '%s'", topicEntry.ID()) 208 } 209 return nil 210 } 211 212 /* 213 UpdateLocalTopicCache sync local topic cache with existing topics in project 214 215 @param ctxt context.Context - execution context 216 */ 217 func (p *pubsubClientImpl) UpdateLocalTopicCache(ctxt context.Context) error { 218 p.topicLock.Lock() 219 defer p.topicLock.Unlock() 220 return p.updateLocalTopicCacheCore(ctxt) 221 } 222 223 /* 224 UpdateLocalSubscriptionCache sync local subscription cache with existing subscriptions in project 225 226 @param ctxt context.Context - execution context 227 */ 228 func (p *pubsubClientImpl) UpdateLocalSubscriptionCache(ctxt context.Context) error { 229 logTag := p.GetLogTagsForContext(ctxt) 230 p.subLock.Lock() 231 defer p.subLock.Unlock() 232 subItr := p.client.Subscriptions(ctxt) 233 for { 234 subEntry, err := subItr.Next() 235 if err == iterator.Done { 236 break 237 } 238 if err != nil { 239 log.WithError(err).WithFields(logTag).Error("Topic iterator query failure") 240 return err 241 } 242 p.subscriptions[subEntry.ID()] = subEntry 243 log.WithFields(logTag).Debugf("Found subscription '%s'", subEntry.ID()) 244 } 245 return nil 246 } 247 248 /* 249 Close close and clean up the client 250 251 @param ctxt context.Context - execution context 252 */ 253 func (p *pubsubClientImpl) Close(ctxt context.Context) error { 254 logTag := p.GetLogTagsForContext(ctxt) 255 { 256 // Stop all the topics 257 p.topicLock.Lock() 258 defer p.topicLock.Unlock() 259 260 for topicName, topic := range p.topics { 261 log.WithFields(logTag).Debugf("Stopping topic '%s'", topicName) 262 topic.Stop() 263 log.WithFields(logTag).Infof("Stopped topic '%s'", topicName) 264 } 265 } 266 return nil 267 } 268 269 // ========================================================================================== 270 // Topics 271 272 /* 273 CreateTopic create PubSub topic 274 275 @param ctxt context.Context - execution context 276 @param topic string - topic name 277 @param config *pubsub.TopicConfig - optionally, provide config on the topic 278 */ 279 func (p *pubsubClientImpl) CreateTopic( 280 ctxt context.Context, topic string, config *pubsub.TopicConfig, 281 ) error { 282 p.topicLock.Lock() 283 defer p.topicLock.Unlock() 284 285 logTag := p.GetLogTagsForContext(ctxt) 286 287 // If this instance has created this topic before 288 if _, ok := p.topics[topic]; ok { 289 log.WithFields(logTag).Infof("Topic '%s' already exist", topic) 290 return nil 291 } 292 293 log.WithFields(logTag).Debugf("Creating topic '%s'", topic) 294 topicHandle, err := p.client.CreateTopicWithConfig(ctxt, topic, config) 295 if err != nil { 296 log.WithError(err).WithFields(logTag).Errorf("Topic '%s' creation failed", topic) 297 return err 298 } 299 log.WithFields(logTag).Infof("Created topic '%s'", topic) 300 301 p.topics[topic] = topicHandle 302 303 return nil 304 } 305 306 // getTopicHandle get topic handle 307 func (p *pubsubClientImpl) getTopicHandle( 308 ctxt context.Context, topic string, doLock bool, 309 ) (*pubsub.Topic, error) { 310 logTag := p.GetLogTagsForContext(ctxt) 311 312 if doLock { 313 p.topicLock.RLock() 314 defer p.topicLock.RUnlock() 315 } 316 317 t, ok := p.topics[topic] 318 if !ok { 319 // Update the local topic cache 320 if err := p.updateLocalTopicCacheCore(ctxt); err != nil { 321 log.WithError(err).WithFields(logTag).Error("Failed syncing local topic cache") 322 return nil, err 323 } 324 325 t, ok = p.topics[topic] 326 if !ok { 327 // Topic is really missing 328 err := fmt.Errorf("topic '%s' is unknown", topic) 329 return nil, err 330 } 331 } 332 return t, nil 333 } 334 335 /* 336 DeleteTopic delete PubSub topic 337 338 @param ctxt context.Context - execution context 339 @param topic string - topic name 340 */ 341 func (p *pubsubClientImpl) DeleteTopic(ctxt context.Context, topic string) error { 342 p.topicLock.Lock() 343 defer p.topicLock.Unlock() 344 345 logTag := p.GetLogTagsForContext(ctxt) 346 347 topicHandle, err := p.getTopicHandle(ctxt, topic, false) 348 if err != nil { 349 log.WithError(err).WithFields(logTag).Errorf("Unable to delete topic '%s'", topic) 350 return err 351 } 352 353 // This instance has initialized this topic 354 log.WithFields(logTag).Infof("Stopping handler for topic '%s'", topic) 355 topicHandle.Stop() 356 log.WithFields(logTag).Debugf("Deleting topic '%s'", topic) 357 if err := topicHandle.Delete(ctxt); err != nil { 358 log.WithError(err).WithFields(logTag).Errorf("Unable to delete topic '%s'", topic) 359 return err 360 } 361 log.WithFields(logTag).Infof("Deleted topic '%s'", topic) 362 363 // Forgot the handle 364 delete(p.topics, topic) 365 366 return nil 367 } 368 369 /* 370 GetTopic get the topic config for a topic 371 372 @param ctxt context.Context - execution context 373 @param topic string - topic name 374 @returns if topic is known, the topic config 375 */ 376 func (p *pubsubClientImpl) GetTopic(ctxt context.Context, topic string) (pubsub.TopicConfig, error) { 377 p.topicLock.RLock() 378 defer p.topicLock.RUnlock() 379 380 logTag := p.GetLogTagsForContext(ctxt) 381 382 topicHandle, err := p.getTopicHandle(ctxt, topic, false) 383 if err != nil { 384 log.WithError(err).WithFields(logTag).Errorf("Unable to find topic '%s'", topic) 385 return pubsub.TopicConfig{}, err 386 } 387 388 log.WithFields(logTag).Debugf("Reading topic '%s' config", topic) 389 config, err := topicHandle.Config(ctxt) 390 if err != nil { 391 log.WithError(err).WithFields(logTag).Errorf("Unable to read topic '%s' config", topic) 392 return pubsub.TopicConfig{}, err 393 } 394 log.WithFields(logTag).Infof("Read topic '%s' config", topic) 395 396 return config, nil 397 } 398 399 /* 400 UpdateTopic update the topic config 401 402 @param ctxt context.Context - execution context 403 @param topic string - topic name 404 @param newConfig pubsub.TopicConfigToUpdate - the new config 405 */ 406 func (p *pubsubClientImpl) UpdateTopic( 407 ctxt context.Context, topic string, newConfig pubsub.TopicConfigToUpdate, 408 ) error { 409 p.topicLock.Lock() 410 defer p.topicLock.Unlock() 411 412 logTag := p.GetLogTagsForContext(ctxt) 413 414 topicHandle, err := p.getTopicHandle(ctxt, topic, false) 415 if err != nil { 416 log.WithError(err).WithFields(logTag).Errorf("Unable to find topic '%s'", topic) 417 return err 418 } 419 420 log.WithFields(logTag).Debugf("Updating topic '%s' config", topic) 421 if _, err := topicHandle.Update(ctxt, newConfig); err != nil { 422 log.WithError(err).WithFields(logTag).Errorf("Unable to update topic '%s'", topic) 423 return err 424 } 425 log.WithFields(logTag).Infof("Updated topic '%s' config", topic) 426 427 return nil 428 } 429 430 // ========================================================================================== 431 // Subscriptions 432 433 /* 434 CreateSubscription create PubSub subscription to attach to topic 435 436 @param ctxt context.Context - execution context 437 @param targetTopic string - target topic 438 @param subscription string - subscription name 439 @param config pubsub.SubscriptionConfig - subscription config 440 */ 441 func (p *pubsubClientImpl) CreateSubscription( 442 ctxt context.Context, targetTopic, subscription string, config pubsub.SubscriptionConfig, 443 ) error { 444 logTag := p.GetLogTagsForContext(ctxt) 445 446 // Get the topic handle 447 topic, err := p.getTopicHandle(ctxt, targetTopic, true) 448 if err != nil { 449 log.WithError(err).WithFields(logTag).Errorf("Unable to create subscription '%s'", subscription) 450 return err 451 } 452 453 // Create the subscription 454 config.Topic = topic 455 { 456 p.subLock.Lock() 457 defer p.subLock.Unlock() 458 459 // If this instance has created this topic before 460 if _, ok := p.subscriptions[subscription]; ok { 461 log.WithFields(logTag).Infof("Subscription '%s' already exist", subscription) 462 return nil 463 } 464 465 log.WithFields(logTag).Debugf("Creating subscription '%s'", subscription) 466 subHandle, err := p.client.CreateSubscription(ctxt, subscription, config) 467 if err != nil { 468 log.WithError(err).WithFields(logTag).Errorf("Unable to create subscription '%s'", subscription) 469 return err 470 } 471 log.WithFields(logTag).Infof("Created subscription '%s'", subscription) 472 473 p.subscriptions[subscription] = subHandle 474 } 475 476 return nil 477 } 478 479 /* 480 DeleteSubscription delete PubSub subscription 481 482 @param ctxt context.Context - execution context 483 @param subscription string - subscription name 484 */ 485 func (p *pubsubClientImpl) DeleteSubscription(ctxt context.Context, subscription string) error { 486 p.subLock.Lock() 487 defer p.subLock.Unlock() 488 489 logTag := p.GetLogTagsForContext(ctxt) 490 491 subHandle, ok := p.subscriptions[subscription] 492 if !ok { 493 err := fmt.Errorf("this instance does not know of subscription '%s'", subscription) 494 log.WithError(err).WithFields(logTag).Errorf("Unable to delete subscription '%s'", subscription) 495 return err 496 } 497 498 log.WithFields(logTag).Debugf("Deleting subscription '%s'", subscription) 499 if err := subHandle.Delete(ctxt); err != nil { 500 log.WithError(err).WithFields(logTag).Errorf("Unable to delete subscription '%s'", subscription) 501 return err 502 } 503 log.WithFields(logTag).Infof("Deleted subscription '%s'", subscription) 504 505 // Forgot the handle 506 delete(p.subscriptions, subscription) 507 508 return nil 509 } 510 511 /* 512 GetSubscription get the subscription config for a subscription 513 514 @param ctxt context.Context - execution context 515 @param subscription string - subscription name 516 @returns if subscription is known, the subscription config 517 */ 518 func (p *pubsubClientImpl) GetSubscription( 519 ctxt context.Context, subscription string, 520 ) (pubsub.SubscriptionConfig, error) { 521 p.subLock.Lock() 522 defer p.subLock.Unlock() 523 524 logTag := p.GetLogTagsForContext(ctxt) 525 526 subHandle, ok := p.subscriptions[subscription] 527 if !ok { 528 err := fmt.Errorf("this instance does not know of subscription '%s'", subscription) 529 log.WithError(err).WithFields(logTag).Errorf("Unable to find subscription '%s'", subscription) 530 return pubsub.SubscriptionConfig{}, err 531 } 532 533 log.WithFields(logTag).Debugf("Reading subscription '%s' config", subscription) 534 config, err := subHandle.Config(ctxt) 535 if err != nil { 536 log. 537 WithError(err). 538 WithFields(logTag). 539 Errorf("Unable to read subscription '%s' config", subscription) 540 return pubsub.SubscriptionConfig{}, err 541 } 542 log.WithFields(logTag).Infof("Read subscription '%s' config", subscription) 543 544 return config, nil 545 } 546 547 /* 548 UpdateSubscription update the subscription config 549 550 @param ctxt context.Context - execution context 551 @param subscription string - subscription name 552 @param newConfig pubsub.SubscriptionConfigToUpdate - the new config 553 */ 554 func (p *pubsubClientImpl) UpdateSubscription( 555 ctxt context.Context, subscription string, newConfig pubsub.SubscriptionConfigToUpdate, 556 ) error { 557 p.subLock.Lock() 558 defer p.subLock.Unlock() 559 560 logTag := p.GetLogTagsForContext(ctxt) 561 562 subHandle, ok := p.subscriptions[subscription] 563 if !ok { 564 err := fmt.Errorf("this instance does not know of subscription '%s'", subscription) 565 log.WithError(err).WithFields(logTag).Errorf("Unable to find subscription '%s'", subscription) 566 return err 567 } 568 569 log.WithFields(logTag).Debugf("Updating subscription '%s' config", subscription) 570 if _, err := subHandle.Update(ctxt, newConfig); err != nil { 571 log.WithError(err).WithFields(logTag).Errorf("Unable to update subscription '%s'", subscription) 572 return err 573 } 574 log.WithFields(logTag).Infof("Updated subscription '%s' config", subscription) 575 576 return nil 577 } 578 579 // ========================================================================================== 580 // Message Passing 581 582 /* 583 Publish publish a message to a topic 584 585 @param ctxt context.Context - execution context 586 @param topic string - topic name 587 @param message []byte - message content 588 @param metadata map[string]string - message metadata, which will be sent using attributes 589 @param blocking bool - whether the call is blocking until publish is complete 590 @returns when non-blocking, the async result object to check on publish status 591 */ 592 func (p *pubsubClientImpl) Publish( 593 ctxt context.Context, topic string, message []byte, metadata map[string]string, blocking bool, 594 ) (*pubsub.PublishResult, error) { 595 logTag := p.GetLogTagsForContext(ctxt) 596 597 // Get the topic handle 598 topicHandle, err := p.getTopicHandle(ctxt, topic, true) 599 if err != nil { 600 log.WithError(err).WithFields(logTag).Errorf("Publish on topic '%s' failed", topic) 601 if p.metricsHelper != nil { 602 p.metricsHelper.RecordPublish(topic, false, int64(len(message))) 603 } 604 return nil, err 605 } 606 607 log.WithFields(logTag).Debugf("Publishing message on topic '%s'", topic) 608 txHandle := topicHandle.Publish(ctxt, &pubsub.Message{Data: message, Attributes: metadata}) 609 610 if blocking { 611 // Wait for publish to finish 612 txID, err := txHandle.Get(ctxt) 613 if err != nil { 614 log.WithError(err).WithFields(logTag).Errorf("Publish on topic '%s' failed", topic) 615 if p.metricsHelper != nil { 616 p.metricsHelper.RecordPublish(topic, false, int64(len(message))) 617 } 618 return nil, err 619 } 620 log.WithFields(logTag).Debugf("Published message [%s] on topic '%s'", txID, topic) 621 } else { 622 log.WithFields(logTag).Debugf("Published message on topic '%s'", topic) 623 } 624 625 if p.metricsHelper != nil { 626 p.metricsHelper.RecordPublish(topic, true, int64(len(message))) 627 } 628 return txHandle, nil 629 } 630 631 // getSubscriptionHandle get subscription handle 632 func (p *pubsubClientImpl) getSubscriptionHandle(ctxt context.Context, subscription string) (*pubsub.Subscription, error) { 633 p.subLock.RLock() 634 defer p.subLock.RUnlock() 635 636 s, ok := p.subscriptions[subscription] 637 if !ok { 638 err := fmt.Errorf("subscription '%s' is unknown", subscription) 639 return nil, err 640 } 641 642 return s, nil 643 } 644 645 /* 646 Subscribe subscribe for message on a subscription. 647 648 THIS CALL IS BLOCKING!! 649 650 @param ctxt context.Context - execution context 651 @param subscription string - subscription name 652 @param handler PubSubMessageHandler - RX message callback 653 */ 654 func (p *pubsubClientImpl) Subscribe( 655 ctxt context.Context, subscription string, handler PubSubMessageHandler, 656 ) error { 657 logTag := p.GetLogTagsForContext(ctxt) 658 659 // Get the subscription handle 660 subscriptionHandle, err := p.getSubscriptionHandle(ctxt, subscription) 661 if err != nil { 662 log.WithError(err).WithFields(logTag).Errorf("Listen on subscription '%s' failed", subscription) 663 return err 664 } 665 666 // Get the associated topic handle 667 subConfig, err := subscriptionHandle.Config(ctxt) 668 if err != nil { 669 log.WithError(err).WithFields(logTag).Errorf("Listen on subscription '%s' failed", subscription) 670 return err 671 } 672 673 topicHandle := subConfig.Topic 674 675 // Install subscription receive 676 if err := subscriptionHandle.Receive(ctxt, func(ctx context.Context, m *pubsub.Message) { 677 if err := handler(ctx, m.PublishTime, m.Data, m.Attributes); err != nil { 678 log. 679 WithError(err). 680 WithFields(logTag). 681 WithField("topic", topicHandle.ID()). 682 WithField("subscription", subscription). 683 Errorf("Failed to process message [%s]", m.ID) 684 if p.metricsHelper != nil { 685 p.metricsHelper.RecordReceive(topicHandle.ID(), false, int64(len(m.Data))) 686 } 687 m.Nack() 688 } else { 689 log. 690 WithError(err). 691 WithFields(logTag). 692 WithField("topic", topicHandle.ID()). 693 WithField("subscription", subscription). 694 Debugf("Processed message [%s]", m.ID) 695 if p.metricsHelper != nil { 696 p.metricsHelper.RecordReceive(topicHandle.ID(), true, int64(len(m.Data))) 697 } 698 m.Ack() 699 } 700 }); err != nil { 701 log.WithError(err).WithFields(logTag).Errorf("Listen on subscription '%s' failed", subscription) 702 return err 703 } 704 705 return nil 706 }