changeset 1906:686b04914043

Add polling backend. PR2995 Reviewed-by: jerboaa Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2016-June/019218.html
author Jie Kang <jkang@redhat.com>
date Tue, 07 Jun 2016 11:14:49 -0400
parents 5a3e91f53ee3
children fe6cbf89e84e
files agent/core/src/main/java/com/redhat/thermostat/backend/HostPollingAction.java agent/core/src/main/java/com/redhat/thermostat/backend/HostPollingBackend.java agent/core/src/main/java/com/redhat/thermostat/backend/PollingBackend.java agent/core/src/main/java/com/redhat/thermostat/backend/VmPollingAction.java agent/core/src/main/java/com/redhat/thermostat/backend/VmPollingBackend.java agent/core/src/test/java/com/redhat/thermostat/backend/HostPollingBackendTest.java agent/core/src/test/java/com/redhat/thermostat/backend/PollingBackendTest.java agent/core/src/test/java/com/redhat/thermostat/backend/VmPollingBackendTest.java
diffstat 8 files changed, 939 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/agent/core/src/main/java/com/redhat/thermostat/backend/HostPollingAction.java	Tue Jun 07 11:14:49 2016 -0400
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2012-2016 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;
+
+/**
+ * An action to be performed at a regular interval as part of a
+ * {@link HostPollingBackend} implementation.
+ */
+public interface HostPollingAction {
+
+    /**
+     * Run the action.
+     */
+    public void run();
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/agent/core/src/main/java/com/redhat/thermostat/backend/HostPollingBackend.java	Tue Jun 07 11:14:49 2016 -0400
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2012-2016 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.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.ScheduledExecutorService;
+
+import com.redhat.thermostat.common.NotImplementedException;
+import com.redhat.thermostat.common.Version;
+
+/**
+ * Convenience {@link Backend} class for implementations that will take some
+ * action at the Host or system level on a regular interval.  Simply
+ * extend this class, implement any missing methods, and register one or
+ * more {@link HostPollingAction} implementations during instantiation.
+ */
+public abstract class HostPollingBackend extends PollingBackend {
+
+    private final Set<HostPollingAction> actions;
+
+    public HostPollingBackend(String name, String description,
+                              String vendor, Version version, ScheduledExecutorService executor) {
+        super(name, description, vendor, version, executor);
+        actions = new CopyOnWriteArraySet<>();
+    }
+
+    final void doScheduledActions() {
+        for (HostPollingAction action : actions) {
+            action.run();
+        }
+    }
+
+    /**
+     * Register an action to be performed at each polling interval.  It is
+     * recommended that implementations register all such actions during
+     * instantiation.
+     */
+    protected final void registerAction(HostPollingAction action) {
+        actions.add(action);
+    }
+
+    /**
+     * Unregister an action so that it will no longer be performed at each
+     * polling interval.  If no such action has been registered, this
+     * method has no effect.  Depending on thread timing issues, the action
+     * may be performed once even after this method has been called.
+     */
+    protected final void unregisterAction(HostPollingAction action) {
+        actions.remove(action);
+    }
+
+    @Override
+    public void setObserveNewJvm(boolean newValue) {
+        throw new NotImplementedException("This backend does not observe jvms!");
+    }
+
+    // Intentionally final do-nothing.
+    final void preActivate() {};
+    final void postDeactivate() {};
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/agent/core/src/main/java/com/redhat/thermostat/backend/PollingBackend.java	Tue Jun 07 11:14:49 2016 -0400
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2012-2016 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.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import com.redhat.thermostat.common.Version;
+
+/*
+ * Convenience {@link Backend} class for implementations that will take action
+ * on a fixed interval.  This is package private, and plugins should instead
+ * extend child classes {@link HostPollingBackend}
+ * or {@link VmPollingBackend} as appropriate.
+ */
+abstract class PollingBackend extends BaseBackend {
+
+    static final long DEFAULT_INTERVAL = 1000; // TODO make this configurable.
+
+    private ScheduledExecutorService executor;
+    private boolean isActive;
+
+    PollingBackend(String name, String description, String vendor,
+                   Version version,
+                   ScheduledExecutorService executor) {
+        super(name, description, vendor,
+              version.getVersionNumber(), true);
+        this.executor = executor;
+    }
+
+    @Override
+    public final synchronized boolean activate() {
+        if (!isActive) {
+            preActivate();
+            executor.scheduleAtFixedRate(new Runnable() {
+                @Override
+                public void run() {
+                    doScheduledActions();
+                }
+            }, 0, DEFAULT_INTERVAL, TimeUnit.MILLISECONDS);
+
+            isActive = true;
+        }
+        return isActive;
+    }
+
+    @Override
+    public final synchronized boolean deactivate() {
+        if (isActive) {
+            executor.shutdown();
+            postDeactivate();
+            isActive = false;
+        }
+        return !isActive;
+    }
+
+    @Override
+    public final boolean isActive() {
+        return isActive;
+    }
+
+    // Test hook.
+    final void setActive(boolean active) {
+        isActive = active;
+    }
+
+    // Give child classes a chance to specify what should happen at each polling interval.
+    abstract void doScheduledActions();
+
+    // An opportunity for child classes to do some setup upon activation.
+    // Will execute before any actions are scheduled.
+    void preActivate() {}
+
+    // An opportunity for child classes to do some cleanup upon deactivation.
+    // Will execute after all actions are unscheduled.
+    void postDeactivate() {}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/agent/core/src/main/java/com/redhat/thermostat/backend/VmPollingAction.java	Tue Jun 07 11:14:49 2016 -0400
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2012-2016 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;
+
+/**
+ * An action to be performed at a regular interval as part of a
+ * {@link VmPollingBackend} implementation.
+ */
+public interface VmPollingAction {
+
+    /**
+     * Run the action.
+     * 
+     * @param vmId a String representation of the VmID on which to take action.
+     * @param pid the process ID of the JVM instance on which to take action.
+     */
+    public void run(String vmId, int pid);
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/agent/core/src/main/java/com/redhat/thermostat/backend/VmPollingBackend.java	Tue Jun 07 11:14:49 2016 -0400
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2012-2016 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.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.redhat.thermostat.agent.VmStatusListener;
+import com.redhat.thermostat.agent.VmStatusListenerRegistrar;
+import com.redhat.thermostat.common.Version;
+import com.redhat.thermostat.common.utils.LoggingUtils;
+
+/**
+ * Convenience {@link Backend} class for implementations that will take some
+ * action for each monitored JVM process on a regular interval.  Simply
+ * extend this class, implement any missing methods, and register one or
+ * more {@link VmPollingAction} implementations during instantiation.
+ */
+public abstract class VmPollingBackend extends PollingBackend implements VmStatusListener {
+
+    private final Set<VmPollingAction> actions;
+    private final Map<Integer, String> pidsToMonitor = new ConcurrentHashMap<>();
+    private final VmStatusListenerRegistrar registrar;
+    private static final Logger logger = LoggingUtils.getLogger(VmPollingBackend.class);
+
+    public VmPollingBackend(String name, String description,
+                            String vendor, Version version, ScheduledExecutorService executor,
+                            VmStatusListenerRegistrar registrar) {
+        super(name, description, vendor, version, executor);
+        this.registrar = registrar;
+        actions = new CopyOnWriteArraySet<>();
+    }
+
+    @Override
+    final void preActivate() {
+        registrar.register(this);
+    }
+
+    @Override
+    final void postDeactivate() {
+        registrar.unregister(this);
+    }
+
+    @Override
+    final void doScheduledActions() {
+        for (Entry<Integer, String> entry : pidsToMonitor.entrySet()) {
+            int pid = entry.getKey();
+            String vmId = entry.getValue();
+            for (VmPollingAction action : actions) {
+                action.run(vmId, pid);
+            }
+        }
+    }
+
+    /**
+     * Register an action to be performed at each polling interval.  It is
+     * recommended that implementations register all such actions during
+     * instantiation.
+     */
+    protected final void registerAction(VmPollingAction action) {
+        actions.add(action);
+    }
+
+    /**
+     * Unregister an action so that it will no longer be performed at each
+     * polling interval.  If no such action has been registered, this
+     * method has no effect.  Depending on thread timing issues, the action
+     * may be performed once even after this method has been called.
+     */
+    protected final void unregisterAction(VmPollingAction action) {
+        actions.remove(action);
+    }
+
+    @Override
+    public void vmStatusChanged(Status newStatus, String vmId, int pid) {
+        switch (newStatus) {
+            case VM_STARTED:
+            /* fall-through */
+            case VM_ACTIVE:
+                if (getObserveNewJvm()) {
+                    pidsToMonitor.put(pid, vmId);
+                } else {
+                    logger.log(Level.FINE, "skipping new vm " + pid);
+                }
+                break;
+            case VM_STOPPED:
+                pidsToMonitor.remove(pid);
+                break;
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/agent/core/src/test/java/com/redhat/thermostat/backend/HostPollingBackendTest.java	Tue Jun 07 11:14:49 2016 -0400
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2012-2016 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.fail;
+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.concurrent.ScheduledExecutorService;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.redhat.thermostat.common.NotImplementedException;
+import com.redhat.thermostat.common.Version;
+
+public class HostPollingBackendTest {
+
+    private HostPollingBackend backend;
+    private ScheduledExecutorService mockExecutor;
+
+    @Before
+    public void setUp() {
+        mockExecutor = mock(ScheduledExecutorService.class);
+        Version mockVersion = mock(Version.class);
+        when(mockVersion.getVersionNumber()).thenReturn("backend-version");
+        backend = new HostPollingBackend("backend-name", "backend-description",
+                  "backend-vendor", mockVersion, mockExecutor) {
+                    @Override
+                    public int getOrderValue() {
+                        return 0; // Doesn't matter, not being tested.
+                    }
+        };
+        if (!backend.getObserveNewJvm()) {
+            /* At time of writing, default is true.  This is
+             * inherited from parent PollingBackend.  In case
+             * default changes:
+             */
+            backend.setObserveNewJvm(true);
+        }
+    }
+
+    @Test
+    public void verifySetObserveNewJvmThrowsException() {
+        try {
+            backend.setObserveNewJvm(true);
+        } catch (NotImplementedException e) {
+            return; // pass
+        }
+        fail("Should have thrown NotImplementedException");
+    }
+
+    @Test
+    public void verifyRegisteredActionPerformed() {
+        HostPollingAction action = mock(HostPollingAction.class);
+        backend.registerAction(action);
+        backend.doScheduledActions();
+        verify(action).run();
+    }
+
+    @Test
+    public void verifyMultipleRegisteredActionsPerformed() {
+        HostPollingAction action1 = mock(HostPollingAction.class);
+        HostPollingAction action2 = mock(HostPollingAction.class);
+        backend.registerAction(action1);
+        backend.registerAction(action2);
+        backend.doScheduledActions();
+        verify(action1).run();
+        verify(action2).run();
+    }
+
+    @Test
+    public void verifyUnregisteredActionNotPerformed() {
+        HostPollingAction action = mock(HostPollingAction.class);
+        backend.registerAction(action);
+        backend.unregisterAction(action);
+        backend.doScheduledActions();
+        verify(action, never()).run();
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/agent/core/src/test/java/com/redhat/thermostat/backend/PollingBackendTest.java	Tue Jun 07 11:14:49 2016 -0400
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2012-2016 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.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import com.redhat.thermostat.common.Version;
+
+public class PollingBackendTest {
+
+    private PollingBackend backend;
+    private ScheduledExecutorService mockExecutor;
+    private CustomActivateTester mockActivate;
+
+    @Before
+    public void setUp() {
+        mockExecutor = mock(ScheduledExecutorService.class);
+        mockActivate = mock(CustomActivateTester.class);
+        Version mockVersion = mock(Version.class);
+        when(mockVersion.getVersionNumber()).thenReturn("backend-version");
+        backend = new PollingBackend("backend-name", "backend-description",
+                  "backend-vendor", mockVersion, mockExecutor) {
+
+            @Override
+            public int getOrderValue() {
+                return 0; // Doesn't matter, not being tested.
+            }
+
+            @Override
+            void doScheduledActions() {
+                // Won't be called because mock executor.
+            }
+
+            @Override
+            void preActivate() {
+                mockActivate.activate();
+            }
+
+            @Override
+            void postDeactivate() {
+                mockActivate.deactivate();
+            }};
+    }
+
+    @After
+    public void tearDown() {
+        backend = null;
+    }
+
+    @Test
+    public void verifyActivate() {
+        backend.activate();
+        verify(mockExecutor).scheduleAtFixedRate(any(Runnable.class), eq( (long) 0),
+                                                 eq( (long) 1000), eq(TimeUnit.MILLISECONDS));
+        verify(mockActivate).activate();
+    }
+
+    @Test
+    public void verifyNoopActivateWhenAlreadyActive() {
+        backend.setActive(true);
+        backend.activate();
+        verifyNoMoreInteractions(mockExecutor);
+        verifyNoMoreInteractions(mockActivate);
+    }
+
+    @Test
+    public void verifyDeactivate() {
+        backend.setActive(true);
+        backend.deactivate();
+        verify(mockActivate).deactivate();
+        verify(mockExecutor).shutdown();
+    }
+
+    @Test
+    public void verifyNoopDeactivateWhenNotActive() {
+        backend.deactivate();
+        verifyNoMoreInteractions(mockExecutor);
+        verifyNoMoreInteractions(mockActivate);
+    }
+
+    @Test
+    public void verifyScheduledRunnableActuallyRunsScheduledAction() {
+        final ScheduledActionTester mockAction = mock(ScheduledActionTester.class);
+        Version mockVersion = mock(Version.class);
+        when(mockVersion.getVersionNumber()).thenReturn("backend-version");
+        ScheduledExecutorService mockExecutor = mock(ScheduledExecutorService.class);
+        backend = new PollingBackend("backend-name", "backend-description",
+                  "backend-vendor", mockVersion, mockExecutor) {
+
+            @Override
+            public int getOrderValue() {
+                return 0;
+            }
+
+            @Override
+            void doScheduledActions() {
+                mockAction.doAction();
+            }};
+        backend.activate();
+        ArgumentCaptor<Runnable> scheduledRunnableCaptor = ArgumentCaptor.forClass(Runnable.class);
+        verify(mockExecutor).scheduleAtFixedRate(scheduledRunnableCaptor.capture(), eq( (long) 0),
+                eq( (long) 1000), eq(TimeUnit.MILLISECONDS));
+        Runnable scheduledRunnable = scheduledRunnableCaptor.getValue();
+        scheduledRunnable.run();
+        verify(mockAction).doAction();
+        backend.deactivate();
+        verify(mockExecutor).shutdown();
+        verifyNoMoreInteractions(mockAction);
+    }
+
+    private interface CustomActivateTester {
+        public void activate();
+        public void deactivate();
+    }
+
+    private interface ScheduledActionTester {
+        public void doAction();
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/agent/core/src/test/java/com/redhat/thermostat/backend/VmPollingBackendTest.java	Tue Jun 07 11:14:49 2016 -0400
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2012-2016 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.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.concurrent.ScheduledExecutorService;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.redhat.thermostat.agent.VmStatusListener.Status;
+import com.redhat.thermostat.agent.VmStatusListenerRegistrar;
+import com.redhat.thermostat.common.Version;
+
+public class VmPollingBackendTest {
+
+    private VmPollingBackend backend;
+    private ScheduledExecutorService mockExecutor;
+    private VmStatusListenerRegistrar mockRegistrar;
+
+    @Before
+    public void setUp() {
+        mockExecutor = mock(ScheduledExecutorService.class);
+        Version mockVersion = mock(Version.class);
+        when(mockVersion.getVersionNumber()).thenReturn("backend-version");
+        mockRegistrar = mock(VmStatusListenerRegistrar.class);
+        backend = new VmPollingBackend("backend-name", "backend-description",
+                  "backend-vendor", mockVersion, mockExecutor, mockRegistrar) {
+                    @Override
+                    public int getOrderValue() {
+                        return 0; // Doesn't matter, not being tested.
+                    }
+        };
+        if (!backend.getObserveNewJvm()) {
+            /* At time of writing, default is true.  This is
+             * inherited from parent PollingBackend.  In case
+             * default changes:
+             */
+            backend.setObserveNewJvm(true);
+        }
+    }
+
+    @Test
+    public void verifyCustomActivateRegistersListener() {
+        backend.preActivate();
+        verify(mockRegistrar).register(backend);
+    }
+
+    @Test
+    public void verifyCustomDeactivateUnregistersListener() {
+        backend.postDeactivate();
+        verify(mockRegistrar).unregister(backend);
+    }
+
+    @Test
+    public void verifyRegisteredActionPerformed() {
+        String vmId = "test-vm-id";
+        int pid = 123;
+        backend.vmStatusChanged(Status.VM_ACTIVE, vmId, pid);
+        VmPollingAction action = mock(VmPollingAction.class);
+        backend.registerAction(action);
+        backend.doScheduledActions();
+
+        verify(action).run(eq(vmId), eq(pid));
+    }
+
+    @Test
+    public void verifyMultipleRegisteredActionsPerformed() {
+        String vmId = "test-vm-id";
+        int pid = 123;
+        backend.vmStatusChanged(Status.VM_ACTIVE, vmId, pid);
+        VmPollingAction action1 = mock(VmPollingAction.class);
+        VmPollingAction action2 = mock(VmPollingAction.class);
+        backend.registerAction(action1);
+        backend.registerAction(action2);
+        backend.doScheduledActions();
+
+        verify(action1).run(eq(vmId), eq(pid));
+        verify(action2).run(eq(vmId), eq(pid));
+    }
+
+    @Test
+    public void verifyActionsPerformedOnMultipleVms() {
+        String vmId1 = "test-vm-id1", vmId2 = "test-vm-id2";
+        int pid1 = 123, pid2 = 456;
+        backend.vmStatusChanged(Status.VM_ACTIVE, vmId1, pid1);
+        backend.vmStatusChanged(Status.VM_ACTIVE, vmId2, pid2);
+        VmPollingAction action = mock(VmPollingAction.class);
+        backend.registerAction(action);
+        backend.doScheduledActions();
+
+        verify(action).run(eq(vmId1), eq(pid1));
+        verify(action).run(eq(vmId2), eq(pid2));
+    }
+
+    @Test
+    public void verifyMultipleRegisteredActionsPerformedOnMultipleVms() {
+        String vmId1 = "test-vm-id1", vmId2 = "test-vm-id2";
+        int pid1 = 123, pid2 = 456;
+        backend.vmStatusChanged(Status.VM_ACTIVE, vmId1, pid1);
+        backend.vmStatusChanged(Status.VM_ACTIVE, vmId2, pid2);
+        VmPollingAction action1 = mock(VmPollingAction.class);
+        VmPollingAction action2 = mock(VmPollingAction.class);
+        backend.registerAction(action1);
+        backend.registerAction(action2);
+        backend.doScheduledActions();
+
+        verify(action1).run(eq(vmId1), eq(pid1));
+        verify(action1).run(eq(vmId2), eq(pid2));
+        verify(action2).run(eq(vmId1), eq(pid1));
+        verify(action2).run(eq(vmId2), eq(pid2));
+    }
+
+    @Test
+    public void verifyUnregisteredActionNotPerformed() {
+        String vmId = "test-vm-id";
+        int pid = 123;
+        backend.vmStatusChanged(Status.VM_ACTIVE, vmId, pid);
+        VmPollingAction action1 = mock(VmPollingAction.class);
+        VmPollingAction action2 = mock(VmPollingAction.class);
+        backend.registerAction(action1);
+        backend.registerAction(action2);
+        backend.doScheduledActions(); // Triggers both
+        backend.unregisterAction(action1);
+        backend.doScheduledActions(); // Triggers only action2
+
+        verify(action1, times(1)).run(eq(vmId), eq(pid));
+        verify(action2, times(2)).run(eq(vmId), eq(pid));
+    }
+
+    @Test
+    public void verifyVmStatusChangedStartedAndActiveResultInPolling() {
+        String vmId1 = "test-vm-id1", vmId2 = "test-vm-id2";
+        int pid1 = 123, pid2 = 456;
+        backend.vmStatusChanged(Status.VM_STARTED, vmId1, pid1);
+        backend.vmStatusChanged(Status.VM_ACTIVE, vmId2, pid2);
+        VmPollingAction action = mock(VmPollingAction.class);
+        backend.registerAction(action);
+        backend.doScheduledActions();
+
+        verify(action).run(eq(vmId1), eq(pid1));
+        verify(action).run(eq(vmId2), eq(pid2));
+    }
+
+    @Test
+    public void verifyVmStatusChangedStopsResultsInNoMorePolling() {
+        String vmId1 = "test-vm-id1", vmId2 = "test-vm-id2";
+        int pid1 = 123, pid2 = 456;
+        backend.vmStatusChanged(Status.VM_ACTIVE, vmId1, pid1);
+        backend.vmStatusChanged(Status.VM_ACTIVE, vmId2, pid2);
+        VmPollingAction action = mock(VmPollingAction.class);
+        backend.registerAction(action);
+        backend.doScheduledActions(); // Triggers for both vms
+        backend.vmStatusChanged(Status.VM_STOPPED, vmId1, pid1);
+        backend.doScheduledActions(); // Triggers only for vm2
+
+        verify(action, times(1)).run(eq(vmId1), eq(pid1));
+        verify(action, times(2)).run(eq(vmId2), eq(pid2));
+    }
+
+    @Test
+    public void verifyGetSetObserveNewJvmWorksAsExpected() {
+        String vmId1 = "test-vm-id1", vmId2 = "test-vm-id2";
+        int pid1 = 123, pid2 = 456;
+        backend.vmStatusChanged(Status.VM_ACTIVE, vmId1, pid1);
+        backend.setObserveNewJvm(false);
+        backend.vmStatusChanged(Status.VM_ACTIVE, vmId2, pid2); // Should be ignored.
+        VmPollingAction action = mock(VmPollingAction.class);
+        backend.registerAction(action);
+        backend.doScheduledActions();
+
+        verify(action).run(eq(vmId1), eq(pid1));
+        verify(action, never()).run(eq(vmId2), eq(pid2));
+    }
+}