Files
2026-09-15 21:27:46 +05:00

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
}