You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tomee.apache.org by an...@apache.org on 2013/10/10 11:00:43 UTC

svn commit: r1530883 - in /tomee/tomee/trunk/container/openejb-core/src: main/java/org/apache/openejb/core/timer/ test/java/org/apache/openejb/timer/

Author: andygumbrecht
Date: Thu Oct 10 09:00:42 2013
New Revision: 1530883

URL: http://svn.apache.org/r1530883
Log:
Fix and test for https://issues.apache.org/jira/browse/OPENEJB-2044 - Single Action Timer created in @PostConstruct fails to start

Added:
    tomee/tomee/trunk/container/openejb-core/src/test/java/org/apache/openejb/timer/SingleActionTimerTest.java
      - copied, changed from r1530859, tomee/tomee/trunk/container/openejb-core/src/test/java/org/apache/openejb/timer/InitialIntervalTimerTest.java
Modified:
    tomee/tomee/trunk/container/openejb-core/src/main/java/org/apache/openejb/core/timer/EjbTimerServiceImpl.java
    tomee/tomee/trunk/container/openejb-core/src/main/java/org/apache/openejb/core/timer/TimerData.java
    tomee/tomee/trunk/container/openejb-core/src/test/java/org/apache/openejb/timer/ScheduleTest.java

Modified: tomee/tomee/trunk/container/openejb-core/src/main/java/org/apache/openejb/core/timer/EjbTimerServiceImpl.java
URL: http://svn.apache.org/viewvc/tomee/tomee/trunk/container/openejb-core/src/main/java/org/apache/openejb/core/timer/EjbTimerServiceImpl.java?rev=1530883&r1=1530882&r2=1530883&view=diff
==============================================================================
--- tomee/tomee/trunk/container/openejb-core/src/main/java/org/apache/openejb/core/timer/EjbTimerServiceImpl.java (original)
+++ tomee/tomee/trunk/container/openejb-core/src/main/java/org/apache/openejb/core/timer/EjbTimerServiceImpl.java Thu Oct 10 09:00:42 2013
@@ -447,6 +447,7 @@ public class EjbTimerServiceImpl impleme
 
     @Override
     public void start() throws TimerStoreException {
+
         if (isStarted()) {
             return;
         }
@@ -470,9 +471,12 @@ public class EjbTimerServiceImpl impleme
      *
      * @param timerData the timer to schedule
      */
-    public void schedule(final TimerData timerData) {
+    public void schedule(final TimerData timerData) throws TimerStoreException {
+
+        start();
+
         if (scheduler == null) {
-            throw new IllegalStateException("Scheduler is not configured properly");
+            throw new TimerStoreException("Scheduler is not configured properly");
         }
 
         timerData.setScheduler(scheduler);
@@ -514,7 +518,7 @@ public class EjbTimerServiceImpl impleme
             }
         } catch (Exception e) {
             //TODO Any other actions we could do ?
-            log.warning("Could not schedule timer " + timerData, e);
+            log.error("Could not schedule timer " + timerData, e);
         }
     }
 
