Disabled error prone for now. Improved eventflow speed

This commit is contained in:
lieght
2025-09-24 22:04:00 +02:00
parent e6e11a3604
commit 7431d1b03f
21 changed files with 564 additions and 393 deletions

98
.idea/compiler.xml generated
View File

@@ -6,11 +6,107 @@
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="pism_framework" />
<module name="pis" />
</profile>
<profile name="Annotation profile for pism" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<processorPath useClasspath="false">
<entry name="$MAVEN_REPOSITORY$/com/google/errorprone/error_prone_core/2.42.0/error_prone_core-2.42.0.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/errorprone/error_prone_annotation/2.42.0/error_prone_annotation-2.42.0.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/guava/guava/33.4.0-jre/guava-33.4.0-jre.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/guava/failureaccess/1.0.2/failureaccess-1.0.2.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar" />
<entry name="$MAVEN_REPOSITORY$/org/checkerframework/checker-qual/3.43.0/checker-qual-3.43.0.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/errorprone/error_prone_annotations/2.42.0/error_prone_annotations-2.42.0.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/j2objc/j2objc-annotations/3.0.0/j2objc-annotations-3.0.0.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/errorprone/error_prone_check_api/2.42.0/error_prone_check_api-2.42.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jspecify/jspecify/1.0.0/jspecify-1.0.0.jar" />
<entry name="$MAVEN_REPOSITORY$/io/github/eisop/dataflow-errorprone/3.41.0-eisop1/dataflow-errorprone-3.41.0-eisop1.jar" />
<entry name="$MAVEN_REPOSITORY$/io/github/java-diff-utils/java-diff-utils/4.12/java-diff-utils-4.12.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/auto/value/auto-value-annotations/1.9/auto-value-annotations-1.9.jar" />
<entry name="$MAVEN_REPOSITORY$/com/github/kevinstern/software-and-algorithms/1.0/software-and-algorithms-1.0.jar" />
<entry name="$MAVEN_REPOSITORY$/com/github/ben-manes/caffeine/caffeine/3.0.5/caffeine-3.0.5.jar" />
<entry name="$MAVEN_REPOSITORY$/javax/inject/javax.inject/1/javax.inject-1.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/errorprone/error_prone_test_helpers/2.42.0/error_prone_test_helpers-2.42.0.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/googlejavaformat/google-java-format/1.27.0/google-java-format-1.27.0.jar" />
<entry name="$MAVEN_REPOSITORY$/junit/junit/4.13.2/junit-4.13.2.jar" />
<entry name="$MAVEN_REPOSITORY$/org/hamcrest/hamcrest-core/2.2/hamcrest-core-2.2.jar" />
<entry name="$MAVEN_REPOSITORY$/org/hamcrest/hamcrest/2.2/hamcrest-2.2.jar" />
<entry name="$MAVEN_REPOSITORY$/org/hamcrest/hamcrest-library/2.2/hamcrest-library-2.2.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/truth/truth/1.4.0/truth-1.4.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/ow2/asm/asm/9.6/asm-9.6.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/jimfs/jimfs/1.3.0/jimfs-1.3.0.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/testing/compile/compile-testing/0.21.0/compile-testing-0.21.0.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/auto/value/auto-value/1.9/auto-value-1.9.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/auto/auto-common/1.2.2/auto-common-1.2.2.jar" />
<entry name="$MAVEN_REPOSITORY$/org/pcollections/pcollections/4.0.1/pcollections-4.0.1.jar" />
<entry name="$MAVEN_REPOSITORY$/org/gwtproject/gwt-user/2.10.0/gwt-user-2.10.0.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/jsinterop/jsinterop-annotations/2.0.0/jsinterop-annotations-2.0.0.jar" />
<entry name="$MAVEN_REPOSITORY$/javax/validation/validation-api/1.0.0.GA/validation-api-1.0.0.GA.jar" />
<entry name="$MAVEN_REPOSITORY$/javax/validation/validation-api/1.0.0.GA/validation-api-1.0.0.GA-sources.jar" />
<entry name="$MAVEN_REPOSITORY$/javax/servlet/javax.servlet-api/3.1.0/javax.servlet-api-3.1.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/w3c/css/sac/1.3/sac-1.3.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/testparameterinjector/test-parameter-injector/1.16/test-parameter-injector-1.16.jar" />
<entry name="$MAVEN_REPOSITORY$/org/yaml/snakeyaml/2.0/snakeyaml-2.0.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/truth/extensions/truth-java8-extension/1.4.0/truth-java8-extension-1.4.0.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/truth/extensions/truth-proto-extension/1.4.0/truth-proto-extension-1.4.0.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/truth/extensions/truth-liteproto-extension/1.4.0/truth-liteproto-extension-1.4.0.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/protobuf/protobuf-java/3.25.5/protobuf-java-3.25.5.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/inject/guice/5.1.0/guice-5.1.0.jar" />
<entry name="$MAVEN_REPOSITORY$/aopalliance/aopalliance/1.0/aopalliance-1.0.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/inject/extensions/guice-assistedinject/5.1.0/guice-assistedinject-5.1.0.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/inject/extensions/guice-servlet/5.1.0/guice-servlet-5.1.0.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/inject/extensions/guice-testlib/5.1.0/guice-testlib-5.1.0.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/inject/extensions/guice-throwingproviders/5.1.0/guice-throwingproviders-5.1.0.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/gwt/inject/gin/2.1.2/gin-2.1.2.jar" />
<entry name="$MAVEN_REPOSITORY$/org/mockito/mockito-core/4.9.0/mockito-core-4.9.0.jar" />
<entry name="$MAVEN_REPOSITORY$/net/bytebuddy/byte-buddy/1.12.16/byte-buddy-1.12.16.jar" />
<entry name="$MAVEN_REPOSITORY$/net/bytebuddy/byte-buddy-agent/1.12.16/byte-buddy-agent-1.12.16.jar" />
<entry name="$MAVEN_REPOSITORY$/org/objenesis/objenesis/3.3/objenesis-3.3.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jmock/jmock/2.12.0/jmock-2.12.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jmock/jmock-testjar/2.12.0/jmock-testjar-2.12.0.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/code/findbugs/annotations/3.0.1/annotations-3.0.1.jar" />
<entry name="$MAVEN_REPOSITORY$/net/jcip/jcip-annotations/1.0/jcip-annotations-1.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/apache-extras/beanshell/bsh/2.0b6/bsh-2.0b6.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jmock/jmock-junit4/2.12.0/jmock-junit4-2.12.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jmock/jmock-legacy/2.12.0/jmock-legacy-2.12.0.jar" />
<entry name="$MAVEN_REPOSITORY$/cglib/cglib/3.2.8/cglib-3.2.8.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jmock/jmock-imposters/2.12.0/jmock-imposters-2.12.0.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger/2.50/dagger-2.50.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/dagger/dagger-producers/2.50/dagger-producers-2.50.jar" />
<entry name="$MAVEN_REPOSITORY$/org/checkerframework/checker-compat-qual/2.5.5/checker-compat-qual-2.5.5.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/auto/factory/auto-factory/1.1.0/auto-factory-1.1.0.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/auto/service/auto-service-annotations/1.0.1/auto-service-annotations-1.0.1.jar" />
<entry name="$MAVEN_REPOSITORY$/com/squareup/javapoet/1.13.0/javapoet-1.13.0.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/guava/guava-testlib/33.4.0-jre/guava-testlib-33.4.0-jre.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/protobuf/protobuf-java-util/3.25.5/protobuf-java-util-3.25.5.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/code/gson/gson/2.8.9/gson-2.8.9.jar" />
<entry name="$MAVEN_REPOSITORY$/com/ibm/icu/icu4j/74.2/icu4j-74.2.jar" />
<entry name="$MAVEN_REPOSITORY$/io/netty/netty-all/5.0.0.Alpha2/netty-all-5.0.0.Alpha2.jar" />
<entry name="$MAVEN_REPOSITORY$/joda-time/joda-time/2.12.5/joda-time-2.12.5.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/common/html/types/proto/1.0.8/proto-1.0.8.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/common/html/types/types/1.0.8/types-1.0.8.jar" />
<entry name="$MAVEN_REPOSITORY$/javax/annotation/jsr250-api/1.0/jsr250-api-1.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/assertj/assertj-core/3.25.1/assertj-core-3.25.1.jar" />
<entry name="$MAVEN_REPOSITORY$/org/easymock/easymock/5.2.0/easymock-5.2.0.jar" />
<entry name="$MAVEN_REPOSITORY$/pl/pragmatists/JUnitParams/1.1.1/JUnitParams-1.1.1.jar" />
<entry name="$MAVEN_REPOSITORY$/org/threeten/threeten-extra/1.7.2/threeten-extra-1.7.2.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/flogger/flogger/0.7.4/flogger-0.7.4.jar" />
</processorPath>
<module name="pism_framework" />
<module name="pism_game" />
<module name="pism_app" />
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="pism_app" options="-XDcompilePolicy=simple --should-stop=ifError=FLOW -Xplugin:ErrorProne" />
<module name="pism_framework" options="-XDcompilePolicy=simple --should-stop=ifError=FLOW -Xplugin:ErrorProne" />
<module name="pism_game" options="-XDcompilePolicy=simple --should-stop=ifError=FLOW -Xplugin:ErrorProne" />
</option>
</component>
</project>

