Merge branch 'stable-7.3' into stable-7.4
* stable-7.3:
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: Ieed399f728cdb96534e5d6ee2bc65f9bebaaa211
diff --git a/org.eclipse.jgit.ssh.jsch.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.jsch.test/META-INF/MANIFEST.MF
index ef3cbf2..4f1a737 100644
--- a/org.eclipse.jgit.ssh.jsch.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.jsch.test/META-INF/MANIFEST.MF
@@ -18,6 +18,7 @@
org.eclipse.jgit.transport;version="[7.4.1,7.5.0)",
org.eclipse.jgit.transport.ssh.jsch;version="[7.4.1,7.5.0)",
org.eclipse.jgit.util;version="[7.4.1,7.5.0)",
+ org.hamcrest;version="[1.1.0,3.0.0)",
org.junit;version="[4.13,5.0.0)",
org.junit.experimental.theories;version="[4.13,5.0.0)",
org.junit.runner;version="[4.13,5.0.0)"
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 de88670..839d46b 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.4.1";
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() {