1package org.mockitousage.verification;
2
3import static java.lang.System.currentTimeMillis;
4import static java.lang.Thread.MAX_PRIORITY;
5import static java.util.concurrent.Executors.newScheduledThreadPool;
6import static java.util.concurrent.TimeUnit.SECONDS;
7import static java.util.concurrent.locks.LockSupport.parkUntil;
8
9import java.util.concurrent.ScheduledExecutorService;
10import java.util.concurrent.ThreadFactory;
11import java.util.concurrent.TimeUnit;
12
13class DelayedExecution {
14    private static final int CORE_POOL_SIZE = 3;
15    /**
16     * Defines the number of milliseconds we expecting a Thread might need to unpark, we use this to avoid "oversleeping" while awaiting the deadline for
17     */
18    private static final long MAX_EXPECTED_OVERSLEEP_MILLIS = 50;
19
20    private final ScheduledExecutorService executor;
21
22    public DelayedExecution() {
23        this.executor = newScheduledThreadPool(CORE_POOL_SIZE, maxPrioThreadFactory());
24    }
25
26    public void callAsync(long delay, TimeUnit timeUnit, Runnable r) {
27        long deadline = timeUnit.toMillis(delay) + currentTimeMillis();
28
29        executor.submit(delayedExecution(r, deadline));
30    }
31
32    public void close() throws InterruptedException {
33        executor.shutdownNow();
34
35        if (!executor.awaitTermination(5, SECONDS)) {
36            throw new IllegalStateException("This delayed excution did not terminated after 5 seconds");
37        }
38    }
39
40    private static Runnable delayedExecution(final Runnable r, final long deadline) {
41        return new Runnable() {
42            @Override
43            public void run() {
44                //we park the current Thread till 50ms before we want to execute the runnable
45                parkUntil(deadline - MAX_EXPECTED_OVERSLEEP_MILLIS);
46                //now we closing to the deadline by burning CPU-time in a loop
47                burnRemaining(deadline);
48
49                System.out.println("[DelayedExecution] exec delay = "+(currentTimeMillis() - deadline)+"ms");
50
51                r.run();
52            }
53
54            /**
55             * Loop in tight cycles until we reach the dead line. We do this cause sleep or park is very not precise,
56             * this can causes a Thread to under- or oversleep, sometimes by +50ms.
57             */
58            private void burnRemaining(final long deadline) {
59                long remaining;
60                do {
61                    remaining = deadline - currentTimeMillis();
62                } while (remaining > 0);
63            }
64        };
65    }
66
67    private static ThreadFactory maxPrioThreadFactory() {
68        return new ThreadFactory() {
69            @Override
70            public Thread newThread(Runnable r) {
71                Thread t = new Thread(r);
72                t.setDaemon(true);  // allows the JVM to exit when clients forget to call DelayedExecution.close()
73                t.setPriority(MAX_PRIORITY);
74                return t;
75            }
76        };
77    }
78}
79