Implement event handling system with EventExecutor, EventService, and EventHandlerRegistry; add tests for event processing

This commit is contained in:
extio-de
2025-12-21 23:34:19 +01:00
parent dc0e2b6f08
commit 3373576c75
7 changed files with 507 additions and 1 deletions

View File

@@ -0,0 +1,4 @@
package de.extio.game_engine.event;
public interface Event {
}

View File

@@ -0,0 +1,102 @@
package de.extio.game_engine.event;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.BlockingQueue;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class EventExecutor {
private static final Logger LOGGER = LoggerFactory.getLogger(EventExecutor.class);
private final BlockingQueue<QueuedEvent> eventQueue = new LinkedBlockingQueue<>();
private final Thread processorThread;
private final EventHandlerRegistry registry;
public EventExecutor(final EventHandlerRegistry registry) {
this.registry = registry;
this.processorThread = Thread.ofPlatform()
.daemon(true)
.name("Events-Processor")
.start(() -> {
final var tasks = new ArrayList<Subtask<Object>>();
while (!Thread.currentThread().isInterrupted()) {
try {
final QueuedEvent queuedEvent = this.eventQueue.take();
processEvent(tasks, queuedEvent.eventClass(), queuedEvent.event());
}
catch (final InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
}
public void submit(final Event 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 {
final var consumers = this.registry.getHandlers(eventClass);
if (consumers != null) {
switch (consumers.size()) {
case 0:
return;
case 1:
try {
consumers.getFirst().consumer().accept(event);
}
catch (final NoSuchElementException e) {
return;
}
catch (final Exception e) {
LOGGER.error("Event consumer failed", e);
}
return;
default:
try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
tasks.clear();
for (final var consumer : consumers) {
tasks.add(scope.fork(() -> {
consumer.consumer().accept(event);
}));
}
scope.join();
for (final var task : tasks) {
if (task.state() == State.FAILED) {
LOGGER.error("Event consumer failed", task.exception());
}
}
}
catch (final InterruptedException 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);
}
}
}
}
private record QueuedEvent(Class<? extends Event> eventClass, Event event) {
}
}

View File

@@ -0,0 +1,32 @@
package de.extio.game_engine.event;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;
public class EventHandlerRegistry {
private final ConcurrentMap<Class<? extends Event>, List<EventConsumer>> handlers = new ConcurrentHashMap<>();
public void register(final Class<? extends Event> eventClass, final String consumerId, final Consumer<Event> consumer) {
final var eventConsumers = this.handlers.computeIfAbsent(eventClass, k -> new CopyOnWriteArrayList<>());
eventConsumers.removeIf(ec -> ec.id().equals(consumerId));
eventConsumers.add(new EventConsumer(consumerId, consumer));
}
public void unregister(final Class<? extends Event> eventClass, final String consumerId) {
final var consumers = this.handlers.get(eventClass);
if (consumers != null) {
consumers.removeIf(ec -> ec.id().equals(consumerId));
}
}
List<EventConsumer> getHandlers(final Class<? extends Event> eventClass) {
return this.handlers.get(eventClass);
}
record EventConsumer(String id, Consumer<Event> consumer) {
}
}

View File

@@ -0,0 +1,26 @@
package de.extio.game_engine.event;
import java.util.function.Consumer;
public class EventService {
private final EventHandlerRegistry registry;
private final EventExecutor executor;
public EventService(final EventHandlerRegistry registry, final EventExecutor executor) {
this.registry = registry;
this.executor = executor;
}
public void register(final Class<? extends Event> eventClass, final String consumerId, final Consumer<Event> consumer) {
this.registry.register(eventClass, consumerId, consumer);
}
public void unregister(final Class<? extends Event> eventClass, final String consumerId) {
this.registry.unregister(eventClass, consumerId);
}
public void fire(final Event event) {
this.executor.submit(event);
}
}

View File

@@ -0,0 +1,26 @@
package de.extio.game_engine.event;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
@ConditionalOnProperty(name = "game-engine.events.enabled", havingValue = "true", matchIfMissing = true)
public class EventsAutoConfiguration {
@Bean
EventHandlerRegistry eventHandlerRegistry() {
return new EventHandlerRegistry();
}
@Bean
EventExecutor eventExecutor(final EventHandlerRegistry registry) {
return new EventExecutor(registry);
}
@Bean
EventService eventService(final EventHandlerRegistry registry, final EventExecutor executor) {
final EventService eventService = new EventService(registry, executor);
return eventService;
}
}