@@ -676,7 +680,13 @@ public class EjbTimerServiceImpl impleme
         //TODO add more schedule expression validation logic ?
         checkState();
         try {
-            final TimerData timerData = timerStore.createCalendarTimer(this, (String) deployment.getDeploymentID(), primaryKey, timeoutMethod, scheduleExpression, timerConfig, false);
+            final TimerData timerData = timerStore.createCalendarTimer(this,
+                                                                       (String) deployment.getDeploymentID(),
+                                                                       primaryKey,
+                                                                       timeoutMethod,
+                                                                       scheduleExpression,
+                                                                       timerConfig,
+                                                                       false);
             initializeNewTimer(timerData);
             return timerData.getTimer();
         } catch (TimerStoreException e) {

Modified: tomee/tomee/trunk/container/openejb-core/src/main/java/org/apache/openejb/core/timer/TimerData.java
URL: http://svn.apache.org/viewvc/tomee/tomee/trunk/container/openejb-core/src/main/java/org/apache/openejb/core/timer/TimerData.java?rev=1530883&r1=1530882&r2=1530883&view=diff
==============================================================================
--- tomee/tomee/trunk/container/openejb-core/src/main/java/org/apache/openejb/core/timer/TimerData.java (original)
+++ tomee/tomee/trunk/container/openejb-core/src/main/java/org/apache/openejb/core/timer/TimerData.java Thu Oct 10 09:00:42 2013
@@ -45,6 +45,7 @@ import java.util.Iterator;
 import java.util.Map;
 
 public abstract class TimerData implements Serializable {
+
     private static final long serialVersionUID = 1L;
 
     public static final String OPEN_EJB_TIMEOUT_TRIGGER_NAME_PREFIX = "OPEN_EJB_TIMEOUT_TRIGGER_";
@@ -62,17 +63,16 @@ public abstract class TimerData implemen
     private boolean autoScheduled;
 
     protected AbstractTrigger<?> trigger;
-    
+
     protected Scheduler scheduler;
 
-    public void setScheduler(Scheduler scheduler) {
+    public void setScheduler(final Scheduler scheduler) {
         this.scheduler = scheduler;
     }
 
     // EJB Timer object given to user code
     private Timer timer;
 
-
     /**
      * Is this a new timer?  A new timer must be scheduled with the java.util.Timer
      * when the transaction commits.
@@ -92,16 +92,21 @@ public abstract class TimerData implemen
      * when we are registered to avoid multiple registrations.
      */
     private boolean synchronizationRegistered = false;
-    
+
     /**
-     *  Used to set timer to expired state after the timeout callback method has been successfully invoked.
-     *  only apply to 
-     *  1, Single action timer
-     *  2, Calendar timer there are no future timeout.
+     * Used to set timer to expired state after the timeout callback method has been successfully invoked.
+     * only apply to
+     * 1, Single action timer
+     * 2, Calendar timer there are no future timeout.
      */
-    private boolean expired;    
+    private boolean expired;
 
-    public TimerData(long id, EjbTimerServiceImpl timerService, String deploymentId, Object primaryKey, Method timeoutMethod, TimerConfig timerConfig) {
+    public TimerData(final long id,
+                     final EjbTimerServiceImpl timerService,
+                     final String deploymentId,
+                     final Object primaryKey,
+                     final Method timeoutMethod,
+                     final TimerConfig timerConfig) {
         this.id = id;
         this.timerService = timerService;
         this.deploymentId = deploymentId;
@@ -162,7 +167,7 @@ public abstract class TimerData implemen
 
             final Method method = methodContext.getBeanMethod();
             if (method != null && method.getName().equals(mtd)) { // maybe we should check parameters too
-                timeoutMethod = method;
+                setTimeoutMethod(method);
                 break;
             }
         }
@@ -172,8 +177,8 @@ public abstract class TimerData implemen
         if (trigger != null) {
             try {
                 final Scheduler s = timerService.getScheduler();
-                
-                if(!s.isShutdown()) {
+
+                if (!s.isShutdown()) {
                     if (!isPersistent()) {
                         s.unscheduleJob(trigger.getKey());
                     } else {
@@ -219,7 +224,11 @@ public abstract class TimerData implemen
         trigger.setGroup(OPEN_EJB_TIMEOUT_TRIGGER_GROUP_NAME);
         trigger.setName(OPEN_EJB_TIMEOUT_TRIGGER_NAME_PREFIX + deploymentId + "_" + id);
         newTimer = true;
-        registerTimerDataSynchronization();
+        try {
+            registerTimerDataSynchronization();
+        } catch (TimerStoreException e) {
+            throw new EJBException("Failed to register new timer data synchronization", e);
+        }
     }
 
     public boolean isCancelled() {
@@ -235,8 +244,8 @@ public abstract class TimerData implemen
         if (trigger != null) {
             try {
                 final Scheduler s = timerService.getScheduler();
-                
-                if(!s.isShutdown()){
+
+                if (!s.isShutdown()) {
                     s.unscheduleJob(trigger.getKey());
                 }
             } catch (SchedulerException e) {
@@ -244,14 +253,22 @@ public abstract class TimerData implemen
             }
         }
         cancelled = true;
-        registerTimerDataSynchronization();
+        try {
+            registerTimerDataSynchronization();
+        } catch (TimerStoreException e) {
+            throw new EJBException("Failed to register timer data synchronization on cancel", e);
+        }
+    }
+
+    private void setTimeoutMethod(final Method timeoutMethod) {
+        this.timeoutMethod = timeoutMethod;
     }
 
     public Method getTimeoutMethod() {
         return timeoutMethod;
     }
 
-    private void transactionComplete(boolean committed) {
+    private void transactionComplete(final boolean committed) throws TimerStoreException {
         if (newTimer) {
             // you are only a new timer once no matter what
             newTimer = false;
@@ -271,12 +288,14 @@ public abstract class TimerData implemen
         }
     }
 
-    private void registerTimerDataSynchronization() {
-        if (synchronizationRegistered) return;
+    private void registerTimerDataSynchronization() throws TimerStoreException {
+        if (synchronizationRegistered) {
+            return;
+        }
 
         try {
-            Transaction transaction = timerService.getTransactionManager().getTransaction();
-            int status = transaction == null ? Status.STATUS_NO_TRANSACTION : transaction.getStatus();
+            final Transaction transaction = timerService.getTransactionManager().getTransaction();
+            final int status = transaction == null ? Status.STATUS_NO_TRANSACTION : transaction.getStatus();
 
             if (transaction != null && status == Status.STATUS_ACTIVE || status == Status.STATUS_MARKED_ROLLBACK) {
                 transaction.registerSynchronization(new TimerDataSynchronization());
@@ -296,24 +315,28 @@ public abstract class TimerData implemen
     }
 
     private class TimerDataSynchronization implements Synchronization {
+
         @Override
         public void beforeCompletion() {
         }
 
         @Override
-        public void afterCompletion(int status) {
+        public void afterCompletion(final int status) {
             synchronizationRegistered = false;
-            transactionComplete(status == Status.STATUS_COMMITTED);
+            try {
+                transactionComplete(status == Status.STATUS_COMMITTED);
+            } catch (TimerStoreException e) {
+                throw new EJBException("Failed on afterCompletion", e);
+            }
         }
     }
 
-    public boolean isPersistent(){
+    public boolean isPersistent() {
         return persistent;
     }
-    
-    
+
     public Trigger getTrigger() {
-        
+
         if (scheduler != null) {
             try {
                 final TriggerKey key = new TriggerKey(trigger.getName(), trigger.getGroup());
@@ -323,40 +346,40 @@ public abstract class TimerData implemen
             } catch (SchedulerException e) {
                 return null;
             }
-        } 
+        }
 
         return trigger;
     }
 
-    public Date getNextTimeout() {    
-        
+    public Date getNextTimeout() {
+
         try {
             // give the trigger 1 ms to init itself to set correct nextTimeout value.
             Thread.sleep(1);
         } catch (InterruptedException e) {
             log.warning("Interrupted exception when waiting 1ms for the trigger to init", e);
         }
-        
+
         Date nextTimeout = null;
-        
-        if(getTrigger()!=null){
-        
+
+        if (getTrigger() != null) {
+
             nextTimeout = getTrigger().getNextFireTime();
         }
-        
+
         return nextTimeout;
     }
 
     public long getTimeRemaining() {
-        Date nextTimeout = getNextTimeout();
+        final Date nextTimeout = getNextTimeout();
         return nextTimeout.getTime() - System.currentTimeMillis();
     }
-    
+
     public boolean isExpired() {
         return expired;
     }
 
-    public void setExpired(boolean expired){
+    public void setExpired(final boolean expired) {
         this.expired = expired;
     }
 

Modified: tomee/tomee/trunk/container/openejb-core/src/test/java/org/apache/openejb/timer/ScheduleTest.java
URL: http://svn.apache.org/viewvc/tomee/tomee/trunk/container/openejb-core/src/test/java/org/apache/openejb/timer/ScheduleTest.java?rev=1530883&r1=1530882&r2=1530883&view=diff
==============================================================================
--- tomee/tomee/trunk/container/openejb-core/src/test/java/org/apache/openejb/timer/ScheduleTest.java (original)
+++ tomee/tomee/trunk/container/openejb-core/src/test/java/org/apache/openejb/timer/ScheduleTest.java Thu Oct 10 09:00:42 2013
@@ -16,29 +16,14 @@
  */
 package org.apache.openejb.timer;
 
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-import javax.ejb.Local;
-import javax.ejb.Schedule;
-import javax.ejb.Stateful;
-import javax.ejb.Stateless;
-import javax.ejb.TimedObject;
-import javax.ejb.Timeout;
-import javax.interceptor.AroundTimeout;
-import javax.interceptor.InvocationContext;
-
 import junit.framework.TestCase;
-
 import org.apache.openejb.assembler.classic.Assembler;
 import org.apache.openejb.assembler.classic.EjbJarInfo;
 import org.apache.openejb.assembler.classic.ProxyFactoryInfo;
 import org.apache.openejb.assembler.classic.SecurityServiceInfo;
 import org.apache.openejb.assembler.classic.TransactionServiceInfo;
-import org.apache.openejb.core.LocalInitialContextFactory;
 import org.apache.openejb.config.ConfigurationFactory;
+import org.apache.openejb.core.LocalInitialContextFactory;
 import org.apache.openejb.jee.EjbJar;
 import org.apache.openejb.jee.NamedMethod;
 import org.apache.openejb.jee.StatefulBean;
@@ -47,6 +32,19 @@ import org.apache.openejb.jee.Timer;
 import org.apache.openejb.jee.TimerSchedule;
 import org.junit.Assert;
 
+import javax.ejb.Local;
+import javax.ejb.Schedule;
+import javax.ejb.Stateful;
+import javax.ejb.Stateless;
+import javax.ejb.TimedObject;
+import javax.ejb.Timeout;
+import javax.interceptor.AroundTimeout;
+import javax.interceptor.InvocationContext;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
 /**
  * @version $Rev$ $Date$
  */
@@ -58,20 +56,20 @@ public class ScheduleTest extends TestCa
     public void testSchedule() throws Exception {
 
         System.setProperty(javax.naming.Context.INITIAL_CONTEXT_FACTORY, LocalInitialContextFactory.class.getName());
-        Assembler assembler = new Assembler();
-        ConfigurationFactory config = new ConfigurationFactory();
+        final Assembler assembler = new Assembler();
+        final ConfigurationFactory config = new ConfigurationFactory();
 
         assembler.createProxyFactory(config.configureService(ProxyFactoryInfo.class));
         assembler.createTransactionManager(config.configureService(TransactionServiceInfo.class));
         assembler.createSecurityService(config.configureService(SecurityServiceInfo.class));
 
-        EjbJar ejbJar = new EjbJar();
+        final EjbJar ejbJar = new EjbJar();
 
         //Configure schedule by deployment plan
-        StatelessBean subBeanA = new StatelessBean(SubBeanA.class);
-        Timer subBeanATimer = new Timer();
+        final StatelessBean subBeanA = new StatelessBean(SubBeanA.class);
+        final Timer subBeanATimer = new Timer();
         subBeanATimer.setTimeoutMethod(new NamedMethod("subBeanA", "javax.ejb.Timer"));
-        TimerSchedule timerScheduleA = new TimerSchedule();
+        final TimerSchedule timerScheduleA = new TimerSchedule();
         timerScheduleA.setSecond("2");
         timerScheduleA.setMinute("*");
         timerScheduleA.setHour("*");
@@ -81,14 +79,14 @@ public class ScheduleTest extends TestCa
         ejbJar.addEnterpriseBean(subBeanA);
 
         //Configure schedule by annotation
-        StatelessBean subBeanB = new StatelessBean(SubBeanB.class);
+        final StatelessBean subBeanB = new StatelessBean(SubBeanB.class);
         ejbJar.addEnterpriseBean(subBeanB);
 
         //Override aroundTimeout annotation by deployment plan
-        StatelessBean subBeanC = new StatelessBean(SubBeanC.class);
-        Timer subBeanCTimer = new Timer();
+        final StatelessBean subBeanC = new StatelessBean(SubBeanC.class);
+        final Timer subBeanCTimer = new Timer();
         subBeanCTimer.setTimeoutMethod(new NamedMethod("subBeanC", "javax.ejb.Timer"));
-        TimerSchedule timerScheduleC = new TimerSchedule();
+        final TimerSchedule timerScheduleC = new TimerSchedule();
         timerScheduleC.setSecond("2");
         timerScheduleC.setMinute("*");
         timerScheduleC.setHour("*");
@@ -97,9 +95,9 @@ public class ScheduleTest extends TestCa
         subBeanC.getTimer().add(subBeanCTimer);
         ejbJar.addEnterpriseBean(subBeanC);
 
-        StatefulBean subBeanM = new StatefulBean(SubBeanM.class);
+        final StatefulBean subBeanM = new StatefulBean(SubBeanM.class);
         ejbJar.addEnterpriseBean(subBeanM);
-        EjbJarInfo ejbJarInfo = config.configureApplication(ejbJar);
+        final EjbJarInfo ejbJarInfo = config.configureApplication(ejbJar);
         assembler.createApplication(ejbJarInfo);
 
         countDownLatch.await(1L, TimeUnit.MINUTES);
@@ -108,13 +106,13 @@ public class ScheduleTest extends TestCa
         int beforeAroundInvocationCount = 0;
         int afterAroundInvocationCount = 0;
         int timeoutInvocationCount = 0;
-        int size = 0;
+        final int size;
 
         synchronized (result) {
 
             size = result.size();
 
-            for (Call call : result) {
+            for (final Call call : result) {
                 switch (call) {
                     case BEAN_BEFORE_AROUNDTIMEOUT:
                         beforeAroundInvocationCount++;
@@ -142,11 +140,12 @@ public class ScheduleTest extends TestCa
 
     public static class BaseBean implements BeanInterface {
 
+        @Override
         public void simpleMethod() {
         }
 
         @AroundTimeout
-        public Object beanTimeoutAround(InvocationContext context) throws Exception {
+        public Object beanTimeoutAround(final InvocationContext context) throws Exception {
             synchronized (result) {
                 assertNotNull(context.getTimer());
                 result.add(Call.BEAN_BEFORE_AROUNDTIMEOUT);
@@ -170,7 +169,7 @@ public class ScheduleTest extends TestCa
     @Local(BeanInterface.class)
     public static class SubBeanA extends BaseBean implements TimedObject {
 
-        public void subBeanA(javax.ejb.Timer timer) {
+        public void subBeanA(final javax.ejb.Timer timer) {
             synchronized (result) {
                 assertEquals("SubBeanAInfo", timer.getInfo());
                 result.add(Call.TIMEOUT);
@@ -178,7 +177,7 @@ public class ScheduleTest extends TestCa
         }
 
         @Override
-        public void ejbTimeout(javax.ejb.Timer arg0) {
+        public void ejbTimeout(final javax.ejb.Timer arg0) {
             Assert.fail("This method should not be invoked, we might confuse the auto-created timers and timeout timer");
         }
     }
@@ -188,7 +187,7 @@ public class ScheduleTest extends TestCa
     public static class SubBeanM extends BaseBean implements TimedObject {
 
         @Schedule(second = "2", minute = "*", hour = "*", info = "SubBeanBInfo")
-        public void subBeanA(javax.ejb.Timer timer) {
+        public void subBeanA(final javax.ejb.Timer timer) {
             synchronized (result) {
                 assertEquals("SubBeanAInfo", timer.getInfo());
                 result.add(Call.TIMEOUT);
@@ -196,7 +195,7 @@ public class ScheduleTest extends TestCa
         }
 
         @Override
-        public void ejbTimeout(javax.ejb.Timer arg0) {
+        public void ejbTimeout(final javax.ejb.Timer arg0) {
             fail("This method should not be invoked, we might confuse the auto-created timers and timeout timer");
         }
     }
@@ -206,7 +205,7 @@ public class ScheduleTest extends TestCa
     public static class SubBeanB extends BaseBean {
 
         @Schedule(second = "2", minute = "*", hour = "*", info = "SubBeanBInfo")
-        public void subBeanB(javax.ejb.Timer timer) {
+        public void subBeanB(final javax.ejb.Timer timer) {
             synchronized (result) {
                 assertEquals("SubBeanBInfo", timer.getInfo());
                 result.add(Call.TIMEOUT);
@@ -214,7 +213,7 @@ public class ScheduleTest extends TestCa
         }
 
         @Timeout
-        public void ejbT(javax.ejb.Timer timer) {
+        public void ejbT(final javax.ejb.Timer timer) {
             fail("This method should not be invoked, we might confuse the auto-created timers and timeout timer");
         }
     }
@@ -224,7 +223,7 @@ public class ScheduleTest extends TestCa
     public static class SubBeanC extends BaseBean {
 
         @Schedule(info = "badValue")
-        public void subBeanC(javax.ejb.Timer timer) {
+        public void subBeanC(final javax.ejb.Timer timer) {
             synchronized (result) {
                 assertEquals("SubBeanCInfo", timer.getInfo());
                 result.add(Call.TIMEOUT);
@@ -239,6 +238,10 @@ public class ScheduleTest extends TestCa
 
     public static enum Call {
 
-        BEAN_TIMEOUT, BEAN_BEFORE_AROUNDTIMEOUT, BEAN_AFTER_AROUNDTIMEOUT, BAD_VALUE, TIMEOUT
+        BEAN_TIMEOUT,
+        BEAN_BEFORE_AROUNDTIMEOUT,
+        BEAN_AFTER_AROUNDTIMEOUT,
+        BAD_VALUE,
+        TIMEOUT
     }
 }

Copied: tomee/tomee/trunk/container/openejb-core/src/test/java/org/apache/openejb/timer/SingleActionTimerTest.java (from r1530859, tomee/tomee/trunk/container/openejb-core/src/test/java/org/apache/openejb/timer/InitialIntervalTimerTest.java)
URL: http://svn.apache.org/viewvc/tomee/tomee/trunk/container/openejb-core/src/test/java/org/apache/openejb/timer/SingleActionTimerTest.java?p2=tomee/tomee/trunk/container/openejb-core/src/test/java/org/apache/openejb/timer/SingleActionTimerTest.java&p1=tomee/tomee/trunk/container/openejb-core/src/test/java/org/apache/openejb/timer/InitialIntervalTimerTest.java&r1=1530859&r2=1530883&rev=1530883&view=diff
==============================================================================
--- tomee/tomee/trunk/container/openejb-core/src/test/java/org/apache/openejb/timer/InitialIntervalTimerTest.java (original)
+++ tomee/tomee/trunk/container/openejb-core/src/test/java/org/apache/openejb/timer/SingleActionTimerTest.java Thu Oct 10 09:00:42 2013
@@ -35,54 +35,89 @@ import javax.ejb.Timeout;
 import javax.ejb.Timer;
 import javax.ejb.TimerConfig;
 import javax.ejb.TimerService;
+import java.util.concurrent.atomic.AtomicInteger;
 
-import static org.junit.Assert.assertEquals;
+import static junit.framework.TestCase.assertTrue;
 
 @RunWith(ApplicationComposer.class)
-public class InitialIntervalTimerTest {
+public class SingleActionTimerTest {
+
     @Module
     public EnterpriseBean bean() {
-        return new SingletonBean(TimerWithDelay.class).localBean();
+        return new SingletonBean(SingleActionTimer.class).localBean();
     }
 
     @EJB
-    private TimerWithDelay bean;
+    private SingleActionTimer bean;
 
     @Test
     public void test() throws InterruptedException {
-        Thread.sleep(5400);
-        assertEquals(3, bean.getOk());
+        Thread.sleep(1000);
+        assertTrue("TimerWithDelay: Failed to count more than once", this.bean.getCount() > 1);
     }
 
+    @SuppressWarnings("UseOfSystemOutOrSystemErr")
     @Singleton
     @Startup
     @Lock(LockType.READ)
-    public static class TimerWithDelay {
-        @Resource
-        private TimerService ts;
+    public static class SingleActionTimer {
 
+        private static final String TIMER_NAME = "SingleActionTimer";
+        private final AtomicInteger counter = new AtomicInteger(0);
         private Timer timer;
-        private int ok = 0;
+
+        @Resource
+        private TimerService timerService;
 
         @PostConstruct
-        public void start() {
-            timer = ts.createIntervalTimer(3000, 1000, new TimerConfig(System.currentTimeMillis(), false));
+        public void postConstruct() {
+
+            try {
+
+                this.createTimer();
+                System.out.println("SingleActionTimer: Started initial timer");
+
+            } catch (Exception e) {
+                throw new RuntimeException("SingleActionTimer: Failed to start initial timer", e);
+            }
+
         }
 
-        @Timeout
-        public void timeout(final Timer timer) {
-            final long actual = System.currentTimeMillis() - ((Long) timer.getInfo() + 1000 * ok + 3000);
-            assertEquals(0, actual, 500);
-            ok++;
+        @PreDestroy
+        public void preDestroy() {
+
+            if (null != this.timer) {
+                try {
+                    this.timer.cancel();
+                } catch (Throwable e) {
+                    //Ignore
+                }
+            }
         }
 
-        public int getOk() {
-            return ok;
+        private void createTimer() {
+            try {
+                this.timer = this.timerService.createSingleActionTimer(100, new TimerConfig(TIMER_NAME, false));
+            } catch (Exception e) {
+                throw new RuntimeException("SingleActionTimer: Failed to create timer", e);
+            }
         }
 
-        @PreDestroy
-        public void stop() {
-            timer.cancel();
+        @Timeout
+        public void programmaticTimeout(final Timer timer) {
+
+            if (!TIMER_NAME.equals(timer.getInfo())) {
+                return;
+            }
+
+            final int i = this.counter.incrementAndGet();
+            System.out.println("SingleActionTimer: Timeout " + i);
+
+            this.createTimer();
+        }
+
+        public int getCount() {
+            return this.counter.get();
         }
     }
 }