changeset 132:4c91cd792089

Implement TimerFactory for centralizing timer tasks and make timer-using code more testable.
author Roman Kennke <rkennke@redhat.com>
date Thu, 22 Mar 2012 16:34:49 +0100
parents 997c134b844e
children b3b192be55da
files client/src/main/java/com/redhat/thermostat/client/Main.java client/src/main/java/com/redhat/thermostat/client/MainWindowFacadeImpl.java client/src/main/java/com/redhat/thermostat/client/appctx/ApplicationContext.java client/src/test/java/com/redhat/thermostat/client/MainWindowFacadeImplTest.java client/src/test/java/com/redhat/thermostat/client/appctx/ApplicationContextTest.java common/src/main/java/com/redhat/thermostat/common/ThreadPoolTimerFactory.java common/src/main/java/com/redhat/thermostat/common/Timer.java common/src/main/java/com/redhat/thermostat/common/TimerFactory.java common/src/test/java/com/redhat/thermostat/common/ThreadPoolTimerFactoryTest.java
diffstat 9 files changed, 486 insertions(+), 16 deletions(-) [+]
line wrap: on
line diff
--- a/client/src/main/java/com/redhat/thermostat/client/Main.java	Wed Mar 21 22:23:11 2012 +0100
+++ b/client/src/main/java/com/redhat/thermostat/client/Main.java	Thu Mar 22 16:34:49 2012 +0100
@@ -54,6 +54,8 @@
 import com.redhat.thermostat.client.ui.MainWindow;
 import com.redhat.thermostat.common.Constants;
 import com.redhat.thermostat.common.LaunchException;
+import com.redhat.thermostat.common.ThreadPoolTimerFactory;
+import com.redhat.thermostat.common.TimerFactory;
 import com.redhat.thermostat.common.config.StartupConfiguration;
 import com.redhat.thermostat.common.dao.Connection;
 import com.redhat.thermostat.common.dao.Connection.ConnectionListener;
@@ -93,6 +95,8 @@
         ConnectionProvider connProv = new MongoConnectionProvider(config);
         DAOFactory daoFactory = new MongoDAOFactory(connProv);
         ApplicationContext.getInstance().setDAOFactory(daoFactory);
+        TimerFactory timerFactory = new ThreadPoolTimerFactory(1);
+        ApplicationContext.getInstance().setTimerFactory(timerFactory);
     }
 
     private void showGui() {
--- a/client/src/main/java/com/redhat/thermostat/client/MainWindowFacadeImpl.java	Wed Mar 21 22:23:11 2012 +0100
+++ b/client/src/main/java/com/redhat/thermostat/client/MainWindowFacadeImpl.java	Thu Mar 22 16:34:49 2012 +0100
@@ -41,8 +41,6 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
-import java.util.Timer;
-import java.util.TimerTask;
 import java.util.concurrent.TimeUnit;
 import java.util.logging.Level;
 import java.util.logging.Logger;
@@ -52,7 +50,9 @@
 import com.mongodb.DBCollection;
 import com.mongodb.DBCursor;
 import com.mongodb.DBObject;
+import com.redhat.thermostat.client.appctx.ApplicationContext;
 import com.redhat.thermostat.client.ui.MainWindow;
+import com.redhat.thermostat.common.Timer;
 import com.redhat.thermostat.common.dao.HostRef;
 import com.redhat.thermostat.common.dao.VmRef;
 import com.redhat.thermostat.common.utils.LoggingUtils;
@@ -65,7 +65,7 @@
     private final DBCollection hostInfoCollection;
     private final DBCollection vmInfoCollection;
 
-    private final Timer backgroundUpdater = new Timer();
+    private Timer backgroundUpdater;
 
     private MainWindow view;
 
@@ -75,21 +75,32 @@
         this.agentConfigCollection = db.getCollection("agent-config");
         this.hostInfoCollection = db.getCollection("host-info");
         this.vmInfoCollection = db.getCollection("vm-info");
+
+        initializeTimer();
+    }
+
+    private void initializeTimer() {
+        ApplicationContext ctx = ApplicationContext.getInstance();
+        backgroundUpdater = ctx.getTimerFactory().createTimer();
+        backgroundUpdater.setAction(new Runnable() {
+            @Override
+            public void run() {
+                doUpdateTreeAsync();
+            }
+        });
+        backgroundUpdater.setDelay(0);
+        backgroundUpdater.setPeriod(10);
+        backgroundUpdater.setTimeUnit(TimeUnit.SECONDS);
     }
 
     @Override
     public void start() {
-        backgroundUpdater.scheduleAtFixedRate(new TimerTask() {
-            @Override
-            public void run() {
-                doUpdateTreeAsync();
-            }
-        }, 0, TimeUnit.SECONDS.toMillis(10));
+        backgroundUpdater.start();
     }
 
     @Override
     public void stop() {
-        backgroundUpdater.cancel();
+        backgroundUpdater.stop();
     }
 
     @Override
