From ba6891e7d7d75c8fa00ce13b7b273d1726a83e96 Mon Sep 17 00:00:00 2001 From: Paul Ferraro Date: Tue, 29 Oct 2024 16:23:41 +0000 Subject: [PATCH 1/2] WFCORE-7039 Add Installer.Builder methods to run arbitrary tasks on service start/stop/removal. --- .../java/org/wildfly/service/Installer.java | 97 ++++++++++++++++++- .../DefaultServiceInstallerTestCase.java | 51 +++++++++- 2 files changed, 143 insertions(+), 5 deletions(-) diff --git a/service/src/main/java/org/wildfly/service/Installer.java b/service/src/main/java/org/wildfly/service/Installer.java index 27f770ede4e..691c7e0d9f4 100644 --- a/service/src/main/java/org/wildfly/service/Installer.java +++ b/service/src/main/java/org/wildfly/service/Installer.java @@ -5,14 +5,20 @@ package org.wildfly.service; import java.util.Collection; +import java.util.Collections; +import java.util.EnumMap; +import java.util.EnumSet; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import org.jboss.msc.Service; +import org.jboss.msc.service.LifecycleEvent; +import org.jboss.msc.service.LifecycleListener; import org.jboss.msc.service.ServiceBuilder; import org.jboss.msc.service.ServiceController; import org.jboss.msc.service.ServiceController.Mode; @@ -80,6 +86,27 @@ public void accept(SB builder) { */ B asActive(); + /** + * Configures the specified task to be run after the installed service is started. + * @param task a task to execute upon service start + * @return a reference to this builder + */ + B onStart(Runnable task); + + /** + * Configures the specified task to be run after the installed service is stopped. + * @param task a task to execute upon service stop + * @return a reference to this builder + */ + B onStop(Runnable task); + + /** + * Configures the specified task to be run upon removal of the installed service. + * @param task a task to execute upon service removal + * @return a reference to this builder + */ + B onRemove(Runnable task); + /** * Builds a service installer. * @return a service installer @@ -164,7 +191,14 @@ interface Configuration> { * @return a service factory */ Function getServiceFactory(); - } + + /** + * Returns tasks to be run per lifecycle event. + * The returned map is either fully populated, or an empty map, if this service has no lifecycle tasks. + * @return a potentially empty map of tasks to be run per lifecycle event. + */ + Map> getLifecycleTasks(); + } /** * Generic abstract installer implementation that installs a {@link UnaryService}. @@ -180,18 +214,30 @@ class DefaultInstaller dependency; private final Function serviceFactory; + private final Map> lifecycleTasks; protected DefaultInstaller(Installer.Configuration config, Function serviceBuilderFactory) { this.serviceBuilderFactory = serviceBuilderFactory; this.serviceFactory = config.getServiceFactory(); this.mode = config.getInitialMode(); this.dependency = config.getDependency(); + this.lifecycleTasks = config.getLifecycleTasks(); } @Override public ServiceController install(ST target) { SB builder = this.serviceBuilderFactory.apply(target); this.dependency.accept(builder); + // N.B. map of tasks is either empty or fully populated + if (!this.lifecycleTasks.isEmpty()) { + Map> tasks = this.lifecycleTasks; + builder.addListener(new LifecycleListener() { + @Override + public void handleEvent(ServiceController controller, LifecycleEvent event) { + tasks.get(event).forEach(Runnable::run); + } + }); + } return builder.setInstance(this.serviceFactory.apply(builder)).setInitialMode(this.mode).install(); } } @@ -199,6 +245,7 @@ public ServiceController install(ST target) { abstract class AbstractBuilder, ST extends ServiceTarget, SB extends DSB, DSB extends ServiceBuilder> implements Installer.Builder, Installer.Configuration { private volatile ServiceController.Mode mode = ServiceController.Mode.ON_DEMAND; private volatile Consumer dependency = Functions.discardingConsumer(); + private volatile Map> lifecycleTasks = Map.of(); protected abstract B builder(); @@ -220,6 +267,43 @@ public B requires(Consumer dependency) { return this.builder(); } + @Override + public B onStart(Runnable task) { + return this.onEvent(LifecycleEvent.UP, task); + } + + @Override + public B onStop(Runnable task) { + return this.onEvent(LifecycleEvent.DOWN, task); + } + + @Override + public B onRemove(Runnable task) { + return this.onEvent(LifecycleEvent.REMOVED, task); + } + + private B onEvent(LifecycleEvent event, Runnable task) { + if (this.lifecycleTasks.isEmpty()) { + // Create EnumMap lazily, when needed + this.lifecycleTasks = new EnumMap<>(LifecycleEvent.class); + for (LifecycleEvent e : EnumSet.allOf(LifecycleEvent.class)) { + this.lifecycleTasks.put(e, (e == event) ? List.of(task) : List.of()); + } + } else { + List tasks = this.lifecycleTasks.get(event); + if (tasks.isEmpty()) { + this.lifecycleTasks.put(event, List.of(task)); + } else { + if (tasks.size() == 1) { + tasks = new LinkedList<>(tasks); + this.lifecycleTasks.put(event, tasks); + } + tasks.add(task); + } + } + return this.builder(); + } + @Override public Mode getInitialMode() { return this.mode; @@ -229,6 +313,17 @@ public Mode getInitialMode() { public Consumer getDependency() { return this.dependency; } + + @Override + public Map> getLifecycleTasks() { + // Return empty map or fully unmodifiable copy + if (this.lifecycleTasks.isEmpty()) return Map.of(); + Map> result = new EnumMap<>(LifecycleEvent.class); + for (Map.Entry> entry : this.lifecycleTasks.entrySet()) { + result.put(entry.getKey(), List.copyOf(entry.getValue())); + } + return Collections.unmodifiableMap(result); + } } abstract class AbstractNullaryBuilder, ST extends ServiceTarget, SB extends DSB, DSB extends ServiceBuilder> extends AbstractBuilder implements Function { diff --git a/service/src/test/java/org/wildfly/service/DefaultServiceInstallerTestCase.java b/service/src/test/java/org/wildfly/service/DefaultServiceInstallerTestCase.java index 44919e192e5..dd170e462bb 100644 --- a/service/src/test/java/org/wildfly/service/DefaultServiceInstallerTestCase.java +++ b/service/src/test/java/org/wildfly/service/DefaultServiceInstallerTestCase.java @@ -7,11 +7,18 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; import org.jboss.msc.Service; +import org.jboss.msc.service.LifecycleEvent; +import org.jboss.msc.service.LifecycleListener; import org.jboss.msc.service.ServiceBuilder; import org.jboss.msc.service.ServiceController; import org.jboss.msc.service.ServiceName; @@ -38,12 +45,29 @@ public void test() throws StartException { ServiceDependency dependency = mock(ServiceDependency.class); Consumer captor = mock(Consumer.class); Consumer combinedCaptor = mock(Consumer.class); - Consumer startTask = mock(Consumer.class); - Consumer stopTask = mock(Consumer.class); + Consumer startConsumer = mock(Consumer.class); + Consumer stopConsumer = mock(Consumer.class); ArgumentCaptor service = ArgumentCaptor.forClass(Service.class); ArgumentCaptor mode = ArgumentCaptor.forClass(ServiceController.Mode.class); + Map tasks = new EnumMap<>(LifecycleEvent.class); + for (LifecycleEvent event : EnumSet.of(LifecycleEvent.UP, LifecycleEvent.DOWN, LifecycleEvent.REMOVED)) { + tasks.put(event, mock(Runnable.class)); + } - ServiceInstaller installer = ServiceInstaller.builder(mapper, dependency).provides(name).requires(dependency).withCaptor(captor).onStart(startTask).onStop(stopTask).build(); + ArgumentCaptor capturedListener = ArgumentCaptor.forClass(LifecycleListener.class); + + doReturn(builder).when(builder).addListener(capturedListener.capture()); + + ServiceInstaller installer = ServiceInstaller.builder(mapper, dependency) + .provides(name) + .requires(dependency) + .withCaptor(captor) + .onStart(startConsumer) + .onStop(stopConsumer) + .onStart(tasks.get(LifecycleEvent.UP)) + .onStop(tasks.get(LifecycleEvent.DOWN)) + .onRemove(tasks.get(LifecycleEvent.REMOVED)) + .build(); doReturn(builder).when(target).addService(); doReturn(injector).when(builder).provides(name); @@ -59,7 +83,26 @@ public void test() throws StartException { verify(dependency).accept(builder); verify(builder).provides(name); + for (Runnable task : tasks.values()) { + verifyNoInteractions(task); + } + + LifecycleListener listener = capturedListener.getValue(); + + Assert.assertNotNull(listener); + + for (LifecycleEvent event : EnumSet.allOf(LifecycleEvent.class)) { + listener.handleEvent(controller, event); + + for (Map.Entry entry : tasks.entrySet()) { + if (event == entry.getKey()) { + verify(entry.getValue()).run(); + } else { + verifyNoMoreInteractions(entry.getValue()); + } + } + } - DefaultServiceTestCase.test(service.getValue(), "value", "mappedValue", combinedCaptor, mapper, dependency, startTask, stopTask); + DefaultServiceTestCase.test(service.getValue(), "value", "mappedValue", combinedCaptor, mapper, dependency, startConsumer, stopConsumer); } } From af36fb1b7e7ff7009882cf17872a7af451639c5b Mon Sep 17 00:00:00 2001 From: Paul Ferraro Date: Wed, 30 Oct 2024 19:39:17 +0000 Subject: [PATCH 2/2] Javadoc clarifications. --- .../java/org/wildfly/service/capture/FunctionExecutor.java | 2 +- .../org/wildfly/service/capture/FunctionExecutorRegistry.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/service/src/main/java/org/wildfly/service/capture/FunctionExecutor.java b/service/src/main/java/org/wildfly/service/capture/FunctionExecutor.java index f514fbb0b33..4c4ccef431d 100644 --- a/service/src/main/java/org/wildfly/service/capture/FunctionExecutor.java +++ b/service/src/main/java/org/wildfly/service/capture/FunctionExecutor.java @@ -32,7 +32,7 @@ public R execute(ExceptionFunction function) t } /** - * Executes the given function. + * Executes the specified function, using a value provided by an associated {@link ValueRegistry}. * @param the return type * @param the exception type * @param function a function to execute diff --git a/service/src/main/java/org/wildfly/service/capture/FunctionExecutorRegistry.java b/service/src/main/java/org/wildfly/service/capture/FunctionExecutorRegistry.java index a46b054eff6..a939db50225 100644 --- a/service/src/main/java/org/wildfly/service/capture/FunctionExecutorRegistry.java +++ b/service/src/main/java/org/wildfly/service/capture/FunctionExecutorRegistry.java @@ -12,9 +12,9 @@ */ public interface FunctionExecutorRegistry { /** - * Returns the executor for the specified key. + * Returns the executor for the specified key, if one exists. * @param key a registry key - * @return an executor, or null, if no such executor exists in the registry + * @return the executor for the specified key, or null, if no such executor exists in the registry */ FunctionExecutor getExecutor(K key); }