Skip to content

Commit 7d991dc

Browse files
committed
GH-4673 Use ConcurrentCleaner to auto-shutdown SPARQLRepository's dependent session manager on GC; add test
1 parent 683c92c commit 7d991dc

2 files changed

Lines changed: 55 additions & 34 deletions

File tree

core/repository/sparql/src/main/java/org/eclipse/rdf4j/repository/sparql/SPARQLRepository.java

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
package org.eclipse.rdf4j.repository.sparql;
1212

1313
import java.io.File;
14+
import java.lang.ref.Cleaner;
1415
import java.util.Collections;
1516
import java.util.Map;
1617

1718
import org.apache.http.client.HttpClient;
19+
import org.eclipse.rdf4j.common.concurrent.locks.diagnostics.ConcurrentCleaner;
1820
import org.eclipse.rdf4j.http.client.HttpClientDependent;
1921
import org.eclipse.rdf4j.http.client.HttpClientSessionManager;
2022
import org.eclipse.rdf4j.http.client.SPARQLProtocolSession;
@@ -33,6 +35,8 @@
3335
*/
3436
public class SPARQLRepository extends AbstractRepository implements HttpClientDependent, SessionManagerDependent {
3537

38+
private static final ConcurrentCleaner CLEANER = new ConcurrentCleaner();
39+
3640
/**
3741
* Flag indicating if quad mode is enabled in newly created {@link SPARQLConnection}s.
3842
*
@@ -49,6 +53,11 @@ public class SPARQLRepository extends AbstractRepository implements HttpClientDe
4953
*/
5054
private volatile SharedHttpClientSessionManager dependentClient;
5155

56+
/**
57+
* Cleanable registration to auto-invoke cleanup when this repository becomes unreachable.
58+
*/
59+
private volatile Cleaner.Cleanable cleanable;
60+
5261
private String username;
5362

5463
private String password;
@@ -93,7 +102,17 @@ public HttpClientSessionManager getHttpClientSessionManager() {
93102
synchronized (this) {
94103
result = client;
95104
if (result == null) {
96-
result = client = dependentClient = new SharedHttpClientSessionManager();
105+
SharedHttpClientSessionManager created = new SharedHttpClientSessionManager();
106+
result = client = dependentClient = created;
107+
// Register a cleaner that shuts down the dependent client if this repository is GC'ed without
108+
// explicit shutdown
109+
cleanable = CLEANER.register(this, () -> {
110+
try {
111+
created.shutDown();
112+
} catch (Throwable t) {
113+
// ignore
114+
}
115+
});
97116
}
98117
}
99118
}
@@ -110,6 +129,11 @@ public void setHttpClientSessionManager(HttpClientSessionManager client) {
110129
if (toCloseDependentClient != null) {
111130
toCloseDependentClient.shutDown();
112131
}
132+
Cleaner.Cleanable toClean = cleanable;
133+
cleanable = null;
134+
if (toClean != null) {
135+
toClean.clean();
136+
}
113137
}
114138
}
115139

@@ -213,6 +237,11 @@ protected void shutDownInternal() throws RepositoryException {
213237
toCloseDependentClient.shutDown();
214238
}
215239
} finally {
240+
Cleaner.Cleanable toClean = cleanable;
241+
cleanable = null;
242+
if (toClean != null) {
243+
toClean.clean();
244+
}
216245
// remove reference but do not shut down, client may be shared by
217246
// other repos.
218247
client = null;

core/repository/sparql/src/test/java/org/eclipse/rdf4j/repository/sparql/SPARQLRepositoryCleanerTest.java

Lines changed: 25 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@
1010
*******************************************************************************/
1111
package org.eclipse.rdf4j.repository.sparql;
1212

13-
import static org.assertj.core.api.Assertions.fail;
13+
import static org.assertj.core.api.Assertions.assertThat;
1414

15-
import java.util.concurrent.CountDownLatch;
15+
import java.lang.reflect.Field;
16+
import java.util.concurrent.ExecutorService;
1617
import java.util.concurrent.TimeUnit;
1718

19+
import org.eclipse.rdf4j.http.client.SharedHttpClientSessionManager;
1820
import org.eclipse.rdf4j.repository.RepositoryConnection;
19-
import org.eclipse.rdf4j.repository.RepositoryException;
2021
import org.junit.jupiter.api.Test;
2122

2223
/**
@@ -31,42 +32,33 @@ public class SPARQLRepositoryCleanerTest {
3132

3233
@Test
3334
void autoShutdownOnUnreachable() throws Exception {
34-
CountDownLatch shutdownInvoked = new CountDownLatch(1);
35+
SPARQLRepository repo = new SPARQLRepository("http://example.org/sparql");
3536

36-
runRepo(shutdownInvoked);
37-
38-
// Encourage GC and wait briefly for cleaner to run
39-
boolean observed = false;
40-
for (int i = 0; i < 20 && !observed; i++) {
41-
System.gc();
42-
System.runFinalization();
43-
observed = shutdownInvoked.await(250, TimeUnit.MILLISECONDS);
37+
// Ensure dependent client is created
38+
try (RepositoryConnection conn = repo.getConnection()) {
39+
// no-op
4440
}
4541

46-
if (!observed) {
47-
fail("Expected shutDownInternal() to be invoked when SPARQLRepository became unreachable");
48-
}
49-
}
42+
SharedHttpClientSessionManager mgr = (SharedHttpClientSessionManager) repo.getHttpClientSessionManager();
5043

51-
private static void runRepo(CountDownLatch shutdownInvoked) {
52-
// Create a repository instance that signals when shutDownInternal() is invoked
53-
SPARQLRepository repo = new SPARQLRepository("http://example.org/sparql") {
54-
@Override
55-
protected void shutDownInternal() throws RepositoryException {
56-
try {
57-
super.shutDownInternal();
58-
} finally {
59-
shutdownInvoked.countDown();
60-
}
61-
}
62-
};
44+
// Access internal executor to verify shutdown state
45+
Field f = SharedHttpClientSessionManager.class.getDeclaredField("executor");
46+
f.setAccessible(true);
47+
ExecutorService exec = (ExecutorService) f.get(mgr);
6348

64-
// Exercise minimal usage without hitting the network
65-
try (RepositoryConnection conn = repo.getConnection()) {
66-
// no-op
49+
// Drop strong reference and encourage GC to trigger Cleaner
50+
repo = null;
51+
52+
boolean cleaned = false;
53+
for (int i = 0; i < 40 && !cleaned; i++) {
54+
System.gc();
55+
System.runFinalization();
56+
TimeUnit.MILLISECONDS.sleep(100);
57+
cleaned = exec.isShutdown() || exec.isTerminated();
6758
}
6859

69-
// Drop strong reference and ask GC to collect
70-
repo = null;
60+
assertThat(cleaned)
61+
.as("dependent session manager executor should be shut down by Cleaner")
62+
.isTrue();
7163
}
7264
}

0 commit comments

Comments
 (0)