Files
upstream-watermill/_examples/basic/6-cqrs-ordered-events/main.go
2026-09-15 21:27:46 +05:00

263 lines
7.6 KiB
Go

package main
import (
"context"
"fmt"
"log/slog"
"time"
"github.com/ThreeDotsLabs/watermill"
"github.com/ThreeDotsLabs/watermill-kafka/v3/pkg/kafka"
"github.com/ThreeDotsLabs/watermill/components/cqrs"
"github.com/ThreeDotsLabs/watermill/message"
"github.com/ThreeDotsLabs/watermill/message/router/middleware"
)
func main() {
slog.SetLogLoggerLevel(slog.LevelDebug)
logger := watermill.NewSlogLoggerWithLevelMapping(nil, map[slog.Level]slog.Level{
slog.LevelInfo: slog.LevelDebug,
})
// We are decorating ProtobufMarshaler to add extra metadata to the message.
cqrsMarshaler := CqrsMarshalerDecorator{
cqrs.ProtoMarshaler{
// It will generate topic names based on the event/command type.
// So for example, for "RoomBooked" name will be "RoomBooked".
GenerateName: cqrs.StructName,
},
}
watermillLogger := watermill.NewSlogLoggerWithLevelMapping(
slog.With("watermill", true),
map[slog.Level]slog.Level{
slog.LevelInfo: slog.LevelDebug,
},
)
// This marshaler converts Watermill messages to Kafka messages.
// We are using it to add partition key to the Kafka message.
kafkaMarshaler := kafka.NewWithPartitioningMarshaler(GenerateKafkaPartitionKey)
// You can use any Pub/Sub implementation from here: https://watermill.io/pubsubs/
publisher, err := kafka.NewPublisher(
kafka.PublisherConfig{
Brokers: []string{"kafka:9092"},
Marshaler: kafkaMarshaler,
},
watermillLogger,
)
if err != nil {
panic(err)
}
// CQRS is built on messages router. Detailed documentation: https://watermill.io/docs/messages-router/
router, err := message.NewRouter(message.RouterConfig{}, logger)
if err != nil {
panic(err)
}
// Simple middleware which will recover panics from event or command handlers.
// More about router middlewares you can find in the documentation:
// https://watermill.io/docs/messages-router/#middleware
//
// List of available middlewares you can find in message/router/middleware.
router.AddMiddleware(middleware.Recoverer)
router.AddMiddleware(func(h message.HandlerFunc) message.HandlerFunc {
return func(msg *message.Message) ([]*message.Message, error) {
slog.Debug("Received message", "metadata", msg.Metadata)
return h(msg)
}
})
commandBus, err := cqrs.NewCommandBusWithConfig(publisher, cqrs.CommandBusConfig{
GeneratePublishTopic: func(params cqrs.CommandBusGeneratePublishTopicParams) (string, error) {
// We are using one topic for all commands to maintain the order of commands.
return "commands", nil
},
Marshaler: cqrsMarshaler,
Logger: logger,
})
if err != nil {
panic(err)
}
eventBus, err := cqrs.NewEventBusWithConfig(publisher, cqrs.EventBusConfig{
GeneratePublishTopic: func(params cqrs.GenerateEventPublishTopicParams) (string, error) {
// We are using one topic for all events to maintain the order of events.
return "events", nil
},
Marshaler: cqrsMarshaler,
Logger: logger,
})
if err != nil {
panic(err)
}
commandProcessor, err := cqrs.NewCommandProcessorWithConfig(
router,
cqrs.CommandProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.CommandProcessorGenerateSubscribeTopicParams) (string, error) {
return "commands", nil
},
SubscriberConstructor: func(params cqrs.CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return kafka.NewSubscriber(
kafka.SubscriberConfig{
Brokers: []string{"kafka:9092"},
ConsumerGroup: params.HandlerName,
Unmarshaler: kafkaMarshaler,
},
watermillLogger,
)
},
Marshaler: cqrsMarshaler,
Logger: logger,
},
)
if err != nil {
panic(err)
}
eventProcessor, err := cqrs.NewEventGroupProcessorWithConfig(
router,
cqrs.EventGroupProcessorConfig{
GenerateSubscribeTopic: func(params cqrs.EventGroupProcessorGenerateSubscribeTopicParams) (string, error) {
return "events", nil
},
SubscriberConstructor: func(params cqrs.EventGroupProcessorSubscriberConstructorParams) (message.Subscriber, error) {
return kafka.NewSubscriber(
kafka.SubscriberConfig{
Brokers: []string{"kafka:9092"},
ConsumerGroup: params.EventGroupName,
Unmarshaler: kafkaMarshaler,
},
watermillLogger,
)
},
Marshaler: cqrsMarshaler,
Logger: logger,
},
)
if err != nil {
panic(err)
}
err = commandProcessor.AddHandlers(
cqrs.NewCommandHandler("SubscribeHandler", SubscribeHandler{eventBus}.Handle),
cqrs.NewCommandHandler("UnsubscribeHandler", UnsubscribeHandler{eventBus}.Handle),
cqrs.NewCommandHandler("UpdateEmailHandler", UpdateEmailHandler{eventBus}.Handle),
)
if err != nil {
panic(err)
}
subscribersReadModel := NewSubscriberReadModel()
// All messages from this group will have one subscription.
// When message arrives, Watermill will match it with the correct handler.
err = eventProcessor.AddHandlersGroup(
"SubscriberReadModel",
cqrs.NewGroupEventHandler(subscribersReadModel.OnSubscribed),
cqrs.NewGroupEventHandler(subscribersReadModel.OnUnsubscribed),
cqrs.NewGroupEventHandler(subscribersReadModel.OnEmailUpdated),
)
if err != nil {
panic(err)
}
activityReadModel := NewActivityTimelineModel()
// All messages from this group will have one subscription.
// When message arrives, Watermill will match it with the correct handler.
err = eventProcessor.AddHandlersGroup(
"ActivityTimelineReadModel",
cqrs.NewGroupEventHandler(activityReadModel.OnSubscribed),
cqrs.NewGroupEventHandler(activityReadModel.OnUnsubscribed),
cqrs.NewGroupEventHandler(activityReadModel.OnEmailUpdated),
)
if err != nil {
panic(err)
}
slog.Info("Starting service")
go simulateTraffic(commandBus)
if err := router.Run(context.Background()); err != nil {
panic(err)
}
}
func simulateTraffic(commandBus *cqrs.CommandBus) {
for i := 0; ; i++ {
subscriberID := watermill.NewUUID()
err := commandBus.Send(context.Background(), &Subscribe{
Metadata: GenerateMessageMetadata(subscriberID),
SubscriberId: subscriberID,
Email: fmt.Sprintf("user%d@example.com", i),
})
if err != nil {
slog.Error("Error sending Subscribe command", "err", err)
}
time.Sleep(time.Millisecond * 500)
err = commandBus.Send(context.Background(), &UpdateEmail{
Metadata: GenerateMessageMetadata(subscriberID),
SubscriberId: subscriberID,
NewEmail: fmt.Sprintf("updated%d@example.com", i),
})
if err != nil {
slog.Error("Error sending UpdateEmail command", "err", err)
}
time.Sleep(time.Millisecond * 500)
if i%3 == 0 {
err = commandBus.Send(context.Background(), &Unsubscribe{
Metadata: GenerateMessageMetadata(subscriberID),
SubscriberId: subscriberID,
})
if err != nil {
slog.Error("Error sending Unsubscribe command", "err", err)
}
}
time.Sleep(time.Millisecond * 500)
}
}
type SubscribeHandler struct {
eventBus *cqrs.EventBus
}
func (h SubscribeHandler) Handle(ctx context.Context, cmd *Subscribe) error {
return h.eventBus.Publish(ctx, &SubscriberSubscribed{
Metadata: GenerateMessageMetadata(cmd.SubscriberId),
SubscriberId: cmd.SubscriberId,
Email: cmd.Email,
})
}
type UnsubscribeHandler struct {
eventBus *cqrs.EventBus
}
func (h UnsubscribeHandler) Handle(ctx context.Context, cmd *Unsubscribe) error {
return h.eventBus.Publish(ctx, &SubscriberUnsubscribed{
Metadata: GenerateMessageMetadata(cmd.SubscriberId),
SubscriberId: cmd.SubscriberId,
})
}
type UpdateEmailHandler struct {
eventBus *cqrs.EventBus
}
func (h UpdateEmailHandler) Handle(ctx context.Context, cmd *UpdateEmail) error {
return h.eventBus.Publish(ctx, &SubscriberEmailUpdated{
Metadata: GenerateMessageMetadata(cmd.SubscriberId),
SubscriberId: cmd.SubscriberId,
NewEmail: cmd.NewEmail,
})
}