-
Notifications
You must be signed in to change notification settings - Fork 2
Listeners & Handlers
← Working with Middleware | FAQ & Debugging →
In the Messenger ecosystem, in addition to message handlers, there are event listeners. They are designed to respond to various events inside Messenger – for example, an attempt to send a message to a transport, a failure to send, etc. The events themselves are represented by structures in the core/event package. For example, the following events are defined:
-
SendMessageToTransportsEvent– occurs before sending a message to the transports. It contains the fieldsEnvelope(the message itself and its brands),TransportNames(list of transports to be sent to) andCtx(context). -
SendFailedMessageEvent– occurs after an unsuccessful message has been sent to the transport, if the logic of repeated shipments is provided. It contains information about the message, the error, and the number of remaining attempts (or the escape strategy).
Messenger has a built-in event manager (EventDispatcher) that allows you to register listeners for certain types of events and send (dispatch) events to listeners during execution. For example, SendMessageMiddleware generates the SendMessageToTransportsEvent event just before sending the message, and Builder.Build() registers a SendFailedMessageEvent listener for resending messages when setting up retrays.
You can add custom listeners to monitor or extend Messenger behavior:
-
Implement the listener. The listener can be a function or an object with the
Handlemethod.
Messenger supports two signatures:
- A function or method that takes 1 argument is the event itself (the event structure).
- A function or method that takes 2 arguments is context.Context and event.
In addition, if the method/function returns error, then when the error returns, Messenger will stop further sending this event and may generate an error.
Example: create a listener that logs all attempts to send a message:
type SendLoggingListener struct{}
func (l *SendLoggingListener) Handle(evt event.SendMessageToTransportsEvent) {
msgType := reflect.TypeOf(evt.Envelope.Message()).String()
fmt.Printf("[Event] Sending message of type %s to transports: %v\n", msgType, evt.TransportNames)
}This listener is implemented as an object with a Handle method that accepts an event (with one parameter). It displays the type of message and the list of transports to which it will be sent. Since the method does not return an error, the error will never be interrupted (and we do not use context.Context is here, we don't need it for logging).
-
Register the listener. The method
registerListener(event any, listener any)of the builder is used to register the listener. The first parameter is an instance of the event (usually it is enough to pass an "empty" structure of the desired type), the second is either a function or a listener object. An example of registering our logger:
b := builder.NewBuilder(cfg, logger)
// ... registration of transports, middleware, handlers ...
b.RegisterListener(event.SendMessageToTransportsEvent{}, &SendLoggingListener{})Here we specify that the listener SendLoggingListener should receive all events of the type SendMessageToTransportsEvent. Inside the registerListener, there is a subscription to the specified event type via the event manager.
- Features of listener execution: EventDispatcher Messenger calls all subscribed listeners to the event sequentially in the order of their registration. Listeners can be functions or methods; as with handlers, the Dispatcher finds the Handle method of an object through reflection if the listener is not a function.
When calling:
- If the listener is waiting for 2 arguments, it is passed the
ctx(the same as at the time of the Dispatch event) and the event object itself. - If 1 argument is expected, only the event object is passed.
If the listener returns an error (for example, a func Handle(evt SomeEvent) error), the Dispatcher will block this error and stop sending this event to other listeners.
-
Using Listeners: User listeners are useful for monitoring Messenger or integrating with external monitoring systems. For example, you can listen to
SendFailedMessageEventand send an alert or metric in case of delivery failures, or listen to an event before sending in order to modify the message or dynamically decide which transports to send (however, it is not recommended to change the structure ofSendMessageToTransportsEvent, it is better to influence it through middleware or routing itself for such purposes). -
Listeners vs. Middleware vs. Handlers: Don't confuse these concepts.
Handlers– process the contents of your messages (business logic). 'Middleware` – handle the process of delivering/receiving messages (technical logic) within the bus, they can affect whether messages are sent or how they are processed. Listeners – respond to Messenger events (meta-level of events: sending, errors, etc.), without affecting the transmission of the message itself, but rather performing side tasks or logging.
Handlers (Message handlers): Although the handlers have already been discussed in the Quick Start section, here we summarize the important points:
-
Handler is a type with the
Handle(ctx context) method.Context, msg *T) error(orHandle(ctx context.Context, msg *T) (R, error)). The message typeTcan be a pointer or a value – usually a pointer is used. The handler is registered via the 'RegisterHandler(handlerInstance)`. Messenger, through reflection, determines which message is processed by this handler (by the signature of the Handle method) and retains this relationship. If the structure does not have a Handle method of the required form, registration returns an error. -
One handler can process only one type of message (the
Handlemethod with a specificmsg*T). Messenger is looking specifically for a method namedHandle. -
You can register multiple handlers for the same message type (even within the same process). Internally, Messenger stores a cross-section of handler functions for each type of message. They will be called sequentially in the order of registration. As mentioned, if one of them returns an error, the subsequent ones will not be called.
-
The handler can optionally implement the
api.MessageHandlerTypeinterface with theGetBusName() stringmethod to indicate that this handler should receive messages from a specific bus (notdefault). This is useful if you have several buses and you want to attach the handler to a non-standard bus. Messenger checks this interface during registration and saves the specified bus name. During the construction, Messenger will compare the messages and select for each type which bus to send to based on this (see Usage Scenarios, where an example of usingGetBusNameis given). -
Handler return value: If the handler returns two values
(result, error), Messenger does not use the result directly, but includes it (and its type) in theHandledStampstamp in the message envelope. This is mainly for debugging purposes or, for example, the potential use of the result in the future (at the moment, Messenger does not have a mechanism for delivering the result back to the sender). However, if you callbus.Dispatchmanually and get anEnvelopein response, you can extract aHandledStampfrom it and see the handler result. In normal scenarios, when messages are sent asynchronously, this feature is rarely needed, but it is there.
To summarize: Handlers contain your business logic and respond to incoming messages, Listeners monitor technical events in the exchange system (without interfering with the business logic of messages), and 'Middleware' allow you to implement logic in the process of message delivery and processing (for example, logging, transformations, control flow). By combining them correctly, you can achieve a high level of control over the behavior of the messaging system.