blob: 4116cbefbbf2ffa771932352a317adf9a4d17087 [file] [log] [blame]
Aaron Kruglikov4ce0b042015-10-07 14:04:17 -07001/*
2 * Copyright 2015 Open Networking Laboratory
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package org.onlab.util;
18
19import com.google.common.collect.Lists;
20import org.onlab.junit.TestUtils;
21import org.slf4j.Logger;
22
23import java.util.Date;
24import java.util.Iterator;
25import java.util.LinkedList;
26import java.util.TimerTask;
27import java.util.concurrent.ExecutorService;
28import java.util.concurrent.Executors;
29
30import static com.google.common.base.Preconditions.checkNotNull;
31import static org.onlab.junit.TestTools.delay;
32import static org.slf4j.LoggerFactory.getLogger;
33
34
35/**
36 * Provides manually scheduled timer utility. All schedulable methods are subject to overflow (you can set a period of
37 * max long). Additionally if a skip skips a period of time greater than one period for a periodic task that task will
38 * only be executed once for that skip and scheduled it's period after the last execution.
39 */
40public class ManuallyAdvancingTimer extends java.util.Timer {
41
42 /* States whether or not the static values from timer task have been set ensures population will only occur once.*/
43 private boolean staticsPopulated = false;
44
45 /* Virgin value from timer task */
46 private int virginState;
47
48 /* Scheduled value from timer task */
49 private int scheduledState;
50
51 /* Executed value from timer task */
52 private int executedState;
53
54 /* Cancelled value from timer task */
55 private int cancelledState;
56
57 private final Logger logger = getLogger(getClass());
58
59 /* Service for executing timer tasks */
60 private final ExecutorService executorService = Executors.newSingleThreadExecutor();
61
62 /* Internal time representation independent of system time, manually advanced */
63 private final TimerKeeper timerKeeper = new TimerKeeper();
64
65 /* Data structure for tracking tasks */
66 private final TaskQueue queue = new TaskQueue();
67
68 @Override
69 public void schedule(TimerTask task, long delay) {
70 if (!staticsPopulated) {
71 populateStatics(task);
72 }
73 if (!submitTask(task, delay > 0 ? timerKeeper.currentTimeInMillis() + delay :
74 timerKeeper.currentTimeInMillis() - delay, 0)) {
75 logger.error("Failed to submit task");
76 }
77 }
78
79 @Override
80 public void schedule(TimerTask task, Date time) {
81 if (!staticsPopulated) {
82 populateStatics(task);
83 }
84 if (!submitTask(task, time.getTime(), 0)) {
85 logger.error("Failed to submit task");
86 }
87 }
88
89 @Override
90 public void schedule(TimerTask task, long delay, long period) {
91 if (!staticsPopulated) {
92 populateStatics(task);
93 }
94 if (!submitTask(task, delay > 0 ? timerKeeper.currentTimeInMillis() + delay :
95 timerKeeper.currentTimeInMillis() - delay, period)) {
96 logger.error("Failed to submit task");
97 }
98 }
99
100 @Override
101 public void schedule(TimerTask task, Date firstTime, long period) {
102 if (!staticsPopulated) {
103 populateStatics(task);
104 }
105 if (!submitTask(task, firstTime.getTime(), period)) {
106 logger.error("Failed to submit task");
107 }
108 }
109
110 /*################################################WARNING################################################*/
111 /* Schedule at fixed rate methods do not work exactly as in the java timer. They are clones of the periodic
112 *scheduling methods. */
113 @Override
114 public void scheduleAtFixedRate(TimerTask task, long delay, long period) {
115 if (!staticsPopulated) {
116 populateStatics(task);
117 }
118 if (!submitTask(task, delay > 0 ? timerKeeper.currentTimeInMillis() + delay :
119 timerKeeper.currentTimeInMillis() - delay, period)) {
120 logger.error("Failed to submit task");
121 }
122 }
123
124 @Override
125 public void scheduleAtFixedRate(TimerTask task, Date firstTime, long period) {
126 if (!staticsPopulated) {
127 populateStatics(task);
128 }
129 if (!submitTask(task, firstTime.getTime(), period)) {
130 logger.error("Failed to submit task");
131 }
132 }
133
134 @Override
135 public void cancel() {
136 executorService.shutdown();
137 queue.clear();
138 }
139
140 @Override
141 public int purge() {
142 return queue.removeCancelled();
143 }
144
145 /**
146 * Returns the virtual current time in millis.
147 *
148 * @return long representing simulated current time.
149 */
150 public long currentTimeInMillis() {
151 return timerKeeper.currentTimeInMillis();
152 }
153
154 /**
155 * Returns the new simulated current time in millis after advancing the absolute value of millis to advance.
156 * Triggers event execution of all events scheduled for execution at times up to and including the returned time.
157 * Passing in the number zero has no effect.
158 *
159 * @param millisToAdvance the number of millis to advance.
160 * @return a long representing the current simulated time in millis
161 */
162 public long advanceTimeMillis(long millisToAdvance) {
163 return timerKeeper.advanceTimeMillis(millisToAdvance);
164 }
165
166 /**
167 * Advances the virtual time a certain number of millis triggers execution delays a certain amount to
168 * allow time for execution.
169 *
170 * @param virtualTimeAdvance the time to be advances in millis of simulated time.
171 * @param realTimeDelay the time to delay in real time to allow for processing.
172 */
173 public void advanceTimeMillis(long virtualTimeAdvance, int realTimeDelay) {
174 timerKeeper.advanceTimeMillis(virtualTimeAdvance);
175 delay(realTimeDelay);
176 }
177
178 /**
179 * Sets up the task and submits it to the queue.
180 *
181 * @param task the task to be added to the queue
182 * @param runtime the first runtime of the task
183 * @param period the period between runs thereafter
184 * @return returns true if the task was successfully submitted, false otherwise
185 */
186 private boolean submitTask(TimerTask task, long runtime, long period) {
187 checkNotNull(task);
188 try {
189 TestUtils.setField(task, "state", scheduledState);
190 TestUtils.setField(task, "nextExecutionTime", runtime);
191 TestUtils.setField(task, "period", period);
192 } catch (TestUtils.TestUtilsException e) {
193 e.printStackTrace();
194 return false;
195 }
196 queue.insertOrdered(task);
197 return true;
198 }
199
200 /**
201 * Executes the given task (only if it is in the scheduled state) and proceeds to reschedule it or mark it as
202 * executed. Does not remove from the queue (this must be done outside).
203 *
204 * @param task the timer task to be executed
205 */
206 private boolean executeTask(TimerTask task) {
207 checkNotNull(task);
208 int currentState;
209 try {
210 currentState = TestUtils.getField(task, "state");
211 } catch (TestUtils.TestUtilsException e) {
212 logger.error("Could not get state of task.");
213 e.printStackTrace();
214 return false;
215 }
216 //If cancelled or already executed stop here.
217 if (currentState == executedState || currentState == cancelledState) {
218 return false;
219 } else if (currentState == virginState) {
220 logger.error("Task was set for execution without being scheduled.");
221 return false;
222 } else if (currentState == scheduledState) {
223 long period;
224
225 try {
226 period = TestUtils.getField(task, "period");
227 } catch (TestUtils.TestUtilsException e) {
228 logger.error("Could not read period of task.");
229 e.printStackTrace();
230 return false;
231 }
232 //Period of zero means one time execution.
233 if (period == 0) {
234 try {
235 TestUtils.setField(task, "state", executedState);
236 } catch (TestUtils.TestUtilsException e) {
237 logger.error("Could not set executed state.");
238 e.printStackTrace();
239 return false;
240 }
241 executorService.execute(task);
242 return true;
243 } else {
244 //Calculate next execution time, using absolute value of period
245 long nextTime = (period > 0) ? (timerKeeper.currentTimeInMillis() + period) :
246 (timerKeeper.currentTimeInMillis() - period);
247 try {
248 TestUtils.setField(task, "nextExecutionTime", nextTime);
249 } catch (TestUtils.TestUtilsException e) {
250 logger.error("Could not set next execution time.");
251 e.printStackTrace();
252 return false;
253 }
254 //Schedule next execution
255 queue.insertOrdered(task);
256 executorService.execute(task);
257 return true;
258 }
259 }
260 logger.error("State property of {} is in an illegal state and did not execute.", task);
261 return false;
262 }
263
264 /**
265 * Executes all tasks in the queue scheduled for execution up to and including the current time.
266 *
267 * @return the total number of tasks run, -1 if failure
268 */
269 private int executeEventsUpToPresent() {
270 int totalRun = 0;
271 if (queue.isEmpty()) {
272 return -1;
273 }
274 TimerTask currTask = queue.peek();
275 long currExecTime;
276 try {
277 currExecTime = TestUtils.getField(currTask, "nextExecutionTime");
278 } catch (TestUtils.TestUtilsException e) {
279 e.printStackTrace();
280 throw new RuntimeException("Could not get nextExecutionTime");
281 }
282 while (currExecTime <= timerKeeper.currentTimeInMillis()) {
283 if (executeTask(queue.pop())) {
284 totalRun++;
285 }
286 if (queue.isEmpty()) {
287 break;
288 }
289 currTask = queue.peek();
290 try {
291 currExecTime = TestUtils.getField(currTask, "nextExecutionTime");
292 } catch (TestUtils.TestUtilsException e) {
293 e.printStackTrace();
294 throw new RuntimeException("Could not get nextExecutionTime");
295 }
296 }
297 return totalRun;
298 }
299
300 /**
301 * Populates the static fields from timer task. Should only be called once.
302 */
303 private void populateStatics(TimerTask task) {
304 try {
305 virginState = TestUtils.getField(task, "VIRGIN");
306 scheduledState = TestUtils.getField(task, "SCHEDULED");
307 executedState = TestUtils.getField(task, "EXECUTED");
308 cancelledState = TestUtils.getField(task, "CANCELLED");
309 staticsPopulated = true;
310 } catch (TestUtils.TestUtilsException e) {
311 e.printStackTrace();
312 }
313 }
314
315 /**
316 * A class used to maintain the virtual time.
317 */
318 private class TimerKeeper {
319
320 private long currentTime = 0;
321
322 /**
323 * Returns the virtual current time in millis.
324 *
325 * @return long representing simulated current time.
326 */
327 long currentTimeInMillis() {
328 return currentTime;
329 }
330
331 /**
332 * Returns the new simulated current time in millis after advancing the absolute value of millis to advance.
333 * Triggers event execution of all events scheduled for execution at times up to and including the returned
334 * time. Passing in the number zero has no effect.
335 *
336 * @param millisToAdvance the number of millis to advance.
337 * @return a long representing the current simulated time in millis
338 */
339 long advanceTimeMillis(long millisToAdvance) {
340 currentTime = (millisToAdvance >= 0) ? (currentTime + millisToAdvance) : (currentTime - millisToAdvance);
341 if (millisToAdvance != 0) {
342 executeEventsUpToPresent();
343 }
344 return currentTime;
345 }
346 }
347
348 /**
349 * A queue backed by a linked list. Keeps elements sorted in ascending order of execution time. All calls are safe
350 * even on empty queue's.
351 */
352 private class TaskQueue {
353 private final LinkedList<TimerTask> taskList = Lists.newLinkedList();
354
355 /**
356 * Adds the task to the queue in ascending order of scheduled execution. If execution time has already passed
357 * execute immediately.
358 *
359 * @param task the task to be added to the queue
360 */
361 void insertOrdered(TimerTask task) {
362 //Using O(N) insertion because random access is expensive in linked lists worst case is 2N links followed
363 // for binary insertion vs N for simple insertion.
364 checkNotNull(task);
365 if (!staticsPopulated) {
366 populateStatics(task);
367 }
368 long insertTime;
369 try {
370 insertTime = TestUtils.getField(task, "nextExecutionTime");
371 TestUtils.setField(task, "state", scheduledState);
372 } catch (TestUtils.TestUtilsException e) {
373 e.printStackTrace();
374 return;
375 }
376 //If the task was scheduled in the past or for the current time run it immediately and do not add to the
377 // queue, subsequent executions will be scheduled as normal
378 if (insertTime <= timerKeeper.currentTimeInMillis()) {
379 executeTask(task);
380 return;
381 }
382
383 Iterator<TimerTask> iter = taskList.iterator();
384 int positionCounter = 0;
385 long nextTaskTime;
386 TimerTask currentTask;
387 while (iter.hasNext()) {
388 currentTask = iter.next();
389 try {
390 nextTaskTime = TestUtils.getField(currentTask, "nextExecutionTime");
391 } catch (TestUtils.TestUtilsException e) {
392 e.printStackTrace();
393 return;
394 }
395 if (insertTime < nextTaskTime) {
396 taskList.add(positionCounter, task);
397 return;
398 }
399 positionCounter++;
400 }
401 taskList.addLast(task);
402 }
403
404 /**
405 * Returns the first item in the queue (next scheduled for execution) without removing it, returns null if the
406 * queue is empty.
407 *
408 * @return the next TimerTask to run or null if the queue is empty
409 */
410 TimerTask peek() {
411 if (taskList.isEmpty()) {
412 return null;
413 }
414 return taskList.getFirst();
415 }
416
417 /**
418 * Returns and removes the first item in the queue or null if it is empty.
419 *
420 * @return the first element of the queue or null if the queue is empty
421 */
422 TimerTask pop() {
423 if (taskList.isEmpty()) {
424 return null;
425 }
426 return taskList.pop();
427 }
428
429 /**
430 * Performs a sort on the set of timer tasks, earliest task is first. Does nothing if queue is empty.
431 */
432 void sort() {
433 if (taskList.isEmpty()) {
434 return;
435 }
436 taskList.sort((o1, o2) -> {
437 checkNotNull(o1);
438 checkNotNull(o2);
439 long executionTimeOne;
440 long executionTimeTwo;
441 try {
442 executionTimeOne = TestUtils.getField(o1, "nextExecutionTime");
443 executionTimeTwo = TestUtils.getField(o2, "nextExecutionTime");
444 } catch (TestUtils.TestUtilsException e) {
445 e.printStackTrace();
446 throw new RuntimeException("Could not get next execution time.");
447 }
448 if (executionTimeOne == executionTimeTwo) {
449 return 0;
450 } else if (executionTimeOne < executionTimeTwo) {
451 return -1;
452 } else {
453 return 1;
454 }
455 });
456 }
457
458 /**
459 * Returns whether the queue is currently empty.
460 *
461 * @return true if the queue is empty, false otherwise
462 */
463 boolean isEmpty() {
464 return taskList.isEmpty();
465 }
466
467 /**
468 * Clears the underlying list of the queue.
469 */
470 void clear() {
471 taskList.clear();
472 }
473
474 /**
475 * Removes all cancelled tasks from the queue. Has no effect on behavior.
476 *
477 * @return returns the total number of items removed, -1 if list is empty or failure occurs.
478 */
479 int removeCancelled() {
480 if (taskList.isEmpty()) {
481 return -1;
482 }
483 int removedCount = 0;
484 Iterator<TimerTask> taskIterator = taskList.iterator();
485 TimerTask currTask;
486 int currState;
487 while (taskIterator.hasNext()) {
488 currTask = taskIterator.next();
489 try {
490 currState = TestUtils.getField(currTask, "state");
491 } catch (TestUtils.TestUtilsException e) {
492 logger.error("Could not get task state.");
493 e.printStackTrace();
494 return -1;
495 }
496 if (currState == cancelledState) {
497 removedCount++;
498 taskIterator.remove();
499 }
500 }
501 return removedCount;
502 }
503 }
504}