Mercurial > hg > thermostat-ng > agent
changeset 2767:30a68cb57107
Fix agent to properly activate backends asynchronously
Reviewed-by: jerboaa, jkang
Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-June/023942.html
Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-July/023978.html
Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-August/024496.html
Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-September/024955.html
Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-October/025248.html
author | Christopher Koehler <chkoehle@redhat.com> |
---|---|
date | Wed, 04 Oct 2017 15:08:14 -0400 |
parents | 4761e39f2d8a |
children | 13f7923af927 |
files | agent/core/src/main/java/com/redhat/thermostat/agent/Agent.java agent/core/src/main/java/com/redhat/thermostat/backend/BackendActivator.java agent/core/src/test/java/com/redhat/thermostat/agent/AgentTest.java agent/core/src/test/java/com/redhat/thermostat/agent/internal/AgentApplicationTest.java agent/core/src/test/java/com/redhat/thermostat/backend/BackendActivatorTest.java agent/core/src/test/java/com/redhat/thermostat/backend/adverse/AdverseBackend.java agent/core/src/test/java/com/redhat/thermostat/backend/adverse/AdverseBackendActivation.java agent/core/src/test/java/com/redhat/thermostat/backend/adverse/AdverseBackendDeactivation.java |
diffstat | 8 files changed, 883 insertions(+), 49 deletions(-) [+] |
line wrap: on
line diff
--- a/agent/core/src/main/java/com/redhat/thermostat/agent/Agent.java Tue Oct 03 13:25:56 2017 -0400 +++ b/agent/core/src/main/java/com/redhat/thermostat/agent/Agent.java Wed Oct 04 15:08:14 2017 -0400 @@ -45,6 +45,7 @@ import com.redhat.thermostat.agent.dao.AgentInfoDAO; import com.redhat.thermostat.agent.dao.BackendInfoDAO; import com.redhat.thermostat.backend.Backend; +import com.redhat.thermostat.backend.BackendActivator; import com.redhat.thermostat.backend.BackendRegistry; import com.redhat.thermostat.common.ActionEvent; import com.redhat.thermostat.common.ActionListener; @@ -61,6 +62,9 @@ public class Agent { private static final Logger logger = LoggingUtils.getLogger(Agent.class); + private static final int BACKEND_ACTIVATION_TIMEOUT_SECONDS = 65; + private static final int BACKEND_DEACTIVATION_TIMEOUT_SECONDS = 10; + private static final int BACKEND_ACTIVATOR_THREAD_COUNT = 2; private final BackendRegistry backendRegistry; private final AgentStartupConfiguration config; @@ -68,6 +72,8 @@ private final AgentInfoDAO agentDao; private final BackendInfoDAO backendDao; private final WriterID writerID; + private final BackendActivator backendActivator; + private final Object backendActivationLock = new Object(); private AgentInformation agentInfo; private boolean started = false; @@ -77,39 +83,39 @@ { @Override public void actionPerformed(ActionEvent<ThermostatExtensionRegistry.Action> actionEvent) { - Backend backend = (Backend) actionEvent.getPayload(); + final Backend backend = (Backend) actionEvent.getPayload(); switch (actionEvent.getActionId()) { - case SERVICE_ADDED: { if (!backendInfos.containsKey(backend)) { - - logger.info("Adding backend: " + backend); - - backend.activate(); - - BackendInformation info = AgentHelper.createBackendInformation(backend, getId()); - backendDao.addBackendInformation(info); - backendInfos.put(backend, info); - } else { - logger.warning("Backend registered that agent already knows about:" + backend); + backendActivator.activateBackend(backend, new Runnable() { + @Override + public void run() { + synchronized (backendActivationLock) { + BackendInformation info = AgentHelper.createBackendInformation(backend, getId()); + backendDao.addBackendInformation(info); + backendInfos.put(backend, info); + } + } + }); } break; } - case SERVICE_REMOVED: { - BackendInformation info = backendInfos.get(backend); + final BackendInformation info = backendInfos.get(backend); if (info != null) { - logger.info("removing backend: " + backend); - - backend.deactivate(); - - backendDao.removeBackendInformation(info); - backendInfos.remove(backend); + backendActivator.deactivateBackend(backend, new Runnable() { + @Override + public void run() { + synchronized (backendActivationLock) { + backendDao.removeBackendInformation(info); + backendInfos.remove(backend); + } + } + }); } break; } - default: { logger.log(Level.WARNING, "received unknown event from BackendRegistry: " + actionEvent.getActionId()); break; @@ -117,17 +123,23 @@ } } }; - + public Agent(BackendRegistry registry, AgentStartupConfiguration config, - AgentInfoDAO agentInfoDao, BackendInfoDAO backendInfoDao, WriterID writerId) { + AgentInfoDAO agentInfoDao, BackendInfoDAO backendInfoDao, WriterID writerId) { + this(registry, config, agentInfoDao, backendInfoDao, writerId, + new BackendActivator(BACKEND_ACTIVATOR_THREAD_COUNT, BACKEND_ACTIVATION_TIMEOUT_SECONDS)); + } + + Agent(BackendRegistry registry, AgentStartupConfiguration config, AgentInfoDAO agentInfoDao, + BackendInfoDAO backendInfoDao, WriterID writerId, BackendActivator backendActivator) { this.backendRegistry = registry; this.config = config; this.agentDao = agentInfoDao; this.backendDao = backendInfoDao; this.writerID = writerId; - - backendInfos = new ConcurrentHashMap<>(); - + this.backendActivator = backendActivator; + this.backendInfos = new ConcurrentHashMap<>(); + backendRegistry.addActionListener(backendRegistryListener); } @@ -156,10 +168,11 @@ return agentInfo; } - public synchronized void stop() { if (started) { backendRegistry.stop(); + backendActivator.waitForAllBackendsToDeactivate(BACKEND_DEACTIVATION_TIMEOUT_SECONDS); + backendActivator.shutdown(); if (config.purge()) { removeAllAgentRelatedInformation();
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/agent/core/src/main/java/com/redhat/thermostat/backend/BackendActivator.java Wed Oct 04 15:08:14 2017 -0400 @@ -0,0 +1,294 @@ +/* + * Copyright 2012-2017 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.backend; + +import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Logger; + +import com.redhat.thermostat.common.utils.LoggingUtils; + +/** + * An asynchronous backend activator, designed to process activate/deactivate + * calls such that any misbehaving backends will not block the other backends + * from activating or deactivating. This class is designed to be thread safe. + * Once no more activation and deactivation will be done, it should have the + * {@link #shutdown()} method called. Any operations requested from this object + * after it has shutdown will do nothing (and will print a warning message). + * + * Note: This implementation currently does not handle removing all backends, + * but not shutting down the agent itself. + */ +public class BackendActivator { + + private static final Logger logger = LoggingUtils.getLogger(BackendActivator.class); + private static final int NO_SCHEDULE_DELAY_SECONDS = 0; + private static final int AMOUNT_OF_RESUBMISSIONS_TO_EXECUTOR = 4; + private static final int RESUBMISSION_DURATION_SECONDS = 15; + private static final int STARTING_BACKEND_TRACKER_ATTEMPT_NUMBER = 1; + + private final long timeoutSeconds; + private final ScheduledExecutorService activationExecutorService; + private final ScheduledExecutorService deactivationExecutorService; + private final ScheduledExecutorService timeoutExecutorService; + private final ExecutorService postActivateService; + private final CountDownLatch allBackendsDeactivated; + private AtomicBoolean shutdown = new AtomicBoolean(false); + private AtomicInteger numBackendsActivate = new AtomicInteger(0); + + /** + * Creates a new backend activator with the provided number of threads and + * timeout to (attempt to) kill misbehaving backends. This will generate + * a total of (2 * numBackendsInParallel) + 2 threads. + * @param numBackendsInParallel How many threads this should allocate to + * activating/deactivating backends. + * @param timeoutSeconds How many seconds before a backend is considered to + * be misbehaving and should attempt to interrupt it. + * This should be non-negative. + * @throws IllegalArgumentException If the timeout is negative. + */ + public BackendActivator(int numBackendsInParallel, long timeoutSeconds) { + this(timeoutSeconds, Executors.newScheduledThreadPool(numBackendsInParallel), + Executors.newScheduledThreadPool(numBackendsInParallel), Executors.newScheduledThreadPool(1), + Executors.newFixedThreadPool(1), new CountDownLatch(1)); + } + + BackendActivator(long timeoutSeconds, ScheduledExecutorService activationExecutorService, + ScheduledExecutorService deactivationExecutorService, ScheduledExecutorService timeoutExecutorService, + ExecutorService postActivateService, CountDownLatch allBackendsDeactivated) { + if (timeoutSeconds < 0) { + throw new IllegalArgumentException("Cannot have a negative backend activation timeout"); + } + + this.timeoutSeconds = timeoutSeconds; + this.activationExecutorService = Objects.requireNonNull(activationExecutorService); + this.deactivationExecutorService = Objects.requireNonNull(deactivationExecutorService); + this.timeoutExecutorService = Objects.requireNonNull(timeoutExecutorService); + this.postActivateService = Objects.requireNonNull(postActivateService); + this.allBackendsDeactivated = Objects.requireNonNull(allBackendsDeactivated); + } + + /** + * Keeps recursively scheduling the backend tracker every period of + * RESUBMISSION_DURATION_SECONDS. Termination of recursion occurs when the + * attempt number is greater than AMOUNT_OF_RESUBMISSIONS_TO_EXECUTOR. + */ + private void scheduleBackendTrackerRecursively(final BackendTracker backendTracker) { + timeoutExecutorService.schedule(new Runnable() { + @Override + public void run() { + if (!backendTracker.getFuture().isDone() && backendTracker.getAttemptNumber() <= AMOUNT_OF_RESUBMISSIONS_TO_EXECUTOR) { + String logMessage = String.format("Backend '%s' is taking a while to activate/deactivate [%d/%d]", + backendTracker.getBackend(), backendTracker.getAttemptNumber(), + AMOUNT_OF_RESUBMISSIONS_TO_EXECUTOR); + logger.warning(logMessage); + backendTracker.incrementAttemptNumber(); + scheduleBackendTrackerRecursively(backendTracker); + } + } + }, RESUBMISSION_DURATION_SECONDS, TimeUnit.SECONDS); + } + + private void scheduleAction(final Backend backend, final boolean isActivating, Runnable runnable) { + ScheduledExecutorService executor = (isActivating ? activationExecutorService : deactivationExecutorService); + final ScheduledFuture<?> future = executor.schedule(runnable, NO_SCHEDULE_DELAY_SECONDS, TimeUnit.SECONDS); + + timeoutExecutorService.schedule(new Runnable() { + @Override + public void run() { + if (!future.isDone()) { + logger.warning("Backend '" + backend + "' is taking too long to " + (isActivating ? "activate" : "deactivate")); + future.cancel(true); + } + } + }, timeoutSeconds, TimeUnit.SECONDS); + + BackendTracker backendTracker = new BackendTracker(backend, future, STARTING_BACKEND_TRACKER_ATTEMPT_NUMBER); + scheduleBackendTrackerRecursively(backendTracker); + } + + /** + * Activates a backend. Will attempt to kill activation if it takes longer + * than the value set in the constructor for timing out. + * @param backend The backend to activate, should not be null. + * @param postActivate Any actions to be performed after activation. This + * may be null if you do not want to run anything after + * activation. + * @throws NullPointerException If the backend is null. + */ + public void activateBackend(final Backend backend, final Runnable postActivate) { + Objects.requireNonNull(backend); + + if (shutdown.get()) { + logger.info("Cannot activate '" + backend + "', backend activator shut down"); + return; + } + + logger.info("Adding backend: " + backend); + scheduleAction(backend, true, new Runnable() { + @Override + public void run() { + final boolean didActivate = backend.activate(); + if (!didActivate) { + logger.warning("Backend '" + backend + "' failed to activate."); + } + if (postActivate != null && didActivate) { + postActivateService.submit(postActivate); + } + numBackendsActivate.incrementAndGet(); + } + }); + } + + /** + * Deactivates a backend. Will attempt to kill it if it takes longer than + * the value set in the constructor for timing out. + * @param backend The backend to activate, should not be null. + * @param postDeactivate Any actions to be performed after activation. This + * may be null if you do not want to run anything + * after deactivation. + * @throws NullPointerException If the backend is null. + */ + public void deactivateBackend(final Backend backend, final Runnable postDeactivate) { + Objects.requireNonNull(backend); + + if (shutdown.get()) { + logger.info("Cannot deactivate '" + backend + "', backend activator shut down"); + return; + } + + logger.info("Removing backend: " + backend); + scheduleAction(backend, false, new Runnable() { + @Override + public void run() { + if (!backend.deactivate()) { + logger.warning("Backend '" + backend + "' failed to deactivate."); + return; + } + if (postDeactivate != null) { + postActivateService.submit(postDeactivate); + } + if (numBackendsActivate.decrementAndGet() == 0) { + allBackendsDeactivated.countDown(); + } + } + }); + } + + /** + * Waits for backends to deactivate. This depends on how many backends you + * have registered with this object, as it counts the number activated and + * deactivated, and will either return when the net change of activation to + * deactivation is zero, or if it times out based on the provided argument. + * @param timeoutSeconds How long until this method will return. If this is + * zero of negative, it will return immediately as + * stated in the CountDownLatch specification. + * @return True if there was no timeout, false otherwise. + */ + public boolean waitForAllBackendsToDeactivate(int timeoutSeconds) { + try { + if (allBackendsDeactivated.await(timeoutSeconds, TimeUnit.SECONDS)) { + return true; + } else { + logger.warning("Timed out waiting for backends to deactivate"); + } + } catch (InterruptedException e) { + logger.warning("Waiting for deactivation latch interrupted"); + } + + return false; + } + + /** + * Shuts down the backend activator so it can no longer accept any backends + * for activation or deactivation. + */ + public void shutdown() { + if (shutdown.get()) { + logger.warning("Already shut down the backend activator"); + return; + } + + activationExecutorService.shutdownNow(); + deactivationExecutorService.shutdownNow(); + timeoutExecutorService.shutdownNow(); + postActivateService.shutdownNow(); + + shutdown.set(true); + } + + // For unit testing. + int getNumberOfBackendsActive() { + return numBackendsActivate.get(); + } + + static class BackendTracker { + private Backend backend; + private Future<?> future; + private int attemptNumber; + + BackendTracker(Backend backend, Future<?> future, int attemptNumber) { + this.backend = Objects.requireNonNull(backend); + this.future = Objects.requireNonNull(future); + this.attemptNumber = attemptNumber; + } + + void incrementAttemptNumber() { + attemptNumber++; + } + + Backend getBackend() { + return backend; + } + + Future<?> getFuture() { + return future; + } + + int getAttemptNumber() { + return attemptNumber; + } + } +}
--- a/agent/core/src/test/java/com/redhat/thermostat/agent/AgentTest.java Tue Oct 03 13:25:56 2017 -0400 +++ b/agent/core/src/test/java/com/redhat/thermostat/agent/AgentTest.java Wed Oct 04 15:08:14 2017 -0400 @@ -48,6 +48,7 @@ import java.util.UUID; +import com.redhat.thermostat.backend.BackendActivator; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; @@ -70,9 +71,9 @@ private AgentStartupConfiguration config; private BackendRegistry backendRegistry; private Backend backend; - private AgentInfoDAO agentInfoDao; private BackendInfoDAO backendInfoDao; + private BackendActivator backendActivator; @Before public void setUp() throws Exception { @@ -91,6 +92,7 @@ when(backend.isActive()).thenReturn(true); backendRegistry = mock(BackendRegistry.class); + backendActivator = new InstantBackendActivator(); } @SuppressWarnings("unused") @@ -108,7 +110,7 @@ UUID uuid = UUID.randomUUID(); WriterID id = mock(WriterID.class); when(id.getWriterID()).thenReturn(uuid.toString()); - Agent agent = new Agent(backendRegistry, config, agentInfoDao, backendInfoDao, id); + Agent agent = new Agent(backendRegistry, config, agentInfoDao, backendInfoDao, id, backendActivator); agent.start(); @@ -127,7 +129,7 @@ // Start agent. WriterID id = mock(WriterID.class); - Agent agent = new Agent(backendRegistry, config, agentInfoDao, backendInfoDao, id); + Agent agent = new Agent(backendRegistry, config, agentInfoDao, backendInfoDao, id, backendActivator); verify(backendRegistry).addActionListener(backendListener.capture()); agent.start(); @@ -207,5 +209,24 @@ verify(agentInfoDao).updateAgentInformation(isA(AgentInformation.class)); //verify(storage, times(0)).purge(anyString()); TODO } + + static class InstantBackendActivator extends BackendActivator { + + public InstantBackendActivator() { + super(1, 1); + } + + @Override + public void activateBackend(final Backend backend, final Runnable postActivate) { + backend.activate(); + postActivate.run(); + } + + @Override + public void deactivateBackend(final Backend backend, final Runnable postDeactivate) { + backend.deactivate(); + postDeactivate.run(); + } + } }
--- a/agent/core/src/test/java/com/redhat/thermostat/agent/internal/AgentApplicationTest.java Tue Oct 03 13:25:56 2017 -0400 +++ b/agent/core/src/test/java/com/redhat/thermostat/agent/internal/AgentApplicationTest.java Wed Oct 04 15:08:14 2017 -0400 @@ -36,6 +36,7 @@ package com.redhat.thermostat.agent.internal; +import static com.redhat.thermostat.testutils.Asserts.assertCommandIsRegistered; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -54,6 +55,22 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import com.redhat.thermostat.agent.Agent; +import com.redhat.thermostat.agent.config.AgentStartupConfiguration; +import com.redhat.thermostat.agent.dao.AgentInfoDAO; +import com.redhat.thermostat.agent.dao.BackendInfoDAO; +import com.redhat.thermostat.agent.internal.AgentApplication.ConfigurationCreator; +import com.redhat.thermostat.backend.BackendRegistry; +import com.redhat.thermostat.common.ExitStatus; +import com.redhat.thermostat.common.LaunchException; +import com.redhat.thermostat.common.Version; +import com.redhat.thermostat.common.cli.Arguments; +import com.redhat.thermostat.common.cli.CommandContext; +import com.redhat.thermostat.common.cli.CommandException; +import com.redhat.thermostat.shared.config.CommonPaths; +import com.redhat.thermostat.shared.config.InvalidConfigurationException; +import com.redhat.thermostat.storage.core.WriterID; +import com.redhat.thermostat.testutils.StubBundleContext; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -67,25 +84,6 @@ import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; -import static com.redhat.thermostat.testutils.Asserts.assertCommandIsRegistered; - -import com.redhat.thermostat.agent.Agent; -import com.redhat.thermostat.agent.internal.AgentApplication.ConfigurationCreator; -import com.redhat.thermostat.agent.config.AgentStartupConfiguration; -import com.redhat.thermostat.agent.dao.AgentInfoDAO; -import com.redhat.thermostat.agent.dao.BackendInfoDAO; -import com.redhat.thermostat.backend.BackendRegistry; -import com.redhat.thermostat.common.ExitStatus; -import com.redhat.thermostat.common.LaunchException; -import com.redhat.thermostat.common.Version; -import com.redhat.thermostat.common.cli.Arguments; -import com.redhat.thermostat.common.cli.CommandContext; -import com.redhat.thermostat.common.cli.CommandException; -import com.redhat.thermostat.shared.config.CommonPaths; -import com.redhat.thermostat.shared.config.InvalidConfigurationException; -import com.redhat.thermostat.storage.core.WriterID; -import com.redhat.thermostat.testutils.StubBundleContext; - @RunWith(PowerMockRunner.class) public class AgentApplicationTest {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/agent/core/src/test/java/com/redhat/thermostat/backend/BackendActivatorTest.java Wed Oct 04 15:08:14 2017 -0400 @@ -0,0 +1,277 @@ +/* + * Copyright 2012-2017 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.backend; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import com.redhat.thermostat.backend.adverse.AdverseBackend; +import com.redhat.thermostat.backend.adverse.AdverseBackendActivation; +import com.redhat.thermostat.backend.adverse.AdverseBackendDeactivation; +import org.junit.Test; + +public class BackendActivatorTest { + + @Test(expected = IllegalArgumentException.class) + public void testDoesNotAcceptNegativeArguments() { + new BackendActivator(-5, -2); + } + + @Test + public void testActivateBackend() { + Backend backend = mock(Backend.class); + when(backend.activate()).thenReturn(true); + Runnable postActivateRunnable = mock(Runnable.class); + ScheduledExecutorService scheduledExecutorService = new InstantScheduledThreadPoolExecutor(); + ExecutorService executorService = new InstantExecutorService(); + BackendActivator backendActivator = new BackendActivator(3, scheduledExecutorService, scheduledExecutorService, + scheduledExecutorService, executorService, new CountDownLatch(1)); + + assertEquals(0, backendActivator.getNumberOfBackendsActive()); + backendActivator.activateBackend(backend, postActivateRunnable); + assertEquals(1, backendActivator.getNumberOfBackendsActive()); + + verify(backend, times(1)).activate(); + verify(postActivateRunnable, times(1)).run(); + } + + @Test + public void testDeactivateBackend() { + Backend backend = mock(Backend.class); + when(backend.deactivate()).thenReturn(true); + Runnable postDeactivateRunnable = mock(Runnable.class); + ScheduledExecutorService scheduledExecutorService = new InstantScheduledThreadPoolExecutor(); + ExecutorService executorService = new InstantExecutorService(); + BackendActivator backendActivator = new BackendActivator(3, scheduledExecutorService, scheduledExecutorService, + scheduledExecutorService, executorService, new CountDownLatch(1)); + backendActivator.deactivateBackend(backend, postDeactivateRunnable); + + verify(backend, times(1)).deactivate(); + verify(postDeactivateRunnable, times(1)).run(); + } + + @Test + public void testActivateBackendNotTrackedOrPostActivated() { + Backend backend = mock(Backend.class); + when(backend.activate()).thenReturn(false); + Runnable postActivateRunnable = mock(Runnable.class); + ScheduledExecutorService scheduledExecutorService = new InstantScheduledThreadPoolExecutor(); + ExecutorService executorService = new InstantExecutorService(); + BackendActivator backendActivator = new BackendActivator(3, scheduledExecutorService, scheduledExecutorService, + scheduledExecutorService, executorService, new CountDownLatch(1)); + backendActivator.activateBackend(backend, postActivateRunnable); + + verify(backend, times(1)).activate(); + verify(postActivateRunnable, times(0)).run(); + assertEquals(1, backendActivator.getNumberOfBackendsActive()); + } + + @Test + public void testDeactivateBackendNotTrackedOrPostDeactivated() { + Backend backend = mock(Backend.class); + when(backend.deactivate()).thenReturn(false); + Runnable postDeactivateRunnable = mock(Runnable.class); + ScheduledExecutorService scheduledExecutorService = new InstantScheduledThreadPoolExecutor(); + ExecutorService executorService = new InstantExecutorService(); + BackendActivator backendActivator = new BackendActivator(3, scheduledExecutorService, scheduledExecutorService, + scheduledExecutorService, executorService, new CountDownLatch(1)); + backendActivator.deactivateBackend(backend, postDeactivateRunnable); + + verify(backend, times(1)).deactivate(); + verify(postDeactivateRunnable, times(0)).run(); + assertEquals(0, backendActivator.getNumberOfBackendsActive()); + } + + @Test + public void testActivateAndDeactivate() { + Backend backend = mock(Backend.class); + when(backend.activate()).thenReturn(true); + when(backend.deactivate()).thenReturn(true); + Runnable postActivateRunnable = mock(Runnable.class); + Runnable postDeactivateRunnable = mock(Runnable.class); + ScheduledExecutorService scheduledExecutorService = new InstantScheduledThreadPoolExecutor(); + ExecutorService executorService = new InstantExecutorService(); + BackendActivator backendActivator = new BackendActivator(3, scheduledExecutorService, scheduledExecutorService, + scheduledExecutorService, executorService, new CountDownLatch(1)); + + assertEquals(0, backendActivator.getNumberOfBackendsActive()); + backendActivator.activateBackend(backend, postActivateRunnable); + assertEquals(1, backendActivator.getNumberOfBackendsActive()); + backendActivator.deactivateBackend(backend, postDeactivateRunnable); + assertEquals(0, backendActivator.getNumberOfBackendsActive()); + + verify(backend, times(1)).activate(); + verify(backend, times(1)).deactivate(); + verify(postActivateRunnable, times(1)).run(); + verify(postDeactivateRunnable, times(1)).run(); + } + + @Test(timeout = 3000) + public void testAdverseActivation() { + AdverseBackend backend = new AdverseBackendActivation(); + Runnable postDeactivateRunnable = mock(Runnable.class); + + BackendActivator backendActivator = new BackendActivator(2, 1); + backendActivator.activateBackend(backend, postDeactivateRunnable); + backend.waitUntilInterrupted(); + } + + @Test(timeout = 3000) + public void testAdverseDeactivation() { + AdverseBackend backend = new AdverseBackendDeactivation(); + Runnable postDeactivateRunnable = mock(Runnable.class); + + BackendActivator backendActivator = new BackendActivator(2, 1); + backendActivator.deactivateBackend(backend, postDeactivateRunnable); + backend.waitUntilInterrupted(); + } + + @Test(timeout = 1000) + public void testCountsDownBackend() { + CountDownLatch countDownLatch = mock(CountDownLatch.class); + Backend backend = mock(Backend.class); + when(backend.activate()).thenReturn(true); + when(backend.deactivate()).thenReturn(true); + ScheduledExecutorService scheduledExecutorService = new InstantScheduledThreadPoolExecutor(); + ExecutorService executorService = new InstantExecutorService(); + BackendActivator backendActivator = new BackendActivator(1, scheduledExecutorService, scheduledExecutorService, + scheduledExecutorService, executorService, countDownLatch); + + verify(countDownLatch, times(0)).countDown(); + backendActivator.activateBackend(backend, mock(Runnable.class)); + verify(countDownLatch, times(0)).countDown(); + + backendActivator.deactivateBackend(backend, mock(Runnable.class)); + verify(countDownLatch, times(1)).countDown(); + } + + @Test(timeout = 1000) + public void testWaitsForDeactivation() { + CountDownLatch countDownLatch = new CountDownLatch(1); + Backend backend = mock(Backend.class); + when(backend.activate()).thenReturn(true); + when(backend.deactivate()).thenReturn(true); + ScheduledExecutorService scheduledExecutorService = new InstantScheduledThreadPoolExecutor(); + ExecutorService executorService = new InstantExecutorService(); + BackendActivator backendActivator = new BackendActivator(1, scheduledExecutorService, scheduledExecutorService, + scheduledExecutorService, executorService, countDownLatch); + + backendActivator.activateBackend(backend, mock(Runnable.class)); + backendActivator.deactivateBackend(backend, mock(Runnable.class)); + + // This should guarantee we overshoot the timeout if the implementation + // is broken (by not waiting on the latch, or not counting down). + backendActivator.waitForAllBackendsToDeactivate(60000); + } + + @Test + public void testCannotAddActivationAfterShutdown() { + Backend backend = mock(Backend.class); + when(backend.activate()).thenReturn(true); + when(backend.deactivate()).thenReturn(true); + Runnable runnable = mock(Runnable.class); + BackendActivator backendActivator = new BackendActivator(2, 10); + + backendActivator.shutdown(); + + assertEquals(0, backendActivator.getNumberOfBackendsActive()); + backendActivator.activateBackend(backend, runnable); + assertEquals(0, backendActivator.getNumberOfBackendsActive()); + } + + @Test + public void testCannotAddDeactivationAfterShutdown() { + Backend backend = mock(Backend.class); + when(backend.activate()).thenReturn(true); + when(backend.deactivate()).thenReturn(true); + Runnable runnable = mock(Runnable.class); + CountDownLatch countDownLatch = new CountDownLatch(1); + ScheduledExecutorService scheduledExecutorService = new InstantScheduledThreadPoolExecutor(); + ExecutorService executorService = new InstantExecutorService(); + BackendActivator backendActivator = new BackendActivator(1, scheduledExecutorService, scheduledExecutorService, + scheduledExecutorService, executorService, countDownLatch); + + backendActivator.activateBackend(backend, runnable); + assertEquals(1, backendActivator.getNumberOfBackendsActive()); + + backendActivator.shutdown(); + + backendActivator.deactivateBackend(backend, runnable); + assertEquals(1, backendActivator.getNumberOfBackendsActive()); + } + + private static class InstantScheduledThreadPoolExecutor extends ScheduledThreadPoolExecutor { + private InstantScheduledThreadPoolExecutor() { + super(1); + } + + @Override + public ScheduledFuture<?> schedule(Runnable runnable, long timeout, TimeUnit timeoutTimeUnit) { + ScheduledFuture scheduledFutureMock = mock(ScheduledFuture.class); + when(scheduledFutureMock.isDone()).thenReturn(true); + runnable.run(); + return scheduledFutureMock; + } + } + + private static class InstantExecutorService extends ThreadPoolExecutor { + private InstantExecutorService() { + super(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()); + } + + @Override + public Future<?> submit(Runnable runnable) { + Future futureMock = mock(Future.class); + when(futureMock.isDone()).thenReturn(true); + runnable.run(); + return futureMock; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/agent/core/src/test/java/com/redhat/thermostat/backend/adverse/AdverseBackend.java Wed Oct 04 15:08:14 2017 -0400 @@ -0,0 +1,84 @@ +/* + * Copyright 2012-2017 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.backend.adverse; + +import com.redhat.thermostat.backend.BaseBackend; +import org.apache.felix.scr.annotations.Activate; +import org.apache.felix.scr.annotations.Deactivate; + +import java.util.concurrent.CountDownLatch; + +public abstract class AdverseBackend extends BaseBackend { + + protected final CountDownLatch latch; + protected volatile boolean wasInterrupted; + + public AdverseBackend(String name, String description, String vendor) { + super(name, description, vendor); + this.latch = new CountDownLatch(1); + } + + public void waitUntilInterrupted() { + while (!wasInterrupted) { + } + } + + @Override + public boolean activate() { + return true; + } + + @Override + public boolean deactivate() { + return true; + } + + @Override + public boolean isActive() { + return false; + } + + @Activate + public void activateDS() { + // Nothing. Make DS happy. + } + + @Deactivate + public void deactivateDS() { + // Nothing. Make DS happy. + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/agent/core/src/test/java/com/redhat/thermostat/backend/adverse/AdverseBackendActivation.java Wed Oct 04 15:08:14 2017 -0400 @@ -0,0 +1,72 @@ +/* + * Copyright 2012-2017 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.backend.adverse; + +import com.redhat.thermostat.common.utils.LoggingUtils; + +import java.util.logging.Logger; + +public class AdverseBackendActivation extends AdverseBackend { + + private static final Logger logger = LoggingUtils.getLogger(AdverseBackendActivation.class); + + public AdverseBackendActivation() { + super("Bad activation backend", "Starves other backends", "Adversary"); + } + + @Override + public boolean activate() { + try { + logger.info("I'm going to never return. Bye!"); + latch.await(); // This never returns. + } catch (InterruptedException e) { + logger.info("Adverse activation backend latch interrupted"); + wasInterrupted = true; + } + return true; + } + + @Override + public synchronized boolean deactivate() { + return true; + } + + @Override + public boolean isActive() { + return false; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/agent/core/src/test/java/com/redhat/thermostat/backend/adverse/AdverseBackendDeactivation.java Wed Oct 04 15:08:14 2017 -0400 @@ -0,0 +1,75 @@ +/* + * Copyright 2012-2017 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.backend.adverse; + +import com.redhat.thermostat.common.utils.LoggingUtils; + +import java.util.logging.Logger; + +public class AdverseBackendDeactivation extends AdverseBackend { + + private static final Logger logger = LoggingUtils.getLogger(AdverseBackendDeactivation.class); + private boolean isActive; + + public AdverseBackendDeactivation() { + super("Bad deactivating backend", "Starves other backends for deactivation", "Adversary Deactivation"); + } + + @Override + public boolean activate() { + isActive = true; + return true; + } + + @Override + public boolean deactivate() { + try { + logger.info("I'm going to never return for deactivation. Bye!"); + latch.await(); // This never returns. + } catch (InterruptedException e) { + logger.info("Adverse backend deactivation latch interrupted"); + wasInterrupted = true; + } + isActive = false; + return true; + } + + @Override + public boolean isActive() { + return isActive; + } +}