github.com/alwitt/goutils@v0.6.4/bus.go (about)

     1  package goutils
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"sync"
     7  	"time"
     8  
     9  	"github.com/apex/log"
    10  )
    11  
    12  // MessageBus an application scoped local message bus
    13  type MessageBus interface {
    14  	/*
    15  		CreateTopic create new message topic
    16  
    17  			@param ctxt context.Context - execution context
    18  			@param topicName string - topic name
    19  			@param topicLogTags log.Fields - metadata fields to include in the logs of the topic entity
    20  			@return new MessageTopic instance
    21  	*/
    22  	CreateTopic(
    23  		ctxt context.Context, topicName string, topicLogTags log.Fields,
    24  	) (MessageTopic, error)
    25  
    26  	/*
    27  		GetTopic fetch a message topic
    28  
    29  			@param ctxt context.Context - execution context
    30  			@param topicName string - topic name
    31  			@returns MessageTopic instance
    32  	*/
    33  	GetTopic(ctxt context.Context, topicName string) (MessageTopic, error)
    34  
    35  	/*
    36  		DeleteTopic delete a message topic
    37  
    38  			@param ctxt context.Context - execution context
    39  			@param topicName string - topic name
    40  	*/
    41  	DeleteTopic(ctxt context.Context, topicName string) error
    42  }
    43  
    44  // messageBusImpl implements MessageBus
    45  type messageBusImpl struct {
    46  	Component
    47  
    48  	// manage access to subscriptions
    49  	lock sync.RWMutex
    50  
    51  	// collection of topic
    52  	topics map[string]MessageTopic
    53  
    54  	operationContext context.Context
    55  	contextCancel    context.CancelFunc
    56  }
    57  
    58  /*
    59  GetNewMessageBusInstance get message bus instance
    60  
    61  	@param parentCtxt context.Context - parent execution context
    62  	@param logTags log.Fields - metadata fields to include in the logs
    63  	@return new MessageBus instance
    64  */
    65  func GetNewMessageBusInstance(parentCtxt context.Context, logTags log.Fields) (MessageBus, error) {
    66  	optCtxt, cancel := context.WithCancel(parentCtxt)
    67  
    68  	instance := &messageBusImpl{
    69  		Component:        Component{LogTags: logTags},
    70  		lock:             sync.RWMutex{},
    71  		topics:           make(map[string]MessageTopic),
    72  		operationContext: optCtxt,
    73  		contextCancel:    cancel,
    74  	}
    75  
    76  	return instance, nil
    77  }
    78  
    79  /*
    80  CreateTopic create new message topic
    81  
    82  	@param ctxt context.Context - execution context
    83  	@param topicName string - topic name
    84  	@param topicLogTags log.Fields - metadata fields to include in the logs of the topic entity
    85  	@return new MessageTopic instance
    86  */
    87  func (b *messageBusImpl) CreateTopic(
    88  	ctxt context.Context, topicName string, topicLogTags log.Fields,
    89  ) (MessageTopic, error) {
    90  	logTags := b.GetLogTagsForContext(ctxt)
    91  
    92  	b.lock.Lock()
    93  	defer b.lock.Unlock()
    94  
    95  	if existing, ok := b.topics[topicName]; ok {
    96  		log.WithFields(logTags).WithField("topic", topicName).Debug("Topic already defined")
    97  		return existing, nil
    98  	}
    99  
   100  	topic, err := getNewMessageTopicInstance(b.operationContext, topicName, topicLogTags)
   101  
   102  	if err != nil {
   103  		return nil, err
   104  	}
   105  
   106  	b.topics[topicName] = topic
   107  
   108  	log.WithFields(logTags).WithField("topic", topicName).Info("Defined new topic")
   109  
   110  	return topic, nil
   111  }
   112  
   113  /*
   114  GetTopic fetch a message topic
   115  
   116  	@param ctxt context.Context - execution context
   117  	@param topicName string - topic name
   118  	@returns MessageTopic instance
   119  */
   120  func (b *messageBusImpl) GetTopic(ctxt context.Context, topicName string) (MessageTopic, error) {
   121  	b.lock.RLock()
   122  	defer b.lock.RUnlock()
   123  
   124  	if existing, ok := b.topics[topicName]; ok {
   125  		return existing, nil
   126  	}
   127  
   128  	return nil, fmt.Errorf("topic is unknown")
   129  }
   130  
   131  /*
   132  DeleteTopic delete a message topic
   133  
   134  	@param ctxt context.Context - execution context
   135  	@param topicName string - topic name
   136  */
   137  func (b *messageBusImpl) DeleteTopic(ctxt context.Context, topicName string) error {
   138  	logTags := b.GetLogTagsForContext(ctxt)
   139  
   140  	b.lock.RLock()
   141  	defer b.lock.RUnlock()
   142  
   143  	if _, ok := b.topics[topicName]; !ok {
   144  		return fmt.Errorf("topic is unknown")
   145  	}
   146  
   147  	delete(b.topics, topicName)
   148  
   149  	log.WithFields(logTags).WithField("topic", topicName).Info("Deleted topic")
   150  	return nil
   151  }
   152  
   153  // ======================================================================================
   154  
   155  // MessageTopic a message bus topic, responsible for managing its child subscriptions.
   156  type MessageTopic interface {
   157  	/*
   158  		Publish publish a message on the topic in parallel.
   159  
   160  			@param ctxt context.Context - execution context
   161  			@param message interface{} - the message to send
   162  			@param blockFor time.Duration - how long to block for the publish to complete. If >0,
   163  			    this is a non-blocking call; blocking call otherwise.
   164  	*/
   165  	Publish(ctxt context.Context, message interface{}, blockFor time.Duration) error
   166  
   167  	/*
   168  		CreateSubscription create a new topic subscription
   169  
   170  			@param ctxt context.Context - execution context
   171  			@param subscriber string - name of the subscription
   172  			@param bufferLen int - length of message buffer
   173  			@returns the channel to receive messages on
   174  	*/
   175  	CreateSubscription(
   176  		ctxt context.Context, subscriber string, bufferLen int,
   177  	) (chan interface{}, error)
   178  
   179  	/*
   180  		DeleteSubscription delete an existing topic subscription
   181  
   182  			@param ctxt context.Context - execution context
   183  			@param subscriber string - subscription to delete
   184  	*/
   185  	DeleteSubscription(ctxt context.Context, subscriber string) error
   186  }
   187  
   188  // messageTopicImpl implements MessageTopic
   189  type messageTopicImpl struct {
   190  	Component
   191  	topic string
   192  
   193  	// manage access to subscriptions
   194  	lock sync.RWMutex
   195  
   196  	// collection of subscription of this topic
   197  	subscriptions map[string]chan interface{}
   198  
   199  	operationContext context.Context
   200  	contextCancel    context.CancelFunc
   201  }
   202  
   203  /*
   204  getNewMessageTopicInstance get message topic instance
   205  
   206  	@param parentCtxt context.Context - parent execution context
   207  	@param topic string - topic name
   208  	@param logTags log.Fields - metadata fields to include in the logs
   209  	@return new MessageTopic instance
   210  */
   211  func getNewMessageTopicInstance(
   212  	parentCtxt context.Context, topic string, logTags log.Fields,
   213  ) (MessageTopic, error) {
   214  	optCtxt, cancel := context.WithCancel(parentCtxt)
   215  
   216  	instance := &messageTopicImpl{
   217  		Component:        Component{LogTags: logTags},
   218  		topic:            topic,
   219  		lock:             sync.RWMutex{},
   220  		subscriptions:    make(map[string]chan interface{}),
   221  		operationContext: optCtxt,
   222  		contextCancel:    cancel,
   223  	}
   224  
   225  	return instance, nil
   226  }
   227  
   228  /*
   229  Publish publish a message on the topic in parallel.
   230  
   231  	@param ctxt context.Context - execution context
   232  	@param message interface{} - the message to send
   233  	@param blockFor time.Duration - how long to block for the publish to complete. If >0,
   234  	    this is a non-blocking call; blocking call otherwise.
   235  */
   236  func (t *messageTopicImpl) Publish(
   237  	ctxt context.Context, message interface{}, blockFor time.Duration,
   238  ) error {
   239  	logTags := t.GetLogTagsForContext(ctxt)
   240  
   241  	blocking := blockFor <= 0
   242  
   243  	var lclCtxt context.Context
   244  	var lclCancel context.CancelFunc
   245  
   246  	if blocking {
   247  		lclCtxt, lclCancel = context.WithCancel(ctxt)
   248  	} else {
   249  		lclCtxt, lclCancel = context.WithTimeout(ctxt, blockFor)
   250  	}
   251  
   252  	defer lclCancel()
   253  
   254  	// Duplicate the message to all subscribers in parallel
   255  	wg := sync.WaitGroup{}
   256  
   257  	// Function to write the message to the subscriber
   258  	tx := func(subscriber string, buffer chan interface{}) {
   259  		wg.Done()
   260  		// Write message
   261  		select {
   262  		case buffer <- message:
   263  			break
   264  		case <-lclCtxt.Done():
   265  			// write timed out
   266  			log.
   267  				WithFields(logTags).
   268  				WithField("subscriber", subscriber).
   269  				Error("Timed out sending message to subscriber")
   270  		}
   271  
   272  		log.
   273  			WithFields(logTags).
   274  			WithField("subscriber", subscriber).
   275  			Debug("Published message to subscriber")
   276  	}
   277  
   278  	t.lock.RLock()
   279  	defer t.lock.RUnlock()
   280  
   281  	// Start all the message sending helpers
   282  	wg.Add(len(t.subscriptions))
   283  	for subscriber, channel := range t.subscriptions {
   284  		go tx(subscriber, channel)
   285  	}
   286  
   287  	if blocking {
   288  		wg.Wait()
   289  		return nil
   290  	}
   291  	return TimeBoundedWaitGroupWait(lclCtxt, &wg, blockFor)
   292  }
   293  
   294  /*
   295  CreateSubscription create a new topic subscription
   296  
   297  	@param ctxt context.Context - execution context
   298  	@param subscriber string - name of the subscription
   299  	@param bufferLen int - length of message buffer
   300  	@returns the channel to receive messages on
   301  */
   302  func (t *messageTopicImpl) CreateSubscription(
   303  	ctxt context.Context, subscriber string, bufferLen int,
   304  ) (chan interface{}, error) {
   305  	logTags := t.GetLogTagsForContext(ctxt)
   306  
   307  	t.lock.Lock()
   308  	defer t.lock.Unlock()
   309  
   310  	if _, ok := t.subscriptions[subscriber]; ok {
   311  		return nil, fmt.Errorf("subscription '%s' already exist", subscriber)
   312  	}
   313  
   314  	msgBuffer := make(chan interface{}, bufferLen)
   315  
   316  	t.subscriptions[subscriber] = msgBuffer
   317  
   318  	log.
   319  		WithFields(logTags).
   320  		WithField("buffer-len", bufferLen).
   321  		Infof("Created new subscription '%s'", subscriber)
   322  
   323  	return msgBuffer, nil
   324  }
   325  
   326  /*
   327  DeleteSubscription delete an existing topic subscription
   328  
   329  	@param ctxt context.Context - execution context
   330  	@param subscriber string - subscription to delete
   331  */
   332  func (t *messageTopicImpl) DeleteSubscription(ctxt context.Context, subscriber string) error {
   333  	logTags := t.GetLogTagsForContext(ctxt)
   334  
   335  	t.lock.Lock()
   336  	defer t.lock.Unlock()
   337  
   338  	existing, ok := t.subscriptions[subscriber]
   339  	if !ok {
   340  		return fmt.Errorf("unknown subscription '%s'", subscriber)
   341  	}
   342  	log.
   343  		WithFields(logTags).
   344  		Debugf("Closing subscription '%s' channel", subscriber)
   345  	close(existing)
   346  	log.
   347  		WithFields(logTags).
   348  		Infof("Closed subscription '%s' channel", subscriber)
   349  	delete(t.subscriptions, subscriber)
   350  
   351  	log.
   352  		WithFields(logTags).
   353  		Infof("Deleted subscription '%s'", subscriber)
   354  
   355  	return nil
   356  }