feat(module): Introduce ModuleExecutor and ModuleExecutorImpl for task execution management

This commit is contained in:
extio-de
2025-12-22 20:23:48 +01:00
parent 1e4ebb08a4
commit a971033d97
6 changed files with 207 additions and 7 deletions

View File

@@ -31,11 +31,11 @@ public abstract class AbstractClientModule extends AbstractModule {
}
public void runUiPreEvents() {
public void runUiPre() {
}
public void runUiPostEvents() {
public void runUiPost() {
}

View File

@@ -0,0 +1,7 @@
package de.extio.game_engine.module;
public interface ModuleExecutor {
void execute();
}

View File

@@ -0,0 +1,62 @@
package de.extio.game_engine.module;
import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.Joiner;
import java.util.function.Consumer;
import java.util.function.Function;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ModuleExecutorImpl implements ModuleExecutor {
private static final Logger LOGGER = LoggerFactory.getLogger(ModuleExecutorImpl.class);
private final ModuleManager moduleManager;
public ModuleExecutorImpl(final ModuleManager moduleManager) {
this.moduleManager = moduleManager;
}
@Override
public void execute() {
this.runTasks(module -> () -> {
if (module instanceof final AbstractClientModule clientModule && this.moduleManager.getModulesDisplayedClientModules().contains(clientModule)) {
this.invokeSafe(clientModule, AbstractClientModule::runUiPre);
}
});
this.runTasks(module -> () -> {
this.invokeSafe(module, AbstractModule::run);
});
this.runTasks(module -> () -> {
if (module instanceof final AbstractClientModule clientModule && this.moduleManager.getModulesDisplayedClientModules().contains(clientModule)) {
this.invokeSafe(clientModule, AbstractClientModule::runUiPost);
}
});
}
private <T extends AbstractModule> void invokeSafe(final T module, final Consumer<T> consumer) {
try {
consumer.accept(module);
}
catch (final Exception exc) {
LOGGER.error("Error in module " + module.getClass().getName(), exc);
}
}
private void runTasks(final Function<AbstractModule, Runnable> taskSupplier) {
try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
for (final var module : this.moduleManager.getModulesActive()) {
final var task = taskSupplier.apply(module);
if (task != null) {
scope.fork(task);
}
}
scope.join();
}
catch (final InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}

View File

@@ -10,11 +10,17 @@ import org.springframework.context.annotation.Bean;
@AutoConfiguration
@ConditionalOnProperty(name = "game-engine.module.enabled", havingValue = "true", matchIfMissing = true)
public class ModuleManagerAutoConfiguration {
@Bean
@ConditionalOnMissingBean
ModuleManager moduleManager(final List<AbstractModule> initialModules) {
return new ModuleManagerImpl(initialModules);
}
@Bean
@ConditionalOnMissingBean
ModuleExecutor moduleExecutor(final ModuleManager moduleManager) {
return new ModuleExecutorImpl(moduleManager);
}
}

View File

@@ -6,13 +6,13 @@ import java.util.Deque;
import java.util.List;
import java.util.function.Consumer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
public class ModuleManagerImpl implements InitializingBean, ModuleManager {
private static final Logger LOGGER = LogManager.getLogger();
private static final Logger LOGGER = LoggerFactory.getLogger(ModuleManagerImpl.class);
private final List<AbstractModule> modulesInitial;

View File

@@ -0,0 +1,125 @@
package de.extio.game_engine.module;
import static org.mockito.Mockito.*;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
public class ModuleExecutorImplTest {
@Mock
private ModuleManager moduleManager;
private ModuleExecutorImpl moduleExecutor;
@BeforeEach
public void setUp() {
this.moduleExecutor = new ModuleExecutorImpl(this.moduleManager);
}
@Test
public void testExecute_CallsAllPhasesForActiveAndDisplayedClientModule() {
final AbstractClientModule clientModule = mock(AbstractClientModule.class);
when(this.moduleManager.getModulesActive()).thenReturn(List.of(clientModule));
when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of(clientModule));
this.moduleExecutor.execute();
verify(clientModule).runUiPre();
verify(clientModule).run();
verify(clientModule).runUiPost();
}
@Test
public void testExecute_CallsOnlyRunForActiveNonClientModule() {
final AbstractModule module = mock(AbstractModule.class);
when(this.moduleManager.getModulesActive()).thenReturn(List.of(module));
this.moduleExecutor.execute();
verify(module).run();
verify(module, never()).onActivate(); // Just checking it doesn't call other things
}
@Test
public void testExecute_CallsOnlyRunForActiveButNotDisplayedClientModule() {
final AbstractClientModule clientModule = mock(AbstractClientModule.class);
when(this.moduleManager.getModulesActive()).thenReturn(List.of(clientModule));
when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of());
this.moduleExecutor.execute();
verify(clientModule, never()).runUiPre();
verify(clientModule).run();
verify(clientModule, never()).runUiPost();
}
@Test
public void testExecute_HandlesExceptionInModule() {
final AbstractModule module1 = mock(AbstractModule.class);
final AbstractModule module2 = mock(AbstractModule.class);
doThrow(new RuntimeException("Test Exception")).when(module1).run();
when(this.moduleManager.getModulesActive()).thenReturn(List.of(module1, module2));
this.moduleExecutor.execute();
verify(module1).run();
verify(module2).run();
}
@Test
public void testExecute_HandlesExceptionInUiPre() {
final AbstractClientModule clientModule = mock(AbstractClientModule.class);
doThrow(new RuntimeException("UI Pre Exception")).when(clientModule).runUiPre();
when(this.moduleManager.getModulesActive()).thenReturn(List.of(clientModule));
when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of(clientModule));
this.moduleExecutor.execute();
verify(clientModule).runUiPre();
verify(clientModule).run();
verify(clientModule).runUiPost();
}
@Test
public void testExecute_EmptyModules() {
when(this.moduleManager.getModulesActive()).thenReturn(List.of());
this.moduleExecutor.execute();
verify(this.moduleManager, times(3)).getModulesActive();
}
@Test
public void testExecute_MultipleModules() {
final AbstractClientModule clientModule1 = mock(AbstractClientModule.class);
final AbstractClientModule clientModule2 = mock(AbstractClientModule.class);
final AbstractModule module1 = mock(AbstractModule.class);
when(this.moduleManager.getModulesActive()).thenReturn(List.of(clientModule1, clientModule2, module1));
when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of(clientModule1));
this.moduleExecutor.execute();
// clientModule1: displayed, so all phases
verify(clientModule1).runUiPre();
verify(clientModule1).run();
verify(clientModule1).runUiPost();
// clientModule2: active but not displayed, so only run
verify(clientModule2, never()).runUiPre();
verify(clientModule2).run();
verify(clientModule2, never()).runUiPost();
// module1: only run
verify(module1).run();
}
}