Support listener interception
For Sponge we would like to be able to intercept either the listener registration or the invocation of it. For EB6 we are using reflection to replace the EventBus with our own implementation just to wrap the individual listener invocation with our own phase tracker system, this is not ideal.
To support our needs I'm proposing to add a service that can be implemented to provide an interceptor for this scenario. We are fine with either being able to wrap the listener function itself or the invocation of it.
The benefit of being able to wrap the registration itself is that we might be able to extract more information upfront about which mod registered the listener that we can then bake in to the wrapper function itself. This is a bit of moot point as EB doesn't require to provide any context objects like the ModInfo that we can easily rely on.
// Rough example of invocation interception.
public interface EventBusInterceptorProvider {
Consumer<Runnable> createInvocationInterceptor(BusGroup group, EventBus<?> bus);
}
Additional context about this on the SpongePowered Discord: https://discord.com/channels/142425412096491520/539537645152239626/1380522060757074041
Would this API be suitable for your needs? This is a rough idea I have for now, may be able to simplify it further with more refinement:
/**
* An interface that allows users to intercept the addition of listeners to an EventBus. This is intended to support
* auditing and rollback use-cases, such as keeping track of which exact listener mutated an event and what was changed.
* <p>For simpler uses - such as when keeping track of the exact listener at fault is not needed - you should instead
* either:</p>
* <ul>
* <li>Use a couple of listeners at opposing priorities ({@link Priority#HIGHEST} and {@link Priority#MONITOR})</li>
* <li>Implement a custom sorting algorithm by providing an implementation of the {@link ListenerSorter} service</li>
* </ul>
*
* @implSpec Listeners are a very performance-sensitive part of EventBus. You should take care to only apply interception
* when needed and not to all EventBuses indiscriminately. There may only be one instance of this service per
* application. It must be a final class.
*/
public interface RegistrationInterceptor {
/**
* Intercepts the addition of a listener to an EventBus. This method is called after the native functional interface
* type is converted to an {@link EventListener} but before it is added to the EventBus.
* @param busGroupName the name of the {@link BusGroup} the {@code bus} belongs to
* @param bus the EventBus the listener is being added to. Can be a {@link CancellableEventBus} if the
* {@link EventListener#eventType() listener's event type} implements {@link Cancellable}.
* @param listener the listener being added to the EventBus.
* @return {@code true} if you have already handled adding the listener yourself, {@code false} for EventBus to
* proceed with the addition.
*
* @implSpec The listener must always be added to the EventBus, even if you return {@code true} from this method.
*/
boolean onAddListener(String busGroupName, EventBus<?> bus, EventListener listener);
/**
* Intercepts the removal of a listener from an EventBus. This method is called before the listener is removed from
* the EventBus and is intended to allow any additiona listeners added by the interceptor to be removed when the
* associated original listener is removed.
* @param busGroupName the name of the {@link BusGroup} the {@code bus} belongs to
* @param bus the EventBus the listener is being removed from. Can be a {@link CancellableEventBus} if the
* {@link EventListener#eventType() listener's event type} implements {@link Cancellable}.
* @param listener the listener being removed from the EventBus.
*/
void onRemoveListener(String busGroupName, EventBus<?> bus, EventListener listener);
/**
* Checks if the provided listener is natively a {@link Consumer}.
* <p>A listener is considered "natively" a given functional interface type when the user provided said type
* directly to the EventBus API - that is, it is the underlying type before any internal conversions by EventBus.</p>
* <p>For cancellable events, it may be conditionally wrapped in a predicate that always returns {@code false} if
* the associated {@link CancellableEventBus} contains other listeners that might cancel the event
* (that is - are natively {@link Predicate}s).</p>
*
* @param listener the listener to check
* @return {@code true} if the listener is natively a {@link Consumer}, {@code false} otherwise
* @see #isPredicate(EventListener)
* @see EventListener#alwaysCancelling()
*/
default boolean isConsumer(EventListener listener) {
return switch (listener) {
case EventListenerImpl.HasConsumer<?> hasConsumer -> true;
case EventListenerImpl.MonitoringListener monitoringListener -> throw new UnsupportedOperationException(
"This version of EventBus does not support distinguishing the native FI type of monitoring listeners"
);
default -> false;
};
}
/**
* Checks if the provided listener is natively a {@link Predicate}.
* <p>A listener is considered "natively" a given functional interface type when the user provided said type
* directly to the EventBus API - that is, it is the underlying type before any internal conversions by EventBus.</p>
*
* @param listener the listener to check
* @return {@code true} if the listener is natively a {@link Predicate}, {@code false} otherwise
*/
default boolean isPredicate(EventListener listener) {
return listener instanceof EventListenerImpl.PredicateListener;
}
/**
* Checks if the provided listener is a monitoring listener.
* <p><u>Monitoring listeners have the following characteristics:</u></p>
* <ul>
* <li>They are always called last, even if the event was cancelled</li>
* <li>They cannot cancel or uncancel the event</li>
* <li>They are not supposed to mutate the event in any way, and mutation is explicitly guarded against when the
* event implements {@link MonitorAware}</li>
* </ul>
*
* @param listener the listener to check
* @return {@code true} if the listener is a monitoring listener, {@code false} otherwise
* @see MonitorAware
* @see Priority#MONITOR
*/
default boolean isMonitor(EventListener listener) {
return listener.priority() == Priority.MONITOR;
}
/**
* Converts the provided listener to a {@link Consumer} if it is natively a {@link Consumer}.
* <p>It is recommended to check if the listener is a {@link Consumer} first using
* {@link #isConsumer(EventListener)}.</p>
*
* @param listener the listener to convert
* @return the listener as a {@link Consumer}
* @throws IllegalArgumentException if the listener is not natively a {@link Consumer}
*/
@SuppressWarnings("unchecked")
default <T> Consumer<T> getAsConsumer(EventListener listener) {
return switch (listener) {
case EventListenerImpl.HasConsumer<?> hasConsumer -> (Consumer<T>) hasConsumer.consumer();
case EventListenerImpl.MonitoringListener monitoringListener -> throw new UnsupportedOperationException();
default -> throw new IllegalArgumentException("Listener is not natively a consumer. Check isConsumer(EventListener) first.");
};
}
/**
* Converts the provided listener to a {@link Predicate} if it is natively a {@link Predicate}.
* <p>It is recommended to check if the listener is a {@link Predicate} first using
* {@link #isPredicate(EventListener)}.</p>
*
* @param listener the listener to convert
* @return the listener as a {@link Predicate}
* @throws IllegalArgumentException if the listener is not natively a {@link Predicate}
*/
@SuppressWarnings("unchecked")
default <T> Predicate<T> getAsPredicate(EventListener listener) {
if (listener instanceof EventListenerImpl.PredicateListener predicateListener)
return (Predicate<T>) predicateListener.predicate();
throw new IllegalArgumentException("Listener is not natively a predicate. Check isPredicate(EventListener) first.");
}
/**
* Converts the provided listener to an {@link ObjBooleanBiConsumer} if it is a monitoring listener.
* <p>It is recommended to check if the listener is a monitoring listener first using
* {@link #isMonitor(EventListener)}.</p>
*
* @param listener the listener to convert
* @return the listener as an {@link ObjBooleanBiConsumer}
* @throws IllegalArgumentException if the listener is not natively an {@link ObjBooleanBiConsumer}
*/
@SuppressWarnings("unchecked")
default <T> ObjBooleanBiConsumer<T> getAsObjBooleanBiConsumer(EventListener listener) {
if (listener instanceof EventListenerImpl.MonitoringListener monitoringListener)
return (ObjBooleanBiConsumer<T>) monitoringListener.booleanBiConsumer();
throw new IllegalArgumentException("Listener is not a monitoring listener. Check isMonitor(EventListener) first.");
}
}
Example usage:
public final class Example implements RegistrationInterceptor {
private static final Map<EventListener, EventListener[]> INTERCEPTORS = new HashMap<>();
@Override
public boolean onAddListener(String busGroupName, EventBus<?> bus, EventListener listener) {
if (!busGroupName.equals(BusGroup.DEFAULT.name()))
return false;
if (bus != ExampleEvent.BUS)
return false;
if (isConsumer(listener)) {
Consumer<ExampleEvent> before = event -> {
// do something before the listener is invoked. avoid accessing untrusted final fields if possible
// (access static finals or record components instead).
};
Consumer<ExampleEvent> after = event -> {
// do something after the listener is invoked. avoid accessing untrusted final fields if possible
// (access static finals or record components instead).
};
var beforeEventListener = bus.addListener(before);
bus.addListener(listener);
var afterEventListener = bus.addListener(after);
INTERCEPTORS.put(listener, new EventListener[] { beforeEventListener, afterEventListener });
return true;
} else if (isPredicate(listener)) {
// etc...
return true;
} else if (isMonitor(listener)) {
// etc...
return true;
}
return false;
}
@Override
public void onRemoveListener(String busGroupName, EventBus<?> bus, EventListener listener) {
if (!busGroupName.equals(BusGroup.DEFAULT.name()))
return;
if (bus != ExampleEvent.BUS)
return;
EventListener[] interceptors = CONSUMER_INTERCEPTORS.get(listener);
if (interceptors != null) {
for (EventListener interceptor : interceptors) {
bus.removeListener(interceptor);
}
CONSUMER_INTERCEPTORS.remove(listener);
}
}
}
The general idea is that by intercepting the addition and removal instead of the listeners themselves, the small listener count optimisations are still able to kick in and wrapping lambdas in more lambdas is avoided, helping with posting performance.