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  }