Skip to content

Commit 798efc4

Browse files
authored
Merge main into develop (#5427)
2 parents 71315dd + ad20b90 commit 798efc4

2 files changed

Lines changed: 94 additions & 1 deletion

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;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Eclipse RDF4J contributors.
3+
*
4+
* All rights reserved. This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Distribution License v1.0
6+
* which accompanies this distribution, and is available at
7+
* http://www.eclipse.org/org/documents/edl-v10.php.
8+
*
9+
* SPDX-License-Identifier: BSD-3-Clause
10+
*******************************************************************************/
11+
package org.eclipse.rdf4j.repository.sparql;
12+
13+
import static org.assertj.core.api.Assertions.assertThat;
14+
15+
import java.lang.reflect.Field;
16+
import java.util.concurrent.ExecutorService;
17+
import java.util.concurrent.TimeUnit;
18+
19+
import org.eclipse.rdf4j.http.client.SharedHttpClientSessionManager;
20+
import org.eclipse.rdf4j.repository.RepositoryConnection;
21+
import org.junit.jupiter.api.Test;
22+
23+
/**
24+
* Verifies that a SPARQLRepository performs internal shutdown when it becomes unreachable.
25+
*
26+
* <p>
27+
* This test intentionally does not call {@code repository.shutDown()}. It expects the repository to arrange for its
28+
* internal {@code shutDownInternal()} to run when the object is no longer reachable (e.g., by using Java 9 Cleaner).
29+
* </p>
30+
*/
31+
public class SPARQLRepositoryCleanerTest {
32+
33+
@Test
34+
void autoShutdownOnUnreachable() throws Exception {
35+
SPARQLRepository repo = new SPARQLRepository("http://example.org/sparql");
36+
37+
// Ensure dependent client is created
38+
try (RepositoryConnection conn = repo.getConnection()) {
39+
// no-op
40+
}
41+
42+
SharedHttpClientSessionManager mgr = (SharedHttpClientSessionManager) repo.getHttpClientSessionManager();
43+
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);
48+
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();
58+
}
59+
60+
assertThat(cleaned)
61+
.as("dependent session manager executor should be shut down by Cleaner")
62+
.isTrue();
63+
}
64+
}

0 commit comments

Comments
 (0)