| /* | |
| * Copyright (C) 2009, Google Inc. and others | |
| * | |
| * This program and the accompanying materials are made available under the | |
| * terms of the Eclipse Distribution License v. 1.0 which is available at | |
| * https://www.eclipse.org/org/documents/edl-v10.php. | |
| * | |
| * SPDX-License-Identifier: BSD-3-Clause | |
| */ | |
| package org.eclipse.jgit.util.io; | |
| import java.text.MessageFormat; | |
| import org.eclipse.jgit.internal.JGitText; | |
| /** | |
| * Triggers an interrupt on the calling thread if it doesn't complete a block. | |
| * <p> | |
| * Classes can use this to trip an alarm interrupting the calling thread if it | |
| * doesn't complete a block within the specified timeout. Typical calling | |
| * pattern is: | |
| * | |
| * <pre> | |
| * private InterruptTimer myTimer = ...; | |
| * void foo() { | |
| * try { | |
| * myTimer.begin(timeout); | |
| * // work | |
| * } finally { | |
| * myTimer.end(); | |
| * } | |
| * } | |
| * </pre> | |
| * <p> | |
| * An InterruptTimer is not recursive. To implement recursive timers, | |
| * independent InterruptTimer instances are required. A single InterruptTimer | |
| * may be shared between objects which won't recursively call each other. | |
| * <p> | |
| * Each InterruptTimer spawns one background thread to sleep the specified time | |
| * and interrupt the thread which called {@link #begin(int)}. It is up to the | |
| * caller to ensure that the operations within the work block between the | |
| * matched begin and end calls tests the interrupt flag (most IO operations do). | |
| * <p> | |
| * To terminate the background thread, use {@link #terminate()}. If the | |
| * application fails to terminate the thread, it will (eventually) terminate | |
| * itself when the InterruptTimer instance is garbage collected. | |
| * | |
| * @see TimeoutInputStream | |
| */ | |
| public final class InterruptTimer { | |
| private final AlarmState state; | |
| private final AlarmThread thread; | |
| final AutoKiller autoKiller; | |
| /** | |
| * Create a new timer with a default thread name. | |
| */ | |
| public InterruptTimer() { | |
| this("JGit-InterruptTimer"); //$NON-NLS-1$ | |
| } | |
| /** | |
| * Create a new timer to signal on interrupt on the caller. | |
| * <p> | |
| * The timer thread is created in the calling thread's ThreadGroup. | |
| * | |
| * @param threadName | |
| * name of the timer thread. | |
| */ | |
| public InterruptTimer(String threadName) { | |
| state = new AlarmState(); | |
| autoKiller = new AutoKiller(state); | |
| thread = new AlarmThread(threadName, state); | |
| thread.start(); | |
| } | |
| /** | |
| * Arm the interrupt timer before entering a blocking operation. | |
| * | |
| * @param timeout | |
| * number of milliseconds before the interrupt should trigger. | |
| * Must be > 0. | |
| */ | |
| public void begin(int timeout) { | |
| if (timeout <= 0) | |
| throw new IllegalArgumentException(MessageFormat.format( | |
| JGitText.get().invalidTimeout, Integer.valueOf(timeout))); | |
| Thread.interrupted(); | |
| state.begin(timeout); | |
| } | |
| /** | |
| * Disable the interrupt timer, as the operation is complete. | |
| */ | |
| public void end() { | |
| state.end(); | |
| } | |
| /** | |
| * Shutdown the timer thread, and wait for it to terminate. | |
| */ | |
| public void terminate() { | |
| state.terminate(); | |
| boolean interrupted = false; | |
| try { | |
| while (true) { | |
| try { | |
| thread.join(); | |
| return; | |
| } catch (InterruptedException e) { | |
| interrupted = true; | |
| } | |
| } | |
| } finally { | |
| if (interrupted) { | |
| Thread.currentThread().interrupt(); | |
| } | |
| } | |
| } | |
| static final class AlarmThread extends Thread { | |
| AlarmThread(String name, AlarmState q) { | |
| super(q); | |
| setName(name); | |
| setDaemon(true); | |
| } | |
| } | |
| // The trick here is, the AlarmThread does not have a reference to the | |
| // AutoKiller instance, only the InterruptTimer itself does. Thus when | |
| // the InterruptTimer is GC'd, the AutoKiller is also unreachable and | |
| // can be GC'd. When it gets finalized, it tells the AlarmThread to | |
| // terminate, triggering the thread to exit gracefully. | |
| // | |
| private static final class AutoKiller { | |
| private final AlarmState state; | |
| AutoKiller(AlarmState s) { | |
| state = s; | |
| } | |
| @Override | |
| protected void finalize() throws Throwable { | |
| state.terminate(); | |
| } | |
| } | |
| static final class AlarmState implements Runnable { | |
| private Thread callingThread; | |
| private int lastTimeout; | |
| private long deadline; | |
| // Absolute time when the current idle wait will expire; 0 when not idle. | |
| private long idleWakeAt; | |
| private boolean terminated; | |
| AlarmState() { | |
| callingThread = Thread.currentThread(); | |
| } | |
| @Override | |
| public synchronized void run() { | |
| while (!terminated && callingThread.isAlive()) { | |
| try { | |
| if (deadline > 0) { | |
| final long delay = deadline - now(); | |
| if (delay <= 0) { | |
| deadline = 0; | |
| callingThread.interrupt(); | |
| } else { | |
| wait(delay); | |
| } | |
| } else { | |
| // When the timer is not running, avoid waking up more than once a second | |
| long idleMs = lastTimeout == 0 ? 1000 : lastTimeout; | |
| idleWakeAt = now() + idleMs; | |
| try { | |
| wait(idleMs); | |
| } finally { | |
| idleWakeAt = 0; | |
| lastTimeout = 0; | |
| } | |
| } | |
| } catch (InterruptedException e) { | |
| // Treat an interrupt as notice to examine state. | |
| } | |
| } | |
| } | |
| synchronized void begin(int timeout) { | |
| if (terminated) | |
| throw new IllegalStateException(JGitText.get().timerAlreadyTerminated); | |
| callingThread = Thread.currentThread(); | |
| deadline = now() + timeout; | |
| if (lastTimeout != timeout) { | |
| boolean isNotify = lastTimeout == 0 || lastTimeout > timeout; | |
| lastTimeout = timeout; | |
| if (isNotify) { | |
| notifyAll(); | |
| } // else avoid the expensive notify when the runloop will already timeout in time | |
| } else if (idleWakeAt > 0 && idleWakeAt >= deadline) { | |
| // Same timeout as before, but the idle wait could expire at or | |
| // after the new deadline — wake the thread so it re-arms with | |
| // the correct deadline. | |
| notifyAll(); | |
| } | |
| } | |
| synchronized void end() { | |
| if (0 == deadline) { | |
| Thread.interrupted(); | |
| } else { | |
| deadline = 0; | |
| } | |
| } | |
| synchronized void terminate() { | |
| if (!terminated) { | |
| deadline = 0; | |
| terminated = true; | |
| notifyAll(); | |
| } | |
| } | |
| private static long now() { | |
| return System.currentTimeMillis(); | |
| } | |
| } | |
| } |