InterruptTimer.java

  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. package org.eclipse.jgit.util.io;

  11. import java.text.MessageFormat;

  12. import org.eclipse.jgit.internal.JGitText;

  13. /**
  14.  * Triggers an interrupt on the calling thread if it doesn't complete a block.
  15.  * <p>
  16.  * Classes can use this to trip an alarm interrupting the calling thread if it
  17.  * doesn't complete a block within the specified timeout. Typical calling
  18.  * pattern is:
  19.  *
  20.  * <pre>
  21.  * private InterruptTimer myTimer = ...;
  22.  * void foo() {
  23.  *   try {
  24.  *     myTimer.begin(timeout);
  25.  *     // work
  26.  *   } finally {
  27.  *     myTimer.end();
  28.  *   }
  29.  * }
  30.  * </pre>
  31.  * <p>
  32.  * An InterruptTimer is not recursive. To implement recursive timers,
  33.  * independent InterruptTimer instances are required. A single InterruptTimer
  34.  * may be shared between objects which won't recursively call each other.
  35.  * <p>
  36.  * Each InterruptTimer spawns one background thread to sleep the specified time
  37.  * and interrupt the thread which called {@link #begin(int)}. It is up to the
  38.  * caller to ensure that the operations within the work block between the
  39.  * matched begin and end calls tests the interrupt flag (most IO operations do).
  40.  * <p>
  41.  * To terminate the background thread, use {@link #terminate()}. If the
  42.  * application fails to terminate the thread, it will (eventually) terminate
  43.  * itself when the InterruptTimer instance is garbage collected.
  44.  *
  45.  * @see TimeoutInputStream
  46.  */
  47. public final class InterruptTimer {
  48.     private final AlarmState state;

  49.     private final AlarmThread thread;

  50.     final AutoKiller autoKiller;

  51.     /**
  52.      * Create a new timer with a default thread name.
  53.      */
  54.     public InterruptTimer() {
  55.         this("JGit-InterruptTimer"); //$NON-NLS-1$
  56.     }

  57.     /**
  58.      * Create a new timer to signal on interrupt on the caller.
  59.      * <p>
  60.      * The timer thread is created in the calling thread's ThreadGroup.
  61.      *
  62.      * @param threadName
  63.      *            name of the timer thread.
  64.      */
  65.     public InterruptTimer(String threadName) {
  66.         state = new AlarmState();
  67.         autoKiller = new AutoKiller(state);
  68.         thread = new AlarmThread(threadName, state);
  69.         thread.start();
  70.     }

  71.     /**
  72.      * Arm the interrupt timer before entering a blocking operation.
  73.      *
  74.      * @param timeout
  75.      *            number of milliseconds before the interrupt should trigger.
  76.      *            Must be &gt; 0.
  77.      */
  78.     public void begin(int timeout) {
  79.         if (timeout <= 0)
  80.             throw new IllegalArgumentException(MessageFormat.format(
  81.                     JGitText.get().invalidTimeout, Integer.valueOf(timeout)));
  82.         Thread.interrupted();
  83.         state.begin(timeout);
  84.     }

  85.     /**
  86.      * Disable the interrupt timer, as the operation is complete.
  87.      */
  88.     public void end() {
  89.         state.end();
  90.     }

  91.     /**
  92.      * Shutdown the timer thread, and wait for it to terminate.
  93.      */
  94.     public void terminate() {
  95.         state.terminate();
  96.         try {
  97.             thread.join();
  98.         } catch (InterruptedException e) {
  99.             //
  100.         }
  101.     }

  102.     static final class AlarmThread extends Thread {
  103.         AlarmThread(String name, AlarmState q) {
  104.             super(q);
  105.             setName(name);
  106.             setDaemon(true);
  107.         }
  108.     }

  109.     // The trick here is, the AlarmThread does not have a reference to the
  110.     // AutoKiller instance, only the InterruptTimer itself does. Thus when
  111.     // the InterruptTimer is GC'd, the AutoKiller is also unreachable and
  112.     // can be GC'd. When it gets finalized, it tells the AlarmThread to
  113.     // terminate, triggering the thread to exit gracefully.
  114.     //
  115.     private static final class AutoKiller {
  116.         private final AlarmState state;

  117.         AutoKiller(AlarmState s) {
  118.             state = s;
  119.         }

  120.         @Override
  121.         protected void finalize() throws Throwable {
  122.             state.terminate();
  123.         }
  124.     }

  125.     static final class AlarmState implements Runnable {
  126.         private Thread callingThread;

  127.         private long deadline;

  128.         private boolean terminated;

  129.         AlarmState() {
  130.             callingThread = Thread.currentThread();
  131.         }

  132.         @Override
  133.         public synchronized void run() {
  134.             while (!terminated && callingThread.isAlive()) {
  135.                 try {
  136.                     if (0 < deadline) {
  137.                         final long delay = deadline - now();
  138.                         if (delay <= 0) {
  139.                             deadline = 0;
  140.                             callingThread.interrupt();
  141.                         } else {
  142.                             wait(delay);
  143.                         }
  144.                     } else {
  145.                         wait(1000);
  146.                     }
  147.                 } catch (InterruptedException e) {
  148.                     // Treat an interrupt as notice to examine state.
  149.                 }
  150.             }
  151.         }

  152.         synchronized void begin(int timeout) {
  153.             if (terminated)
  154.                 throw new IllegalStateException(JGitText.get().timerAlreadyTerminated);
  155.             callingThread = Thread.currentThread();
  156.             deadline = now() + timeout;
  157.             notifyAll();
  158.         }

  159.         synchronized void end() {
  160.             if (0 == deadline)
  161.                 Thread.interrupted();
  162.             else
  163.                 deadline = 0;
  164.             notifyAll();
  165.         }

  166.         synchronized void terminate() {
  167.             if (!terminated) {
  168.                 deadline = 0;
  169.                 terminated = true;
  170.                 notifyAll();
  171.             }
  172.         }

  173.         private static long now() {
  174.             return System.currentTimeMillis();
  175.         }
  176.     }
  177. }