github.com/wfusion/gofusion@v1.1.14/common/infra/watermill/components/cqrs/command_processor.go (about) 1 package cqrs 2 3 import ( 4 "fmt" 5 6 "github.com/pkg/errors" 7 "go.uber.org/multierr" 8 9 "github.com/wfusion/gofusion/common/infra/watermill" 10 "github.com/wfusion/gofusion/common/infra/watermill/message" 11 ) 12 13 type CommandProcessorConfig struct { 14 // GenerateSubscribeTopic is used to generate topic for subscribing command. 15 GenerateSubscribeTopic CommandProcessorGenerateSubscribeTopicFn 16 17 // SubscriberConstructor is used to create subscriber for CommandHandler. 18 SubscriberConstructor CommandProcessorSubscriberConstructorFn 19 20 // OnHandle is called before handling command. 21 // OnHandle works in a similar way to middlewares: you can inject additional logic before and after handling a command. 22 // 23 // Because of that, you need to explicitly call params.Handler.Handle() to handle the command. 24 // func(params CommandProcessorOnHandleParams) (err error) { 25 // // logic before handle 26 // // (...) 27 // 28 // err := params.Handler.Handle(params.Message.Context(), params.Command) 29 // 30 // // logic after handle 31 // // (...) 32 // 33 // return err 34 // } 35 // 36 // This option is not required. 37 OnHandle CommandProcessorOnHandleFn 38 39 // Marshaler is used to marshal and unmarshal commands. 40 // It is required. 41 Marshaler CommandEventMarshaler 42 43 // Logger instance used to log. 44 // If not provided, watermill.NopLogger is used. 45 Logger watermill.LoggerAdapter 46 47 // If true, CommandProcessor will ack messages even if CommandHandler returns an error. 48 // If RequestReplyBackend is not null and sending reply fails, the message will be nack-ed anyway. 49 // 50 // Warning: It's not recommended to use this option when you are using requestreply component 51 // (requestreply.NewCommandHandler or requestreply.NewCommandHandlerWithResult), as it may ack the 52 // command when sending reply failed. 53 // 54 // When you are using requestreply, you should use requestreply.PubSubBackendConfig.AckCommandErrors. 55 AckCommandHandlingErrors bool 56 57 // disableRouterAutoAddHandlers is used to keep backwards compatibility. 58 // it is set when CommandProcessor is created by NewCommandProcessor. 59 // Deprecated: please migrate to NewCommandProcessorWithConfig. 60 disableRouterAutoAddHandlers bool 61 } 62 63 func (c *CommandProcessorConfig) setDefaults() { 64 if c.Logger == nil { 65 c.Logger = watermill.NopLogger{} 66 } 67 } 68 69 func (c CommandProcessorConfig) Validate() error { 70 var err error 71 72 if c.Marshaler == nil { 73 err = multierr.Append(err, errors.New("missing Marshaler")) 74 } 75 76 if c.GenerateSubscribeTopic == nil { 77 err = multierr.Append(err, errors.New("missing GenerateSubscribeTopic")) 78 } 79 if c.SubscriberConstructor == nil { 80 err = multierr.Append(err, errors.New("missing SubscriberConstructor")) 81 } 82 83 return err 84 } 85 86 type CommandProcessorGenerateSubscribeTopicFn func(CommandProcessorGenerateSubscribeTopicParams) (string, error) 87 88 type CommandProcessorGenerateSubscribeTopicParams struct { 89 CommandName string 90 CommandHandler CommandHandler 91 } 92 93 // CommandProcessorSubscriberConstructorFn creates subscriber for CommandHandler. 94 // It allows you to create a separate customized Subscriber for every command handler. 95 type CommandProcessorSubscriberConstructorFn func(CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) 96 97 type CommandProcessorSubscriberConstructorParams struct { 98 HandlerName string 99 Handler CommandHandler 100 } 101 102 type CommandProcessorOnHandleFn func(params CommandProcessorOnHandleParams) error 103 104 type CommandProcessorOnHandleParams struct { 105 Handler CommandHandler 106 107 CommandName string 108 Command any 109 110 // Message is never nil and can be modified. 111 Message *message.Message 112 } 113 114 // CommandProcessor determines which CommandHandler should handle the command received from the command bus. 115 type CommandProcessor struct { 116 router *message.Router 117 118 handlers []CommandHandler 119 120 config CommandProcessorConfig 121 } 122 123 func NewCommandProcessorWithConfig(router *message.Router, config CommandProcessorConfig) (*CommandProcessor, error) { 124 config.setDefaults() 125 126 if err := config.Validate(); err != nil { 127 return nil, err 128 } 129 130 if router == nil && !config.disableRouterAutoAddHandlers { 131 return nil, errors.New("missing router") 132 } 133 134 return &CommandProcessor{ 135 router: router, 136 config: config, 137 }, nil 138 } 139 140 // NewCommandProcessor creates a new CommandProcessor. 141 // Deprecated. Use NewCommandProcessorWithConfig instead. 142 func NewCommandProcessor( 143 handlers []CommandHandler, 144 generateTopic func(commandName string) string, 145 subscriberConstructor CommandsSubscriberConstructor, 146 marshaler CommandEventMarshaler, 147 logger watermill.LoggerAdapter, 148 ) (*CommandProcessor, error) { 149 if len(handlers) == 0 { 150 return nil, errors.New("missing handlers") 151 } 152 if generateTopic == nil { 153 return nil, errors.New("missing generateTopic") 154 } 155 if subscriberConstructor == nil { 156 return nil, errors.New("missing subscriberConstructor") 157 } 158 159 cp, err := NewCommandProcessorWithConfig( 160 nil, 161 CommandProcessorConfig{ 162 GenerateSubscribeTopic: func(params CommandProcessorGenerateSubscribeTopicParams) (string, error) { 163 return generateTopic(params.CommandName), nil 164 }, 165 SubscriberConstructor: func(params CommandProcessorSubscriberConstructorParams) (message.Subscriber, error) { 166 return subscriberConstructor(params.HandlerName) 167 }, 168 Marshaler: marshaler, 169 Logger: logger, 170 disableRouterAutoAddHandlers: true, 171 }, 172 ) 173 if err != nil { 174 return nil, err 175 } 176 177 for _, handler := range handlers { 178 if err := cp.AddHandlers(handler); err != nil { 179 return nil, err 180 } 181 } 182 183 return cp, nil 184 } 185 186 // CommandsSubscriberConstructor creates subscriber for CommandHandler. 187 // It allows you to create a separate customized Subscriber for every command handler. 188 // 189 // Deprecated: please use CommandProcessorSubscriberConstructorFn instead. 190 type CommandsSubscriberConstructor func(handlerName string) (message.Subscriber, error) 191 192 // AddHandlers adds a new CommandHandler to the CommandProcessor and adds it to the router. 193 func (p *CommandProcessor) AddHandlers(handlers ...CommandHandler) error { 194 handledCommands := map[string]struct{}{} 195 for _, handler := range handlers { 196 commandName := p.config.Marshaler.Name(handler.NewCommand()) 197 if _, ok := handledCommands[commandName]; ok { 198 return DuplicateCommandHandlerError{commandName} 199 } 200 201 handledCommands[commandName] = struct{}{} 202 } 203 204 if p.config.disableRouterAutoAddHandlers { 205 p.handlers = append(p.handlers, handlers...) 206 return nil 207 } 208 209 for _, handler := range handlers { 210 if err := p.addHandlerToRouter(p.router, handler); err != nil { 211 return err 212 } 213 214 p.handlers = append(p.handlers, handler) 215 } 216 217 return nil 218 } 219 220 // DuplicateCommandHandlerError occurs when a handler with the same name already exists. 221 type DuplicateCommandHandlerError struct { 222 CommandName string 223 } 224 225 func (d DuplicateCommandHandlerError) Error() string { 226 return fmt.Sprintf("command handler for command %s already exists", d.CommandName) 227 } 228 229 // AddHandlersToRouter adds the CommandProcessor's handlers to the given router. 230 // It should be called only once per CommandProcessor instance. 231 // 232 // It is required to call AddHandlersToRouter only if command processor is created with NewCommandProcessor (disableRouterAutoAddHandlers is set to true). 233 // Deprecated: please migrate to command processor created by NewCommandProcessorWithConfig. 234 func (p CommandProcessor) AddHandlersToRouter(r *message.Router) error { 235 if !p.config.disableRouterAutoAddHandlers { 236 return errors.New("AddHandlersToRouter should be called only when using deprecated NewCommandProcessor") 237 } 238 239 for i := range p.Handlers() { 240 handler := p.handlers[i] 241 242 if err := p.addHandlerToRouter(r, handler); err != nil { 243 return err 244 } 245 } 246 247 return nil 248 } 249 250 func (p CommandProcessor) addHandlerToRouter(r *message.Router, handler CommandHandler) error { 251 handlerName := handler.HandlerName() 252 commandName := p.config.Marshaler.Name(handler.NewCommand()) 253 254 topicName, err := p.config.GenerateSubscribeTopic(CommandProcessorGenerateSubscribeTopicParams{ 255 CommandName: commandName, 256 CommandHandler: handler, 257 }) 258 if err != nil { 259 return errors.Wrapf(err, "cannot generate topic for command handler %s", handlerName) 260 } 261 262 logger := p.config.Logger.With(watermill.LogFields{ 263 "command_handler_name": handlerName, 264 "topic": topicName, 265 }) 266 267 handlerFunc, err := p.routerHandlerFunc(handler, logger) 268 if err != nil { 269 return err 270 } 271 272 logger.Debug("Adding CQRS command handler to router", nil) 273 274 subscriber, err := p.config.SubscriberConstructor(CommandProcessorSubscriberConstructorParams{ 275 HandlerName: handlerName, 276 Handler: handler, 277 }) 278 if err != nil { 279 return errors.Wrap(err, "cannot create subscriber for command processor") 280 } 281 282 r.AddNoPublisherHandler( 283 handlerName, 284 topicName, 285 subscriber, 286 handlerFunc, 287 ) 288 289 return nil 290 } 291 292 // Handlers returns the CommandProcessor's handlers. 293 func (p CommandProcessor) Handlers() []CommandHandler { 294 return p.handlers 295 } 296 297 func (p CommandProcessor) routerHandlerFunc(handler CommandHandler, logger watermill.LoggerAdapter) (message.NoPublishHandlerFunc, error) { 298 cmd := handler.NewCommand() 299 cmdName := p.config.Marshaler.Name(cmd) 300 301 if err := p.validateCommand(cmd); err != nil { 302 return nil, err 303 } 304 305 return func(msg *message.Message) error { 306 cmd := handler.NewCommand() 307 messageCmdName := p.config.Marshaler.NameFromMessage(msg) 308 309 if messageCmdName != cmdName { 310 logger.Trace("Received different command type than expected, ignoring", watermill.LogFields{ 311 "message_uuid": msg.UUID, 312 "expected_command_type": cmdName, 313 "received_command_type": messageCmdName, 314 }) 315 return nil 316 } 317 318 logger.Debug("Handling command", watermill.LogFields{ 319 "message_uuid": msg.UUID, 320 "received_command_type": messageCmdName, 321 }) 322 323 ctx := CtxWithOriginalMessage(msg.Context(), msg) 324 msg.SetContext(ctx) 325 326 if err := p.config.Marshaler.Unmarshal(msg, cmd); err != nil { 327 return err 328 } 329 330 handle := func(params CommandProcessorOnHandleParams) (err error) { 331 return params.Handler.Handle(ctx, params.Command) 332 } 333 if p.config.OnHandle != nil { 334 handle = p.config.OnHandle 335 } 336 337 err := handle(CommandProcessorOnHandleParams{ 338 Handler: handler, 339 CommandName: messageCmdName, 340 Command: cmd, 341 Message: msg, 342 }) 343 344 if p.config.AckCommandHandlingErrors && err != nil { 345 logger.Error("Error when handling command, acking (AckCommandHandlingErrors is enabled)", err, nil) 346 return nil 347 } 348 if err != nil { 349 logger.Debug("Error when handling command, nacking", watermill.LogFields{"err": err}) 350 return err 351 } 352 353 return nil 354 }, nil 355 } 356 357 func (p CommandProcessor) validateCommand(cmd any) error { 358 // CommandHandler's NewCommand must return a pointer, because it is used to unmarshal 359 if err := isPointer(cmd); err != nil { 360 return errors.Wrap(err, "command must be a non-nil pointer") 361 } 362 363 return nil 364 }