diff options
Diffstat (limited to 'framework/src/onos/utils/misc/src/test/java/org/onlab/util/ManuallyAdvancingTimer.java')
-rw-r--r-- | framework/src/onos/utils/misc/src/test/java/org/onlab/util/ManuallyAdvancingTimer.java | 504 |
1 files changed, 504 insertions, 0 deletions
diff --git a/framework/src/onos/utils/misc/src/test/java/org/onlab/util/ManuallyAdvancingTimer.java b/framework/src/onos/utils/misc/src/test/java/org/onlab/util/ManuallyAdvancingTimer.java new file mode 100644 index 00000000..4116cbef --- /dev/null +++ b/framework/src/onos/utils/misc/src/test/java/org/onlab/util/ManuallyAdvancingTimer.java @@ -0,0 +1,504 @@ +/* + * Copyright 2015 Open Networking Laboratory + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.onlab.util; + +import com.google.common.collect.Lists; +import org.onlab.junit.TestUtils; +import org.slf4j.Logger; + +import java.util.Date; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.TimerTask; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.onlab.junit.TestTools.delay; +import static org.slf4j.LoggerFactory.getLogger; + + +/** + * Provides manually scheduled timer utility. All schedulable methods are subject to overflow (you can set a period of + * max long). Additionally if a skip skips a period of time greater than one period for a periodic task that task will + * only be executed once for that skip and scheduled it's period after the last execution. + */ +public class ManuallyAdvancingTimer extends java.util.Timer { + + /* States whether or not the static values from timer task have been set ensures population will only occur once.*/ + private boolean staticsPopulated = false; + + /* Virgin value from timer task */ + private int virginState; + + /* Scheduled value from timer task */ + private int scheduledState; + + /* Executed value from timer task */ + private int executedState; + + /* Cancelled value from timer task */ + private int cancelledState; + + private final Logger logger = getLogger(getClass()); + + /* Service for executing timer tasks */ + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + + /* Internal time representation independent of system time, manually advanced */ + private final TimerKeeper timerKeeper = new TimerKeeper(); + + /* Data structure for tracking tasks */ + private final TaskQueue queue = new TaskQueue(); + + @Override + public void schedule(TimerTask task, long delay) { + if (!staticsPopulated) { + populateStatics(task); + } + if (!submitTask(task, delay > 0 ? timerKeeper.currentTimeInMillis() + delay : + timerKeeper.currentTimeInMillis() - delay, 0)) { + logger.error("Failed to submit task"); + } + } + + @Override + public void schedule(TimerTask task, Date time) { + if (!staticsPopulated) { + populateStatics(task); + } + if (!submitTask(task, time.getTime(), 0)) { + logger.error("Failed to submit task"); + } + } + + @Override + public void schedule(TimerTask task, long delay, long period) { + if (!staticsPopulated) { + populateStatics(task); + } + if (!submitTask(task, delay > 0 ? timerKeeper.currentTimeInMillis() + delay : + timerKeeper.currentTimeInMillis() - delay, period)) { + logger.error("Failed to submit task"); + } + } + + @Override + public void schedule(TimerTask task, Date firstTime, long period) { + if (!staticsPopulated) { + populateStatics(task); + } + if (!submitTask(task, firstTime.getTime(), period)) { + logger.error("Failed to submit task"); + } + } + + /*################################################WARNING################################################*/ + /* Schedule at fixed rate methods do not work exactly as in the java timer. They are clones of the periodic + *scheduling methods. */ + @Override + public void scheduleAtFixedRate(TimerTask task, long delay, long period) { + if (!staticsPopulated) { + populateStatics(task); + } + if (!submitTask(task, delay > 0 ? timerKeeper.currentTimeInMillis() + delay : + timerKeeper.currentTimeInMillis() - delay, period)) { + logger.error("Failed to submit task"); + } + } + + @Override + public void scheduleAtFixedRate(TimerTask task, Date firstTime, long period) { + if (!staticsPopulated) { + populateStatics(task); + } + if (!submitTask(task, firstTime.getTime(), period)) { + logger.error("Failed to submit task"); + } + } + + @Override + public void cancel() { + executorService.shutdown(); + queue.clear(); + } + + @Override + public int purge() { + return queue.removeCancelled(); + } + + /** + * Returns the virtual current time in millis. + * + * @return long representing simulated current time. + */ + public long currentTimeInMillis() { + return timerKeeper.currentTimeInMillis(); + } + + /** + * Returns the new simulated current time in millis after advancing the absolute value of millis to advance. + * Triggers event execution of all events scheduled for execution at times up to and including the returned time. + * Passing in the number zero has no effect. + * + * @param millisToAdvance the number of millis to advance. + * @return a long representing the current simulated time in millis + */ + public long advanceTimeMillis(long millisToAdvance) { + return timerKeeper.advanceTimeMillis(millisToAdvance); + } + + /** + * Advances the virtual time a certain number of millis triggers execution delays a certain amount to + * allow time for execution. + * + * @param virtualTimeAdvance the time to be advances in millis of simulated time. + * @param realTimeDelay the time to delay in real time to allow for processing. + */ + public void advanceTimeMillis(long virtualTimeAdvance, int realTimeDelay) { + timerKeeper.advanceTimeMillis(virtualTimeAdvance); + delay(realTimeDelay); + } + + /** + * Sets up the task and submits it to the queue. + * + * @param task the task to be added to the queue + * @param runtime the first runtime of the task + * @param period the period between runs thereafter + * @return returns true if the task was successfully submitted, false otherwise + */ + private boolean submitTask(TimerTask task, long runtime, long period) { + checkNotNull(task); + try { + TestUtils.setField(task, "state", scheduledState); + TestUtils.setField(task, "nextExecutionTime", runtime); + TestUtils.setField(task, "period", period); + } catch (TestUtils.TestUtilsException e) { + e.printStackTrace(); + return false; + } + queue.insertOrdered(task); + return true; + } + + /** + * Executes the given task (only if it is in the scheduled state) and proceeds to reschedule it or mark it as + * executed. Does not remove from the queue (this must be done outside). + * + * @param task the timer task to be executed + */ + private boolean executeTask(TimerTask task) { + checkNotNull(task); + int currentState; + try { + currentState = TestUtils.getField(task, "state"); + } catch (TestUtils.TestUtilsException e) { + logger.error("Could not get state of task."); + e.printStackTrace(); + return false; + } + //If cancelled or already executed stop here. + if (currentState == executedState || currentState == cancelledState) { + return false; + } else if (currentState == virginState) { + logger.error("Task was set for execution without being scheduled."); + return false; + } else if (currentState == scheduledState) { + long period; + + try { + period = TestUtils.getField(task, "period"); + } catch (TestUtils.TestUtilsException e) { + logger.error("Could not read period of task."); + e.printStackTrace(); + return false; + } + //Period of zero means one time execution. + if (period == 0) { + try { + TestUtils.setField(task, "state", executedState); + } catch (TestUtils.TestUtilsException e) { + logger.error("Could not set executed state."); + e.printStackTrace(); + return false; + } + executorService.execute(task); + return true; + } else { + //Calculate next execution time, using absolute value of period + long nextTime = (period > 0) ? (timerKeeper.currentTimeInMillis() + period) : + (timerKeeper.currentTimeInMillis() - period); + try { + TestUtils.setField(task, "nextExecutionTime", nextTime); + } catch (TestUtils.TestUtilsException e) { + logger.error("Could not set next execution time."); + e.printStackTrace(); + return false; + } + //Schedule next execution + queue.insertOrdered(task); + executorService.execute(task); + return true; + } + } + logger.error("State property of {} is in an illegal state and did not execute.", task); + return false; + } + + /** + * Executes all tasks in the queue scheduled for execution up to and including the current time. + * + * @return the total number of tasks run, -1 if failure + */ + private int executeEventsUpToPresent() { + int totalRun = 0; + if (queue.isEmpty()) { + return -1; + } + TimerTask currTask = queue.peek(); + long currExecTime; + try { + currExecTime = TestUtils.getField(currTask, "nextExecutionTime"); + } catch (TestUtils.TestUtilsException e) { + e.printStackTrace(); + throw new RuntimeException("Could not get nextExecutionTime"); + } + while (currExecTime <= timerKeeper.currentTimeInMillis()) { + if (executeTask(queue.pop())) { + totalRun++; + } + if (queue.isEmpty()) { + break; + } + currTask = queue.peek(); + try { + currExecTime = TestUtils.getField(currTask, "nextExecutionTime"); + } catch (TestUtils.TestUtilsException e) { + e.printStackTrace(); + throw new RuntimeException("Could not get nextExecutionTime"); + } + } + return totalRun; + } + + /** + * Populates the static fields from timer task. Should only be called once. + */ + private void populateStatics(TimerTask task) { + try { + virginState = TestUtils.getField(task, "VIRGIN"); + scheduledState = TestUtils.getField(task, "SCHEDULED"); + executedState = TestUtils.getField(task, "EXECUTED"); + cancelledState = TestUtils.getField(task, "CANCELLED"); + staticsPopulated = true; + } catch (TestUtils.TestUtilsException e) { + e.printStackTrace(); + } + } + + /** + * A class used to maintain the virtual time. + */ + private class TimerKeeper { + + private long currentTime = 0; + + /** + * Returns the virtual current time in millis. + * + * @return long representing simulated current time. + */ + long currentTimeInMillis() { + return currentTime; + } + + /** + * Returns the new simulated current time in millis after advancing the absolute value of millis to advance. + * Triggers event execution of all events scheduled for execution at times up to and including the returned + * time. Passing in the number zero has no effect. + * + * @param millisToAdvance the number of millis to advance. + * @return a long representing the current simulated time in millis + */ + long advanceTimeMillis(long millisToAdvance) { + currentTime = (millisToAdvance >= 0) ? (currentTime + millisToAdvance) : (currentTime - millisToAdvance); + if (millisToAdvance != 0) { + executeEventsUpToPresent(); + } + return currentTime; + } + } + + /** + * A queue backed by a linked list. Keeps elements sorted in ascending order of execution time. All calls are safe + * even on empty queue's. + */ + private class TaskQueue { + private final LinkedList<TimerTask> taskList = Lists.newLinkedList(); + + /** + * Adds the task to the queue in ascending order of scheduled execution. If execution time has already passed + * execute immediately. + * + * @param task the task to be added to the queue + */ + void insertOrdered(TimerTask task) { + //Using O(N) insertion because random access is expensive in linked lists worst case is 2N links followed + // for binary insertion vs N for simple insertion. + checkNotNull(task); + if (!staticsPopulated) { + populateStatics(task); + } + long insertTime; + try { + insertTime = TestUtils.getField(task, "nextExecutionTime"); + TestUtils.setField(task, "state", scheduledState); + } catch (TestUtils.TestUtilsException e) { + e.printStackTrace(); + return; + } + //If the task was scheduled in the past or for the current time run it immediately and do not add to the + // queue, subsequent executions will be scheduled as normal + if (insertTime <= timerKeeper.currentTimeInMillis()) { + executeTask(task); + return; + } + + Iterator<TimerTask> iter = taskList.iterator(); + int positionCounter = 0; + long nextTaskTime; + TimerTask currentTask; + while (iter.hasNext()) { + currentTask = iter.next(); + try { + nextTaskTime = TestUtils.getField(currentTask, "nextExecutionTime"); + } catch (TestUtils.TestUtilsException e) { + e.printStackTrace(); + return; + } + if (insertTime < nextTaskTime) { + taskList.add(positionCounter, task); + return; + } + positionCounter++; + } + taskList.addLast(task); + } + + /** + * Returns the first item in the queue (next scheduled for execution) without removing it, returns null if the + * queue is empty. + * + * @return the next TimerTask to run or null if the queue is empty + */ + TimerTask peek() { + if (taskList.isEmpty()) { + return null; + } + return taskList.getFirst(); + } + + /** + * Returns and removes the first item in the queue or null if it is empty. + * + * @return the first element of the queue or null if the queue is empty + */ + TimerTask pop() { + if (taskList.isEmpty()) { + return null; + } + return taskList.pop(); + } + + /** + * Performs a sort on the set of timer tasks, earliest task is first. Does nothing if queue is empty. + */ + void sort() { + if (taskList.isEmpty()) { + return; + } + taskList.sort((o1, o2) -> { + checkNotNull(o1); + checkNotNull(o2); + long executionTimeOne; + long executionTimeTwo; + try { + executionTimeOne = TestUtils.getField(o1, "nextExecutionTime"); + executionTimeTwo = TestUtils.getField(o2, "nextExecutionTime"); + } catch (TestUtils.TestUtilsException e) { + e.printStackTrace(); + throw new RuntimeException("Could not get next execution time."); + } + if (executionTimeOne == executionTimeTwo) { + return 0; + } else if (executionTimeOne < executionTimeTwo) { + return -1; + } else { + return 1; + } + }); + } + + /** + * Returns whether the queue is currently empty. + * + * @return true if the queue is empty, false otherwise + */ + boolean isEmpty() { + return taskList.isEmpty(); + } + + /** + * Clears the underlying list of the queue. + */ + void clear() { + taskList.clear(); + } + + /** + * Removes all cancelled tasks from the queue. Has no effect on behavior. + * + * @return returns the total number of items removed, -1 if list is empty or failure occurs. + */ + int removeCancelled() { + if (taskList.isEmpty()) { + return -1; + } + int removedCount = 0; + Iterator<TimerTask> taskIterator = taskList.iterator(); + TimerTask currTask; + int currState; + while (taskIterator.hasNext()) { + currTask = taskIterator.next(); + try { + currState = TestUtils.getField(currTask, "state"); + } catch (TestUtils.TestUtilsException e) { + logger.error("Could not get task state."); + e.printStackTrace(); + return -1; + } + if (currState == cancelledState) { + removedCount++; + taskIterator.remove(); + } + } + return removedCount; + } + } +} |