--- a/client/src/main/java/com/redhat/thermostat/client/appctx/ApplicationContext.java	Wed Mar 21 22:23:11 2012 +0100
+++ b/client/src/main/java/com/redhat/thermostat/client/appctx/ApplicationContext.java	Thu Mar 22 16:34:49 2012 +0100
@@ -36,6 +36,7 @@
 
 package com.redhat.thermostat.client.appctx;
 
+import com.redhat.thermostat.common.TimerFactory;
 import com.redhat.thermostat.common.dao.DAOFactory;
 
 public class ApplicationContext {
@@ -44,6 +45,8 @@
 
     private DAOFactory daoFactory;
 
+    private TimerFactory timerFactory;
+
     public static ApplicationContext getInstance() {
         return instance;
     }
@@ -65,4 +68,12 @@
         return daoFactory;
     }
 
+    public void setTimerFactory(TimerFactory timerFactory) {
+        this.timerFactory = timerFactory;
+    }
+
+    public TimerFactory getTimerFactory() {
+        return timerFactory;
+    }
+
 }
--- a/client/src/test/java/com/redhat/thermostat/client/MainWindowFacadeImplTest.java	Wed Mar 21 22:23:11 2012 +0100
+++ b/client/src/test/java/com/redhat/thermostat/client/MainWindowFacadeImplTest.java	Thu Mar 22 16:34:49 2012 +0100
@@ -37,12 +37,11 @@
 package com.redhat.thermostat.client;
 
 import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.argThat;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import java.beans.PropertyChangeEvent;
 import java.beans.PropertyChangeListener;
@@ -51,10 +50,12 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
-import org.mockito.ArgumentMatcher;
 
 import com.mongodb.DB;
+import com.redhat.thermostat.client.appctx.ApplicationContextUtil;
 import com.redhat.thermostat.client.ui.MainWindow;
+import com.redhat.thermostat.common.Timer;
+import com.redhat.thermostat.common.TimerFactory;
 
 public class MainWindowFacadeImplTest {
 
@@ -64,17 +65,21 @@
 
     private MainWindow view;
 
+    private Timer mainWindowTimer;
+
     @Before
     public void setUp() {
-        
+        ApplicationContextUtil.resetApplicationContext();
         DB db = mock(DB.class);
         controller = new MainWindowFacadeImpl(db);
-        controller = spy(controller);
         view = mock(MainWindow.class);
         ArgumentCaptor<PropertyChangeListener> grabListener = ArgumentCaptor.forClass(PropertyChangeListener.class);
         doNothing().when(view).addViewPropertyListener(grabListener.capture());
         controller.initView(view);
         l = grabListener.getValue();
+        mainWindowTimer = mock(Timer.class);
+        TimerFactory timerFactory = mock(TimerFactory.class);
+        when(timerFactory.createTimer()).thenReturn(mainWindowTimer);
     }
 
     @After
@@ -82,6 +87,7 @@
         view = null;
         controller = null;
         l = null;
+        ApplicationContextUtil.resetApplicationContext();
     }
 
     @Test
@@ -89,7 +95,7 @@
 
         l.propertyChange(new PropertyChangeEvent(view, MainWindow.SHUTDOWN_PROPERTY, false, true));
 
-        verify(controller).stop();
+        verify(mainWindowTimer).stop();
 
     }
 
--- a/client/src/test/java/com/redhat/thermostat/client/appctx/ApplicationContextTest.java	Wed Mar 21 22:23:11 2012 +0100
+++ b/client/src/test/java/com/redhat/thermostat/client/appctx/ApplicationContextTest.java	Thu Mar 22 16:34:49 2012 +0100
@@ -44,6 +44,7 @@
 import org.junit.Test;
 
 import com.redhat.thermostat.client.appctx.ApplicationContext;
