From 1162f4b61e6893c0f44d1f9d5d8abc81a94bed48 Mon Sep 17 00:00:00 2001 From: Jim Hahn Date: Mon, 11 Mar 2019 11:37:53 -0400 Subject: Add ServiceManager class Added ServiceManager class to start a list of services, in order, and stop them in reverse order. Also addressed minor checkstyle issue in TopicSinkClient. Enabled logging from tests. Updated some comments. Updated license date. Added state checks and support for multi-threading. Change-Id: Ie7f053d9884766fe199895691a57eb5a51b1d155 Issue-ID: POLICY-1542 Signed-off-by: Jim Hahn --- .../event/comm/client/TopicSinkClient.java | 3 - utils/pom.xml | 10 + .../common/utils/services/ServiceManager.java | 190 ++++++++++++++++ .../utils/services/ServiceManagerException.java | 44 ++++ .../common/utils/resources/ResourceUtilsTest.java | 16 +- .../services/ServiceManagerExceptionTest.java | 63 ++++++ .../common/utils/services/ServiceManagerTest.java | 249 +++++++++++++++++++++ utils/src/test/resources/logback-test.xml | 37 +++ 8 files changed, 601 insertions(+), 11 deletions(-) create mode 100644 utils/src/main/java/org/onap/policy/common/utils/services/ServiceManager.java create mode 100644 utils/src/main/java/org/onap/policy/common/utils/services/ServiceManagerException.java create mode 100644 utils/src/test/java/org/onap/policy/common/utils/services/ServiceManagerExceptionTest.java create mode 100644 utils/src/test/java/org/onap/policy/common/utils/services/ServiceManagerTest.java create mode 100644 utils/src/test/resources/logback-test.xml diff --git a/policy-endpoints/src/main/java/org/onap/policy/common/endpoints/event/comm/client/TopicSinkClient.java b/policy-endpoints/src/main/java/org/onap/policy/common/endpoints/event/comm/client/TopicSinkClient.java index 3188b8d4..e44d1f76 100644 --- a/policy-endpoints/src/main/java/org/onap/policy/common/endpoints/event/comm/client/TopicSinkClient.java +++ b/policy-endpoints/src/main/java/org/onap/policy/common/endpoints/event/comm/client/TopicSinkClient.java @@ -21,11 +21,8 @@ package org.onap.policy.common.endpoints.event.comm.client; - import java.util.List; - import lombok.Getter; - import org.onap.policy.common.endpoints.event.comm.TopicEndpoint; import org.onap.policy.common.endpoints.event.comm.TopicSink; import org.onap.policy.common.utils.coder.Coder; diff --git a/utils/pom.xml b/utils/pom.xml index 3263b7b9..84fe4626 100644 --- a/utils/pom.xml +++ b/utils/pom.xml @@ -36,6 +36,11 @@ com.google.code.gson gson + + org.onap.policy.common + capabilities + ${project.version} + org.apache.commons commons-lang3 @@ -45,6 +50,11 @@ junit test + + ch.qos.logback + logback-classic + test + org.eclipse.persistence javax.persistence 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 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 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 running) throws ServiceManagerException { + Exception ex = null; + + // stop everything, in reverse order + Iterator 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 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 @@ + + + + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36}.%M\(%line\) - %msg%n + + + + + + + + + -- cgit 1.2.3-korg