Merge branch 'stable-7.4' * stable-7.4: InterruptTimer: don't use Yoda-style condition InterruptTimer: avoid expensive notify when begin is soon after end InterruptTimer: avoid unneeded notify for end() Disable MergeToolTest#testEmptyToolName Add InterruptTimer Tests Allow to discover bitmap on disk created after the packfile Prepare 5.13.6-SNAPSHOT builds JGit v5.13.5.202508271544-r Remove resolver option from target-platform-configuration Add missing release property to maven build Suppress API errors for minor API changes in service releases Remove unnecessary casts Change-Id: I662bf9782e418a4d1c037fdd0bc3a02e0a8e5c44
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/io/InterruptTimerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/io/InterruptTimerTest.java new file mode 100644 index 0000000..68de7d6 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/io/InterruptTimerTest.java
@@ -0,0 +1,182 @@ +/* + * Copyright (C) 2025, NVIDIA CORPORATION + * + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import java.util.Arrays; +import java.util.List; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class InterruptTimerTest { + private static final int MULTIPLIER = 1; // Increase if tests get flaky + private static final int BUFFER = 5; // Increase if tests get flaky + private static final int REPEATS = 100; // Increase to stress test more + + private static final int TOO_LONG = 3 * MULTIPLIER + BUFFER; + private static final int SHORT_ENOUGH = 1 * MULTIPLIER; + private static final int TIMEOUT_LONG_ENOUGH = TOO_LONG; + private static final int TIMEOUT_TOO_SHORT = SHORT_ENOUGH; + + private InterruptTimer timer; + + @Before + public void setUp() { + timer = new InterruptTimer(); + } + + @After + public void tearDown() { + timer.terminate(); + for (Thread t : active()) + assertFalse(t instanceof InterruptTimer.AlarmThread); + } + + @Test + public void testShortEnough() { + int interrupted = 0; + try { + timer.begin(TIMEOUT_LONG_ENOUGH); + Thread.sleep(SHORT_ENOUGH); + timer.end(); + } catch (InterruptedException e) { + interrupted++; + } + assertEquals("Was Not Interrupted", interrupted, 0); + } + + @Test + public void testTooLong() { + int interrupted = 0; + try { + timer.begin(TIMEOUT_TOO_SHORT); + Thread.sleep(TOO_LONG); + timer.end(); + } catch (InterruptedException e) { + interrupted++; + } + assertEquals("Was Interrupted", interrupted, 1); + } + + @Test + public void testNotInterruptedAfterEnd() { + int interrupted = 0; + try { + timer.begin(TIMEOUT_LONG_ENOUGH); + Thread.sleep(SHORT_ENOUGH); + timer.end(); + Thread.sleep(TIMEOUT_LONG_ENOUGH * 3); + } catch (InterruptedException e) { + interrupted++; + } + assertEquals("Was Not Interrupted Even After End", interrupted, 0); + } + + @Test + public void testRestartBeforeTimeout() { + int interrupted = 0; + try { + timer.begin(TIMEOUT_LONG_ENOUGH * 2); + Thread.sleep(SHORT_ENOUGH); + timer.end(); + timer.begin(TIMEOUT_LONG_ENOUGH); + Thread.sleep(SHORT_ENOUGH); + timer.end(); + } catch (InterruptedException e) { + interrupted++; + } + assertEquals("Was Not Interrupted Even When Restarted Before Timeout", interrupted, 0); + } + + @Test + public void testSecondExpiresBeforeFirst() { + int interrupted = 0; + try { + timer.begin(TIMEOUT_LONG_ENOUGH * 3); + Thread.sleep(SHORT_ENOUGH); + timer.end(); + timer.begin(TIMEOUT_TOO_SHORT); + Thread.sleep(TOO_LONG); + timer.end(); + } catch (InterruptedException e) { + interrupted++; + } + assertEquals("Was Interrupted Even When Second Timeout Expired Before First", interrupted, 1); + } + + @Test + public void testRepeatedShortEnough() { + int interrupted = 0; + for (int i = 0; i < REPEATS; i++) { + try { + timer.begin(TIMEOUT_LONG_ENOUGH); + Thread.sleep(SHORT_ENOUGH); + timer.end(); + } catch (InterruptedException e) { + interrupted++; + } + } + assertEquals("Was Never Interrupted", interrupted, 0); + } + + @Test + public void testRepeatedTooLong() { + int interrupted = 0; + for (int i = 0; i < REPEATS; i++) { + try { + timer.begin(TIMEOUT_TOO_SHORT); + Thread.sleep(TOO_LONG); + timer.end(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + interrupted++; + } + } + assertEquals("Was always Interrupted", interrupted, REPEATS); + } + + @Test + public void testRepeatedShortThanTooLong() { + int interrupted = 0; + for (int i = 0; i < REPEATS; i++) { + try { + timer.begin(TIMEOUT_LONG_ENOUGH); + Thread.sleep(SHORT_ENOUGH); + timer.end(); + } catch (InterruptedException e) { + interrupted++; + } + } + assertEquals("Was Not Interrupted Early", interrupted, 0); + try { + timer.begin(TIMEOUT_TOO_SHORT); + Thread.sleep(TOO_LONG); + timer.end(); + } catch (InterruptedException e) { + interrupted++; + } + assertEquals("Was Interrupted On Long", interrupted, 1); + } + + private static List<Thread> active() { + Thread[] all = new Thread[16]; + int n = Thread.currentThread().getThreadGroup().enumerate(all); + while (n == all.length) { + all = new Thread[all.length * 2]; + n = Thread.currentThread().getThreadGroup().enumerate(all); + } + return Arrays.asList(all).subList(0, n); + } +}
diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF index f0cf71d..d760003 100644 --- a/org.eclipse.jgit/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit/META-INF/MANIFEST.MF
@@ -103,9 +103,12 @@ org.eclipse.jgit.junit.http, org.eclipse.jgit.http.server, org.eclipse.jgit.lfs, + org.eclipse.jgit.lfs.test, org.eclipse.jgit.pgm, org.eclipse.jgit.pgm.test, - org.eclipse.jgit.ssh.apache", + org.eclipse.jgit.ssh.apache, + org.eclipse.jgit.ssh.apache.test, + org.eclipse.jgit.ssh.jsch.test", org.eclipse.jgit.internal.storage.io;version="7.5.0"; x-friends:="org.eclipse.jgit.junit, org.eclipse.jgit.test,
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 888b8fb..e717412 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
@@ -151,6 +151,7 @@ protected void finalize() throws Throwable { static final class AlarmState implements Runnable { private Thread callingThread; + private int lastTimeout; private long deadline; private boolean terminated; @@ -163,7 +164,7 @@ static final class AlarmState implements Runnable { public synchronized void run() { while (!terminated && callingThread.isAlive()) { try { - if (0 < deadline) { + if (deadline > 0) { final long delay = deadline - now(); if (delay <= 0) { deadline = 0; @@ -172,7 +173,9 @@ public synchronized void run() { wait(delay); } } else { - wait(1000); + // When the timer is not running, avoid waking up more than once a second + wait(lastTimeout == 0 ? 1000 : lastTimeout); + lastTimeout = 0; } } catch (InterruptedException e) { // Treat an interrupt as notice to examine state. @@ -185,15 +188,21 @@ synchronized void begin(int timeout) { throw new IllegalStateException(JGitText.get().timerAlreadyTerminated); callingThread = Thread.currentThread(); deadline = now() + timeout; - notifyAll(); + 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 + } } synchronized void end() { - if (0 == deadline) + if (0 == deadline) { Thread.interrupted(); - else + } else { deadline = 0; - notifyAll(); + } } synchronized void terminate() {