InterruptTimer: fix spurious interrupt when begin() reuses same timeout

When begin() was called with the same timeout as the previous call,
the notify to the AlarmThread was skipped as an optimisation. This was
unsafe when the AlarmThread was in its idle wait (after end()): if that
idle wait was started long before begin() was called, it could expire
at or after the new deadline, causing the AlarmThread to see delay <= 0
and fire a spurious interrupt.

Fix by tracking idleWakeAt — the absolute time at which the current
idle wait will expire. begin() now sends notifyAll() when the thread is
idle and its wait would expire at or after the new deadline, ensuring
the AlarmThread re-arms itself correctly. The optimisation is preserved
for the common case where the thread is actively timing (idleWakeAt==0).

This manifested as a flaky
InterruptTimerTest.testRepeatedShortThanTooLong()
in CI job running on Linux where coarser timer resolution and higher
scheduling jitter made the race window large enough to hit reliably.

Change-Id: I5d5635a66f9fa5b9f68791017e09d5d3d08135dd
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/InterruptTimer.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/InterruptTimer.java
index e717412..a076e4e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/InterruptTimer.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/InterruptTimer.java
@@ -154,6 +154,9 @@ static final class AlarmState implements Runnable {
 		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() {
@@ -174,8 +177,14 @@ public synchronized void run() {
 						}
 					} else {
 						// When the timer is not running, avoid waking up more than once a second
-						wait(lastTimeout == 0 ? 1000 : lastTimeout);
-						lastTimeout = 0;
+						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.
@@ -194,6 +203,11 @@ synchronized void begin(int 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();
 			}
 		}