package org.mockitousage.verification; import static java.lang.System.currentTimeMillis; import static java.lang.Thread.MAX_PRIORITY; import static java.util.concurrent.Executors.newScheduledThreadPool; import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.concurrent.locks.LockSupport.parkUntil; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; class DelayedExecution { private static final int CORE_POOL_SIZE = 3; /** * Defines the number of milliseconds we expecting a Thread might need to unpark, we use this to avoid "oversleeping" while awaiting the deadline for */ private static final long MAX_EXPECTED_OVERSLEEP_MILLIS = 50; private final ScheduledExecutorService executor; public DelayedExecution() { this.executor = newScheduledThreadPool(CORE_POOL_SIZE, maxPrioThreadFactory()); } public void callAsync(long delay, TimeUnit timeUnit, Runnable r) { long deadline = timeUnit.toMillis(delay) + currentTimeMillis(); executor.submit(delayedExecution(r, deadline)); } public void close() throws InterruptedException { executor.shutdownNow(); if (!executor.awaitTermination(5, SECONDS)) { throw new IllegalStateException("This delayed excution did not terminated after 5 seconds"); } } private static Runnable delayedExecution(final Runnable r, final long deadline) { return new Runnable() { @Override public void run() { //we park the current Thread till 50ms before we want to execute the runnable parkUntil(deadline - MAX_EXPECTED_OVERSLEEP_MILLIS); //now we closing to the deadline by burning CPU-time in a loop burnRemaining(deadline); System.out.println("[DelayedExecution] exec delay = "+(currentTimeMillis() - deadline)+"ms"); r.run(); } /** * Loop in tight cycles until we reach the dead line. We do this cause sleep or park is very not precise, * this can causes a Thread to under- or oversleep, sometimes by +50ms. */ private void burnRemaining(final long deadline) { long remaining; do { remaining = deadline - currentTimeMillis(); } while (remaining > 0); } }; } private static ThreadFactory maxPrioThreadFactory() { return new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread t = new Thread(r); t.setDaemon(true); // allows the JVM to exit when clients forget to call DelayedExecution.close() t.setPriority(MAX_PRIORITY); return t; } }; } }