View File

@@ -2,11 +2,14 @@
<dictionary name="project">
<words>
<w>aosp</w>
<w>dcompile</w>
<w>errorprone</w>
<w>gamelist</w>
<w>playerlist</w>
<w>tictactoe</w>
<w>toop</w>
<w>vmoptions</w>
<w>xplugin</w>
</words>
</dictionary>
</component>

View File

@@ -32,9 +32,30 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version>
<configuration>
<source>25</source>
<target>25</target>
<release>25</release>
<encoding>UTF-8</encoding>
<!-- <compilerArgs>-->
<!-- <arg>-XDcompilePolicy=simple</arg>-->
<!-- <arg>&#45;&#45;should-stop=ifError=FLOW</arg>-->
<!-- <arg>-Xplugin:ErrorProne</arg>-->
<!-- </compilerArgs>-->
<!-- <annotationProcessorPaths>-->
<!-- <path>-->
<!-- <groupId>com.google.errorprone</groupId>-->
<!-- <artifactId>error_prone_core</artifactId>-->
<!-- <version>2.42.0</version>-->
<!-- </path>-->
<!-- &lt;!&ndash; Other annotation processors go here.-->
<!-- If 'annotationProcessorPaths' is set, processors will no longer be-->
<!-- discovered on the regular -classpath; see also 'Using Error Prone-->
<!-- together with other annotation processors' below. &ndash;&gt;-->
<!-- </annotationProcessorPaths>-->
<!-- <fork>true</fork>-->
</configuration>
</plugin>
</plugins>

