Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
package org.eclipse.rdf4j.repository.sparql;

import java.io.File;
import java.lang.ref.Cleaner;
import java.util.Collections;
import java.util.Map;

import org.apache.http.client.HttpClient;
import org.eclipse.rdf4j.common.concurrent.locks.diagnostics.ConcurrentCleaner;
import org.eclipse.rdf4j.http.client.HttpClientDependent;
import org.eclipse.rdf4j.http.client.HttpClientSessionManager;
import org.eclipse.rdf4j.http.client.SPARQLProtocolSession;
Expand All @@ -33,6 +35,8 @@
*/
public class SPARQLRepository extends AbstractRepository implements HttpClientDependent, SessionManagerDependent {

private static final ConcurrentCleaner CLEANER = new ConcurrentCleaner();

/**
* Flag indicating if quad mode is enabled in newly created {@link SPARQLConnection}s.
*
Expand All @@ -49,6 +53,11 @@ public class SPARQLRepository extends AbstractRepository implements HttpClientDe
*/
private volatile SharedHttpClientSessionManager dependentClient;

/**
* Cleanable registration to auto-invoke cleanup when this repository becomes unreachable.
*/
private volatile Cleaner.Cleanable cleanable;

private String username;

private String password;
Expand Down Expand Up @@ -93,7 +102,17 @@ public HttpClientSessionManager getHttpClientSessionManager() {
synchronized (this) {
result = client;
if (result == null) {
result = client = dependentClient = new SharedHttpClientSessionManager();
SharedHttpClientSessionManager created = new SharedHttpClientSessionManager();
result = client = dependentClient = created;
// Register a cleaner that shuts down the dependent client if this repository is GC'ed without
// explicit shutdown
cleanable = CLEANER.register(this, () -> {
try {
created.shutDown();
} catch (Throwable t) {
// ignore
}
});
}
}
}
Expand All @@ -110,6 +129,11 @@ public void setHttpClientSessionManager(HttpClientSessionManager client) {
if (toCloseDependentClient != null) {
toCloseDependentClient.shutDown();
}
Cleaner.Cleanable toClean = cleanable;
cleanable = null;
if (toClean != null) {
toClean.clean();
}
}
}

Expand Down Expand Up @@ -213,6 +237,11 @@ protected void shutDownInternal() throws RepositoryException {
toCloseDependentClient.shutDown();
}
} finally {
Cleaner.Cleanable toClean = cleanable;
cleanable = null;
if (toClean != null) {
toClean.clean();
}
// remove reference but do not shut down, client may be shared by
// other repos.
client = null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*******************************************************************************
* Copyright (c) 2025 Eclipse RDF4J contributors.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Distribution License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*******************************************************************************/
package org.eclipse.rdf4j.repository.sparql;

import static org.assertj.core.api.Assertions.assertThat;

import java.lang.reflect.Field;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;

import org.eclipse.rdf4j.http.client.SharedHttpClientSessionManager;
import org.eclipse.rdf4j.repository.RepositoryConnection;
import org.junit.jupiter.api.Test;

/**
* Verifies that a SPARQLRepository performs internal shutdown when it becomes unreachable.
*
* <p>
* This test intentionally does not call {@code repository.shutDown()}. It expects the repository to arrange for its
* internal {@code shutDownInternal()} to run when the object is no longer reachable (e.g., by using Java 9 Cleaner).
* </p>
*/
public class SPARQLRepositoryCleanerTest {

@Test
void autoShutdownOnUnreachable() throws Exception {
SPARQLRepository repo = new SPARQLRepository("http://example.org/sparql");

// Ensure dependent client is created
try (RepositoryConnection conn = repo.getConnection()) {
// no-op
}

SharedHttpClientSessionManager mgr = (SharedHttpClientSessionManager) repo.getHttpClientSessionManager();

// Access internal executor to verify shutdown state
Field f = SharedHttpClientSessionManager.class.getDeclaredField("executor");
f.setAccessible(true);
ExecutorService exec = (ExecutorService) f.get(mgr);

// Drop strong reference and encourage GC to trigger Cleaner
repo = null;

boolean cleaned = false;
for (int i = 0; i < 40 && !cleaned; i++) {
System.gc();
System.runFinalization();
TimeUnit.MILLISECONDS.sleep(100);
cleaned = exec.isShutdown() || exec.isTerminated();
}

assertThat(cleaned)
.as("dependent session manager executor should be shut down by Cleaner")
.isTrue();
}
}
Loading