Skip to content
Open
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 @@ -22,10 +22,13 @@
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.FileSystem;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -74,11 +77,16 @@
*/
public class ClasspathSkillRepository implements AgentSkillRepository {

private static final Object FILE_SYSTEM_MONITOR = new Object();

private static final Map<URI, SharedFileSystemRef> SHARED_FILE_SYSTEMS = new HashMap<>();

private final Logger logger = LoggerFactory.getLogger(ClasspathSkillRepository.class);

private final FileSystem fileSystem;
private final Path skillBasePath;
private final boolean isJar;
private final URI jarFileSystemUri;
private final String source;
private final String resourcePath;
private final AtomicBoolean closed = new AtomicBoolean(false);
Expand Down Expand Up @@ -139,7 +147,8 @@ protected ClasspathSkillRepository(String resourcePath, String source, ClassLoad

if ("jar".equals(uri.getScheme())) {
this.isJar = true;
this.fileSystem = FileSystems.newFileSystem(uri, Collections.emptyMap());
this.jarFileSystemUri = uri;
this.fileSystem = acquireFileSystem(uri);
String schemeSpecificUriPath = uri.getSchemeSpecificPart();
String actualResourcePath =
schemeSpecificUriPath.substring(schemeSpecificUriPath.lastIndexOf("!") + 1);
Expand All @@ -148,6 +157,7 @@ protected ClasspathSkillRepository(String resourcePath, String source, ClassLoad
} else {
this.isJar = false;
this.fileSystem = null;
this.jarFileSystemUri = null;
this.skillBasePath = Path.of(uri);
}
logger.info("is in Jar environment: {}", this.isJar);
Expand Down Expand Up @@ -266,6 +276,48 @@ public boolean isJarEnvironment() {
return isJar;
}

private static FileSystem acquireFileSystem(URI uri) throws IOException {
synchronized (FILE_SYSTEM_MONITOR) {
SharedFileSystemRef existingRef = SHARED_FILE_SYSTEMS.get(uri);
if (existingRef != null && existingRef.fileSystem.isOpen()) {
existingRef.refCount++;
return existingRef.fileSystem;
}
SHARED_FILE_SYSTEMS.remove(uri);

FileSystem fileSystem;
boolean closeWhenUnused = false;
try {
fileSystem = FileSystems.getFileSystem(uri);
} catch (FileSystemNotFoundException ignored) {
fileSystem = FileSystems.newFileSystem(uri, Collections.emptyMap());
closeWhenUnused = true;
}

SHARED_FILE_SYSTEMS.put(uri, new SharedFileSystemRef(fileSystem, closeWhenUnused));
return fileSystem;
}
}

private static void releaseFileSystem(URI uri) throws IOException {
synchronized (FILE_SYSTEM_MONITOR) {
SharedFileSystemRef ref = SHARED_FILE_SYSTEMS.get(uri);
if (ref == null) {
return;
}

ref.refCount--;
if (ref.refCount > 0) {
return;
}

SHARED_FILE_SYSTEMS.remove(uri);
if (ref.closeWhenUnused && ref.fileSystem.isOpen()) {
ref.fileSystem.close();
}
}
}

private void checkNotClosed() {
if (closed.get()) {
throw new IllegalStateException("ClasspathSkillRepository has been closed");
Expand All @@ -283,12 +335,24 @@ public void close() {
if (!closed.compareAndSet(false, true)) {
return;
}
if (fileSystem != null) {
if (jarFileSystemUri != null) {
try {
fileSystem.close();
releaseFileSystem(jarFileSystemUri);
} catch (IOException e) {
throw new RuntimeException("Failed to close classpath file system", e);
}
}
}

private static final class SharedFileSystemRef {

private final FileSystem fileSystem;
private final boolean closeWhenUnused;
private int refCount = 1;

private SharedFileSystemRef(FileSystem fileSystem, boolean closeWhenUnused) {
this.fileSystem = fileSystem;
this.closeWhenUnused = closeWhenUnused;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,30 @@ void testLoadAllSkillsFromJar() throws Exception {
}
}

@Test
@DisplayName("Should reuse JAR file system across repository instances")
void testReuseJarFileSystemAcrossRepositories() throws Exception {
Path jarPath = createTestJarInFolder("shared-skill", "Shared Skill", "Shared content");

try (URLClassLoader classLoader = new URLClassLoader(new URL[] {jarPath.toUri().toURL()});
ClasspathSkillRepository firstRepository =
new ClasspathSkillRepositoryWithClassLoader("jar-skills", classLoader);
ClasspathSkillRepository secondRepository =
new ClasspathSkillRepositoryWithClassLoader("jar-skills", classLoader)) {

AgentSkill firstSkill = firstRepository.getSkill("shared-skill");
AgentSkill secondSkill = secondRepository.getSkill("shared-skill");

assertEquals("shared-skill", firstSkill.getName());
assertEquals("shared-skill", secondSkill.getName());

firstRepository.close();

AgentSkill stillAccessible = secondRepository.getSkill("shared-skill");
assertEquals("shared-skill", stillAccessible.getName());
}
}

@Test
@DisplayName("Should load skills from Spring Boot Fat JAR (BOOT-INF/classes/)")
void testLoadFromSpringBootJar() throws Exception {
Expand Down Expand Up @@ -317,6 +341,37 @@ protected URLConnection openConnection(URL u)
}
}

@Test
@DisplayName("Should reuse nested JAR file system across repository instances")
void testReuseNestedJarFileSystemAcrossRepositories() throws Exception {
Path outerJarPath =
createSpringBootNestedLibTestJar(
"nested-shared-skill", "Nested Shared Skill", "Nested shared content");
Path innerJarPath = extractInnerJar(outerJarPath, "BOOT-INF/lib/nested-skill.jar");

TestNestedFileSystemProvider.configuredInnerJarPath = innerJarPath;
try (ClasspathSkillRepository firstRepository =
new ClasspathSkillRepositoryWithClassLoader(
"jar-skills", createNestedJarClassLoader(outerJarPath));
ClasspathSkillRepository secondRepository =
new ClasspathSkillRepositoryWithClassLoader(
"jar-skills", createNestedJarClassLoader(outerJarPath))) {

AgentSkill firstSkill = firstRepository.getSkill("nested-shared-skill");
AgentSkill secondSkill = secondRepository.getSkill("nested-shared-skill");

assertEquals("nested-shared-skill", firstSkill.getName());
assertEquals("nested-shared-skill", secondSkill.getName());

firstRepository.close();

AgentSkill stillAccessible = secondRepository.getSkill("nested-shared-skill");
assertEquals("nested-shared-skill", stillAccessible.getName());
} finally {
TestNestedFileSystemProvider.configuredInnerJarPath = null;
}
}

// ==================== getSource Tests ====================

@Test
Expand Down Expand Up @@ -695,6 +750,37 @@ private Path extractInnerJar(Path outerJarPath, String innerEntryName) throws IO
return innerJarPath;
}

private ClassLoader createNestedJarClassLoader(Path outerJarPath) {
return new ClassLoader(ClassLoader.getSystemClassLoader()) {
@Override
public URL getResource(String name) {
if ("jar-skills".equals(name)) {
try {
String nestedUrlStr =
"jar:nested:"
+ outerJarPath.toUri().getRawPath()
+ "/!BOOT-INF/lib/nested-skill.jar!/"
+ name;
return new URL(
null,
nestedUrlStr,
new URLStreamHandler() {
@Override
protected URLConnection openConnection(URL u)
throws IOException {
throw new UnsupportedOperationException(
"nested URL for test only");
}
});
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return super.getResource(name);
}
};
}

/**
* Custom adapter that uses a specific ClassLoader for testing JAR loading.
*/
Expand Down
Loading