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 }