diff options
author | Liam Fallon <liam.fallon@est.tech> | 2019-03-12 14:25:52 +0000 |
---|---|---|
committer | Gerrit Code Review <gerrit@onap.org> | 2019-03-12 14:25:52 +0000 |
commit | 5f3a884d55e0e7e74df813f2b278bf12a31acbbb (patch) | |
tree | b9e2e314dcf0ead59f20e8d69cc03a39d8050fe5 /utils/src | |
parent | 33ef186e1e34b74cea33a77a043c0c8100d5c467 (diff) | |
parent | 1162f4b61e6893c0f44d1f9d5d8abc81a94bed48 (diff) |
Merge "Add ServiceManager class"
Diffstat (limited to 'utils/src')
6 files changed, 591 insertions, 8 deletions
diff --git a/utils/src/main/java/org/onap/policy/common/utils/services/ServiceManager.java b/utils/src/main/java/org/onap/policy/common/utils/services/ServiceManager.java new file mode 100644 index 00000000..8bf89d56 --- /dev/null +++ b/utils/src/main/java/org/onap/policy/common/utils/services/ServiceManager.java @@ -0,0 +1,190 @@ +/* + * ============LICENSE_START======================================================= + * ONAP PAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.services; + +import java.util.Deque; +import java.util.Iterator; +import java.util.LinkedList; +import org.onap.policy.common.capabilities.Startable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Manages a series of services. The services are started in order, and stopped in reverse + * order. + */ +public class ServiceManager { + private static final Logger logger = LoggerFactory.getLogger(ServiceManager.class); + + /** + * Services to be started/stopped. + */ + private final Deque<Service> items = new LinkedList<>(); + + /** + * {@code True} if the services are currently running, {@code false} otherwise. + */ + private boolean running; + + /** + * Adds a pair of service actions to the manager. + * + * @param stepName name to be logged when the service is started/stopped + * @param starter function to start the service + * @param stopper function to stop the service + * @return this manager + */ + public synchronized ServiceManager addAction(String stepName, RunnableWithEx starter, RunnableWithEx stopper) { + if (running) { + throw new IllegalStateException("services are already running; cannot add " + stepName); + } + + items.add(new Service(stepName, starter, stopper)); + return this; + } + + /** + * Adds a service to the manager. The manager will invoke the service's + * {@link Startable#start()} and {@link Startable#stop()} methods. + * + * @param stepName name to be logged when the service is started/stopped + * @param service object to be started/stopped + * @return this manager + */ + public synchronized ServiceManager addService(String stepName, Startable service) { + if (running) { + throw new IllegalStateException("services are already running; cannot add " + stepName); + } + + items.add(new Service(stepName, () -> service.start(), () -> service.stop())); + return this; + } + + /** + * Starts each service, in order. If a service throws an exception, then the + * previously started services are stopped, in reverse order. + * + * @throws ServiceManagerException if a service fails to start + */ + public synchronized void start() throws ServiceManagerException { + if (running) { + throw new IllegalStateException("services are already running"); + } + + // tracks the services that have been started so far + Deque<Service> started = new LinkedList<>(); + Exception ex = null; + + for (Service item : items) { + try { + logger.info("starting {}", item.stepName); + item.starter.run(); + started.add(item); + + } catch (Exception e) { + logger.error("failed to start {}; rewinding steps", item.stepName); + ex = e; + break; + } + } + + if (ex == null) { + running = true; + return; + } + + // one of the services failed to start - rewind those we've previously started + try { + rewind(started); + + } catch (ServiceManagerException e) { + logger.error("rewind failed", e); + } + + throw new ServiceManagerException(ex); + } + + /** + * Stops the services, in reverse order from which they were started. Stops all of the + * services, even if one of the "stop" functions throws an exception. Assumes that + * {@link #start()} has completed successfully. + * + * @throws ServiceManagerException if a service fails to stop + */ + public synchronized void stop() throws ServiceManagerException { + if (!running) { + throw new IllegalStateException("services are not running"); + } + + running = false; + rewind(items); + } + + /** + * Rewinds a list of services, stopping them in reverse order. Stops all of the + * services, even if one of the "stop" functions throws an exception. + * + * @param running services that are running, in the order they were started + * @throws ServiceManagerException if a service fails to stop + */ + private void rewind(Deque<Service> running) throws ServiceManagerException { + Exception ex = null; + + // stop everything, in reverse order + Iterator<Service> it = running.descendingIterator(); + while (it.hasNext()) { + Service item = it.next(); + try { + logger.info("stopping {}", item.stepName); + item.stopper.run(); + } catch (Exception e) { + logger.error("failed to stop {}", item.stepName); + ex = e; + + // do NOT break or re-throw, as we must stop ALL remaining items + } + } + + if (ex != null) { + throw new ServiceManagerException(ex); + } + } + + /** + * Service information. + */ + private static class Service { + private String stepName; + private RunnableWithEx starter; + private RunnableWithEx stopper; + + public Service(String stepName, RunnableWithEx starter, RunnableWithEx stopper) { + this.stepName = stepName; + this.starter = starter; + this.stopper = stopper; + } + } + + @FunctionalInterface + public static interface RunnableWithEx { + public void run() throws Exception; + } +} diff --git a/utils/src/main/java/org/onap/policy/common/utils/services/ServiceManagerException.java b/utils/src/main/java/org/onap/policy/common/utils/services/ServiceManagerException.java new file mode 100644 index 00000000..3daa441a --- /dev/null +++ b/utils/src/main/java/org/onap/policy/common/utils/services/ServiceManagerException.java @@ -0,0 +1,44 @@ +/* + * ============LICENSE_START======================================================= + * ONAP PAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.services; + +/** + * Exceptions thrown by the ServiceManager. + */ +public class ServiceManagerException extends Exception { + private static final long serialVersionUID = 1L; + + public ServiceManagerException() { + super(); + } + + public ServiceManagerException(String message) { + super(message); + } + + public ServiceManagerException(Throwable cause) { + super(cause); + } + + public ServiceManagerException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/utils/src/test/java/org/onap/policy/common/utils/resources/ResourceUtilsTest.java b/utils/src/test/java/org/onap/policy/common/utils/resources/ResourceUtilsTest.java index d1aa59d5..eb918d35 100644 --- a/utils/src/test/java/org/onap/policy/common/utils/resources/ResourceUtilsTest.java +++ b/utils/src/test/java/org/onap/policy/common/utils/resources/ResourceUtilsTest.java @@ -5,15 +5,15 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * + * * SPDX-License-Identifier: Apache-2.0 * ============LICENSE_END========================================================= */ @@ -158,10 +158,10 @@ public class ResourceUtilsTest { theUrl = ResourceUtils.getLocalFile("file:///"); assertNotNull(theUrl); - + theUrl = ResourceUtils.getLocalFile("file:///testdir/testfile.xml"); assertNull(theUrl); - + theUrl = ResourceUtils.getLocalFile(null); assertNull(theUrl); } @@ -185,7 +185,7 @@ public class ResourceUtilsTest { theStream = ResourceUtils.getResourceAsStream(jarFileResource); assertNotNull(theStream); - + theStream = ResourceUtils.getResourceAsStream(pathDirResource); assertNotNull(theStream); @@ -250,7 +250,7 @@ public class ResourceUtilsTest { assertNull(theString); theString = ResourceUtils.getResourceAsString(""); - assertEquals("org\ntestdir\n", theString); + assertEquals("logback-test.xml\norg\ntestdir\n", theString); } @Test @@ -295,7 +295,7 @@ public class ResourceUtilsTest { assertEquals("/something/else", ResourceUtils.getFilePath4Resource("/something/else")); assertTrue(ResourceUtils.getFilePath4Resource("xml/example.xml").endsWith("xml/example.xml")); } - + /** * Cleandown resource utils test. */ diff --git a/utils/src/test/java/org/onap/policy/common/utils/services/ServiceManagerExceptionTest.java b/utils/src/test/java/org/onap/policy/common/utils/services/ServiceManagerExceptionTest.java new file mode 100644 index 00000000..5fe321e8 --- /dev/null +++ b/utils/src/test/java/org/onap/policy/common/utils/services/ServiceManagerExceptionTest.java @@ -0,0 +1,63 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.services; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +import org.junit.Test; + +public class ServiceManagerExceptionTest { + private ServiceManagerException sme; + + @Test + public void testServiceManagerException() { + sme = new ServiceManagerException(); + assertNull(sme.getMessage()); + assertNull(sme.getCause()); + } + + @Test + public void testServiceManagerExceptionString() { + sme = new ServiceManagerException("hello"); + assertEquals("hello", sme.getMessage()); + assertNull(sme.getCause()); + } + + @Test + public void testServiceManagerExceptionThrowable() { + Throwable thrown = new Throwable("expected exception"); + sme = new ServiceManagerException(thrown); + assertNotNull(sme.getMessage()); + assertSame(thrown, sme.getCause()); + } + + @Test + public void testServiceManagerExceptionStringThrowable() { + Throwable thrown = new Throwable("another expected exception"); + sme = new ServiceManagerException("world", thrown); + assertEquals("world", sme.getMessage()); + assertSame(thrown, sme.getCause()); + } + +} diff --git a/utils/src/test/java/org/onap/policy/common/utils/services/ServiceManagerTest.java b/utils/src/test/java/org/onap/policy/common/utils/services/ServiceManagerTest.java new file mode 100644 index 00000000..49c0599b --- /dev/null +++ b/utils/src/test/java/org/onap/policy/common/utils/services/ServiceManagerTest.java @@ -0,0 +1,249 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.services; + +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.LinkedList; +import org.junit.Before; +import org.junit.Test; +import org.onap.policy.common.capabilities.Startable; +import org.onap.policy.common.utils.services.ServiceManager.RunnableWithEx; + +public class ServiceManagerTest { + private static final String ALREADY_RUNNING = "services are already running"; + private static final String EXPECTED_EXCEPTION = "expected exception"; + + private ServiceManager svcmgr; + + /** + * Initializes {@link #svcmgr}. + */ + @Before + public void setUp() { + svcmgr = new ServiceManager(); + } + + @Test + public void testAddAction() throws Exception { + RunnableWithEx start1 = mock(RunnableWithEx.class); + RunnableWithEx stop1 = mock(RunnableWithEx.class); + svcmgr.addAction("first action", start1, stop1); + + RunnableWithEx start2 = mock(RunnableWithEx.class); + RunnableWithEx stop2 = mock(RunnableWithEx.class); + svcmgr.addAction("second action", start2, stop2); + + svcmgr.start(); + verify(start1).run(); + verify(start2).run(); + verify(stop1, never()).run(); + verify(stop2, never()).run(); + + // cannot add while running + assertThatIllegalStateException().isThrownBy(() -> svcmgr.addAction("fail action", start1, stop1)) + .withMessage(ALREADY_RUNNING + "; cannot add fail action"); + + svcmgr.stop(); + verify(start1).run(); + verify(start2).run(); + verify(stop1).run(); + verify(stop2).run(); + } + + @Test + public void testAddStartable() throws Exception { + Startable start1 = mock(Startable.class); + svcmgr.addService("first startable", start1); + + Startable start2 = mock(Startable.class); + svcmgr.addService("second startable", start2); + + svcmgr.start(); + verify(start1).start(); + verify(start1, never()).stop(); + verify(start2).start(); + verify(start2, never()).stop(); + + // cannot add while running + assertThatIllegalStateException().isThrownBy(() -> svcmgr.addService("fail startable", start1)) + .withMessage(ALREADY_RUNNING + "; cannot add fail startable"); + + svcmgr.stop(); + verify(start1).start(); + verify(start1).stop(); + verify(start2).start(); + verify(start2).stop(); + } + + @Test + public void testStart() throws Exception { + Startable start1 = mock(Startable.class); + svcmgr.addService("test start", start1); + + svcmgr.start(); + verify(start1).start(); + verify(start1, never()).stop(); + + // cannot re-start + assertThatIllegalStateException().isThrownBy(() -> svcmgr.start()) + .withMessage(ALREADY_RUNNING); + + // verify that it didn't try to start the service again + verify(start1).start(); + } + + @Test + public void testStart_Ex() { + Startable start1 = mock(Startable.class); + svcmgr.addService("test start ex", start1); + + Startable start2 = mock(Startable.class); + svcmgr.addService("second test start ex", start2); + + // this one will throw an exception + Startable start3 = mock(Startable.class); + RuntimeException exception = new RuntimeException(EXPECTED_EXCEPTION); + when(start3.start()).thenThrow(exception); + svcmgr.addService("third test start ex", start3); + + Startable start4 = mock(Startable.class); + svcmgr.addService("fourth test start ex", start4); + + Startable start5 = mock(Startable.class); + svcmgr.addService("fifth test start ex", start5); + + assertThatThrownBy(() -> svcmgr.start()).isInstanceOf(ServiceManagerException.class).hasCause(exception); + + verify(start1).start(); + verify(start2).start(); + verify(start3).start(); + verify(start4, never()).start(); + verify(start5, never()).start(); + + verify(start1).stop(); + verify(start2).stop(); + verify(start3, never()).stop(); + verify(start4, never()).stop(); + verify(start5, never()).stop(); + } + + @Test + public void testStart_RewindEx() { + Startable start1 = mock(Startable.class); + svcmgr.addService("test start rewind", start1); + + // this one will throw an exception during rewind + Startable start2 = mock(Startable.class); + RuntimeException exception2 = new RuntimeException(EXPECTED_EXCEPTION); + when(start2.stop()).thenThrow(exception2); + svcmgr.addService("second test start rewind", start2); + + // this one will throw an exception + Startable start3 = mock(Startable.class); + RuntimeException exception = new RuntimeException(EXPECTED_EXCEPTION); + when(start3.start()).thenThrow(exception); + svcmgr.addService("third test start rewind", start3); + + Startable start4 = mock(Startable.class); + svcmgr.addService("fourth test start rewind", start4); + + Startable start5 = mock(Startable.class); + svcmgr.addService("fifth test start rewind", start5); + + assertThatThrownBy(() -> svcmgr.start()).isInstanceOf(ServiceManagerException.class).hasCause(exception); + } + + @Test + public void testStop() throws Exception { + Startable start1 = mock(Startable.class); + svcmgr.addService("first stop", start1); + + // cannot stop until started + assertThatIllegalStateException().isThrownBy(() -> svcmgr.stop()) + .withMessage("services are not running"); + + // verify that it didn't try to stop the service + verify(start1, never()).stop(); + + // start it + svcmgr.start(); + + svcmgr.stop(); + verify(start1).stop(); + } + + @Test + public void testStop_Ex() throws Exception { + RunnableWithEx start1 = mock(RunnableWithEx.class); + RunnableWithEx stop1 = mock(RunnableWithEx.class); + svcmgr.addAction("first stop ex", start1, stop1); + + Startable start2 = mock(Startable.class); + svcmgr.addService("second stop ex", start2); + + svcmgr.start(); + verify(start1).run(); + verify(stop1, never()).run(); + verify(start2).start(); + verify(start2, never()).stop(); + + svcmgr.stop(); + verify(start1).run(); + verify(stop1).run(); + verify(start2).start(); + verify(start2).stop(); + } + + @Test + public void testRewind() throws Exception { + RunnableWithEx starter = mock(RunnableWithEx.class); + LinkedList<String> lst = new LinkedList<>(); + + svcmgr.addAction("first rewind", starter, () -> lst.add("rewind1")); + svcmgr.addAction("second rewind", starter, () -> lst.add("rewind2")); + + // this one will throw an exception during rewind + RuntimeException exception = new RuntimeException(EXPECTED_EXCEPTION); + svcmgr.addAction("third rewind", starter, () -> { + lst.add("rewind3"); + throw exception; + }); + + svcmgr.addAction("fourth rewind", starter, () -> lst.add("rewind4")); + svcmgr.addAction("fifth rewind", starter, () -> lst.add("rewind5")); + + svcmgr.start(); + + assertThatThrownBy(() -> svcmgr.stop()).isInstanceOf(ServiceManagerException.class).hasCause(exception); + + // all of them should have been stopped, in reverse order + assertEquals(Arrays.asList("rewind5", "rewind4", "rewind3", "rewind2", "rewind1").toString(), lst.toString()); + } + +} diff --git a/utils/src/test/resources/logback-test.xml b/utils/src/test/resources/logback-test.xml new file mode 100644 index 00000000..01d5c861 --- /dev/null +++ b/utils/src/test/resources/logback-test.xml @@ -0,0 +1,37 @@ +<!-- + ============LICENSE_START======================================================= + ONAP - Common Modules + ================================================================================ + Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + ================================================================================ + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ============LICENSE_END========================================================= + --> + +<!-- Controls the output of logs for JUnit tests --> + +<configuration> + + <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> + <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> + <Pattern> + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36}.%M\(%line\) - %msg%n + </Pattern> + </encoder> + </appender> + + <root level="debug"> + <appender-ref ref="STDOUT" /> + </root> + +</configuration> |