refactor(EventExecutor, ModuleExecutorImpl): replace structured concurrency with virtual threads to not rely on preview features
This commit is contained in:
@@ -202,7 +202,7 @@ This automatic cleanup ensures that modules can be safely loaded, activated, dea
|
|||||||
#### Purpose and Overview
|
#### Purpose and Overview
|
||||||
A lightweight, type-safe event bus that enables decoupled communication between game components. Components can register event handlers for specific event types and fire events that will be dispatched to all registered consumers. This pattern eliminates tight coupling between modules and simplifies complex interactions.
|
A lightweight, type-safe event bus that enables decoupled communication between game components. Components can register event handlers for specific event types and fire events that will be dispatched to all registered consumers. This pattern eliminates tight coupling between modules and simplifies complex interactions.
|
||||||
|
|
||||||
Events are processed asynchronously on a dedicated event processor thread using a blocking queue. When multiple handlers are registered for the same event type, they are executed concurrently using structured concurrency (virtual threads via `StructuredTaskScope`), providing excellent scalability for event-heavy workloads. Single handlers execute directly without concurrency overhead. Each consumer is identified by a unique string ID to enable selective unregistration.
|
Events are processed asynchronously on a dedicated event processor thread using a blocking queue. When multiple handlers are registered for the same event type, they are executed concurrently using structured concurrency (virtual threads), providing excellent scalability for event-heavy workloads. Single handlers execute directly without concurrency overhead. Each consumer is identified by a unique string ID to enable selective unregistration.
|
||||||
|
|
||||||
#### Setup / Autoconfiguration
|
#### Setup / Autoconfiguration
|
||||||
**Configuration Class**: `EventsAutoConfiguration`
|
**Configuration Class**: `EventsAutoConfiguration`
|
||||||
|
|||||||
6
pom.xml
6
pom.xml
@@ -4,7 +4,7 @@
|
|||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<groupId>de.extio</groupId>
|
<groupId>de.extio</groupId>
|
||||||
<artifactId>game-engine</artifactId>
|
<artifactId>game-engine</artifactId>
|
||||||
<version>1.0.12</version>
|
<version>1.0.13</version>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<java.version>25</java.version>
|
<java.version>25</java.version>
|
||||||
@@ -84,16 +84,12 @@
|
|||||||
<version>3.15.0</version>
|
<version>3.15.0</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<release>${java.version}</release>
|
<release>${java.version}</release>
|
||||||
<enablePreview>true</enablePreview>
|
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-surefire-plugin</artifactId>
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
<version>3.5.5</version>
|
<version>3.5.5</version>
|
||||||
<configuration>
|
|
||||||
<argLine>--enable-preview</argLine>
|
|
||||||
</configuration>
|
|
||||||
</plugin>
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|||||||
@@ -3,13 +3,11 @@ package de.extio.game_engine.event;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.NoSuchElementException;
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
import java.util.concurrent.BlockingQueue;
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
import java.util.concurrent.StructuredTaskScope;
|
|
||||||
import java.util.concurrent.StructuredTaskScope.FailedException;
|
|
||||||
import java.util.concurrent.StructuredTaskScope.Joiner;
|
|
||||||
import java.util.concurrent.StructuredTaskScope.Subtask;
|
|
||||||
import java.util.concurrent.StructuredTaskScope.Subtask.State;
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -18,9 +16,11 @@ import org.slf4j.LoggerFactory;
|
|||||||
public class EventExecutor {
|
public class EventExecutor {
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(EventExecutor.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(EventExecutor.class);
|
||||||
|
private static final ExecutorService EXECUTOR = Executors.newVirtualThreadPerTaskExecutor();
|
||||||
|
|
||||||
private final BlockingQueue<QueuedEvent> eventQueue = new LinkedBlockingQueue<>();
|
private final BlockingQueue<QueuedEvent> eventQueue = new LinkedBlockingQueue<>();
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
private final Thread processorThread;
|
private final Thread processorThread;
|
||||||
|
|
||||||
private final EventHandlerRegistry registry;
|
private final EventHandlerRegistry registry;
|
||||||
@@ -31,7 +31,7 @@ public class EventExecutor {
|
|||||||
.daemon(true)
|
.daemon(true)
|
||||||
.name("Events-Processor")
|
.name("Events-Processor")
|
||||||
.start(() -> {
|
.start(() -> {
|
||||||
final var tasks = new ArrayList<Subtask<Object>>();
|
final var tasks = new ArrayList<Callable<Void>>();
|
||||||
while (!Thread.currentThread().isInterrupted()) {
|
while (!Thread.currentThread().isInterrupted()) {
|
||||||
try {
|
try {
|
||||||
final QueuedEvent queuedEvent = this.eventQueue.take();
|
final QueuedEvent queuedEvent = this.eventQueue.take();
|
||||||
@@ -49,7 +49,8 @@ public class EventExecutor {
|
|||||||
this.eventQueue.offer(new QueuedEvent(event.getClass(), event));
|
this.eventQueue.offer(new QueuedEvent(event.getClass(), event));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processEvent(final List<Subtask<Object>> tasks, final Class<? extends Event> eventClass, final Event event) throws InterruptedException {
|
private void processEvent(final List<Callable<Void>> tasks, final Class<? extends Event> eventClass, final Event event) throws InterruptedException {
|
||||||
|
tasks.clear();
|
||||||
final var consumers = this.registry.getHandlers(eventClass);
|
final var consumers = this.registry.getHandlers(eventClass);
|
||||||
if (consumers != null) {
|
if (consumers != null) {
|
||||||
switch (consumers.size()) {
|
switch (consumers.size()) {
|
||||||
@@ -58,7 +59,8 @@ public class EventExecutor {
|
|||||||
|
|
||||||
case 1:
|
case 1:
|
||||||
try {
|
try {
|
||||||
((Consumer<Event>) consumers.getFirst().consumer()).accept(event);
|
@SuppressWarnings("unchecked") final Consumer<Event> consumer = (Consumer<Event>) consumers.getFirst().consumer();
|
||||||
|
consumer.accept(event);
|
||||||
}
|
}
|
||||||
catch (final NoSuchElementException e) {
|
catch (final NoSuchElementException e) {
|
||||||
return;
|
return;
|
||||||
@@ -69,31 +71,26 @@ public class EventExecutor {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
|
for (final var consumer : consumers) {
|
||||||
tasks.clear();
|
tasks.add(() -> {
|
||||||
for (final var consumer : consumers) {
|
@SuppressWarnings("unchecked") final Consumer<Event> eventConsumer = (Consumer<Event>) consumer.consumer();
|
||||||
tasks.add(scope.fork(() -> {
|
eventConsumer.accept(event);
|
||||||
((Consumer<Event>) consumer.consumer()).accept(event);
|
return null;
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
scope.join();
|
for (final var future : EXECUTOR.invokeAll(tasks)) {
|
||||||
|
try {
|
||||||
for (final var task : tasks) {
|
future.get();
|
||||||
if (task.state() == State.FAILED) {
|
}
|
||||||
LOGGER.error("Event consumer failed", task.exception());
|
catch (final Exception e) {
|
||||||
|
LOGGER.error("Event consumer failed", e.getCause() == null ? e : e.getCause());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (final InterruptedException e) {
|
catch (final InterruptedException e) {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
catch (final FailedException e) {
|
|
||||||
LOGGER.error("One or more event consumers failed", e);
|
|
||||||
}
|
|
||||||
catch (final Exception e) {
|
|
||||||
LOGGER.error("Unexpected error while processing event", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package de.extio.game_engine.module;
|
package de.extio.game_engine.module;
|
||||||
|
|
||||||
import java.util.concurrent.StructuredTaskScope;
|
import java.util.ArrayList;
|
||||||
import java.util.concurrent.StructuredTaskScope.Joiner;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
@@ -11,6 +14,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
public class ModuleExecutorImpl implements ModuleExecutor {
|
public class ModuleExecutorImpl implements ModuleExecutor {
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(ModuleExecutorImpl.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(ModuleExecutorImpl.class);
|
||||||
|
private static final ExecutorService EXECUTOR = Executors.newVirtualThreadPerTaskExecutor();
|
||||||
|
|
||||||
private final ModuleService moduleManager;
|
private final ModuleService moduleManager;
|
||||||
|
|
||||||
@@ -50,16 +54,25 @@ public class ModuleExecutorImpl implements ModuleExecutor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
|
final List<Callable<Void>> tasks = new ArrayList<>();
|
||||||
synchronized (subscribers) {
|
synchronized (subscribers) {
|
||||||
for (final var module : subscribers) {
|
for (final var module : subscribers) {
|
||||||
final var task = taskSupplier.apply(module);
|
final var task = taskSupplier.apply(module);
|
||||||
if (task != null) {
|
if (task != null) {
|
||||||
scope.fork(task);
|
tasks.add(() -> {
|
||||||
}
|
task.run();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scope.join();
|
}
|
||||||
|
|
||||||
|
if (tasks.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
EXECUTOR.invokeAll(tasks);
|
||||||
}
|
}
|
||||||
catch (final InterruptedException e) {
|
catch (final InterruptedException e) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
|
|||||||
Reference in New Issue
Block a user