mirror of
https://github.com/2OOP/pism.git
synced 2026-02-04 10:54:51 +00:00
refactor
This commit is contained in:
16
framework/pom.xml
Normal file
16
framework/pom.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.toop</groupId>
|
||||
<artifactId>pism</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-all</artifactId>
|
||||
<version>4.2.6.Final</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
202
framework/src/main/java/org/toop/Logging.java
Normal file
202
framework/src/main/java/org/toop/Logging.java
Normal file
@@ -0,0 +1,202 @@
|
||||
package org.toop.framework;
|
||||
|
||||
import org.apache.logging.log4j.Level;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.core.LoggerContext;
|
||||
import org.apache.logging.log4j.core.config.Configuration;
|
||||
import org.apache.logging.log4j.core.config.LoggerConfig;
|
||||
|
||||
/**
|
||||
* Utility class for configuring logging levels dynamically at runtime using Log4j 2.
|
||||
*
|
||||
* <p>Provides methods to enable or disable logs globally or per class, with support for specifying
|
||||
* log levels either via {@link Level} enums or string names.
|
||||
*/
|
||||
// Todo: refactor
|
||||
public final class Logging {
|
||||
|
||||
/** Disables all logging globally by setting the root logger level to {@link Level#OFF}. */
|
||||
public static void disableAllLogs() {
|
||||
LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
|
||||
Configuration config = ctx.getConfiguration();
|
||||
LoggerConfig rootLoggerConfig = config.getRootLogger();
|
||||
rootLoggerConfig.setLevel(Level.OFF);
|
||||
ctx.updateLoggers();
|
||||
}
|
||||
|
||||
/** Enables all logging globally by setting the root logger level to {@link Level#ALL}. */
|
||||
public static void enableAllLogs() {
|
||||
LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
|
||||
Configuration config = ctx.getConfiguration();
|
||||
LoggerConfig rootLoggerConfig = config.getRootLogger();
|
||||
rootLoggerConfig.setLevel(Level.ALL);
|
||||
ctx.updateLoggers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables global logging at a specific level by setting the root logger.
|
||||
*
|
||||
* @param level the logging level to enable for all logs
|
||||
*/
|
||||
public static void enableAllLogs(Level level) {
|
||||
LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
|
||||
Configuration config = ctx.getConfiguration();
|
||||
LoggerConfig rootLoggerConfig = config.getRootLogger();
|
||||
rootLoggerConfig.setLevel(level);
|
||||
ctx.updateLoggers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies whether the provided string corresponds to a valid class name.
|
||||
*
|
||||
* @param className fully-qualified class name to check
|
||||
* @return true if the class exists, false otherwise
|
||||
*/
|
||||
private static boolean verifyStringIsActualClass(String className) {
|
||||
try {
|
||||
Class.forName(className);
|
||||
return true;
|
||||
} catch (ClassNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper to disable logs for a specific class by name.
|
||||
*
|
||||
* @param className fully-qualified class name
|
||||
*/
|
||||
private static void disableLogsForClassInternal(String className) {
|
||||
LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
|
||||
Configuration config = ctx.getConfiguration();
|
||||
config.removeLogger(className);
|
||||
LoggerConfig specificConfig = new LoggerConfig(className, Level.OFF, false);
|
||||
config.addLogger(className, specificConfig);
|
||||
ctx.updateLoggers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables logs for a specific class.
|
||||
*
|
||||
* @param class_ the class for which logs should be disabled
|
||||
* @param <T> type of the class
|
||||
*/
|
||||
public static <T> void disableLogsForClass(Class<T> class_) {
|
||||
disableLogsForClassInternal(class_.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables logs for a class specified by fully-qualified name, if the class exists.
|
||||
*
|
||||
* @param className fully-qualified class name
|
||||
*/
|
||||
public static void disableLogsForClass(String className) {
|
||||
if (verifyStringIsActualClass(className)) {
|
||||
disableLogsForClassInternal(className);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper to enable logs for a specific class at a specific level.
|
||||
*
|
||||
* @param className fully-qualified class name
|
||||
* @param level logging level to set
|
||||
*/
|
||||
private static void enableLogsForClassInternal(String className, Level level) {
|
||||
LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
|
||||
Configuration config = ctx.getConfiguration();
|
||||
LoggerConfig loggerConfig = config.getLoggers().get(className);
|
||||
if (loggerConfig == null) {
|
||||
loggerConfig = new LoggerConfig(className, level, false);
|
||||
config.addLogger(className, loggerConfig);
|
||||
} else {
|
||||
loggerConfig.setLevel(level);
|
||||
}
|
||||
ctx.updateLoggers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables logging for a class at a specific level.
|
||||
*
|
||||
* @param class_ class to configure
|
||||
* @param levelToLog the logging level to set
|
||||
* @param <T> type of the class
|
||||
*/
|
||||
public static <T> void enableLogsForClass(Class<T> class_, Level levelToLog) {
|
||||
enableLogsForClassInternal(class_.getName(), levelToLog);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables logging for a class specified by name at a specific level, if the class exists.
|
||||
*
|
||||
* @param className fully-qualified class name
|
||||
* @param levelToLog the logging level to set
|
||||
*/
|
||||
public static void enableLogsForClass(String className, Level levelToLog) {
|
||||
if (verifyStringIsActualClass(className)) {
|
||||
enableLogsForClassInternal(className, levelToLog);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables logging for a class specified by name at a specific level using a string.
|
||||
*
|
||||
* @param className fully-qualified class name
|
||||
* @param levelToLog name of the logging level (e.g., "DEBUG", "INFO")
|
||||
*/
|
||||
public static void enableLogsForClass(String className, String levelToLog) {
|
||||
Level level = Level.valueOf(levelToLog.trim().toUpperCase());
|
||||
if (level != null && verifyStringIsActualClass(className)) {
|
||||
enableLogsForClassInternal(className, level);
|
||||
}
|
||||
}
|
||||
|
||||
/** Convenience methods for enabling logs at specific levels for classes. */
|
||||
public static <T> void enableAllLogsForClass(Class<T> class_) {
|
||||
enableLogsForClass(class_, Level.ALL);
|
||||
}
|
||||
|
||||
public static void enableAllLogsForClass(String className) {
|
||||
enableLogsForClass(className, Level.ALL);
|
||||
}
|
||||
|
||||
public static <T> void enableDebugLogsForClass(Class<T> class_) {
|
||||
enableLogsForClass(class_, Level.DEBUG);
|
||||
}
|
||||
|
||||
public static void enableDebugLogsForClass(String className) {
|
||||
enableLogsForClass(className, Level.DEBUG);
|
||||
}
|
||||
|
||||
public static <T> void enableErrorLogsForClass(Class<T> class_) {
|
||||
enableLogsForClass(class_, Level.ERROR);
|
||||
}
|
||||
|
||||
public static void enableErrorLogsForClass(String className) {
|
||||
enableLogsForClass(className, Level.ERROR);
|
||||
}
|
||||
|
||||
public static <T> void enableFatalLogsForClass(Class<T> class_) {
|
||||
enableLogsForClass(class_, Level.FATAL);
|
||||
}
|
||||
|
||||
public static void enableFatalLogsForClass(String className) {
|
||||
enableLogsForClass(className, Level.FATAL);
|
||||
}
|
||||
|
||||
public static <T> void enableInfoLogsForClass(Class<T> class_) {
|
||||
enableLogsForClass(class_, Level.INFO);
|
||||
}
|
||||
|
||||
public static void enableInfoLogsForClass(String className) {
|
||||
enableLogsForClass(className, Level.INFO);
|
||||
}
|
||||
|
||||
public static <T> void enableTraceLogsForClass(Class<T> class_) {
|
||||
enableLogsForClass(class_, Level.TRACE);
|
||||
}
|
||||
|
||||
public static void enableTraceLogsForClass(String className) {
|
||||
enableLogsForClass(className, Level.TRACE);
|
||||
}
|
||||
}
|
||||
221
framework/src/main/java/org/toop/eventbus/EventFlow.java
Normal file
221
framework/src/main/java/org/toop/eventbus/EventFlow.java
Normal file
@@ -0,0 +1,221 @@
|
||||
package org.toop.framework.eventbus;
|
||||
|
||||
import org.toop.framework.eventbus.events.EventWithUuid;
|
||||
import org.toop.framework.eventbus.events.IEvent;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* EventFlow is a utility class for creating, posting, and optionally subscribing to events
|
||||
* in a type-safe and chainable manner. It is designed to work with the {@link GlobalEventBus}.
|
||||
*
|
||||
* <p>This class supports automatic UUID assignment for {@link EventWithUuid} events,
|
||||
* and allows filtering subscribers so they only respond to events with a specific UUID.
|
||||
* All subscription methods are chainable, and you can configure automatic unsubscription
|
||||
* after an event has been successfully handled.</p>
|
||||
*/
|
||||
public class EventFlow {
|
||||
|
||||
/** Lookup object used for dynamically invoking constructors via MethodHandles. */
|
||||
private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
|
||||
|
||||
/** Cache of constructor handles for event classes to avoid repeated reflection lookups. */
|
||||
private static final Map<Class<?>, MethodHandle> CONSTRUCTOR_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
/** Automatically assigned UUID for {@link EventWithUuid} events. */
|
||||
private String eventId = null;
|
||||
|
||||
/** The event instance created by this publisher. */
|
||||
private IEvent event = null;
|
||||
|
||||
/** The listener returned by GlobalEventBus subscription. Used for unsubscription. */
|
||||
private Object listener;
|
||||
|
||||
/** Flag indicating whether to automatically unsubscribe the listener after success. */
|
||||
private boolean unsubscribeAfterSuccess = false;
|
||||
|
||||
/** Holds the results returned from the subscribed event, if any. */
|
||||
private Map<String, Object> result = null;
|
||||
|
||||
/** Empty constructor (event must be added via {@link #addPostEvent}). */
|
||||
public EventFlow() {}
|
||||
|
||||
/**
|
||||
* Instantiate an event of the given class and store it in this publisher.
|
||||
*/
|
||||
public <T extends IEvent> EventFlow addPostEvent(Class<T> eventClass, Object... args) {
|
||||
try {
|
||||
boolean isUuidEvent = EventWithUuid.class.isAssignableFrom(eventClass);
|
||||
|
||||
MethodHandle ctorHandle = CONSTRUCTOR_CACHE.computeIfAbsent(eventClass, cls -> {
|
||||
try {
|
||||
Class<?>[] paramTypes = cls.getDeclaredConstructors()[0].getParameterTypes();
|
||||
MethodType mt = MethodType.methodType(void.class, paramTypes);
|
||||
return LOOKUP.findConstructor(cls, mt);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to find constructor handle for " + cls, e);
|
||||
}
|
||||
});
|
||||
|
||||
Object[] finalArgs;
|
||||
int expectedParamCount = ctorHandle.type().parameterCount();
|
||||
|
||||
if (isUuidEvent && args.length < expectedParamCount) {
|
||||
this.eventId = UUID.randomUUID().toString();
|
||||
finalArgs = new Object[args.length + 1];
|
||||
System.arraycopy(args, 0, finalArgs, 0, args.length);
|
||||
finalArgs[args.length] = this.eventId;
|
||||
} else if (isUuidEvent) {
|
||||
this.eventId = (String) args[args.length - 1];
|
||||
finalArgs = args;
|
||||
} else {
|
||||
finalArgs = args;
|
||||
}
|
||||
|
||||
this.event = (IEvent) ctorHandle.invokeWithArguments(finalArgs);
|
||||
return this;
|
||||
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("Failed to instantiate event", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start listening for a response event type, chainable with perform().
|
||||
*/
|
||||
public <TT extends IEvent> ResponseBuilder<TT> onResponse(Class<TT> eventClass) {
|
||||
return new ResponseBuilder<>(this, eventClass);
|
||||
}
|
||||
|
||||
public static class ResponseBuilder<R extends IEvent> {
|
||||
private final EventFlow parent;
|
||||
private final Class<R> responseClass;
|
||||
|
||||
ResponseBuilder(EventFlow parent, Class<R> responseClass) {
|
||||
this.parent = parent;
|
||||
this.responseClass = responseClass;
|
||||
}
|
||||
|
||||
/** Finalize the subscription */
|
||||
public EventFlow perform(Consumer<R> action) {
|
||||
parent.listener = GlobalEventBus.subscribe(responseClass, event -> {
|
||||
action.accept(responseClass.cast(event));
|
||||
if (parent.unsubscribeAfterSuccess && parent.listener != null) {
|
||||
GlobalEventBus.unsubscribe(parent.listener);
|
||||
}
|
||||
});
|
||||
return parent;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe by ID: only fires if UUID matches this publisher's eventId.
|
||||
*/
|
||||
public <TT extends EventWithUuid> EventFlow onResponse(Class<TT> eventClass, Consumer<TT> action) {
|
||||
this.listener = GlobalEventBus.subscribe(eventClass, event -> {
|
||||
if (event.eventId().equals(this.eventId)) {
|
||||
action.accept(event);
|
||||
if (unsubscribeAfterSuccess && listener != null) {
|
||||
GlobalEventBus.unsubscribe(listener);
|
||||
}
|
||||
this.result = event.result();
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe by ID without explicit class.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <TT extends EventWithUuid> EventFlow onResponse(Consumer<TT> action) {
|
||||
this.listener = GlobalEventBus.subscribe(event -> {
|
||||
if (event instanceof EventWithUuid uuidEvent) {
|
||||
if (uuidEvent.eventId().equals(this.eventId)) {
|
||||
try {
|
||||
TT typedEvent = (TT) uuidEvent;
|
||||
action.accept(typedEvent);
|
||||
if (unsubscribeAfterSuccess && listener != null) {
|
||||
GlobalEventBus.unsubscribe(listener);
|
||||
}
|
||||
this.result = typedEvent.result();
|
||||
} catch (ClassCastException ignored) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
// choose event type
|
||||
public <TT extends IEvent> EventSubscriberBuilder<TT> onEvent(Class<TT> eventClass) {
|
||||
return new EventSubscriberBuilder<>(this, eventClass);
|
||||
}
|
||||
|
||||
// One-liner shorthand
|
||||
public <TT extends IEvent> EventFlow listen(Class<TT> eventClass, Consumer<TT> action) {
|
||||
return this.onEvent(eventClass).perform(action);
|
||||
}
|
||||
|
||||
// Builder for chaining .onEvent(...).perform(...)
|
||||
public static class EventSubscriberBuilder<TT extends IEvent> {
|
||||
private final EventFlow publisher;
|
||||
private final Class<TT> eventClass;
|
||||
|
||||
EventSubscriberBuilder(EventFlow publisher, Class<TT> eventClass) {
|
||||
this.publisher = publisher;
|
||||
this.eventClass = eventClass;
|
||||
}
|
||||
|
||||
public EventFlow perform(Consumer<TT> action) {
|
||||
publisher.listener = GlobalEventBus.subscribe(eventClass, event -> {
|
||||
action.accept(eventClass.cast(event));
|
||||
if (publisher.unsubscribeAfterSuccess && publisher.listener != null) {
|
||||
GlobalEventBus.unsubscribe(publisher.listener);
|
||||
}
|
||||
});
|
||||
return publisher;
|
||||
}
|
||||
}
|
||||
|
||||
/** Post synchronously */
|
||||
public EventFlow postEvent() {
|
||||
GlobalEventBus.post(event);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Post asynchronously */
|
||||
public EventFlow asyncPostEvent() {
|
||||
GlobalEventBus.postAsync(event);
|
||||
return this;
|
||||
}
|
||||
|
||||
public EventFlow unsubscribeAfterSuccess() {
|
||||
this.unsubscribeAfterSuccess = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public EventFlow unsubscribeNow() {
|
||||
if (unsubscribeAfterSuccess && listener != null) {
|
||||
GlobalEventBus.unsubscribe(listener);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Map<String, Object> getResult() {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
public IEvent getEvent() {
|
||||
return event;
|
||||
}
|
||||
|
||||
public String getEventId() {
|
||||
return eventId;
|
||||
}
|
||||
}
|
||||
203
framework/src/main/java/org/toop/eventbus/GlobalEventBus.java
Normal file
203
framework/src/main/java/org/toop/eventbus/GlobalEventBus.java
Normal file
@@ -0,0 +1,203 @@
|
||||
package org.toop.framework.eventbus;
|
||||
|
||||
import org.toop.framework.eventbus.events.EventWithUuid;
|
||||
import org.toop.framework.eventbus.events.IEvent;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* GlobalEventBus is a high-throughput, thread-safe event bus for publishing and subscribing
|
||||
* to events within the application.
|
||||
*
|
||||
* <p>It supports:</p>
|
||||
* <ul>
|
||||
* <li>Type-specific subscriptions via {@link #subscribe(Class, Consumer)}</li>
|
||||
* <li>UUID-specific subscriptions via {@link #subscribeById(Class, String, Consumer)}</li>
|
||||
* <li>Asynchronous posting of events with automatic queueing and fallback</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p><b>Performance note:</b> Directly using {@link GlobalEventBus} is possible,
|
||||
* but for safer type handling, automatic UUID management, and easier unsubscription,
|
||||
* it is recommended to use {@link EventPublisher} whenever possible.</p>
|
||||
*
|
||||
* <p>The bus maintains a fixed pool of worker threads that continuously process queued events.</p>
|
||||
*/
|
||||
public final class GlobalEventBus {
|
||||
|
||||
/** Number of worker threads, set to the number of available CPU cores. */
|
||||
private static final int WORKERS = Runtime.getRuntime().availableProcessors();
|
||||
|
||||
/** Queue for asynchronous event processing. */
|
||||
private static final BlockingQueue<IEvent> EVENT_QUEUE = new LinkedBlockingQueue<>(WORKERS * 1024);
|
||||
|
||||
/** Map of event class to type-specific listeners. */
|
||||
private static final Map<Class<?>, CopyOnWriteArrayList<Consumer<? super IEvent>>> LISTENERS = new ConcurrentHashMap<>();
|
||||
|
||||
/** Map of event class to UUID-specific listeners. */
|
||||
private static final Map<Class<?>, ConcurrentHashMap<String, Consumer<? extends EventWithUuid>>> UUID_LISTENERS = new ConcurrentHashMap<>();
|
||||
|
||||
/** Thread pool for worker threads processing queued events. */
|
||||
private static final ExecutorService WORKER_POOL = Executors.newFixedThreadPool(WORKERS, r -> {
|
||||
Thread t = new Thread(r, "EventBus-Worker-" + r.hashCode());
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
|
||||
// Initialize worker threads
|
||||
static {
|
||||
for (int i = 0; i < WORKERS; i++) {
|
||||
WORKER_POOL.submit(GlobalEventBus::workerLoop);
|
||||
}
|
||||
}
|
||||
|
||||
/** Private constructor to prevent instantiation. */
|
||||
private GlobalEventBus() {}
|
||||
|
||||
/** Continuously processes events from the queue and dispatches them to listeners. */
|
||||
private static void workerLoop() {
|
||||
try {
|
||||
while (true) {
|
||||
IEvent event = EVENT_QUEUE.take();
|
||||
dispatchEvent(event);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes a type-specific listener for all events of a given class.
|
||||
*
|
||||
* @param eventClass the class of events to subscribe to
|
||||
* @param listener the action to execute when the event is posted
|
||||
* @param <T> the event type
|
||||
* @return the provided listener for possible unsubscription
|
||||
*/
|
||||
public static <T extends IEvent> Consumer<T> subscribe(Class<T> eventClass, Consumer<T> listener) {
|
||||
CopyOnWriteArrayList<Consumer<? super IEvent>> list =
|
||||
LISTENERS.computeIfAbsent(eventClass, k -> new CopyOnWriteArrayList<>());
|
||||
list.add(event -> listener.accept(eventClass.cast(event)));
|
||||
return listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes a generic listener for all events (no type filtering).
|
||||
*
|
||||
* @param listener the action to execute on any event
|
||||
* @return the provided listener for possible unsubscription
|
||||
*/
|
||||
public static Consumer<Object> subscribe(Consumer<Object> listener) {
|
||||
LISTENERS.computeIfAbsent(Object.class, _ -> new CopyOnWriteArrayList<>())
|
||||
.add(listener);
|
||||
return listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes a listener for a specific {@link EventWithUuid} identified by its UUID.
|
||||
*
|
||||
* @param eventClass the class of the UUID event
|
||||
* @param eventId the UUID of the event to listen for
|
||||
* @param listener the action to execute when the event with the matching UUID is posted
|
||||
* @param <T> the event type extending EventWithUuid
|
||||
*/
|
||||
public static <T extends EventWithUuid> void subscribeById(Class<T> eventClass, String eventId, Consumer<T> listener) {
|
||||
UUID_LISTENERS
|
||||
.computeIfAbsent(eventClass, _ -> new ConcurrentHashMap<>())
|
||||
.put(eventId, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribes a previously registered listener.
|
||||
*
|
||||
* @param listener the listener to remove
|
||||
*/
|
||||
public static void unsubscribe(Object listener) {
|
||||
LISTENERS.values().forEach(list -> list.remove(listener));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribes a UUID-specific listener.
|
||||
*
|
||||
* @param eventClass the class of the UUID event
|
||||
* @param eventId the UUID of the listener to remove
|
||||
* @param <T> the event type extending EventWithUuid
|
||||
*/
|
||||
public static <T extends EventWithUuid> void unsubscribeById(Class<T> eventClass, String eventId) {
|
||||
Map<String, Consumer<? extends EventWithUuid>> map = UUID_LISTENERS.get(eventClass);
|
||||
if (map != null) map.remove(eventId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Posts an event synchronously to all subscribed listeners.
|
||||
*
|
||||
* @param event the event instance to post
|
||||
* @param <T> the event type
|
||||
*/
|
||||
public static <T extends IEvent> void post(T event) {
|
||||
dispatchEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Posts an event asynchronously by adding it to the internal queue.
|
||||
* If the queue is full, the event is dispatched synchronously.
|
||||
*
|
||||
* @param event the event instance to post
|
||||
* @param <T> the event type
|
||||
*/
|
||||
public static <T extends IEvent> void postAsync(T event) {
|
||||
if (!EVENT_QUEUE.offer(event)) {
|
||||
dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
/** Dispatches an event to all type-specific, generic, and UUID-specific listeners. */
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void dispatchEvent(IEvent event) {
|
||||
Class<?> clazz = event.getClass();
|
||||
|
||||
CopyOnWriteArrayList<Consumer<? super IEvent>> classListeners = LISTENERS.get(clazz);
|
||||
if (classListeners != null) {
|
||||
for (Consumer<? super IEvent> listener : classListeners) {
|
||||
try { listener.accept(event); } catch (Throwable ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
CopyOnWriteArrayList<Consumer<? super IEvent>> genericListeners = LISTENERS.get(Object.class);
|
||||
if (genericListeners != null) {
|
||||
for (Consumer<? super IEvent> listener : genericListeners) {
|
||||
try { listener.accept(event); } catch (Throwable ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
if (event instanceof EventWithUuid uuidEvent) {
|
||||
Map<String, Consumer<? extends EventWithUuid>> map = UUID_LISTENERS.get(clazz);
|
||||
if (map != null) {
|
||||
Consumer<EventWithUuid> listener = (Consumer<EventWithUuid>) map.remove(uuidEvent.eventId());
|
||||
if (listener != null) {
|
||||
try { listener.accept(uuidEvent); } catch (Throwable ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts down the bus immediately, clearing all listeners and queued events.
|
||||
* Worker threads are stopped.
|
||||
*/
|
||||
public static void shutdown() {
|
||||
WORKER_POOL.shutdownNow();
|
||||
LISTENERS.clear();
|
||||
UUID_LISTENERS.clear();
|
||||
EVENT_QUEUE.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all listeners and UUID-specific subscriptions without stopping worker threads.
|
||||
*/
|
||||
public static void reset() {
|
||||
LISTENERS.clear();
|
||||
UUID_LISTENERS.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.toop.framework.eventbus.events;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface EventWithUuid extends IEvent {
|
||||
Map<String, Object> result();
|
||||
String eventId();
|
||||
}
|
||||
184
framework/src/main/java/org/toop/eventbus/events/Events.java
Normal file
184
framework/src/main/java/org/toop/eventbus/events/Events.java
Normal file
@@ -0,0 +1,184 @@
|
||||
package org.toop.framework.eventbus.events;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/** Events that are used in the GlobalEventBus class. */
|
||||
public class Events implements IEvent {
|
||||
|
||||
/**
|
||||
* WIP, DO NOT USE!
|
||||
*
|
||||
* @param eventName
|
||||
* @param args
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
public static Object get(String eventName, Object... args) throws Exception {
|
||||
Class<?> clazz = Class.forName("org.toop.eventbus.events.Events$ServerEvents$" + eventName);
|
||||
Class<?>[] paramTypes = Arrays.stream(args).map(Object::getClass).toArray(Class<?>[]::new);
|
||||
Constructor<?> constructor = clazz.getConstructor(paramTypes);
|
||||
return constructor.newInstance(args);
|
||||
}
|
||||
|
||||
/**
|
||||
* WIP, DO NOT USE!
|
||||
*
|
||||
* @param eventCategory
|
||||
* @param eventName
|
||||
* @param args
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
public static Object get(String eventCategory, String eventName, Object... args)
|
||||
throws Exception {
|
||||
Class<?> clazz =
|
||||
Class.forName("org.toop.eventbus.events.Events$" + eventCategory + "$" + eventName);
|
||||
Class<?>[] paramTypes = Arrays.stream(args).map(Object::getClass).toArray(Class<?>[]::new);
|
||||
Constructor<?> constructor = clazz.getConstructor(paramTypes);
|
||||
return constructor.newInstance(args);
|
||||
}
|
||||
|
||||
/**
|
||||
* WIP, DO NOT USE!
|
||||
*
|
||||
* @param eventName
|
||||
* @param args
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
public static Object get2(String eventName, Object... args) throws Exception {
|
||||
// Fully qualified class name
|
||||
String className = "org.toop.server.backend.Events$ServerEvents$" + eventName;
|
||||
|
||||
// Load the class
|
||||
Class<?> clazz = Class.forName(className);
|
||||
|
||||
// Build array of argument types
|
||||
Class<?>[] paramTypes = new Class[args.length];
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
paramTypes[i] = args[i].getClass();
|
||||
}
|
||||
|
||||
// Get the constructor
|
||||
Constructor<?> constructor = clazz.getConstructor(paramTypes);
|
||||
|
||||
// Create a new instance
|
||||
return constructor.newInstance(args);
|
||||
}
|
||||
|
||||
public static class ServerEvents {
|
||||
|
||||
/**
|
||||
* BLOCKING Requests all active servers. The result is returned via the provided
|
||||
* CompletableFuture.
|
||||
*
|
||||
* @param future List of all servers in string form.
|
||||
*/
|
||||
public record RequestsAllServers(CompletableFuture<String> future) {}
|
||||
|
||||
/** Forces closing all active servers immediately. */
|
||||
public record ForceCloseAllServers() implements IEvent {}
|
||||
|
||||
/**
|
||||
* Requests starting a server with a specific port and game type.
|
||||
*
|
||||
* @param port The port to open the server.
|
||||
* @param gameType Either "tictactoe" or ...
|
||||
*/
|
||||
public record StartServer(int port, String gameType) implements IEvent {}
|
||||
|
||||
/**
|
||||
* BLOCKING Requests starting a server with a specific port and game type, and returns a
|
||||
* CompletableFuture that completes when the server has started.
|
||||
*
|
||||
* @param port The port to open the server.
|
||||
* @param gameType Either "tictactoe" or ...
|
||||
* @param future The uuid of the server.
|
||||
*/
|
||||
public record StartServerRequest(
|
||||
int port, String gameType, CompletableFuture<String> future) implements IEvent{}
|
||||
|
||||
/**
|
||||
* Represents a server that has successfully started.
|
||||
*
|
||||
* @param uuid The unique identifier of the server.
|
||||
* @param port The port the server is listening on.
|
||||
*/
|
||||
public record ServerStarted(String uuid, int port) implements IEvent {}
|
||||
|
||||
/**
|
||||
* BLOCKING Requests creation of a TicTacToe game on a specific server.
|
||||
*
|
||||
* @param serverUuid The unique identifier of the server where the game will be created.
|
||||
* @param playerA The name of the first player.
|
||||
* @param playerB The name of the second player.
|
||||
* @param future The game UUID when the game is created.
|
||||
*/
|
||||
public record CreateTicTacToeGameRequest(
|
||||
String serverUuid,
|
||||
String playerA,
|
||||
String playerB,
|
||||
CompletableFuture<String> future) implements IEvent {}
|
||||
|
||||
/**
|
||||
* Requests running a TicTacToe game on a specific server.
|
||||
*
|
||||
* @param serverUuid The unique identifier of the server.
|
||||
* @param gameUuid The UUID of the game to run.
|
||||
*/
|
||||
public record RunTicTacToeGame(String serverUuid, String gameUuid) implements IEvent {}
|
||||
|
||||
/**
|
||||
* Requests ending a TicTacToe game on a specific server.
|
||||
*
|
||||
* @param serverUuid The UUID of the server the game is running on.
|
||||
* @param gameUuid The UUID of the game to end.
|
||||
*/
|
||||
public record EndTicTacToeGame(String serverUuid, String gameUuid) implements IEvent {}
|
||||
|
||||
// public record StartGameConnectionRequest(String ip, String port,
|
||||
// CompletableFuture<String> future) {}
|
||||
|
||||
/**
|
||||
* Triggers on changing the server IP.
|
||||
*
|
||||
* @param ip The new IP address.
|
||||
*/
|
||||
public record OnChangingServerIp(String ip) {}
|
||||
|
||||
/**
|
||||
* Triggers on changing the server port.
|
||||
*
|
||||
* @param port The new port.
|
||||
*/
|
||||
public record OnChangingServerPort(int port) {}
|
||||
|
||||
/** Triggers when a cell is clicked in one of the game boards. */
|
||||
public record CellClicked(int cell) {}
|
||||
}
|
||||
|
||||
public static class EventBusEvents {}
|
||||
|
||||
public static class WindowEvents {
|
||||
/** Triggers when the window wants to quit. */
|
||||
public record OnQuitRequested() implements IEvent {}
|
||||
|
||||
/** Triggers when the window is resized. */
|
||||
// public record OnResize(Window.Size size) {}
|
||||
|
||||
/** Triggers when the mouse is moved within the window. */
|
||||
public record OnMouseMove(int x, int y) implements IEvent {}
|
||||
|
||||
/** Triggers when the mouse is clicked within the window. */
|
||||
public record OnMouseClick(int button) {}
|
||||
|
||||
/** Triggers when the mouse is released within the window. */
|
||||
public record OnMouseRelease(int button) {}
|
||||
}
|
||||
|
||||
public static class TttEvents {}
|
||||
|
||||
public static class AiTttEvents {}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package org.toop.framework.eventbus.events;
|
||||
|
||||
public interface IEvent {}
|
||||
@@ -0,0 +1,189 @@
|
||||
package org.toop.framework.eventbus.events;
|
||||
|
||||
import org.toop.backend.tictactoe.TicTacToeServer;
|
||||
import org.toop.framework.networking.NetworkingGameClientHandler;
|
||||
|
||||
import java.lang.reflect.RecordComponent;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class NetworkEvents extends Events {
|
||||
|
||||
/**
|
||||
* BLOCKING Requests all active connections. The result is returned via the provided
|
||||
* CompletableFuture.
|
||||
*
|
||||
* @param future List of all connections in string form.
|
||||
*/
|
||||
public record RequestsAllClients(CompletableFuture<String> future) implements IEvent {}
|
||||
|
||||
/** Forces closing all active connections immediately. */
|
||||
public record ForceCloseAllClients() implements IEvent {}
|
||||
|
||||
public record CloseClientRequest(CompletableFuture<String> future) {}
|
||||
|
||||
public record CloseClient(String connectionId) implements IEvent {}
|
||||
|
||||
/**
|
||||
* Event to start a new client connection to a server.
|
||||
* <p>
|
||||
* This event is typically posted to the {@code GlobalEventBus} to initiate the creation of
|
||||
* a client connection, and carries all information needed to establish that connection:
|
||||
* <br>
|
||||
* - A factory for creating the Netty handler that will manage the connection
|
||||
* <br>
|
||||
* - The server's IP address and port
|
||||
* <br>
|
||||
* - A unique event identifier for correlation with follow-up events
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* The {@link #eventId()} allows callers to correlate the {@code StartClient} event
|
||||
* with subsequent success/failure events. For example, a {@code StartClientSuccess}
|
||||
* or {@code StartClientFailure} event may carry the same {@code eventId}.
|
||||
* </p>
|
||||
*
|
||||
* @param handlerFactory Factory for constructing a {@link NetworkingGameClientHandler}.
|
||||
* @param ip The IP address of the server to connect to.
|
||||
* @param port The port number of the server to connect to.
|
||||
* @param eventId A unique identifier for this event, typically injected
|
||||
* automatically by the {@link org.toop.eventbus.EventPublisher}.
|
||||
*/
|
||||
public record StartClient(
|
||||
Supplier<? extends NetworkingGameClientHandler> handlerFactory,
|
||||
String ip,
|
||||
int port,
|
||||
String eventId
|
||||
) implements EventWithUuid {
|
||||
|
||||
/**
|
||||
* Returns a map representation of this event, where keys are record component names
|
||||
* and values are their corresponding values. Useful for generic logging, debugging,
|
||||
* or serializing events without hardcoding field names.
|
||||
*
|
||||
* @return a {@code Map<String, Object>} containing field names and values
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Object> result() {
|
||||
return Stream.of(this.getClass().getRecordComponents())
|
||||
.collect(Collectors.toMap(
|
||||
RecordComponent::getName,
|
||||
rc -> {
|
||||
try {
|
||||
return rc.getAccessor().invoke(this);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the unique event identifier used for correlating this event.
|
||||
*
|
||||
* @return the event ID string
|
||||
*/
|
||||
@Override
|
||||
public String eventId() {
|
||||
return this.eventId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Update docs new input.
|
||||
* BLOCKING Triggers starting a server connection and returns a future.
|
||||
*
|
||||
* @param ip The IP address of the server to connect to.
|
||||
* @param port The port of the server to connect to.
|
||||
* @param future Returns the UUID of the connection, when connection is established.
|
||||
*/
|
||||
public record StartClientRequest(
|
||||
Supplier<? extends NetworkingGameClientHandler> handlerFactory,
|
||||
String ip, int port, CompletableFuture<String> future) implements IEvent {}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param clientId The ID of the client to be used in requests.
|
||||
* @param eventId The eventID used in checking if event is for you.
|
||||
*/
|
||||
public record StartClientSuccess(String clientId, String eventId)
|
||||
implements EventWithUuid {
|
||||
@Override
|
||||
public Map<String, Object> result() {
|
||||
return Stream.of(this.getClass().getRecordComponents())
|
||||
.collect(Collectors.toMap(
|
||||
RecordComponent::getName,
|
||||
rc -> {
|
||||
try {
|
||||
return rc.getAccessor().invoke(this);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String eventId() {
|
||||
return this.eventId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers sending a command to a server.
|
||||
*
|
||||
* @param connectionId The UUID of the connection to send the command on.
|
||||
* @param args The command arguments.
|
||||
*/
|
||||
public record SendCommand(String connectionId, String... args) implements IEvent {}
|
||||
|
||||
/**
|
||||
* WIP Triggers when a command is sent to a server.
|
||||
*
|
||||
* @param command The TicTacToeServer instance that executed the command.
|
||||
* @param args The command arguments.
|
||||
* @param result The result returned from executing the command.
|
||||
*/
|
||||
public record OnCommand(
|
||||
TicTacToeServer command, String[] args, String result) {} // TODO old
|
||||
|
||||
/**
|
||||
* Triggers reconnecting to a previous address.
|
||||
*
|
||||
* @param connectionId The identifier of the connection being reconnected.
|
||||
*/
|
||||
public record Reconnect(Object connectionId) {}
|
||||
|
||||
|
||||
/**
|
||||
* Triggers when the server client receives a message.
|
||||
*
|
||||
* @param ConnectionUuid The UUID of the connection that received the message.
|
||||
* @param message The message received.
|
||||
*/
|
||||
public record ReceivedMessage(String ConnectionUuid, String message) implements IEvent {}
|
||||
|
||||
/**
|
||||
* Triggers changing connection to a new address.
|
||||
*
|
||||
* @param connectionId The identifier of the connection being changed.
|
||||
* @param ip The new IP address.
|
||||
* @param port The new port.
|
||||
*/
|
||||
public record ChangeClient(Object connectionId, String ip, int port) {}
|
||||
|
||||
|
||||
/**
|
||||
* Triggers when the server couldn't connect to the desired address.
|
||||
*
|
||||
* @param connectionId The identifier of the connection that failed.
|
||||
*/
|
||||
public record CouldNotConnect(Object connectionId) {}
|
||||
|
||||
/** WIP Triggers when a connection closes. */
|
||||
public record ClosedConnection() {}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package org.toop.framework.eventbus.events;
|
||||
|
||||
public class ServerEvents {
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package org.toop.framework.networking;
|
||||
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.channel.*;
|
||||
import io.netty.channel.nio.NioIoHandler;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||
import io.netty.handler.codec.LineBasedFrameDecoder;
|
||||
import io.netty.handler.codec.string.StringDecoder;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class NetworkingClient {
|
||||
private static final Logger logger = LogManager.getLogger(NetworkingClient.class);
|
||||
|
||||
final Bootstrap bootstrap = new Bootstrap();
|
||||
final EventLoopGroup workerGroup = new MultiThreadIoEventLoopGroup(NioIoHandler.newFactory());
|
||||
|
||||
private String connectionUuid;
|
||||
private Channel channel;
|
||||
private NetworkingGameClientHandler handler;
|
||||
|
||||
public NetworkingClient(
|
||||
Supplier<? extends NetworkingGameClientHandler> handlerFactory,
|
||||
String host,
|
||||
int port) {
|
||||
try {
|
||||
this.bootstrap.group(this.workerGroup);
|
||||
this.bootstrap.channel(NioSocketChannel.class);
|
||||
this.bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
|
||||
this.bootstrap.handler(new ChannelInitializer<SocketChannel>() {
|
||||
@Override
|
||||
public void initChannel(SocketChannel ch) {
|
||||
handler = handlerFactory.get();
|
||||
|
||||
ChannelPipeline pipeline = ch.pipeline();
|
||||
pipeline.addLast(new LineBasedFrameDecoder(1024)); // split at \n
|
||||
pipeline.addLast(new StringDecoder()); // bytes -> String
|
||||
pipeline.addLast(handler);
|
||||
}
|
||||
});
|
||||
ChannelFuture channelFuture = this.bootstrap.connect(host, port).sync();
|
||||
this.channel = channelFuture.channel();
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to create networking client instance", e);
|
||||
}
|
||||
}
|
||||
|
||||
public NetworkingGameClientHandler getHandler() {
|
||||
return handler;
|
||||
}
|
||||
|
||||
public void setConnectionUuid(String connectionUuid) {
|
||||
this.connectionUuid = connectionUuid;
|
||||
}
|
||||
|
||||
public boolean isChannelActive() {
|
||||
return this.channel != null && this.channel.isActive();
|
||||
}
|
||||
|
||||
public void writeAndFlush(String msg) {
|
||||
String literalMsg = msg.replace("\n", "\\n").replace("\r", "\\r");
|
||||
if (isChannelActive()) {
|
||||
this.channel.writeAndFlush(msg);
|
||||
logger.info("Connection {} sent message: {}", this.channel.remoteAddress(), literalMsg);
|
||||
} else {
|
||||
logger.warn("Cannot send message: {}, connection inactive.", literalMsg);
|
||||
}
|
||||
}
|
||||
|
||||
public void writeAndFlushnl(String msg) {
|
||||
if (isChannelActive()) {
|
||||
this.channel.writeAndFlush(msg + "\n");
|
||||
logger.info("Connection {} sent message: {}", this.channel.remoteAddress(), msg);
|
||||
} else {
|
||||
logger.warn("Cannot send message: {}, connection inactive.", msg);
|
||||
}
|
||||
}
|
||||
|
||||
public void login(String username) {
|
||||
this.writeAndFlush("login " + username + "\n");
|
||||
}
|
||||
|
||||
public void logout() {
|
||||
this.writeAndFlush("logout\n");
|
||||
}
|
||||
|
||||
public void sendMove(int move) {
|
||||
this.writeAndFlush("move " + move + "\n"); // append \n so server receives a full line
|
||||
}
|
||||
|
||||
public void getGamelist() {
|
||||
this.writeAndFlush("get gamelist\n");
|
||||
}
|
||||
|
||||
public void getPlayerlist() {
|
||||
this.writeAndFlush("get playerlist\n");
|
||||
}
|
||||
|
||||
public void subscribe(String gameType) {
|
||||
this.writeAndFlush("subscribe " + gameType + "\n");
|
||||
}
|
||||
|
||||
public void forfeit() {
|
||||
this.writeAndFlush("forfeit\n");
|
||||
}
|
||||
|
||||
public void challenge(String playerName, String gameType) {
|
||||
this.writeAndFlush("challenge " + playerName + " " + gameType + "\n");
|
||||
}
|
||||
|
||||
public void acceptChallenge(String challengeNumber) {
|
||||
this.writeAndFlush("challenge accept " + challengeNumber + "\n");
|
||||
}
|
||||
|
||||
public void sendChatMessage(String message) {
|
||||
this.writeAndFlush("message " + "\"" + message + "\"" + "\n");
|
||||
}
|
||||
|
||||
public void help(String command) {
|
||||
this.writeAndFlush("help " + command + "\n");
|
||||
}
|
||||
|
||||
public void closeConnection() {
|
||||
if (this.channel != null && this.channel.isActive()) {
|
||||
this.channel.close().addListener(future -> {
|
||||
if (future.isSuccess()) {
|
||||
logger.info("Connection {} closed successfully", this.channel.remoteAddress());
|
||||
} else {
|
||||
logger.error("Error closing connection {}. Error: {}",
|
||||
this.channel.remoteAddress(),
|
||||
future.cause().getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package org.toop.framework.networking;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.toop.eventbus.EventPublisher;
|
||||
import org.toop.framework.eventbus.events.NetworkEvents;
|
||||
|
||||
public class NetworkingClientManager {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger(NetworkingClientManager.class);
|
||||
|
||||
/** Map of serverId -> Server instances */
|
||||
private final Map<String, NetworkingClient> networkClients = new ConcurrentHashMap<>();
|
||||
|
||||
/** Starts a connection manager, to manage, connections. */
|
||||
public NetworkingClientManager() {
|
||||
new EventPublisher<>(NetworkEvents.StartClientRequest.class, this::handleStartClientRequest);
|
||||
new EventPublisher<>(NetworkEvents.StartClient.class, this::handleStartClient);
|
||||
new EventPublisher<>(NetworkEvents.SendCommand.class, this::handleCommand);
|
||||
new EventPublisher<>(NetworkEvents.CloseClient.class, this::handleCloseClient);
|
||||
new EventPublisher<>(NetworkEvents.RequestsAllClients.class, this::getAllConnections);
|
||||
new EventPublisher<>(NetworkEvents.ForceCloseAllClients.class, this::shutdownAll);
|
||||
}
|
||||
|
||||
private String startClientRequest(Supplier<? extends NetworkingGameClientHandler> handlerFactory,
|
||||
String ip,
|
||||
int port) {
|
||||
String connectionUuid = UUID.randomUUID().toString();
|
||||
try {
|
||||
NetworkingClient client = new NetworkingClient(
|
||||
handlerFactory,
|
||||
ip,
|
||||
port);
|
||||
this.networkClients.put(connectionUuid, client);
|
||||
} catch (Exception e) {
|
||||
logger.error(e);
|
||||
}
|
||||
logger.info("Client {} started", connectionUuid);
|
||||
return connectionUuid;
|
||||
}
|
||||
|
||||
private void handleStartClientRequest(NetworkEvents.StartClientRequest request) {
|
||||
request.future()
|
||||
.complete(
|
||||
this.startClientRequest(
|
||||
request.handlerFactory(),
|
||||
request.ip(),
|
||||
request.port())); // TODO: Maybe post ConnectionEstablished event.
|
||||
}
|
||||
|
||||
private void handleStartClient(NetworkEvents.StartClient event) {
|
||||
String uuid = this.startClientRequest(event.handlerFactory(), event.ip(), event.port());
|
||||
new EventPublisher<>(NetworkEvents.StartClientSuccess.class,
|
||||
uuid, event.eventId()
|
||||
).asyncPostEvent();
|
||||
}
|
||||
|
||||
private void handleCommand(
|
||||
NetworkEvents.SendCommand
|
||||
event) { // TODO: Move this to ServerConnection class, keep it internal.
|
||||
NetworkingClient client = this.networkClients.get(event.connectionId());
|
||||
logger.info("Preparing to send command: {} to server: {}", event.args(), client);
|
||||
if (client != null) {
|
||||
String args = String.join(" ", event.args()) + "\n";
|
||||
client.writeAndFlush(args);
|
||||
} else {
|
||||
logger.warn("Server {} not found for command '{}'", event.connectionId(), event.args());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCloseClient(NetworkEvents.CloseClient event) {
|
||||
NetworkingClient client = this.networkClients.get(event.connectionId());
|
||||
client.closeConnection(); // TODO: Check if not blocking, what if error, mb not remove?
|
||||
this.networkClients.remove(event.connectionId());
|
||||
logger.info("Client {} closed successfully.", event.connectionId());
|
||||
}
|
||||
|
||||
// private void handleReconnect(Events.ServerEvents.Reconnect event) {
|
||||
// NetworkingClient client = this.networkClients.get(event.connectionId());
|
||||
// if (client != null) {
|
||||
// try {
|
||||
// client;
|
||||
// logger.info("Server {} reconnected", event.connectionId());
|
||||
// } catch (Exception e) {
|
||||
// logger.error("Server {} failed to reconnect", event.connectionId(), e);
|
||||
// GlobalEventBus.post(new Events.ServerEvents.CouldNotConnect(event.connectionId()));
|
||||
// }
|
||||
// }
|
||||
// } // TODO: Reconnect on disconnect
|
||||
|
||||
// private void handleChangeConnection(Events.ServerEvents.ChangeConnection event) {
|
||||
// ServerConnection serverConnection = this.serverConnections.get(event.connectionId());
|
||||
// if (serverConnection != null) {
|
||||
// try {
|
||||
// serverConnection.connect(event.ip(), event.port());
|
||||
// logger.info("Server {} changed connection to {}:{}", event.connectionId(),
|
||||
// event.ip(), event.port());
|
||||
// } catch (Exception e) {
|
||||
// logger.error("Server {} failed to change connection", event.connectionId(),
|
||||
// e);
|
||||
// GlobalEventBus.post(new
|
||||
// Events.ServerEvents.CouldNotConnect(event.connectionId()));
|
||||
// }
|
||||
// }
|
||||
// } TODO
|
||||
|
||||
private void getAllConnections(NetworkEvents.RequestsAllClients request) {
|
||||
List<NetworkingClient> a = new ArrayList<>(this.networkClients.values());
|
||||
request.future().complete(a.toString());
|
||||
}
|
||||
|
||||
public void shutdownAll(NetworkEvents.ForceCloseAllClients request) {
|
||||
this.networkClients.values().forEach(NetworkingClient::closeConnection);
|
||||
this.networkClients.clear();
|
||||
logger.info("All servers shut down");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package org.toop.framework.networking;
|
||||
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
|
||||
private static final Logger logger = LogManager.getLogger(NetworkingGameClientHandler.class);
|
||||
|
||||
public NetworkingGameClientHandler() {}
|
||||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) {
|
||||
logger.debug("Received message from server-{}, data: {}", ctx.channel().remoteAddress(), msg);
|
||||
|
||||
// TODO: Handle server messages
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
|
||||
logger.error(cause.getMessage(), cause);
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
//package org.toop.frontend.networking.handlers;
|
||||
//
|
||||
//import io.netty.channel.ChannelHandlerContext;
|
||||
//import org.apache.logging.log4j.LogManager;
|
||||
//import org.apache.logging.log4j.Logger;
|
||||
//import org.toop.frontend.networking.NetworkingGameClientHandler;
|
||||
//
|
||||
//public class NetworkingTicTacToeClientHandler extends NetworkingGameClientHandler {
|
||||
// static final Logger logger = LogManager.getLogger(NetworkingTicTacToeClientHandler.class);
|
||||
//
|
||||
//
|
||||
//}
|
||||
13
framework/src/main/resources/log4j2.xml
Normal file
13
framework/src/main/resources/log4j2.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="debug" name="AppConfig">
|
||||
<Appenders>
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%17.17t] %-5level %logger{36} - %msg%n"/>
|
||||
</Console>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Root level="info">
|
||||
<AppenderRef ref="Console"/>
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
||||
111
framework/src/test/LoggingTest.java
Normal file
111
framework/src/test/LoggingTest.java
Normal file
@@ -0,0 +1,111 @@
|
||||
package org.toop;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import org.apache.logging.log4j.Level;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.core.LoggerContext;
|
||||
import org.apache.logging.log4j.core.config.LoggerConfig;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.toop.framework.Logging;
|
||||
|
||||
public class LoggingTest {
|
||||
|
||||
private LoggerContext ctx;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
ctx = (LoggerContext) LogManager.getContext(false);
|
||||
Logging.enableAllLogs(Level.DEBUG); // reset root logger before each test
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
Logging.enableAllLogs(Level.DEBUG); // restore root logger after each test
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDisableAllLogs_setsRootLevelOff() {
|
||||
Logging.disableAllLogs();
|
||||
|
||||
LoggerConfig rootConfig = ctx.getConfiguration().getRootLogger();
|
||||
assertEquals(Level.OFF, rootConfig.getLevel());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEnableAllLogs_setsRootLevelAll() {
|
||||
Logging.enableAllLogs();
|
||||
|
||||
LoggerConfig rootConfig = ctx.getConfiguration().getRootLogger();
|
||||
assertEquals(Level.ALL, rootConfig.getLevel());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEnableAllLogs_LevelParam_setsRootLevel() {
|
||||
Logging.enableAllLogs(Level.WARN);
|
||||
|
||||
LoggerConfig rootConfig = ctx.getConfiguration().getRootLogger();
|
||||
assertEquals(Level.WARN, rootConfig.getLevel());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDisableLogsForClass_addsLoggerWithOff() {
|
||||
Logging.disableLogsForClass(LoggingTest.class);
|
||||
|
||||
LoggerConfig loggerConfig =
|
||||
ctx.getConfiguration().getLoggers().get(LoggingTest.class.getName());
|
||||
assertNotNull(loggerConfig);
|
||||
assertEquals(Level.OFF, loggerConfig.getLevel());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEnableLogsForClass_addsLoggerWithLevel() {
|
||||
Logging.enableLogsForClass(LoggingTest.class, Level.ERROR);
|
||||
|
||||
LoggerConfig loggerConfig =
|
||||
ctx.getConfiguration().getLoggers().get(LoggingTest.class.getName());
|
||||
assertNotNull(loggerConfig);
|
||||
assertEquals(Level.ERROR, loggerConfig.getLevel());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEnableLogsForClass_withStringLevel() {
|
||||
Logging.enableLogsForClass(LoggingTest.class.getName(), "INFO");
|
||||
|
||||
LoggerConfig loggerConfig =
|
||||
ctx.getConfiguration().getLoggers().get(LoggingTest.class.getName());
|
||||
assertNotNull(loggerConfig);
|
||||
assertEquals(Level.INFO, loggerConfig.getLevel());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEnableDebugLogsForClass_setsDebug() {
|
||||
Logging.enableDebugLogsForClass(LoggingTest.class);
|
||||
|
||||
LoggerConfig loggerConfig =
|
||||
ctx.getConfiguration().getLoggers().get(LoggingTest.class.getName());
|
||||
assertNotNull(loggerConfig);
|
||||
assertEquals(Level.DEBUG, loggerConfig.getLevel());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEnableInfoLogsForClass_setsInfo() {
|
||||
Logging.enableInfoLogsForClass(LoggingTest.class);
|
||||
|
||||
LoggerConfig loggerConfig =
|
||||
ctx.getConfiguration().getLoggers().get(LoggingTest.class.getName());
|
||||
assertNotNull(loggerConfig);
|
||||
assertEquals(Level.INFO, loggerConfig.getLevel());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDisableLogsForNonexistentClass_doesNothing() {
|
||||
Logging.disableLogsForClass("org.toop.DoesNotExist");
|
||||
|
||||
LoggerConfig loggerConfig =
|
||||
ctx.getConfiguration().getLoggers().get("org.toop.DoesNotExist");
|
||||
assertNull(loggerConfig); // class doesn't exist, so no logger added
|
||||
}
|
||||
}
|
||||
89
framework/src/test/eventbus/EventPublisherSpeedTest.java
Normal file
89
framework/src/test/eventbus/EventPublisherSpeedTest.java
Normal file
@@ -0,0 +1,89 @@
|
||||
package org.toop.eventbus;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.toop.framework.eventbus.GlobalEventBus;
|
||||
import org.toop.framework.eventbus.events.EventWithUuid;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class EventPublisherPerformanceTest {
|
||||
|
||||
public record PerfEvent(String name, String eventId) implements EventWithUuid {
|
||||
@Override
|
||||
public java.util.Map<String, Object> result() {
|
||||
return java.util.Map.of("name", name, "eventId", eventId);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEventCreationSpeed() {
|
||||
int iterations = 10_000;
|
||||
long start = System.nanoTime();
|
||||
|
||||
for (int i = 0; i < iterations; i++) {
|
||||
new EventPublisher<>(PerfEvent.class, "event-" + i);
|
||||
}
|
||||
|
||||
long end = System.nanoTime();
|
||||
long durationMs = (end - start) / 1_000_000;
|
||||
|
||||
System.out.println("Created " + iterations + " events in " + durationMs + " ms");
|
||||
assertTrue(durationMs < 500, "Event creation too slow");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEventPostSpeed() {
|
||||
int iterations = 100_000;
|
||||
AtomicInteger counter = new AtomicInteger(0);
|
||||
|
||||
GlobalEventBus.subscribe(PerfEvent.class, e -> counter.incrementAndGet());
|
||||
|
||||
long start = System.nanoTime();
|
||||
|
||||
for (int i = 0; i < iterations; i++) {
|
||||
new EventPublisher<>(PerfEvent.class, "event-" + i).postEvent();
|
||||
}
|
||||
|
||||
long end = System.nanoTime();
|
||||
long durationMs = (end - start) / 1_000_000;
|
||||
|
||||
System.out.println("Posted " + iterations + " events in " + durationMs + " ms");
|
||||
assertTrue(counter.get() == iterations, "Not all events were received");
|
||||
assertTrue(durationMs < 1000, "Posting events too slow");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConcurrentEventPostSpeed() throws InterruptedException {
|
||||
int threads = 20;
|
||||
int eventsPerThread = 5_000;
|
||||
AtomicInteger counter = new AtomicInteger(0);
|
||||
|
||||
GlobalEventBus.subscribe(PerfEvent.class, e -> counter.incrementAndGet());
|
||||
|
||||
Thread[] workers = new Thread[threads];
|
||||
|
||||
long start = System.nanoTime();
|
||||
|
||||
for (int t = 0; t < threads; t++) {
|
||||
workers[t] = new Thread(() -> {
|
||||
for (int i = 0; i < eventsPerThread; i++) {
|
||||
new EventPublisher<>(PerfEvent.class, "event-" + i).postEvent();
|
||||
}
|
||||
});
|
||||
workers[t].start();
|
||||
}
|
||||
|
||||
for (Thread worker : workers) {
|
||||
worker.join();
|
||||
}
|
||||
|
||||
long end = System.nanoTime();
|
||||
long durationMs = (end - start) / 1_000_000;
|
||||
|
||||
System.out.println("Posted " + (threads * eventsPerThread) + " events concurrently in " + durationMs + " ms");
|
||||
assertTrue(counter.get() == threads * eventsPerThread, "Some events were lost");
|
||||
assertTrue(durationMs < 5000, "Concurrent posting too slow");
|
||||
}
|
||||
}
|
||||
247
framework/src/test/eventbus/EventPublisherStressTest.java
Normal file
247
framework/src/test/eventbus/EventPublisherStressTest.java
Normal file
@@ -0,0 +1,247 @@
|
||||
package org.toop.eventbus;
|
||||
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.toop.framework.eventbus.events.EventWithUuid;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.LongAdder;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class EventPublisherStressTest {
|
||||
|
||||
/** Top-level record to ensure runtime type matches subscription */
|
||||
public record HeavyEvent(String payload, String eventId) implements EventWithUuid {
|
||||
@Override
|
||||
public java.util.Map<String, Object> result() {
|
||||
return java.util.Map.of("payload", payload, "eventId", eventId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String eventId() {
|
||||
return eventId;
|
||||
}
|
||||
}
|
||||
|
||||
public record HeavyEventSuccess(String payload, String eventId) implements EventWithUuid {
|
||||
@Override
|
||||
public java.util.Map<String, Object> result() {
|
||||
return java.util.Map.of("payload", payload, "eventId", eventId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String eventId() {
|
||||
return eventId;
|
||||
}
|
||||
}
|
||||
|
||||
private static final int THREADS = 16;
|
||||
private static final long EVENTS_PER_THREAD = 1_000_000_000;
|
||||
|
||||
@Tag("stress")
|
||||
@Test
|
||||
void extremeConcurrencySendTest_progressWithMemory() throws InterruptedException {
|
||||
LongAdder counter = new LongAdder();
|
||||
ExecutorService executor = Executors.newFixedThreadPool(THREADS);
|
||||
|
||||
BigInteger totalEvents = BigInteger.valueOf(THREADS)
|
||||
.multiply(BigInteger.valueOf(EVENTS_PER_THREAD));
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
// Monitor thread for EPS and memory
|
||||
Thread monitor = new Thread(() -> {
|
||||
long lastCount = 0;
|
||||
long lastTime = System.currentTimeMillis();
|
||||
Runtime runtime = Runtime.getRuntime();
|
||||
|
||||
while (counter.sum() < totalEvents.longValue()) {
|
||||
try { Thread.sleep(200); } catch (InterruptedException ignored) {}
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
long completed = counter.sum();
|
||||
long eventsThisPeriod = completed - lastCount;
|
||||
double eps = eventsThisPeriod / ((now - lastTime) / 1000.0);
|
||||
|
||||
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
|
||||
double usedPercent = usedMemory * 100.0 / runtime.maxMemory();
|
||||
|
||||
System.out.printf(
|
||||
"Progress: %d/%d (%.2f%%), EPS: %.0f, Memory Used: %.2f MB (%.2f%%)%n",
|
||||
completed,
|
||||
totalEvents.longValue(),
|
||||
completed * 100.0 / totalEvents.doubleValue(),
|
||||
eps,
|
||||
usedMemory / 1024.0 / 1024.0,
|
||||
usedPercent
|
||||
);
|
||||
|
||||
lastCount = completed;
|
||||
lastTime = now;
|
||||
}
|
||||
});
|
||||
monitor.setDaemon(true);
|
||||
monitor.start();
|
||||
|
||||
var listener = new EventPublisher<>(HeavyEvent.class, _ -> counter.increment());
|
||||
|
||||
// Submit events asynchronously
|
||||
for (int t = 0; t < THREADS; t++) {
|
||||
executor.submit(() -> {
|
||||
for (int i = 0; i < EVENTS_PER_THREAD; i++) {
|
||||
var _ = new EventPublisher<>(HeavyEvent.class, "payload-" + i)
|
||||
.asyncPostEvent();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
executor.shutdown();
|
||||
executor.awaitTermination(10, TimeUnit.MINUTES);
|
||||
|
||||
listener.getResult();
|
||||
|
||||
long endTime = System.currentTimeMillis();
|
||||
double durationSeconds = (endTime - startTime) / 1000.0;
|
||||
|
||||
System.out.println("Posted " + totalEvents + " events in " + durationSeconds + " seconds");
|
||||
double averageEps = totalEvents.doubleValue() / durationSeconds;
|
||||
System.out.printf("Average EPS: %.0f%n", averageEps);
|
||||
|
||||
assertEquals(totalEvents.longValue(), counter.sum());
|
||||
}
|
||||
|
||||
@Tag("stress")
|
||||
@Test
|
||||
void extremeConcurrencySendAndReturnTest_progressWithMemory() throws InterruptedException {
|
||||
LongAdder counter = new LongAdder();
|
||||
ExecutorService executor = Executors.newFixedThreadPool(THREADS);
|
||||
|
||||
BigInteger totalEvents = BigInteger.valueOf(THREADS)
|
||||
.multiply(BigInteger.valueOf(EVENTS_PER_THREAD));
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
// Monitor thread for EPS and memory
|
||||
Thread monitor = new Thread(() -> {
|
||||
long lastCount = 0;
|
||||
long lastTime = System.currentTimeMillis();
|
||||
Runtime runtime = Runtime.getRuntime();
|
||||
|
||||
while (counter.sum() < totalEvents.longValue()) {
|
||||
try { Thread.sleep(200); } catch (InterruptedException ignored) {}
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
long completed = counter.sum();
|
||||
long eventsThisPeriod = completed - lastCount;
|
||||
double eps = eventsThisPeriod / ((now - lastTime) / 1000.0);
|
||||
|
||||
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
|
||||
double usedPercent = usedMemory * 100.0 / runtime.maxMemory();
|
||||
|
||||
System.out.printf(
|
||||
"Progress: %d/%d (%.2f%%), EPS: %.0f, Memory Used: %.2f MB (%.2f%%)%n",
|
||||
completed,
|
||||
totalEvents.longValue(),
|
||||
completed * 100.0 / totalEvents.doubleValue(),
|
||||
eps,
|
||||
usedMemory / 1024.0 / 1024.0,
|
||||
usedPercent
|
||||
);
|
||||
|
||||
lastCount = completed;
|
||||
lastTime = now;
|
||||
}
|
||||
});
|
||||
monitor.setDaemon(true);
|
||||
monitor.start();
|
||||
|
||||
// Submit events asynchronously
|
||||
for (int t = 0; t < THREADS; t++) {
|
||||
executor.submit(() -> {
|
||||
for (int i = 0; i < EVENTS_PER_THREAD; i++) {
|
||||
var a = new EventPublisher<>(HeavyEvent.class, "payload-" + i)
|
||||
.onEventById(HeavyEventSuccess.class, _ -> counter.increment())
|
||||
.unsubscribeAfterSuccess()
|
||||
.asyncPostEvent();
|
||||
|
||||
new EventPublisher<>(HeavyEventSuccess.class, "payload-" + i, a.getEventId())
|
||||
.asyncPostEvent();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
executor.shutdown();
|
||||
executor.awaitTermination(10, TimeUnit.MINUTES);
|
||||
|
||||
long endTime = System.currentTimeMillis();
|
||||
double durationSeconds = (endTime - startTime) / 1000.0;
|
||||
|
||||
System.out.println("Posted " + totalEvents + " events in " + durationSeconds + " seconds");
|
||||
double averageEps = totalEvents.doubleValue() / durationSeconds;
|
||||
System.out.printf("Average EPS: %.0f%n", averageEps);
|
||||
|
||||
assertEquals(totalEvents.longValue(), counter.sum());
|
||||
}
|
||||
|
||||
|
||||
@Tag("stress")
|
||||
@Test
|
||||
void efficientExtremeConcurrencyTest() throws InterruptedException {
|
||||
final int THREADS = Runtime.getRuntime().availableProcessors();
|
||||
final int EVENTS_PER_THREAD = 5000;
|
||||
|
||||
ExecutorService executor = Executors.newFixedThreadPool(THREADS);
|
||||
ConcurrentLinkedQueue<HeavyEvent> processedEvents = new ConcurrentLinkedQueue<>();
|
||||
|
||||
long start = System.nanoTime();
|
||||
|
||||
for (int t = 0; t < THREADS; t++) {
|
||||
executor.submit(() -> {
|
||||
for (int i = 0; i < EVENTS_PER_THREAD; i++) {
|
||||
new EventPublisher<>(HeavyEvent.class, "payload-" + i)
|
||||
.onEventById(HeavyEvent.class, processedEvents::add)
|
||||
.postEvent();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
executor.shutdown();
|
||||
executor.awaitTermination(10, TimeUnit.MINUTES);
|
||||
|
||||
long end = System.nanoTime();
|
||||
double durationSeconds = (end - start) / 1_000_000_000.0;
|
||||
|
||||
BigInteger totalEvents = BigInteger.valueOf((long) THREADS).multiply(BigInteger.valueOf(EVENTS_PER_THREAD));
|
||||
double eps = totalEvents.doubleValue() / durationSeconds;
|
||||
|
||||
System.out.printf("Posted %s events in %.3f seconds%n", totalEvents, durationSeconds);
|
||||
System.out.printf("Throughput: %.0f events/sec%n", eps);
|
||||
|
||||
Runtime rt = Runtime.getRuntime();
|
||||
System.out.printf("Used memory: %.2f MB%n", (rt.totalMemory() - rt.freeMemory()) / 1024.0 / 1024.0);
|
||||
|
||||
assertEquals(totalEvents.intValue(), processedEvents.size());
|
||||
}
|
||||
|
||||
@Tag("stress")
|
||||
@Test
|
||||
void constructorCacheVsReflection() throws Throwable {
|
||||
int iterations = 1_000_000;
|
||||
long startReflect = System.nanoTime();
|
||||
for (int i = 0; i < iterations; i++) {
|
||||
HeavyEvent.class.getDeclaredConstructors()[0].newInstance("payload", "uuid-" + i);
|
||||
}
|
||||
long endReflect = System.nanoTime();
|
||||
|
||||
long startHandle = System.nanoTime();
|
||||
for (int i = 0; i < iterations; i++) {
|
||||
EventPublisher<HeavyEvent> ep = new EventPublisher<>(HeavyEvent.class, "payload-" + i);
|
||||
}
|
||||
long endHandle = System.nanoTime();
|
||||
|
||||
System.out.println("Reflection: " + (endReflect - startReflect) / 1_000_000 + " ms");
|
||||
System.out.println("MethodHandle Cache: " + (endHandle - startHandle) / 1_000_000 + " ms");
|
||||
}
|
||||
}
|
||||
126
framework/src/test/eventbus/EventPublisherTest.java
Normal file
126
framework/src/test/eventbus/EventPublisherTest.java
Normal file
@@ -0,0 +1,126 @@
|
||||
package org.toop.eventbus;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.toop.framework.eventbus.events.EventWithUuid;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class EventPublisherTest {
|
||||
|
||||
// Simple test event implementing EventWithUuid
|
||||
public record TestEvent(String name, String eventId) implements EventWithUuid {
|
||||
@Override
|
||||
public Map<String, Object> result() {
|
||||
return Map.of("name", name, "eventId", eventId);
|
||||
}
|
||||
}
|
||||
|
||||
public record TestResponseEvent(String msg, String eventId) implements EventWithUuid {
|
||||
@Override
|
||||
public Map<String, Object> result() {
|
||||
return Map.of("msg", msg, "eventId", eventId);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEventPublisherGeneratesUuid() {
|
||||
EventPublisher<TestEvent> publisher = new EventPublisher<>(TestEvent.class, "myTest");
|
||||
assertNotNull(publisher.getEventId());
|
||||
assertEquals(publisher.getEventId(), publisher.getEvent().eventId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPostEvent() {
|
||||
AtomicBoolean triggered = new AtomicBoolean(false);
|
||||
|
||||
EventPublisher<TestEvent> publisher = new EventPublisher<>(TestEvent.class, "myTest");
|
||||
publisher.onEventById(TestEvent.class, event -> triggered.set(true))
|
||||
.postEvent();
|
||||
|
||||
assertTrue(triggered.get(), "Subscriber should have been triggered by postEvent");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOnEventByIdMatchesUuid() {
|
||||
AtomicBoolean triggered = new AtomicBoolean(false);
|
||||
|
||||
EventPublisher<TestEvent> publisher1 = new EventPublisher<>(TestEvent.class, "event1");
|
||||
EventPublisher<TestEvent> publisher2 = new EventPublisher<>(TestEvent.class, "event2");
|
||||
|
||||
publisher1.onEventById(TestEvent.class, event -> triggered.set(true));
|
||||
publisher2.postEvent();
|
||||
|
||||
// Only publisher1's subscriber should trigger for its UUID
|
||||
assertFalse(triggered.get(), "Subscriber should not trigger for a different UUID");
|
||||
|
||||
publisher1.postEvent();
|
||||
assertTrue(triggered.get(), "Subscriber should trigger for matching UUID");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUnregisterAfterSuccess() {
|
||||
AtomicBoolean triggered = new AtomicBoolean(false);
|
||||
AtomicReference<Object> listenerRef = new AtomicReference<>();
|
||||
|
||||
EventPublisher<TestEvent> publisher = new EventPublisher<>(TestEvent.class, "event");
|
||||
publisher.onEventById(TestEvent.class, event -> triggered.set(true))
|
||||
.unsubscribeAfterSuccess()
|
||||
.postEvent();
|
||||
|
||||
// Subscriber should have been removed after first trigger
|
||||
assertTrue(triggered.get(), "Subscriber should trigger first time");
|
||||
|
||||
triggered.set(false);
|
||||
publisher.postEvent();
|
||||
assertFalse(triggered.get(), "Subscriber should not trigger after unregister");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testResultMapPopulated() {
|
||||
AtomicReference<Map<String, Object>> resultRef = new AtomicReference<>();
|
||||
|
||||
EventPublisher<TestEvent> publisher = new EventPublisher<>(TestEvent.class, "myName");
|
||||
publisher.onEventById(TestEvent.class, event -> resultRef.set(event.result()))
|
||||
.postEvent();
|
||||
|
||||
Map<String, Object> result = resultRef.get();
|
||||
assertNotNull(result);
|
||||
assertEquals("myName", result.get("name"));
|
||||
assertEquals(publisher.getEventId(), result.get("eventId"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultipleSubscribers() {
|
||||
AtomicBoolean firstTriggered = new AtomicBoolean(false);
|
||||
AtomicBoolean secondTriggered = new AtomicBoolean(false);
|
||||
|
||||
EventPublisher<TestEvent> publisher = new EventPublisher<>(TestEvent.class, "multi");
|
||||
|
||||
publisher.onEventById(TestEvent.class, e -> firstTriggered.set(true))
|
||||
.onEventById(TestEvent.class, e -> secondTriggered.set(true))
|
||||
.postEvent();
|
||||
|
||||
assertTrue(firstTriggered.get());
|
||||
assertTrue(secondTriggered.get());
|
||||
|
||||
publisher.onEventById(TestEvent.class, e -> firstTriggered.set(true))
|
||||
.onEventById(TestEvent.class, e -> secondTriggered.set(true))
|
||||
.asyncPostEvent();
|
||||
|
||||
assertTrue(firstTriggered.get());
|
||||
assertTrue(secondTriggered.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEventInstanceCreatedCorrectly() {
|
||||
EventPublisher<TestEvent> publisher = new EventPublisher<>(TestEvent.class, "hello");
|
||||
TestEvent event = publisher.getEvent();
|
||||
assertNotNull(event);
|
||||
assertEquals("hello", event.name());
|
||||
assertEquals(publisher.getEventId(), event.eventId());
|
||||
}
|
||||
}
|
||||
110
framework/src/test/eventbus/GlobalEventBusTest.java
Normal file
110
framework/src/test/eventbus/GlobalEventBusTest.java
Normal file
@@ -0,0 +1,110 @@
|
||||
//package org.toop.eventbus;
|
||||
//
|
||||
//import net.engio.mbassy.bus.publication.SyncAsyncPostCommand;
|
||||
//import org.junit.jupiter.api.AfterEach;
|
||||
//import org.junit.jupiter.api.Test;
|
||||
//import org.toop.eventbus.events.IEvent;
|
||||
//
|
||||
//import java.util.concurrent.atomic.AtomicBoolean;
|
||||
//import java.util.concurrent.atomic.AtomicReference;
|
||||
//
|
||||
//import static org.junit.jupiter.api.Assertions.*;
|
||||
//
|
||||
//class GlobalEventBusTest {
|
||||
//
|
||||
// // A simple test event
|
||||
// static class TestEvent implements IEvent {
|
||||
// private final String message;
|
||||
//
|
||||
// TestEvent(String message) {
|
||||
// this.message = message;
|
||||
// }
|
||||
//
|
||||
// String getMessage() {
|
||||
// return message;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// @AfterEach
|
||||
// void tearDown() {
|
||||
// // Reset to avoid leaking subscribers between tests
|
||||
// GlobalEventBus.reset();
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testSubscribeWithType() {
|
||||
// AtomicReference<String> result = new AtomicReference<>();
|
||||
//
|
||||
// GlobalEventBus.subscribe(TestEvent.class, e -> result.set(e.getMessage()));
|
||||
//
|
||||
// GlobalEventBus.post(new TestEvent("hello"));
|
||||
//
|
||||
// assertEquals("hello", result.get());
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testSubscribeWithoutType() {
|
||||
// AtomicReference<String> result = new AtomicReference<>();
|
||||
//
|
||||
// GlobalEventBus.subscribe((TestEvent e) -> result.set(e.getMessage()));
|
||||
//
|
||||
// GlobalEventBus.post(new TestEvent("world"));
|
||||
//
|
||||
// assertEquals("world", result.get());
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testUnsubscribeStopsReceivingEvents() {
|
||||
// AtomicBoolean called = new AtomicBoolean(false);
|
||||
//
|
||||
// Object listener = GlobalEventBus.subscribe(TestEvent.class, e -> called.set(true));
|
||||
//
|
||||
// // First event should trigger
|
||||
// GlobalEventBus.post(new TestEvent("first"));
|
||||
// assertTrue(called.get());
|
||||
//
|
||||
// // Reset flag
|
||||
// called.set(false);
|
||||
//
|
||||
// // Unsubscribe and post again
|
||||
// GlobalEventBus.unsubscribe(listener);
|
||||
// GlobalEventBus.post(new TestEvent("second"));
|
||||
//
|
||||
// assertFalse(called.get(), "Listener should not be called after unsubscribe");
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testResetClearsListeners() {
|
||||
// AtomicBoolean called = new AtomicBoolean(false);
|
||||
//
|
||||
// GlobalEventBus.subscribe(TestEvent.class, e -> called.set(true));
|
||||
//
|
||||
// GlobalEventBus.reset(); // should wipe subscriptions
|
||||
//
|
||||
// GlobalEventBus.post(new TestEvent("ignored"));
|
||||
//
|
||||
// assertFalse(called.get(), "Listener should not survive reset()");
|
||||
// }
|
||||
|
||||
// @Test
|
||||
// void testSetReplacesBus() {
|
||||
// MBassadorMock<IEvent> mockBus = new MBassadorMock<>();
|
||||
// GlobalEventBus.set(mockBus);
|
||||
//
|
||||
// TestEvent event = new TestEvent("test");
|
||||
// GlobalEventBus.post(event);
|
||||
//
|
||||
// assertEquals(event, mockBus.lastPosted, "Custom bus should receive the event");
|
||||
// }
|
||||
//
|
||||
// // Minimal fake MBassador for verifying set()
|
||||
// static class MBassadorMock<T extends IEvent> extends net.engio.mbassy.bus.MBassador<T> {
|
||||
// T lastPosted;
|
||||
//
|
||||
// @Override
|
||||
// public SyncAsyncPostCommand<T> post(T message) {
|
||||
// this.lastPosted = message;
|
||||
// return super.post(message);
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
Reference in New Issue
Block a user