1 /*
2 * Copyright (C) 2009, Google Inc. and others
3 *
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Distribution License v. 1.0 which is available at
6 * https://www.eclipse.org/org/documents/edl-v10.php.
7 *
8 * SPDX-License-Identifier: BSD-3-Clause
9 */
10
11 package org.eclipse.jgit.util.io;
12
13 import java.text.MessageFormat;
14
15 import org.eclipse.jgit.internal.JGitText;
16
17 /**
18 * Triggers an interrupt on the calling thread if it doesn't complete a block.
19 * <p>
20 * Classes can use this to trip an alarm interrupting the calling thread if it
21 * doesn't complete a block within the specified timeout. Typical calling
22 * pattern is:
23 *
24 * <pre>
25 * private InterruptTimer myTimer = ...;
26 * void foo() {
27 * try {
28 * myTimer.begin(timeout);
29 * // work
30 * } finally {
31 * myTimer.end();
32 * }
33 * }
34 * </pre>
35 * <p>
36 * An InterruptTimer is not recursive. To implement recursive timers,
37 * independent InterruptTimer instances are required. A single InterruptTimer
38 * may be shared between objects which won't recursively call each other.
39 * <p>
40 * Each InterruptTimer spawns one background thread to sleep the specified time
41 * and interrupt the thread which called {@link #begin(int)}. It is up to the
42 * caller to ensure that the operations within the work block between the
43 * matched begin and end calls tests the interrupt flag (most IO operations do).
44 * <p>
45 * To terminate the background thread, use {@link #terminate()}. If the
46 * application fails to terminate the thread, it will (eventually) terminate
47 * itself when the InterruptTimer instance is garbage collected.
48 *
49 * @see TimeoutInputStream
50 */
51 public final class InterruptTimer {
52 private final AlarmState state;
53
54 private final AlarmThread thread;
55
56 final AutoKiller autoKiller;
57
58 /**
59 * Create a new timer with a default thread name.
60 */
61 public InterruptTimer() {
62 this("JGit-InterruptTimer"); //$NON-NLS-1$
63 }
64
65 /**
66 * Create a new timer to signal on interrupt on the caller.
67 * <p>
68 * The timer thread is created in the calling thread's ThreadGroup.
69 *
70 * @param threadName
71 * name of the timer thread.
72 */
73 public InterruptTimer(String threadName) {
74 state = new AlarmState();
75 autoKiller = new AutoKiller(state);
76 thread = new AlarmThread(threadName, state);
77 thread.start();
78 }
79
80 /**
81 * Arm the interrupt timer before entering a blocking operation.
82 *
83 * @param timeout
84 * number of milliseconds before the interrupt should trigger.
85 * Must be > 0.
86 */
87 public void begin(int timeout) {
88 if (timeout <= 0)
89 throw new IllegalArgumentException(MessageFormat.format(
90 JGitText.get().invalidTimeout, Integer.valueOf(timeout)));
91 Thread.interrupted();
92 state.begin(timeout);
93 }
94
95 /**
96 * Disable the interrupt timer, as the operation is complete.
97 */
98 public void end() {
99 state.end();
100 }
101
102 /**
103 * Shutdown the timer thread, and wait for it to terminate.
104 */
105 public void terminate() {
106 state.terminate();
107 try {
108 thread.join();
109 } catch (InterruptedException e) {
110 //
111 }
112 }
113
114 static final class AlarmThread extends Thread {
115 AlarmThread(String name, AlarmState q) {
116 super(q);
117 setName(name);
118 setDaemon(true);
119 }
120 }
121
122 // The trick here is, the AlarmThread does not have a reference to the
123 // AutoKiller instance, only the InterruptTimer itself does. Thus when
124 // the InterruptTimer is GC'd, the AutoKiller is also unreachable and
125 // can be GC'd. When it gets finalized, it tells the AlarmThread to
126 // terminate, triggering the thread to exit gracefully.
127 //
128 private static final class AutoKiller {
129 private final AlarmState state;
130
131 AutoKiller(AlarmState s) {
132 state = s;
133 }
134
135 @Override
136 protected void finalize() throws Throwable {
137 state.terminate();
138 }
139 }
140
141 static final class AlarmState implements Runnable {
142 private Thread callingThread;
143
144 private long deadline;
145
146 private boolean terminated;
147
148 AlarmState() {
149 callingThread = Thread.currentThread();
150 }
151
152 @Override
153 public synchronized void run() {
154 while (!terminated && callingThread.isAlive()) {
155 try {
156 if (0 < deadline) {
157 final long delay = deadline - now();
158 if (delay <= 0) {
159 deadline = 0;
160 callingThread.interrupt();
161 } else {
162 wait(delay);
163 }
164 } else {
165 wait(1000);
166 }
167 } catch (InterruptedException e) {
168 // Treat an interrupt as notice to examine state.
169 }
170 }
171 }
172
173 synchronized void begin(int timeout) {
174 if (terminated)
175 throw new IllegalStateException(JGitText.get().timerAlreadyTerminated);
176 callingThread = Thread.currentThread();
177 deadline = now() + timeout;
178 notifyAll();
179 }
180
181 synchronized void end() {
182 if (0 == deadline)
183 Thread.interrupted();
184 else
185 deadline = 0;
186 notifyAll();
187 }
188
189 synchronized void terminate() {
190 if (!terminated) {
191 deadline = 0;
192 terminated = true;
193 notifyAll();
194 }
195 }
196
197 private static long now() {
198 return System.currentTimeMillis();
199 }
200 }
201 }