Implement event handling system with EventExecutor, EventService, and EventHandlerRegistry; add tests for event processing
This commit is contained in:
4
src/main/java/de/extio/game_engine/event/Event.java
Normal file
4
src/main/java/de/extio/game_engine/event/Event.java
Normal file
@@ -0,0 +1,4 @@
|
||||
package de.extio.game_engine.event;
|
||||
|
||||
public interface Event {
|
||||
}
|
||||
102
src/main/java/de/extio/game_engine/event/EventExecutor.java
Normal file
102
src/main/java/de/extio/game_engine/event/EventExecutor.java
Normal 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) {
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
26
src/main/java/de/extio/game_engine/event/EventService.java
Normal file
26
src/main/java/de/extio/game_engine/event/EventService.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user