go-micro.dev/v5@v5.12.0/events/natsjs/nats.go (about)

     1  // Package natsjs provides a NATS Jetstream implementation of the events.Stream interface.
     2  package natsjs
     3  
     4  import (
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/google/uuid"
    12  	nats "github.com/nats-io/nats.go"
    13  	"github.com/pkg/errors"
    14  
    15  	"go-micro.dev/v5/events"
    16  	"go-micro.dev/v5/logger"
    17  )
    18  
    19  const (
    20  	defaultClusterID = "micro"
    21  )
    22  
    23  // NewStream returns an initialized nats stream or an error if the connection to the nats
    24  // server could not be established.
    25  func NewStream(opts ...Option) (events.Stream, error) {
    26  	// parse the options
    27  	options := Options{
    28  		ClientID:  uuid.New().String(),
    29  		ClusterID: defaultClusterID,
    30  		Logger:    logger.DefaultLogger,
    31  	}
    32  	for _, o := range opts {
    33  		o(&options)
    34  	}
    35  
    36  	s := &stream{opts: options}
    37  
    38  	natsJetStreamCtx, err := connectToNatsJetStream(options)
    39  	if err != nil {
    40  		return nil, fmt.Errorf("error connecting to nats cluster %v: %w", options.ClusterID, err)
    41  	}
    42  
    43  	s.natsJetStreamCtx = natsJetStreamCtx
    44  
    45  	return s, nil
    46  }
    47  
    48  type stream struct {
    49  	opts             Options
    50  	natsJetStreamCtx nats.JetStreamContext
    51  }
    52  
    53  func connectToNatsJetStream(options Options) (nats.JetStreamContext, error) {
    54  	nopts := nats.GetDefaultOptions()
    55  	if options.TLSConfig != nil {
    56  		nopts.Secure = true
    57  		nopts.TLSConfig = options.TLSConfig
    58  	}
    59  
    60  	if options.NkeyConfig != "" {
    61  		nopts.Nkey = options.NkeyConfig
    62  	}
    63  
    64  	if len(options.Address) > 0 {
    65  		nopts.Servers = strings.Split(options.Address, ",")
    66  	}
    67  
    68  	if options.Name != "" {
    69  		nopts.Name = options.Name
    70  	}
    71  
    72  	if options.Username != "" && options.Password != "" {
    73  		nopts.User = options.Username
    74  		nopts.Password = options.Password
    75  	}
    76  
    77  	conn, err := nopts.Connect()
    78  	if err != nil {
    79  		tls := nopts.TLSConfig != nil
    80  		return nil, fmt.Errorf("error connecting to nats at %v with tls enabled (%v): %w", options.Address, tls, err)
    81  	}
    82  
    83  	js, err := conn.JetStream()
    84  	if err != nil {
    85  		return nil, fmt.Errorf("error while obtaining JetStream context: %w", err)
    86  	}
    87  
    88  	return js, nil
    89  }
    90  
    91  // Publish a message to a topic.
    92  func (s *stream) Publish(topic string, msg interface{}, opts ...events.PublishOption) error {
    93  	// validate the topic
    94  	if len(topic) == 0 {
    95  		return events.ErrMissingTopic
    96  	}
    97  
    98  	// parse the options
    99  	options := events.PublishOptions{
   100  		Timestamp: time.Now(),
   101  	}
   102  	for _, o := range opts {
   103  		o(&options)
   104  	}
   105  
   106  	// encode the message if it's not already encoded
   107  	var payload []byte
   108  	if p, ok := msg.([]byte); ok {
   109  		payload = p
   110  	} else {
   111  		p, err := json.Marshal(msg)
   112  		if err != nil {
   113  			return events.ErrEncodingMessage
   114  		}
   115  		payload = p
   116  	}
   117  
   118  	// construct the event
   119  	event := &events.Event{
   120  		ID:        uuid.New().String(),
   121  		Topic:     topic,
   122  		Timestamp: options.Timestamp,
   123  		Metadata:  options.Metadata,
   124  		Payload:   payload,
   125  	}
   126  
   127  	// serialize the event to bytes
   128  	bytes, err := json.Marshal(event)
   129  	if err != nil {
   130  		return errors.Wrap(err, "Error encoding event")
   131  	}
   132  
   133  	// publish the event to the topic's channel
   134  	// publish synchronously if configured
   135  	if s.opts.SyncPublish {
   136  		_, err := s.natsJetStreamCtx.Publish(event.Topic, bytes)
   137  		if err != nil {
   138  			err = errors.Wrap(err, "Error publishing message to topic")
   139  		}
   140  
   141  		return err
   142  	}
   143  
   144  	// publish asynchronously by default
   145  	if _, err := s.natsJetStreamCtx.PublishAsync(event.Topic, bytes); err != nil {
   146  		return errors.Wrap(err, "Error publishing message to topic")
   147  	}
   148  
   149  	return nil
   150  }
   151  
   152  // Consume from a topic.
   153  func (s *stream) Consume(topic string, opts ...events.ConsumeOption) (<-chan events.Event, error) {
   154  	// validate the topic
   155  	if len(topic) == 0 {
   156  		return nil, events.ErrMissingTopic
   157  	}
   158  
   159  	log := s.opts.Logger
   160  
   161  	// parse the options
   162  	options := events.ConsumeOptions{
   163  		Group: uuid.New().String(),
   164  	}
   165  	for _, o := range opts {
   166  		o(&options)
   167  	}
   168  
   169  	// setup the subscriber
   170  	channel := make(chan events.Event)
   171  	handleMsg := func(msg *nats.Msg) {
   172  		ctx, cancel := context.WithCancel(context.TODO())
   173  		defer cancel()
   174  
   175  		// decode the message
   176  		var evt events.Event
   177  		if err := json.Unmarshal(msg.Data, &evt); err != nil {
   178  			log.Logf(logger.ErrorLevel, "Error decoding message: %v", err)
   179  			// not acknowledging the message is the way to indicate an error occurred
   180  			return
   181  		}
   182  		if options.AutoAck {
   183  			// set up the ack funcs
   184  			evt.SetAckFunc(func() error {
   185  				return msg.Ack()
   186  			})
   187  
   188  			evt.SetNackFunc(func() error {
   189  				return msg.Nak()
   190  			})
   191  		} else {
   192  			// set up the ack funcs
   193  			evt.SetAckFunc(func() error {
   194  				return nil
   195  			})
   196  			evt.SetNackFunc(func() error {
   197  				return nil
   198  			})
   199  		}
   200  
   201  		// push onto the channel and wait for the consumer to take the event off before we acknowledge it.
   202  		channel <- evt
   203  
   204  		if !options.AutoAck {
   205  			return
   206  		}
   207  
   208  		if err := msg.Ack(nats.Context(ctx)); err != nil {
   209  
   210  			log.Logf(logger.ErrorLevel, "Error acknowledging message: %v", err)
   211  		}
   212  	}
   213  
   214  	// ensure that a stream exists for that topic
   215  	_, err := s.natsJetStreamCtx.StreamInfo(topic)
   216  	if err != nil {
   217  		cfg := &nats.StreamConfig{
   218  			Name: topic,
   219  		}
   220  		if s.opts.RetentionPolicy != 0 {
   221  			cfg.Retention = nats.RetentionPolicy(s.opts.RetentionPolicy)
   222  		}
   223  		if s.opts.MaxAge > 0 {
   224  			cfg.MaxAge = s.opts.MaxAge
   225  		}
   226  
   227  		_, err = s.natsJetStreamCtx.AddStream(cfg)
   228  		if err != nil {
   229  			return nil, errors.Wrap(err, "Stream did not exist and adding a stream failed")
   230  		}
   231  	}
   232  
   233  	// setup the options
   234  	subOpts := []nats.SubOpt{}
   235  
   236  	if options.CustomRetries {
   237  		subOpts = append(subOpts, nats.MaxDeliver(options.GetRetryLimit()))
   238  	}
   239  
   240  	if options.AutoAck {
   241  		subOpts = append(subOpts, nats.AckAll())
   242  	} else {
   243  		subOpts = append(subOpts, nats.AckExplicit())
   244  	}
   245  
   246  	if !options.Offset.IsZero() {
   247  		subOpts = append(subOpts, nats.StartTime(options.Offset))
   248  	} else {
   249  		subOpts = append(subOpts, nats.DeliverNew())
   250  	}
   251  
   252  	if options.AckWait > 0 {
   253  		subOpts = append(subOpts, nats.AckWait(options.AckWait))
   254  	}
   255  
   256  	// connect the subscriber via a queue group only if durable streams are enabled
   257  	if !s.opts.DisableDurableStreams {
   258  		subOpts = append(subOpts, nats.Durable(options.Group))
   259  		_, err = s.natsJetStreamCtx.QueueSubscribe(topic, options.Group, handleMsg, subOpts...)
   260  	} else {
   261  		subOpts = append(subOpts, nats.ConsumerName(options.Group))
   262  		_, err = s.natsJetStreamCtx.Subscribe(topic, handleMsg, subOpts...)
   263  	}
   264  
   265  	if err != nil {
   266  		return nil, errors.Wrap(err, "Error subscribing to topic")
   267  	}
   268  
   269  	return channel, nil
   270  }