170 lines
4.9 KiB
Go
170 lines
4.9 KiB
Go
package forwarder
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/ThreeDotsLabs/watermill"
|
|
"github.com/ThreeDotsLabs/watermill/message"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
const (
|
|
defaultForwarderTopic = "forwarder_topic"
|
|
defaultHandlerName = "events_forwarder"
|
|
)
|
|
|
|
type Config struct {
|
|
// ForwarderTopic is a topic on which the forwarder will be listening to enveloped messages to forward.
|
|
// Defaults to `forwarder_topic`.
|
|
ForwarderTopic string
|
|
|
|
// HandlerName is the name of the forwarder's handler registered in the router.
|
|
// Defaults to `events_forwarder`.
|
|
//
|
|
// Handler names must be unique within a router, so it needs to be set explicitly
|
|
// if you want to run multiple forwarders on the same router.
|
|
HandlerName string
|
|
|
|
// Middlewares are used to decorate forwarder's handler function.
|
|
Middlewares []message.HandlerMiddleware
|
|
|
|
// CloseTimeout determines how long router should work for handlers when closing.
|
|
CloseTimeout time.Duration
|
|
|
|
// AckWhenCannotUnwrap enables acking of messages which cannot be unwrapped from an envelope.
|
|
AckWhenCannotUnwrap bool
|
|
|
|
// Router is a router used by the forwarder.
|
|
// If not provided, a new router will be created.
|
|
//
|
|
// If router is provided, it's not necessary to call `Forwarder.Run()` if the router is started with `router.Run()`.
|
|
Router *message.Router
|
|
|
|
// Marshaler is used to deserialize envelopes received on ForwarderTopic.
|
|
// It must match the Marshaler used by the decorated Publisher.
|
|
// Defaults to DefaultMarshaler, which uses encoding/json.
|
|
Marshaler Marshaler
|
|
}
|
|
|
|
func (c *Config) setDefaults() {
|
|
if c.CloseTimeout == 0 {
|
|
c.CloseTimeout = time.Second * 30
|
|
}
|
|
if c.ForwarderTopic == "" {
|
|
c.ForwarderTopic = defaultForwarderTopic
|
|
}
|
|
if c.HandlerName == "" {
|
|
c.HandlerName = defaultHandlerName
|
|
}
|
|
if c.Marshaler == nil {
|
|
c.Marshaler = DefaultMarshaler{}
|
|
}
|
|
}
|
|
|
|
func (c *Config) Validate() error {
|
|
if c.ForwarderTopic == "" {
|
|
return errors.New("empty forwarder topic")
|
|
}
|
|
if c.HandlerName == "" {
|
|
return errors.New("empty handler name")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Forwarder subscribes to the topic provided in the config and publishes them to the destination topic embedded in the enveloped message.
|
|
type Forwarder struct {
|
|
router *message.Router
|
|
publisher message.Publisher
|
|
logger watermill.LoggerAdapter
|
|
config Config
|
|
}
|
|
|
|
// NewForwarder creates a forwarder which will subscribe to the topic provided in the config using the provided subscriber.
|
|
// It will publish messages received on this subscription to the destination topic embedded in the enveloped message using the provided publisher.
|
|
//
|
|
// Provided subscriber and publisher can be from different Watermill Pub/Sub implementations, i.e. MySQL subscriber and Google Pub/Sub publisher.
|
|
//
|
|
// Note: Keep in mind that by default the forwarder will nack all messages which weren't sent using a decorated publisher.
|
|
// You can change this behavior by passing a middleware which will ack them instead.
|
|
func NewForwarder(
|
|
subscriberIn message.Subscriber,
|
|
publisherOut message.Publisher,
|
|
logger watermill.LoggerAdapter,
|
|
config Config,
|
|
) (*Forwarder, error) {
|
|
config.setDefaults()
|
|
|
|
routerConfig := message.RouterConfig{CloseTimeout: config.CloseTimeout}
|
|
if err := routerConfig.Validate(); err != nil {
|
|
return nil, errors.Wrap(err, "invalid router config")
|
|
}
|
|
|
|
var router *message.Router
|
|
if config.Router != nil {
|
|
router = config.Router
|
|
} else {
|
|
var err error
|
|
router, err = message.NewRouter(routerConfig, logger)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "cannot create a router")
|
|
}
|
|
}
|
|
|
|
f := &Forwarder{router, publisherOut, logger, config}
|
|
|
|
handler := router.AddConsumerHandler(
|
|
config.HandlerName,
|
|
config.ForwarderTopic,
|
|
subscriberIn,
|
|
f.forwardMessage,
|
|
)
|
|
|
|
handler.AddMiddleware(config.Middlewares...)
|
|
|
|
return f, nil
|
|
}
|
|
|
|
// Run runs forwarder's handler responsible for forwarding messages.
|
|
// This call is blocking while the forwarder is running.
|
|
// ctx will be propagated to the forwarder's subscription.
|
|
//
|
|
// To stop Run() you should call Close() on the forwarder.
|
|
func (f *Forwarder) Run(ctx context.Context) error {
|
|
return f.router.Run(ctx)
|
|
}
|
|
|
|
// Close stops forwarder's handler.
|
|
func (f *Forwarder) Close() error {
|
|
return f.router.Close()
|
|
}
|
|
|
|
// Running returns channel which is closed when the forwarder is running.
|
|
func (f *Forwarder) Running() chan struct{} {
|
|
return f.router.Running()
|
|
}
|
|
|
|
func (f *Forwarder) forwardMessage(msg *message.Message) error {
|
|
destTopic, unwrappedMsg, err := unwrapMessageFromEnvelope(msg, f.config.Marshaler)
|
|
if err != nil {
|
|
f.logger.Error("Could not unwrap a message from an envelope", err, watermill.LogFields{
|
|
"uuid": msg.UUID,
|
|
"payload": msg.Payload,
|
|
"metadata": msg.Metadata,
|
|
"acked": f.config.AckWhenCannotUnwrap,
|
|
})
|
|
|
|
if f.config.AckWhenCannotUnwrap {
|
|
return nil
|
|
}
|
|
return errors.Wrap(err, "cannot unwrap message from an envelope")
|
|
}
|
|
|
|
if err := f.publisher.Publish(destTopic, unwrappedMsg); err != nil {
|
|
return errors.Wrap(err, "cannot publish a message")
|
|
}
|
|
|
|
return nil
|
|
}
|