snapshot of https://github.com/ThreeDotsLabs/watermill.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
master / ci (push) Failing after 0s
Some checks failed
master / ci (push) Failing after 0s
This commit is contained in:
262
_examples/basic/6-cqrs-ordered-events/main.go
Normal file
262
_examples/basic/6-cqrs-ordered-events/main.go
Normal file
@@ -0,0 +1,262 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user