2020-08-18 18:19:53 +03:00
|
|
|
package nats
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
2020-08-19 12:55:54 +03:00
|
|
|
"fmt"
|
2020-08-18 18:19:53 +03:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/google/uuid"
|
2020-08-19 12:55:54 +03:00
|
|
|
"github.com/nats-io/nats.go"
|
2020-08-18 18:19:53 +03:00
|
|
|
stan "github.com/nats-io/stan.go"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
|
|
|
|
"github.com/micro/go-micro/v3/events"
|
|
|
|
"github.com/micro/go-micro/v3/logger"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
defaultClusterID = "micro"
|
|
|
|
)
|
|
|
|
|
|
|
|
// NewStream returns an initialized nats stream or an error if the connection to the nats
|
|
|
|
// server could not be established
|
|
|
|
func NewStream(opts ...Option) (events.Stream, error) {
|
|
|
|
// parse the options
|
|
|
|
options := Options{
|
|
|
|
ClientID: uuid.New().String(),
|
|
|
|
ClusterID: defaultClusterID,
|
|
|
|
}
|
|
|
|
for _, o := range opts {
|
|
|
|
o(&options)
|
|
|
|
}
|
|
|
|
|
2020-08-19 12:55:54 +03:00
|
|
|
// connect to nats
|
|
|
|
nopts := nats.GetDefaultOptions()
|
|
|
|
if options.TLSConfig != nil {
|
|
|
|
nopts.Secure = true
|
|
|
|
nopts.TLSConfig = options.TLSConfig
|
|
|
|
}
|
2020-08-18 18:19:53 +03:00
|
|
|
if len(options.Address) > 0 {
|
2020-08-19 12:55:54 +03:00
|
|
|
nopts.Servers = []string{options.Address}
|
|
|
|
}
|
|
|
|
conn, err := nopts.Connect()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Error connecting to nats at %v with tls enabled (%v): %v", options.Address, nopts.TLSConfig != nil, err)
|
2020-08-18 18:19:53 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// connect to the cluster
|
2020-08-19 12:55:54 +03:00
|
|
|
clusterConn, err := stan.Connect(options.ClusterID, options.ClientID, stan.NatsConn(conn))
|
2020-08-18 18:19:53 +03:00
|
|
|
if err != nil {
|
2020-08-19 12:55:54 +03:00
|
|
|
return nil, fmt.Errorf("Error connecting to nats cluster %v: %v", options.ClusterID, err)
|
2020-08-18 18:19:53 +03:00
|
|
|
}
|
|
|
|
|
2020-08-19 12:55:54 +03:00
|
|
|
return &stream{clusterConn}, nil
|
2020-08-18 18:19:53 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
type stream struct {
|
|
|
|
conn stan.Conn
|
|
|
|
}
|
|
|
|
|
|
|
|
// Publish a message to a topic
|
2020-08-20 11:29:29 +03:00
|
|
|
func (s *stream) Publish(topic string, msg interface{}, opts ...events.PublishOption) error {
|
2020-08-18 18:19:53 +03:00
|
|
|
// validate the topic
|
|
|
|
if len(topic) == 0 {
|
|
|
|
return events.ErrMissingTopic
|
|
|
|
}
|
|
|
|
|
|
|
|
// parse the options
|
|
|
|
options := events.PublishOptions{
|
|
|
|
Timestamp: time.Now(),
|
|
|
|
}
|
|
|
|
for _, o := range opts {
|
|
|
|
o(&options)
|
|
|
|
}
|
|
|
|
|
|
|
|
// encode the message if it's not already encoded
|
|
|
|
var payload []byte
|
2020-08-20 11:29:29 +03:00
|
|
|
if p, ok := msg.([]byte); ok {
|
2020-08-18 18:19:53 +03:00
|
|
|
payload = p
|
|
|
|
} else {
|
2020-08-20 11:29:29 +03:00
|
|
|
p, err := json.Marshal(msg)
|
2020-08-18 18:19:53 +03:00
|
|
|
if err != nil {
|
|
|
|
return events.ErrEncodingMessage
|
|
|
|
}
|
|
|
|
payload = p
|
|
|
|
}
|
|
|
|
|
|
|
|
// construct the event
|
|
|
|
event := &events.Event{
|
|
|
|
ID: uuid.New().String(),
|
|
|
|
Topic: topic,
|
|
|
|
Timestamp: options.Timestamp,
|
|
|
|
Metadata: options.Metadata,
|
|
|
|
Payload: payload,
|
|
|
|
}
|
|
|
|
|
|
|
|
// serialize the event to bytes
|
|
|
|
bytes, err := json.Marshal(event)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "Error encoding event")
|
|
|
|
}
|
|
|
|
|
|
|
|
// publish the event to the topic's channel
|
|
|
|
if _, err := s.conn.PublishAsync(event.Topic, bytes, nil); err != nil {
|
|
|
|
return errors.Wrap(err, "Error publishing message to topic")
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Subscribe to a topic
|
2020-08-20 11:29:29 +03:00
|
|
|
func (s *stream) Subscribe(topic string, opts ...events.SubscribeOption) (<-chan events.Event, error) {
|
|
|
|
// validate the topic
|
|
|
|
if len(topic) == 0 {
|
|
|
|
return nil, events.ErrMissingTopic
|
|
|
|
}
|
|
|
|
|
2020-08-18 18:19:53 +03:00
|
|
|
// parse the options
|
|
|
|
options := events.SubscribeOptions{
|
|
|
|
Queue: uuid.New().String(),
|
|
|
|
}
|
|
|
|
for _, o := range opts {
|
|
|
|
o(&options)
|
|
|
|
}
|
|
|
|
|
|
|
|
// setup the subscriber
|
|
|
|
c := make(chan events.Event)
|
|
|
|
handleMsg := func(m *stan.Msg) {
|
|
|
|
// decode the message
|
|
|
|
var evt events.Event
|
|
|
|
if err := json.Unmarshal(m.Data, &evt); err != nil {
|
|
|
|
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
|
|
|
|
logger.Errorf("Error decoding message: %v", err)
|
|
|
|
}
|
|
|
|
// not ackknowledging the message is the way to indicate an error occured
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// push onto the channel and wait for the consumer to take the event off before we acknowledge it.
|
|
|
|
c <- evt
|
|
|
|
|
|
|
|
if err := m.Ack(); err != nil && logger.V(logger.ErrorLevel, logger.DefaultLogger) {
|
|
|
|
logger.Errorf("Error acknowledging message: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// setup the options
|
|
|
|
subOpts := []stan.SubscriptionOption{
|
2020-08-20 11:29:29 +03:00
|
|
|
stan.DurableName(topic),
|
2020-08-18 18:19:53 +03:00
|
|
|
stan.SetManualAckMode(),
|
|
|
|
}
|
|
|
|
if options.StartAtTime.Unix() > 0 {
|
|
|
|
stan.StartAtTime(options.StartAtTime)
|
|
|
|
}
|
|
|
|
|
|
|
|
// connect the subscriber
|
2020-08-20 11:29:29 +03:00
|
|
|
_, err := s.conn.QueueSubscribe(topic, options.Queue, handleMsg, subOpts...)
|
2020-08-18 18:19:53 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "Error subscribing to topic")
|
|
|
|
}
|
|
|
|
|
|
|
|
return c, nil
|
|
|
|
}
|