github.com/xmlking/toolkit/broker/pubsub@v0.3.4/default.go (about)

     1  package broker
     2  
     3  import (
     4  	"context"
     5  	"strings"
     6  
     7  	"cloud.google.com/go/pubsub"
     8  	"github.com/cockroachdb/errors"
     9  	"github.com/rs/zerolog/log"
    10  	"golang.org/x/sync/errgroup"
    11  )
    12  
    13  const (
    14  	DefaultName = "mkit.broker.default"
    15  )
    16  
    17  type pubsubBroker struct {
    18  	client  *pubsub.Client
    19  	options Options
    20  	subs    []*pubsubSubscriber
    21  	pubs    []*pubsubPublisher
    22  }
    23  
    24  type pubsubPublisher struct {
    25  	options PublishOptions
    26  	topic   *pubsub.Topic
    27  }
    28  
    29  func (p *pubsubPublisher) Topic() string {
    30  	return p.topic.String()
    31  }
    32  
    33  // Stop should be called once
    34  func (p *pubsubPublisher) stop() {
    35  	log.Info().Str("component", "pubsub").Msgf("Stopping Publisher: %s", p.Topic())
    36  	// It blocks until all items have been flushed.
    37  	p.topic.Stop()
    38  	log.Info().Str("component", "pubsub").Msgf("Stopped Publisher Gracefully: %s", p.Topic())
    39  }
    40  
    41  func (p *pubsubPublisher) Publish(ctx context.Context, msg *pubsub.Message) (err error) {
    42  	pr := p.topic.Publish(ctx, msg)
    43  	if !p.options.Async {
    44  		if _, err = pr.Get(ctx); err != nil {
    45  			log.Error().Err(err).Msgf("Unable to publish to topic: %s", p.topic.String())
    46  		}
    47  	}
    48  	return
    49  }
    50  
    51  type pubsubSubscriber struct {
    52  	options SubscribeOptions
    53  	sub     *pubsub.Subscription
    54  	hdlr    Handler
    55  	done    chan struct{}
    56  }
    57  
    58  func (s *pubsubSubscriber) start(ctx context.Context) (err error) {
    59  	defer close(s.done)
    60  	log.Info().Str("component", "pubsub").Msgf("Subscribing to: %s", s.sub)
    61  	// If ctx is done, Receive returns nil after all of the outstanding calls to `s.hdlr` have returned
    62  	// and all messages have been acknowledged or have expired.
    63  	if err = s.sub.Receive(ctx, s.hdlr); err == nil {
    64  		log.Info().Str("component", "pubsub").Msgf("Stopped Subscriber Gracefully: %s", s.sub)
    65  	}
    66  	return
    67  }
    68  
    69  func (b *pubsubBroker) NewPublisher(topic string, opts ...PublishOption) (Publisher, error) {
    70  	t := b.client.Topic(topic)
    71  
    72  	options := PublishOptions{
    73  		Async: false,
    74  	}
    75  
    76  	for _, o := range opts {
    77  		o(&options)
    78  	}
    79  
    80  	if exists, err := t.Exists(context.Background()); err != nil {
    81  		return nil, err
    82  	} else if !exists {
    83  		err = errors.Errorf("Doesn't exist Topic: %s", t)
    84  		return nil, err
    85  	}
    86  
    87  	if options.PublishSettings.DelayThreshold != 0 {
    88  		t.PublishSettings.DelayThreshold = options.PublishSettings.DelayThreshold
    89  	}
    90  	if options.PublishSettings.CountThreshold != 0 {
    91  		t.PublishSettings.CountThreshold = options.PublishSettings.CountThreshold
    92  	}
    93  	if options.PublishSettings.ByteThreshold != 0 {
    94  		t.PublishSettings.ByteThreshold = options.PublishSettings.ByteThreshold
    95  	}
    96  	if options.PublishSettings.NumGoroutines != 0 {
    97  		t.PublishSettings.NumGoroutines = options.PublishSettings.NumGoroutines
    98  	}
    99  	if options.PublishSettings.Timeout != 0 {
   100  		t.PublishSettings.Timeout = options.PublishSettings.Timeout
   101  	}
   102  	if options.PublishSettings.BufferedByteLimit != 0 {
   103  		t.PublishSettings.BufferedByteLimit = options.PublishSettings.BufferedByteLimit
   104  	}
   105  
   106  	pub := &pubsubPublisher{
   107  		topic: t,
   108  	}
   109  	// keep track of pubs
   110  	b.pubs = append(b.pubs, pub)
   111  
   112  	return pub, nil
   113  }
   114  
   115  // AddSubscriber registers a subscription to the given topic against the google pubsub api
   116  func (b *pubsubBroker) AddSubscriber(subscription string, hdlr Handler, opts ...SubscribeOption) error {
   117  	options := SubscribeOptions{}
   118  
   119  	for _, o := range opts {
   120  		o(&options)
   121  	}
   122  
   123  	sub := b.client.Subscription(subscription)
   124  	if exists, err := sub.Exists(context.Background()); err != nil {
   125  		return err
   126  	} else if !exists {
   127  		return errors.Errorf("Subscription %s doesn't exists", sub)
   128  	}
   129  
   130  	if options.ReceiveSettings.MaxOutstandingBytes != 0 {
   131  		sub.ReceiveSettings.MaxOutstandingBytes = options.ReceiveSettings.MaxOutstandingBytes
   132  	}
   133  	if options.ReceiveSettings.MaxOutstandingMessages != 0 {
   134  		sub.ReceiveSettings.MaxOutstandingMessages = options.ReceiveSettings.MaxOutstandingMessages
   135  	}
   136  	if options.ReceiveSettings.NumGoroutines != 0 {
   137  		sub.ReceiveSettings.NumGoroutines = options.ReceiveSettings.NumGoroutines
   138  	}
   139  	if options.ReceiveSettings.MaxExtension != 0 {
   140  		sub.ReceiveSettings.MaxExtension = options.ReceiveSettings.MaxExtension
   141  	}
   142  	if options.ReceiveSettings.MaxExtensionPeriod != 0 {
   143  		sub.ReceiveSettings.MaxExtensionPeriod = options.ReceiveSettings.MaxExtensionPeriod
   144  	}
   145  	if options.ReceiveSettings.Synchronous != false {
   146  		sub.ReceiveSettings.Synchronous = options.ReceiveSettings.Synchronous
   147  	}
   148  
   149  	middleware := hdlr
   150  	if rHdlr := options.RecoveryHandler; rHdlr != nil {
   151  		middleware = func(ctx context.Context, msg *pubsub.Message) {
   152  			defer func() {
   153  				if r := recover(); r != nil {
   154  					rHdlr(ctx, msg, r)
   155  				}
   156  			}()
   157  
   158  			hdlr(ctx, msg)
   159  		}
   160  	}
   161  
   162  	subscriber := &pubsubSubscriber{
   163  		options: options,
   164  		done:    make(chan struct{}),
   165  		sub:     sub,
   166  		hdlr:    middleware,
   167  	}
   168  
   169  	// keep track of subs
   170  	b.subs = append(b.subs, subscriber)
   171  
   172  	return nil
   173  }
   174  
   175  // Start is blocking. run as background process.
   176  func (b *pubsubBroker) Start() (err error) {
   177  	ctx := b.options.Context
   178  	g, egCtx := errgroup.WithContext(ctx)
   179  
   180  	// start subscribers in the background.
   181  	// when context cancelled, they exit without error.
   182  	for _, sub := range b.subs {
   183  		g.Go(func() error {
   184  			return sub.start(egCtx)
   185  		})
   186  	}
   187  
   188  	g.Go(func() (err error) {
   189  		// listen for the interrupt signal
   190  		<-ctx.Done()
   191  
   192  		// log situation
   193  		switch ctx.Err() {
   194  		case context.DeadlineExceeded:
   195  			log.Debug().Str("component", "pubsub").Msg("Context timeout exceeded")
   196  		case context.Canceled:
   197  			log.Debug().Str("component", "pubsub").Msg("Context cancelled by interrupt signal")
   198  		}
   199  
   200  		// wait for all subs to stop
   201  		for _, sub := range b.subs {
   202  			log.Info().Str("component", "pubsub").Msgf("Stopping Subscriber: %s", sub.sub)
   203  			<-sub.done
   204  		}
   205  
   206  		// then stop all pubs
   207  		for _, pub := range b.pubs {
   208  			pub.stop()
   209  		}
   210  
   211  		// then disconnection client.
   212  		log.Info().Str("component", "pubsub").Msgf("Closing pubsub client...")
   213  		err = b.client.Close()
   214  
   215  		// Hint: when using pubsub emulator, you receive this error, which you can safely ignore.
   216  		// Live pubsub server will throw this error.
   217  		if err != nil && strings.Contains(err.Error(), "the client connection is closing") {
   218  			err = nil
   219  		}
   220  		return
   221  	})
   222  
   223  	// Wait for all tasks to be finished or return if error occur at any task.
   224  	return g.Wait()
   225  }
   226  
   227  // NewBroker creates a new google pubsub broker
   228  func newBroker(ctx context.Context, opts ...Option) Broker {
   229  	// Default Options
   230  	options := Options{
   231  		Name:    DefaultName,
   232  		Context: ctx,
   233  	}
   234  
   235  	for _, o := range opts {
   236  		o(&options)
   237  	}
   238  
   239  	// retrieve project id
   240  	prjID := options.ProjectID
   241  
   242  	// if `GOOGLE_CLOUD_PROJECT` is present, it will overwrite programmatically set projectID
   243  	//if envPrjID := os.Getenv("GOOGLE_CLOUD_PROJECT"); len(envPrjID) > 0 {
   244  	//	prjID = envPrjID
   245  	//}
   246  
   247  	// create pubsub client
   248  	c, err := pubsub.NewClient(ctx, prjID, options.ClientOptions...)
   249  	if err != nil {
   250  		panic(err.Error())
   251  	}
   252  
   253  	return &pubsubBroker{
   254  		client:  c,
   255  		options: options,
   256  	}
   257  }