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