+import com.redhat.thermostat.common.TimerFactory;
 import com.redhat.thermostat.common.dao.DAOFactory;
 
 public class ApplicationContextTest {
@@ -92,4 +93,33 @@
         DAOFactory actual2 = ctx2.getDAOFactory();
         assertSame(daoFactory, actual2);
     }
+
+    @Test
+    public void  testTimerFactorySetGet() {
+        TimerFactory timerFactory = mock(TimerFactory.class);
+        ApplicationContext ctx = ApplicationContext.getInstance();
+        ctx.setTimerFactory(timerFactory);
+
+        TimerFactory actual1 = ctx.getTimerFactory();
+        assertSame(timerFactory, actual1);
+    }
+
+    @Test
+    public void  verifyTimerFactoryIsNullWhenNotInitialized() {
+        ApplicationContext ctx = ApplicationContext.getInstance();
+
+        TimerFactory actual = ctx.getTimerFactory();
+        assertNull(actual);
+    }
+
+    @Test
+    public void  verifyTimerFactoryStaysSame() {
+        TimerFactory timerFactory = mock(TimerFactory.class);
+        ApplicationContext ctx = ApplicationContext.getInstance();
+        ctx.setTimerFactory(timerFactory);
+
+        ApplicationContext ctx2 = ApplicationContext.getInstance();
+        TimerFactory actual2 = ctx2.getTimerFactory();
+        assertSame(timerFactory, actual2);
+    }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/common/src/main/java/com/redhat/thermostat/common/ThreadPoolTimerFactory.java	Thu Mar 22 16:34:49 2012 +0100
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2012 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.common;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+public class ThreadPoolTimerFactory implements TimerFactory {
+
+    public ThreadPoolTimerFactory(int poolSize) {
+        timerThreadPool = Executors.newScheduledThreadPool(poolSize);
+    }
+
+    private class ThreadPoolTimer implements Timer {
+
+        private Runnable action;
+
+        private long delay;
+
+        private long period;
+
+        private TimeUnit timeUnit = TimeUnit.MILLISECONDS;
+
+        private SchedulingType schedulingType = SchedulingType.ONCE;
+
+        private ScheduledFuture<?> timerTask;
+
+        @Override
+        public void start() {
+            if (action != null) {
+                startScheduling();
+            }          
+        }
+
+        private void startScheduling() {
+            switch (schedulingType) {
+            case FIXED_RATE:
+                timerTask = timerThreadPool.scheduleAtFixedRate(action, delay, period, timeUnit);
+                break;
+            case FIXED_DELAY:
+                timerTask = timerThreadPool.scheduleWithFixedDelay(action, delay, period, timeUnit);
+                break;
+            case ONCE:
+            default:
+                timerTask = timerThreadPool.schedule(action, delay, timeUnit);
+            }
+        }
+
+        @Override
+        public void stop() {
+            if (timerTask != null) {
+                timerTask.cancel(false);
+            }
+        }
+
+        @Override
+        public void setAction(Runnable action) {
+            this.action = action;
+        }
+
+        @Override
+        public void setDelay(long delay) {
+            this.delay = delay;
+        }
+
+        @Override
+        public void setPeriod(long period) {
+            this.period = period;
+        }
+
+        @Override
+        public void setSchedulingType(SchedulingType schedulingType) {
+            if (schedulingType == null) {
+                throw new NullPointerException();
+            }
+            this.schedulingType = schedulingType;
+        }
+
+        @Override
+        public void setTimeUnit(TimeUnit timeUnit) {
+            this.timeUnit = timeUnit;
+        }
+        
+    }
+
+    private ScheduledExecutorService timerThreadPool;
+
+    @Override
+    public Timer createTimer() {
+        return new ThreadPoolTimer();
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/common/src/main/java/com/redhat/thermostat/common/Timer.java	Thu Mar 22 16:34:49 2012 +0100
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2012 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.common;
+
+import java.util.concurrent.TimeUnit;
+
+public interface Timer {
+
+    enum SchedulingType {
+        ONCE, FIXED_RATE, FIXED_DELAY;
+    }
+
+    void start();
+    void stop();
+    void setAction(Runnable action);
+    void setDelay(long delay);
+    void setPeriod(long period);
+    void setSchedulingType(SchedulingType schedulingType);
+    void setTimeUnit(TimeUnit timeUnit);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/common/src/main/java/com/redhat/thermostat/common/TimerFactory.java	Thu Mar 22 16:34:49 2012 +0100
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2012 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.common;
+
+public interface TimerFactory {
+
+    Timer createTimer();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/common/src/test/java/com/redhat/thermostat/common/ThreadPoolTimerFactoryTest.java	Thu Mar 22 16:34:49 2012 +0100
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2012 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.common;
+
+import static org.mockito.Mockito.doAnswer;
+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 java.util.concurrent.TimeUnit;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import com.redhat.thermostat.common.Timer.SchedulingType;
+
+public class ThreadPoolTimerFactoryTest {
+
+    private Timer timer;
+
+    @Before
+    public void setUp() {
+        ThreadPoolTimerFactory timerFactory = new ThreadPoolTimerFactory(1);
+        timer = timerFactory.createTimer();
+    }
+
+    @After
+    public void tearDown() {
+        timer = null;
+    }
+
+    @Test
+    public void testDefault() throws InterruptedException {
+        Runnable action = mock(Runnable.class);
+        timer.setAction(action);
+        timer.start();
+        Thread.sleep(10);
+        verify(action).run();
+    }
+
+    @Test
+    public void testNullAction() {
+        timer.setAction(null);
+        timer.start();
+        // Good when no NPE is thrown.
+    }
+
+    @Test
+    public void testDefaultWithDelay() throws InterruptedException {
+        Runnable action = mock(Runnable.class);
+        timer.setAction(action);
+        timer.setDelay(50);
+        timer.start();
+        Thread.sleep(10);
+        verify(action, never()).run();
+        Thread.sleep(50);
+        verify(action).run();
+        Thread.sleep(50);
+        verify(action).run();
+    }
+
+    @Test
+    public void testTimeUnitSecond() throws InterruptedException {
+        Runnable action = mock(Runnable.class);
+        timer.setAction(action);
+        timer.setDelay(1);
+        timer.setTimeUnit(TimeUnit.SECONDS);
+        timer.start();
+        Thread.sleep(100);
+        verify(action, never()).run();
+        Thread.sleep(1000);
+        verify(action).run();
+        Thread.sleep(50);
+        verify(action).run();
+    }
+
+    @Test(expected=NullPointerException.class)
+    public void testNullType() throws InterruptedException {
+        timer.setSchedulingType(null);
+    }
+
+    @Test
+    public void testWithFixedRate() throws InterruptedException {
+        Runnable action = mock(Runnable.class);
+        doAnswer(new Answer<Void>() {
+
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                Thread.sleep(45);
+                return null;
+            }
+            
+        }).when(action).run();
+        timer.setAction(action);
+        timer.setDelay(50);
+        timer.setPeriod(50);
+        timer.setSchedulingType(SchedulingType.FIXED_RATE);
+        timer.start();
+        Thread.sleep(10);
+        verify(action, never()).run();
+        Thread.sleep(50);
+        verify(action).run();
+        Thread.sleep(50);
+        verify(action, times(2)).run();
+        timer.stop();
+        Thread.sleep(50);
+        verify(action, times(2)).run();
+    }
+
+    @Test
+    public void testStopWithoutStart() {
+        timer.stop();
+        // Good when no exception is thrown.
+    }
+
+    @Test
+    public void testWithFixedDelay() throws InterruptedException {
+        Runnable action = mock(Runnable.class);
+        doAnswer(new Answer<Void>() {
+
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                Thread.sleep(50);
+                return null;
+            }
+            
+        }).when(action).run();
+        timer.setAction(action);
+        timer.setDelay(50);
+        timer.setPeriod(50);
+        timer.setSchedulingType(SchedulingType.FIXED_DELAY);
+        timer.start();
+        Thread.sleep(10);
+        verify(action, never()).run();
+        Thread.sleep(50);
+        verify(action).run();
+        Thread.sleep(50);
+        verify(action).run();
+        Thread.sleep(50);
+        verify(action, times(2)).run();
+        timer.stop();
+        Thread.sleep(50);
+        verify(action, times(2)).run();
+    }
+}