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  }