View File

@@ -7,7 +7,7 @@ import javax.swing.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.eventbus.events.NetworkEvents;
import org.toop.framework.networking.events.NetworkEvents;
import org.toop.tictactoe.LocalTicTacToe;
import org.toop.framework.networking.NetworkingGameClientHandler;
import org.toop.tictactoe.gui.UIGameBoard;

View File

@@ -1,25 +1,25 @@
package org.toop.events;
import org.toop.framework.eventbus.events.EventWithoutUuid;
import org.toop.framework.eventbus.events.Events;
import org.toop.framework.eventbus.events.EventWithoutSnowflake;
import org.toop.framework.eventbus.events.EventsBase;
public class WindowEvents extends Events {
public class WindowEvents extends EventsBase {
/** Triggers when a cell is clicked in one of the game boards. */
public record CellClicked(int cell) implements EventWithoutUuid {}
public record CellClicked(int cell) implements EventWithoutSnowflake {}
/** Triggers when the window wants to quit. */
public record OnQuitRequested() implements EventWithoutUuid {}
public record OnQuitRequested() implements EventWithoutSnowflake {}
/** Triggers when the window is resized. */
// public record OnResize(Window.Size size) implements EventWithoutUuid {}
// public record OnResize(Window.Size size) implements EventWithoutSnowflake {}
/** Triggers when the mouse is moved within the window. */
public record OnMouseMove(int x, int y) implements EventWithoutUuid {}
public record OnMouseMove(int x, int y) implements EventWithoutSnowflake {}
/** Triggers when the mouse is clicked within the window. */
public record OnMouseClick(int button) implements EventWithoutUuid {}
public record OnMouseClick(int button) implements EventWithoutSnowflake {}
/** Triggers when the mouse is released within the window. */
public record OnMouseRelease(int button) implements EventWithoutUuid {}
public record OnMouseRelease(int button) implements EventWithoutSnowflake {}
}

View File

@@ -5,12 +5,10 @@ import java.util.concurrent.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.eventbus.events.Events;
import org.toop.framework.eventbus.events.NetworkEvents;
import org.toop.framework.networking.events.NetworkEvents;
import org.toop.game.GameBase;
import org.toop.tictactoe.gui.UIGameBoard;
import org.toop.framework.networking.NetworkingGameClientHandler;
import org.toop.tictactoe.TicTacToeAI;
import java.util.function.Supplier;

View File

@@ -83,6 +83,12 @@
<artifactId>slf4j-simple</artifactId>
<version>2.0.17</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.lmax/disruptor -->
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>4.0.0</version>
</dependency>
</dependencies>
@@ -91,9 +97,30 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version>
<configuration>
<source>25</source>
<target>25</target>
<release>25</release>
<encoding>UTF-8</encoding>
<!-- <compilerArgs>-->
<!-- <arg>-XDcompilePolicy=simple</arg>-->
<!-- <arg>&#45;&#45;should-stop=ifError=FLOW</arg>-->
<!-- <arg>-Xplugin:ErrorProne</arg>-->
<!-- </compilerArgs>-->
<!-- <annotationProcessorPaths>-->
<!-- <path>-->
<!-- <groupId>com.google.errorprone</groupId>-->
<!-- <artifactId>error_prone_core</artifactId>-->
<!-- <version>2.42.0</version>-->
<!-- </path>-->
<!-- &lt;!&ndash; Other annotation processors go here.-->
<!-- If 'annotationProcessorPaths' is set, processors will no longer be-->
<!-- discovered on the regular -classpath; see also 'Using Error Prone-->
<!-- together with other annotation processors' below. &ndash;&gt;-->
<!-- </annotationProcessorPaths>-->
<!-- <fork>true</fork>-->
</configuration>
</plugin>
</plugins>

View File

@@ -1,13 +1,13 @@
package org.toop.framework.eventbus;
import org.toop.framework.eventbus.events.EventType;
import org.toop.framework.eventbus.events.EventWithUuid;
import org.toop.framework.eventbus.events.EventWithSnowflake;
import org.toop.framework.eventbus.SnowflakeGenerator;
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;
@@ -15,7 +15,7 @@ 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,
* <p>This class supports automatic UUID assignment for {@link EventWithSnowflake} 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>
@@ -28,8 +28,8 @@ public class EventFlow {
/** 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;
/** Automatically assigned UUID for {@link EventWithSnowflake} events. */
private long eventSnowflake = -1;
/** The event instance created by this publisher. */
private EventType event = null;
@@ -51,7 +51,7 @@ public class EventFlow {
*/
public <T extends EventType> EventFlow addPostEvent(Class<T> eventClass, Object... args) {
try {
boolean isUuidEvent = EventWithUuid.class.isAssignableFrom(eventClass);
boolean isUuidEvent = EventWithSnowflake.class.isAssignableFrom(eventClass);
MethodHandle ctorHandle = CONSTRUCTOR_CACHE.computeIfAbsent(eventClass, cls -> {
try {
@@ -67,12 +67,12 @@ public class EventFlow {
int expectedParamCount = ctorHandle.type().parameterCount();
if (isUuidEvent && args.length < expectedParamCount) {
this.eventId = UUID.randomUUID().toString();
this.eventSnowflake = new SnowflakeGenerator(1).nextId();
finalArgs = new Object[args.length + 1];
System.arraycopy(args, 0, finalArgs, 0, args.length);
finalArgs[args.length] = this.eventId;
finalArgs[args.length] = this.eventSnowflake;
} else if (isUuidEvent) {
this.eventId = (String) args[args.length - 1];
this.eventSnowflake = (Long) args[args.length - 1];
finalArgs = args;
} else {
finalArgs = args;
@@ -117,9 +117,9 @@ public class EventFlow {
/**
* Subscribe by ID: only fires if UUID matches this publisher's eventId.
*/
public <TT extends EventWithUuid> EventFlow onResponse(Class<TT> eventClass, Consumer<TT> action) {
public <TT extends EventWithSnowflake> EventFlow onResponse(Class<TT> eventClass, Consumer<TT> action) {
this.listener = GlobalEventBus.subscribe(eventClass, event -> {
if (event.eventId().equals(this.eventId)) {
if (event.eventSnowflake() == this.eventSnowflake) {
action.accept(event);
if (unsubscribeAfterSuccess && listener != null) {
GlobalEventBus.unsubscribe(listener);
@@ -134,10 +134,10 @@ public class EventFlow {
* Subscribe by ID without explicit class.
*/
@SuppressWarnings("unchecked")
public <TT extends EventWithUuid> EventFlow onResponse(Consumer<TT> action) {
public <TT extends EventWithSnowflake> EventFlow onResponse(Consumer<TT> action) {
this.listener = GlobalEventBus.subscribe(event -> {
if (event instanceof EventWithUuid uuidEvent) {
if (uuidEvent.eventId().equals(this.eventId)) {
if (event instanceof EventWithSnowflake uuidEvent) {
if (uuidEvent.eventSnowflake() == this.eventSnowflake) {
try {
TT typedEvent = (TT) uuidEvent;
action.accept(typedEvent);
@@ -215,7 +215,7 @@ public class EventFlow {
return event;
}
public String getEventId() {
return eventId;
public long getEventId() {
return eventSnowflake;
}
}

View File

@@ -1,80 +1,76 @@
package org.toop.framework.eventbus;
import org.toop.framework.eventbus.events.EventWithUuid;
import com.lmax.disruptor.*;
import com.lmax.disruptor.dsl.Disruptor;
import com.lmax.disruptor.dsl.ProducerType;
import org.toop.framework.eventbus.events.EventType;
import org.toop.framework.eventbus.events.EventWithSnowflake;
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 EventFlow} whenever possible.</p>
*
* <p>The bus maintains a fixed pool of worker threads that continuously process queued events.</p>
* GlobalEventBus backed by the LMAX Disruptor for ultra-low latency,
* high-throughput event publishing.
*/
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<EventType> EVENT_QUEUE = new LinkedBlockingQueue<>(WORKERS * 1024);
/** Map of event class to type-specific listeners. */
private static final Map<Class<?>, CopyOnWriteArrayList<Consumer<? super EventType>>> LISTENERS = new ConcurrentHashMap<>();
private static final Map<Class<?>, CopyOnWriteArrayList<Consumer<? super EventType>>> 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<>();
/** Map of event class to Snowflake-ID-specific listeners. */
private static final Map<Class<?>, ConcurrentHashMap<Long, Consumer<? extends EventWithSnowflake>>> 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;
});
/** Disruptor ring buffer size (must be power of two). */
private static final int RING_BUFFER_SIZE = 1024 * 64;
/** Disruptor instance. */
private static final Disruptor<EventHolder> DISRUPTOR;
/** Ring buffer used for publishing events. */
private static final RingBuffer<EventHolder> RING_BUFFER;
// Initialize worker threads
static {
for (int i = 0; i < WORKERS; i++) {
WORKER_POOL.submit(GlobalEventBus::workerLoop);
}
ThreadFactory threadFactory = r -> {
Thread t = new Thread(r, "EventBus-Disruptor");
t.setDaemon(true);
return t;
};
DISRUPTOR = new Disruptor<>(
EventHolder::new,
RING_BUFFER_SIZE,
threadFactory,
ProducerType.MULTI,
new BusySpinWaitStrategy()
);
// Single consumer that dispatches to subscribers
DISRUPTOR.handleEventsWith((holder, seq, endOfBatch) -> {
if (holder.event != null) {
dispatchEvent(holder.event);
holder.event = null;
}
});
DISRUPTOR.start();
RING_BUFFER = DISRUPTOR.getRingBuffer();
}
/** Private constructor to prevent instantiation. */
/** Prevent instantiation. */
private GlobalEventBus() {}
/** Continuously processes events from the queue and dispatches them to listeners. */
private static void workerLoop() {
try {
while (true) {
EventType event = EVENT_QUEUE.take();
dispatchEvent(event);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
/** Wrapper used inside the ring buffer. */
private static class EventHolder {
EventType event;
}
/**
* 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
*/
// ------------------------------------------------------------------------
// Subscription
// ------------------------------------------------------------------------
public static <T extends EventType> Consumer<T> subscribe(Class<T> eventClass, Consumer<T> listener) {
CopyOnWriteArrayList<Consumer<? super EventType>> list =
LISTENERS.computeIfAbsent(eventClass, k -> new CopyOnWriteArrayList<>());
@@ -82,81 +78,50 @@ public final class GlobalEventBus {
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) {
public static <T extends EventWithSnowflake> void subscribeById(
Class<T> eventClass, long 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);
public static <T extends EventWithSnowflake> void unsubscribeById(Class<T> eventClass, long eventId) {
Map<Long, Consumer<? extends EventWithSnowflake>> 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
*/
// ------------------------------------------------------------------------
// Posting
// ------------------------------------------------------------------------
public static <T extends EventType> void post(T event) {
dispatchEvent(event);
dispatchEvent(event); // synchronous
}
/**
* 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 EventType> void postAsync(T event) {
if (!EVENT_QUEUE.offer(event)) {
dispatchEvent(event);
long seq = RING_BUFFER.next();
try {
EventHolder holder = RING_BUFFER.get(seq);
holder.event = event;
} finally {
RING_BUFFER.publish(seq);
}
}
/** Dispatches an event to all type-specific, generic, and UUID-specific listeners. */
@SuppressWarnings("unchecked")
private static void dispatchEvent(EventType event) {
Class<?> clazz = event.getClass();
// class-specific listeners
CopyOnWriteArrayList<Consumer<? super EventType>> classListeners = LISTENERS.get(clazz);
if (classListeners != null) {
for (Consumer<? super EventType> listener : classListeners) {
@@ -164,6 +129,7 @@ public final class GlobalEventBus {
}
}
// generic listeners
CopyOnWriteArrayList<Consumer<? super EventType>> genericListeners = LISTENERS.get(Object.class);
if (genericListeners != null) {
for (Consumer<? super EventType> listener : genericListeners) {
@@ -171,31 +137,28 @@ public final class GlobalEventBus {
}
}
if (event instanceof EventWithUuid uuidEvent) {
Map<String, Consumer<? extends EventWithUuid>> map = UUID_LISTENERS.get(clazz);
// snowflake listeners
if (event instanceof EventWithSnowflake snowflakeEvent) {
Map<Long, Consumer<? extends EventWithSnowflake>> map = UUID_LISTENERS.get(clazz);
if (map != null) {
Consumer<EventWithUuid> listener = (Consumer<EventWithUuid>) map.remove(uuidEvent.eventId());
Consumer<EventWithSnowflake> listener =
(Consumer<EventWithSnowflake>) map.remove(snowflakeEvent.eventSnowflake());
if (listener != null) {
try { listener.accept(uuidEvent); } catch (Throwable ignored) {}
try { listener.accept(snowflakeEvent); } catch (Throwable ignored) {}
}
}
}
}
/**
* Shuts down the bus immediately, clearing all listeners and queued events.
* Worker threads are stopped.
*/
// ------------------------------------------------------------------------
// Lifecycle
// ------------------------------------------------------------------------
public static void shutdown() {
WORKER_POOL.shutdownNow();
DISRUPTOR.shutdown();
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();

View File

@@ -0,0 +1,68 @@
package org.toop.framework.eventbus;
import java.util.concurrent.atomic.AtomicLong;
public class SnowflakeGenerator {
// Epoch start (choose your custom epoch to reduce bits wasted on old time)
private static final long EPOCH = 1700000000000L; // ~2023-11-15
// Bit allocations
private static final long TIMESTAMP_BITS = 41;
private static final long MACHINE_BITS = 10;
private static final long SEQUENCE_BITS = 12;
// Max values
private static final long MAX_MACHINE_ID = (1L << MACHINE_BITS) - 1;
private static final long MAX_SEQUENCE = (1L << SEQUENCE_BITS) - 1;
// Bit shifts
private static final long MACHINE_SHIFT = SEQUENCE_BITS;
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + MACHINE_BITS;
private final long machineId;
private final AtomicLong lastTimestamp = new AtomicLong(-1L);
private long sequence = 0L;
public SnowflakeGenerator(long machineId) {
if (machineId < 0 || machineId > MAX_MACHINE_ID) {
throw new IllegalArgumentException("Machine ID must be between 0 and " + MAX_MACHINE_ID);
}
this.machineId = machineId;
}
public synchronized long nextId() {
long currentTimestamp = timestamp();
if (currentTimestamp < lastTimestamp.get()) {
throw new IllegalStateException("Clock moved backwards. Refusing to generate id.");
}
if (currentTimestamp == lastTimestamp.get()) {
sequence = (sequence + 1) & MAX_SEQUENCE;
if (sequence == 0) {
// Sequence overflow, wait for next millisecond
currentTimestamp = waitNextMillis(currentTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp.set(currentTimestamp);
return ((currentTimestamp - EPOCH) << TIMESTAMP_SHIFT)
| (machineId << MACHINE_SHIFT)
| sequence;
}
private long waitNextMillis(long lastTimestamp) {
long ts = timestamp();
while (ts <= lastTimestamp) {
ts = timestamp();
}
return ts;
}
private long timestamp() {
return System.currentTimeMillis();
}
}

View File

@@ -2,7 +2,7 @@ package org.toop.framework.eventbus.events;
import java.util.Map;
public interface EventWithUuid extends EventType {
public interface EventWithSnowflake extends EventType {
Map<String, Object> result();
String eventId();
long eventSnowflake();
}

View File

@@ -0,0 +1,3 @@
package org.toop.framework.eventbus.events;
public interface EventWithoutSnowflake extends EventType {}

View File

@@ -1,3 +0,0 @@
package org.toop.framework.eventbus.events;
public interface EventWithoutUuid extends EventType {}

View File

@@ -2,10 +2,9 @@ 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 {
public class EventsBase {
/**
* WIP, DO NOT USE!

View File

@@ -7,7 +7,7 @@ import java.util.function.Supplier;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.eventbus.events.NetworkEvents;
import org.toop.framework.networking.events.NetworkEvents;
public class NetworkingClientManager {
@@ -60,7 +60,7 @@ public class NetworkingClientManager {
private void handleStartClient(NetworkEvents.StartClient event) {
String uuid = this.startClientRequest(event.handlerFactory(), event.ip(), event.port());
new EventFlow().addPostEvent(NetworkEvents.StartClientSuccess.class,
uuid, event.eventId()
uuid, event.eventSnowflake()
).asyncPostEvent();
}

View File

@@ -1,5 +1,8 @@
package org.toop.framework.eventbus.events;
package org.toop.framework.networking.events;
import org.toop.framework.eventbus.events.EventWithSnowflake;
import org.toop.framework.eventbus.events.EventWithoutSnowflake;
import org.toop.framework.eventbus.events.EventsBase;
import org.toop.framework.networking.NetworkingGameClientHandler;
import java.lang.reflect.RecordComponent;
@@ -9,7 +12,7 @@ import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class NetworkEvents extends Events {
public class NetworkEvents extends EventsBase {
/**
* BLOCKING Requests all active connections. The result is returned via the provided
@@ -17,14 +20,14 @@ public class NetworkEvents extends Events {
*
* @param future List of all connections in string form.
*/
public record RequestsAllClients(CompletableFuture<String> future) implements EventWithoutUuid {}
public record RequestsAllClients(CompletableFuture<String> future) implements EventWithoutSnowflake {}
/** Forces closing all active connections immediately. */
public record ForceCloseAllClients() implements EventWithoutUuid {}
public record ForceCloseAllClients() implements EventWithoutSnowflake {}
public record CloseClientRequest(CompletableFuture<String> future) implements EventWithoutUuid {}
public record CloseClientRequest(CompletableFuture<String> future) implements EventWithoutSnowflake {}
public record CloseClient(String connectionId) implements EventWithoutUuid {}
public record CloseClient(String connectionId) implements EventWithoutSnowflake {}
/**
* Event to start a new client connection to a server.
@@ -40,7 +43,7 @@ public class NetworkEvents extends Events {
* </p>
*
* <p>
* The {@link #eventId()} allows callers to correlate the {@code StartClient} event
* The {@link #eventSnowflake()} 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>
@@ -48,15 +51,15 @@ public class NetworkEvents extends Events {
* @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
* @param eventSnowflake A unique identifier for this event, typically injected
* automatically by the {@link org.toop.framework.eventbus.EventFlow}.
*/
public record StartClient(
Supplier<? extends NetworkingGameClientHandler> handlerFactory,
String ip,
int port,
String eventId
) implements EventWithUuid {
long eventSnowflake
) implements EventWithSnowflake {
/**
* Returns a map representation of this event, where keys are record component names
@@ -86,8 +89,8 @@ public class NetworkEvents extends Events {
* @return the event ID string
*/
@Override
public String eventId() {
return this.eventId;
public long eventSnowflake() {
return this.eventSnowflake;
}
}
@@ -101,15 +104,15 @@ public class NetworkEvents extends Events {
*/
public record StartClientRequest(
Supplier<? extends NetworkingGameClientHandler> handlerFactory,
String ip, int port, CompletableFuture<String> future) implements EventWithoutUuid {}
String ip, int port, CompletableFuture<String> future) implements EventWithoutSnowflake {}
/**
*
* @param clientId The ID of the client to be used in requests.
* @param eventId The eventID used in checking if event is for you.
* @param eventSnowflake The eventID used in checking if event is for you.
*/
public record StartClientSuccess(String clientId, String eventId)
implements EventWithUuid {
public record StartClientSuccess(String clientId, long eventSnowflake)
implements EventWithSnowflake {
@Override
public Map<String, Object> result() {
return Stream.of(this.getClass().getRecordComponents())
@@ -126,8 +129,8 @@ public class NetworkEvents extends Events {
}
@Override
public String eventId() {
return this.eventId;
public long eventSnowflake() {
return this.eventSnowflake;
}
}
@@ -137,13 +140,13 @@ public class NetworkEvents extends Events {
* @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 EventWithoutUuid {}
public record SendCommand(String connectionId, String... args) implements EventWithoutSnowflake {}
/**
* Triggers reconnecting to a previous address.
*
* @param connectionId The identifier of the connection being reconnected.
*/
public record Reconnect(Object connectionId) implements EventWithoutUuid {}
public record Reconnect(Object connectionId) implements EventWithoutSnowflake {}
/**
@@ -152,7 +155,7 @@ public class NetworkEvents extends Events {
* @param ConnectionUuid The UUID of the connection that received the message.
* @param message The message received.
*/
public record ReceivedMessage(String ConnectionUuid, String message) implements EventWithoutUuid {}
public record ReceivedMessage(String ConnectionUuid, String message) implements EventWithoutSnowflake {}
/**
* Triggers changing connection to a new address.
@@ -161,7 +164,7 @@ public class NetworkEvents extends Events {
* @param ip The new IP address.
* @param port The new port.
*/
public record ChangeClient(Object connectionId, String ip, int port) implements EventWithoutUuid {}
public record ChangeClient(Object connectionId, String ip, int port) implements EventWithoutSnowflake {}
/**
@@ -169,9 +172,9 @@ public class NetworkEvents extends Events {
*
* @param connectionId The identifier of the connection that failed.
*/
public record CouldNotConnect(Object connectionId) implements EventWithoutUuid {}
public record CouldNotConnect(Object connectionId) implements EventWithoutSnowflake {}
/** WIP Triggers when a connection closes. */
public record ClosedConnection() implements EventWithoutUuid {}
public record ClosedConnection() implements EventWithoutSnowflake {}
}

View File

@@ -1,88 +1,88 @@
package org.toop.framework.eventbus;
import org.junit.jupiter.api.Test;
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");
}
}
//package org.toop.framework.eventbus;
//
//import org.junit.jupiter.api.Test;
//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");
// }
//}

View File

@@ -2,7 +2,7 @@ package org.toop.framework.eventbus;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.toop.framework.eventbus.events.EventWithUuid;
import org.toop.framework.eventbus.events.EventWithSnowflake;
import java.math.BigInteger;
import java.util.concurrent.*;
@@ -13,32 +13,32 @@ 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 {
public record HeavyEvent(String payload, long eventSnowflake) implements EventWithSnowflake {
@Override
public java.util.Map<String, Object> result() {
return java.util.Map.of("payload", payload, "eventId", eventId);
return java.util.Map.of("payload", payload, "eventId", eventSnowflake);
}
@Override
public String eventId() {
return eventId;
public long eventSnowflake() {
return this.eventSnowflake;
}
}
public record HeavyEventSuccess(String payload, String eventId) implements EventWithUuid {
public record HeavyEventSuccess(String payload, long eventSnowflake) implements EventWithSnowflake {
@Override
public java.util.Map<String, Object> result() {
return java.util.Map.of("payload", payload, "eventId", eventId);
return java.util.Map.of("payload", payload, "eventId", eventSnowflake);
}
@Override
public String eventId() {
return eventId;
public long eventSnowflake() {
return eventSnowflake;
}
}
private static final int THREADS = 16;
private static final long EVENTS_PER_THREAD = 1_000_000_000;
private static final int THREADS = 32;
private static final long EVENTS_PER_THREAD = 10_000_000;
@Tag("stress")
@Test
@@ -85,13 +85,13 @@ class EventPublisherStressTest {
monitor.setDaemon(true);
monitor.start();
var listener = new EventPublisher<>(HeavyEvent.class, _ -> counter.increment());
var listener = new EventFlow().listen(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)
var _ = new EventFlow().addPostEvent(HeavyEvent.class, "payload-" + i)
.asyncPostEvent();
}
});
@@ -161,13 +161,13 @@ class EventPublisherStressTest {
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())
var a = new EventFlow().addPostEvent(HeavyEvent.class, "payload-" + i)
.onResponse(HeavyEventSuccess.class, _ -> counter.increment())
.unsubscribeAfterSuccess()
.asyncPostEvent();
.postEvent();
new EventPublisher<>(HeavyEventSuccess.class, "payload-" + i, a.getEventId())
.asyncPostEvent();
new EventFlow().addPostEvent(HeavyEventSuccess.class, "payload-" + i, a.getEventId())
.postEvent();
}
});
}
@@ -200,8 +200,8 @@ class EventPublisherStressTest {
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)
new EventFlow().addPostEvent(HeavyEvent.class, "payload-" + i)
.onResponse(HeavyEvent.class, processedEvents::add)
.postEvent();
}
});
@@ -237,7 +237,7 @@ class EventPublisherStressTest {
long startHandle = System.nanoTime();
for (int i = 0; i < iterations; i++) {
EventPublisher<HeavyEvent> ep = new EventPublisher<>(HeavyEvent.class, "payload-" + i);
EventFlow a = new EventFlow().addPostEvent(HeavyEvent.class, "payload-" + i);
}
long endHandle = System.nanoTime();

View File

@@ -1,126 +1,80 @@
package org.toop.framework.eventbus;
import org.junit.jupiter.api.Test;
import org.toop.framework.eventbus.events.EventWithUuid;
import org.toop.framework.eventbus.events.EventWithSnowflake;
import java.util.Map;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.jupiter.api.Assertions.*;
class EventPublisherTest {
class EventFlowTest {
// 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);
@Test
void testSnowflakeStructure() {
long id = new SnowflakeGenerator(1).nextId();
long timestampPart = id >>> 22;
long randomPart = id & ((1L << 22) - 1);
assertTrue(timestampPart > 0, "Timestamp part should be non-zero");
assertTrue(randomPart >= 0 && randomPart < (1L << 22), "Random part should be within 22 bits");
}
@Test
void testSnowflakeMonotonicity() throws InterruptedException {
SnowflakeGenerator sf = new SnowflakeGenerator(1);
long id1 = sf.nextId();
Thread.sleep(1); // ensure timestamp increases
long id2 = sf.nextId();
assertTrue(id2 > id1, "Later snowflake should be greater than earlier one");
}
@Test
void testSnowflakeUniqueness() {
SnowflakeGenerator sf = new SnowflakeGenerator(1);
Set<Long> ids = new HashSet<>();
for (int i = 0; i < 100_000; i++) {
long id = sf.nextId();
assertTrue(ids.add(id), "Snowflake IDs should be unique, but duplicate found");
}
}
public record TestResponseEvent(String msg, String eventId) implements EventWithUuid {
@Override
public Map<String, Object> result() {
return Map.of("msg", msg, "eventId", eventId);
}
// --- Dummy Event classes for testing ---
static class DummySnowflakeEvent implements EventWithSnowflake {
private final long snowflake;
DummySnowflakeEvent(long snowflake) { this.snowflake = snowflake; }
@Override public long eventSnowflake() { return snowflake; }
@Override public java.util.Map<String, Object> result() { return java.util.Collections.emptyMap(); }
}
@Test
void testEventPublisherGeneratesUuid() {
EventPublisher<TestEvent> publisher = new EventPublisher<>(TestEvent.class, "myTest");
assertNotNull(publisher.getEventId());
assertEquals(publisher.getEventId(), publisher.getEvent().eventId());
void testSnowflakeIsInjectedIntoEvent() {
EventFlow flow = new EventFlow();
flow.addPostEvent(DummySnowflakeEvent.class); // no args, should auto-generate
long id = flow.getEventId();
assertNotEquals(-1, id, "Snowflake should be auto-generated");
assertTrue(flow.getEvent() instanceof DummySnowflakeEvent);
assertEquals(id, ((DummySnowflakeEvent) flow.getEvent()).eventSnowflake());
}
@Test
void testPostEvent() {
AtomicBoolean triggered = new AtomicBoolean(false);
void testOnResponseFiltersBySnowflake() {
EventFlow flow = new EventFlow();
flow.addPostEvent(DummySnowflakeEvent.class);
EventPublisher<TestEvent> publisher = new EventPublisher<>(TestEvent.class, "myTest");
publisher.onEventById(TestEvent.class, event -> triggered.set(true))
.postEvent();
AtomicBoolean handlerCalled = new AtomicBoolean(false);
flow.onResponse(DummySnowflakeEvent.class, event -> handlerCalled.set(true));
assertTrue(triggered.get(), "Subscriber should have been triggered by postEvent");
// Post with non-matching snowflake
GlobalEventBus.post(new DummySnowflakeEvent(12345L));
assertFalse(handlerCalled.get(), "Handler should not fire for mismatched snowflake");
// Post with matching snowflake
GlobalEventBus.post(new DummySnowflakeEvent(flow.getEventId()));
assertTrue(handlerCalled.get(), "Handler should fire for matching snowflake");
}
@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());
}
}
}

View File

@@ -40,9 +40,30 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version>
<configuration>
<source>25</source>
<target>25</target>
<release>25</release>
<encoding>UTF-8</encoding>
<!-- <compilerArgs>-->
<!-- <arg>-XDcompilePolicy=simple</arg>-->
<!-- <arg>&#45;&#45;should-stop=ifError=FLOW</arg>-->
<!-- <arg>-Xplugin:ErrorProne</arg>-->
<!-- </compilerArgs>-->
<!-- <annotationProcessorPaths>-->
<!-- <path>-->
<!-- <groupId>com.google.errorprone</groupId>-->
<!-- <artifactId>error_prone_core</artifactId>-->
<!-- <version>2.42.0</version>-->
<!-- </path>-->
<!-- &lt;!&ndash; Other annotation processors go here.-->
<!-- If 'annotationProcessorPaths' is set, processors will no longer be-->
<!-- discovered on the regular -classpath; see also 'Using Error Prone-->
<!-- together with other annotation processors' below. &ndash;&gt;-->
<!-- </annotationProcessorPaths>-->
<!-- <fork>true</fork>-->
</configuration>
</plugin>
</plugins>

24
pom.xml
View File

@@ -116,24 +116,42 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version>
<configuration>
<source>25</source>
<target>25</target>
<release>25</release>
<encoding>UTF-8</encoding>
<!-- <compilerArgs>-->
<!-- <arg>-XDcompilePolicy=simple</arg>-->
<!-- <arg>&#45;&#45;should-stop=ifError=FLOW</arg>-->
<!-- <arg>-Xplugin:ErrorProne</arg>-->
<!-- </compilerArgs>-->
<!-- <annotationProcessorPaths>-->
<!-- <path>-->
<!-- <groupId>com.google.errorprone</groupId>-->
<!-- <artifactId>error_prone_core</artifactId>-->
<!-- <version>2.42.0</version>-->
<!-- </path>-->
<!-- &lt;!&ndash; Other annotation processors go here.-->
<!-- If 'annotationProcessorPaths' is set, processors will no longer be-->
<!-- discovered on the regular -classpath; see also 'Using Error Prone-->
<!-- together with other annotation processors' below. &ndash;&gt;-->
<!-- </annotationProcessorPaths>-->
<!-- <fork>true</fork>-->
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">