Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
8cca806
Started building out RemoteFileSource plugin (#DH-20578)
bmingles Nov 26, 2025
b0871b7
Command resolver now fetches plugin (#DH-20578)
bmingles Nov 26, 2025
e183397
RemoteFileSourceTicketResolverFactoryService (#DH-20578)
bmingles Nov 26, 2025
369e636
Added stub for JsRemoteFileSourceService (#DH-20578)
bmingles Nov 26, 2025
a093cde
Generated GWT bindings
niloc132 Nov 27, 2025
5027810
Fetching plugin service working (#DH-20578)
bmingles Dec 3, 2025
f4f6c31
Wiring up messagestream (#DH-20578)
bmingles Dec 4, 2025
0520366
test bidirectional communication (#DH-20578)
bmingles Dec 4, 2025
2e61f96
set connection id (#DH-20578)
bmingles Dec 4, 2025
f16e317
Moved clientSessionId to plugin fetch instead of separate message (#D…
bmingles Dec 4, 2025
101570e
Cleanup (#DH-20578)
bmingles Dec 4, 2025
30da282
set execution context (#DH-20578)
bmingles Dec 4, 2025
30f2012
Simplified execution context (#DH-20578)
bmingles Dec 5, 2025
f44189c
Basic file sourcing is working (#DH-20578)
bmingles Dec 8, 2025
0d4334d
Comments and cleanup (#DH-20578)
bmingles Dec 10, 2025
6c0ee1a
Made method private (#DH-20578)
bmingles Dec 10, 2025
db3816c
Made method static (#DH-20578)
bmingles Dec 10, 2025
bcbe71a
onResourceRequest method (#DH-20578)
bmingles Dec 10, 2025
0a5a223
JS API types (#DH-20578)
bmingles Dec 10, 2025
68e762c
Refactored to use PluginMarker (#DH-20578)
bmingles Dec 11, 2025
2f2c090
Re-using JsWidget for message stream (#DH-20578)
bmingles Dec 11, 2025
9285c58
Added pluginType field to PluginMarker (#DH-20578)
bmingles Dec 11, 2025
8c2f8d1
Removed redundant field and renamed pluginType to pluginName (#DH-20578)
bmingles Dec 12, 2025
f21ced8
Fixed incorrect plugin name (#DH-20578)
bmingles Dec 12, 2025
47b154a
Cleanup (#DH-20578)
bmingles Dec 16, 2025
4671633
Renamed event (#DH-20578)
bmingles Dec 17, 2025
aaec3da
Passing in relative file paths instead of top-level folder (#DH-20578)
bmingles Dec 18, 2025
dfc7a2f
Added remotefilesource plugin to server build.gradle (#DH-20578)
bmingles Dec 18, 2025
a3f2c72
Regenerate bindings
niloc132 Dec 19, 2025
aa2e5e9
FIxing compile error
niloc132 Dec 19, 2025
1e4b4ac
Replaced util with TextEncoder.encode (#DH-20578)
bmingles Dec 23, 2025
a20d151
Cleanup (#DH-20578)
bmingles Dec 24, 2025
37f427a
Split out JsProtobufUtils (#DH-20578)
bmingles Dec 24, 2025
aba50b1
Made canSourceResource synchronous (#DH-20578)
bmingles Dec 24, 2025
576850c
renamed arg (#DH-20578)
bmingles Dec 24, 2025
ef00be5
Cleanup (#DH-20578)
bmingles Jan 2, 2026
10c8f8e
Sorted members (#DH-20578)
bmingles Jan 2, 2026
2eeba31
Cleanup RemoteFileSourceCommandResolver (#DH-20578)
bmingles Jan 2, 2026
b553d6d
Cleanup RemoteFileSourceMessageStream (#DH-20578)
bmingles Jan 2, 2026
86df3e0
Cleanup RemoteFileSourcePlugin (#DH-20578)
bmingles Jan 2, 2026
ce122c5
Cleanup RemoteFileSourceTicketResolverFactoryService (#DH-20578)
bmingles Jan 2, 2026
6b284d6
Cleanup PluginMarker (#DH-20578)
bmingles Jan 2, 2026
d42d61f
Changed back to resourceName convention to match class loaders (#DH-2…
bmingles Jan 2, 2026
3856646
Moved method (#DH-20578)
bmingles Jan 2, 2026
22a8f25
Cleanup JsProtobufUtils (#DH-20578)
bmingles Jan 2, 2026
8543155
Cleanup JsRemoteFileSourceService (#DH-20578)
bmingles Jan 2, 2026
de0a359
Applying Colin's client proto generation (#DH-20578)
bmingles Jan 5, 2026
b88b4eb
Changed to runtime dependencies and fixed docs links (#DH-20578)
bmingles Jan 5, 2026
7ee9121
Addressed review comments in RemoteFileSourceClassLoader (#DH-20578)
bmingles Jan 6, 2026
a8b8c61
Addressed review comments in PluginMarker (#DH-20578)
bmingles Jan 6, 2026
accb2dc
Addressed review comments in RemoteFileSourceMessageStream (#DH-20578)
bmingles Jan 6, 2026
e79b25f
Addressed review comments in JsRemoteFileSourceService (#DH-20578)
bmingles Jan 6, 2026
f9beca5
Changed to getResource (#DH-20578)
bmingles Jan 13, 2026
81a9de2
Addressed review comments (#DH-20578)
bmingles Jan 14, 2026
8a9481e
Addressed review comments (#DH-20578)
bmingles Jan 14, 2026
2a293e9
Addressed review comments (#DH-20578)
bmingles Jan 14, 2026
1f000f3
Removed timeout (#DH-20578)
bmingles Jan 14, 2026
3f3ed19
Attempt at clearing class cache (#DH-20578)
bmingles Feb 20, 2026
f08fd83
Fixed bug with cache clearing only after running script 2x (#DH-20578)
bmingles Feb 20, 2026
75ff3db
Cleaned up some implementation of groovy session clearing (#DH-20578)
bmingles Feb 24, 2026
90dae47
Clean up (#DH-20578)
bmingles Feb 24, 2026
81785f3
Clean up (#DH-20578)
bmingles Feb 24, 2026
22762e3
Clean up (#DH-20578)
bmingles Feb 24, 2026
1a8a97c
cleanup (#DH-20578)
bmingles Feb 24, 2026
3c67bff
cleanup (#DH-20578)
bmingles Feb 24, 2026
8e6215f
cleanup (#DH-20578)
bmingles Feb 24, 2026
8375252
Simplified to an evict all strategy (#DH-20578)
bmingles Feb 25, 2026
e765acf
comment (#DH-20578)
bmingles Feb 25, 2026
a5f3af5
removed unused method (#DH-20578)
bmingles Feb 25, 2026
24b0d29
comments (#DH-20578)
bmingles Feb 25, 2026
168e776
spotless (#DH-20578)
bmingles Feb 25, 2026
58f5919
Made execution context updating more targetted. (#DH-20578)
bmingles Mar 3, 2026
57e158f
Addressed code review comments (#DH-20578)
bmingles Mar 3, 2026
38e5bae
Renamed RemoteFileSourceClientRequest -> RemoteFileSourceClientMessag…
bmingles Mar 3, 2026
c522e5d
Added is_dirty flag to execution context (#DH-20578)
bmingles Mar 5, 2026
ae41196
regen gwt bindings
niloc132 Mar 5, 2026
d37be22
Merge pull request #5 from niloc132/DH-20578_groovy-remote-file-sourcing
bmingles Mar 5, 2026
38e7189
Ran ./gradlew updateProtobuf (#DH-20578)
bmingles Mar 11, 2026
9664504
Added isDirty flag (#DH-20578)
bmingles Mar 13, 2026
9bcc8b2
Merge branch 'main' into DH-20578_groovy-remote-file-sourcing
bmingles Mar 30, 2026
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
@@ -0,0 +1,254 @@
//
// Copyright (c) 2016-2026 Deephaven Data Labs and Patent Pending
//
package io.deephaven.engine.util;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;

/**
* A custom ClassLoader that fetches source files from remote clients via registered RemoteFileSourceProvider instances.
* This is designed to support Groovy script imports where the source files are provided by remote clients.
*
* <p>
* When a resource is requested (e.g., for a Groovy import), this class loader:
* <ol>
* <li>Checks registered providers to see if they can source the resource</li>
* <li>Returns a custom URL with protocol "remotefile://" if a provider can handle it</li>
* <li>When that URL is opened, fetches the resource bytes from the provider</li>
* </ol>
*/
public class RemoteFileSourceClassLoader extends ClassLoader {
private static final long RESOURCE_TIMEOUT_SECONDS = 5;

private static volatile RemoteFileSourceClassLoader instance;
private final CopyOnWriteArrayList<RemoteFileSourceProvider> providers = new CopyOnWriteArrayList<>();


/**
* Constructs a new RemoteFileSourceClassLoader with the specified parent class loader.
*
* @param parent the parent class loader for delegation
*/
private RemoteFileSourceClassLoader(ClassLoader parent) {
super(parent);
}

/**
* Initializes the singleton RemoteFileSourceClassLoader instance with the specified parent class loader.
*
* <p>
* This method must be called exactly once before any calls to {@link #getInstance()}. The method is synchronized to
* prevent race conditions when multiple threads attempt initialization.
*
* @param parent the parent class loader for delegation
* @return the newly created singleton instance
* @throws IllegalStateException if the instance has already been initialized
*/
public static synchronized RemoteFileSourceClassLoader initialize(ClassLoader parent) {
if (instance != null) {
throw new IllegalStateException("RemoteFileSourceClassLoader is already initialized");
}

instance = new RemoteFileSourceClassLoader(parent);
return instance;
}

/**
* Returns the singleton instance of the RemoteFileSourceClassLoader.
*
* <p>
* This method requires that {@link #initialize(ClassLoader)} has been called first.
*
* @return the singleton instance
* @throws IllegalStateException if the instance has not yet been initialized via {@link #initialize(ClassLoader)}
*/
public static RemoteFileSourceClassLoader getInstance() {
if (instance == null) {
throw new IllegalStateException("RemoteFileSourceClassLoader is not yet initialized");
}
return instance;
}

/**
* Registers a new provider that can source remote resources.
*
* @param provider the provider to register
*/
public void registerProvider(RemoteFileSourceProvider provider) {
providers.add(provider);
}

/**
* Unregisters a previously registered provider.
*
* @param provider the provider to unregister
*/
public void unregisterProvider(RemoteFileSourceProvider provider) {
providers.remove(provider);
}

/**
* Returns whether there are any active providers with non-empty resource paths configured. This indicates that
* remote sources are actually configured, not just that the execution context is set.
*
* @return true if any provider is active and has resource paths configured, false otherwise
*/
public boolean hasConfiguredRemoteSources() {
for (RemoteFileSourceProvider candidate : providers) {
if (candidate.isActive() && candidate.hasConfiguredResources()) {
return true;
}
}
return false;
}

/**
* Returns whether the current execution context is dirty, indicating that remote sources have changed and the cache
* should be cleared. This method is used by GroovyDeephavenSession to determine when to refresh the class cache.
*
* @return true if there is a dirty execution context, false otherwise
*/
public boolean isDirty() {
for (RemoteFileSourceProvider candidate : providers) {
if (candidate.isActive() && candidate.isDirty()) {
return true;
}
}
return false;
}

/**
* Gets the resource with the specified name by checking registered providers.
*
* <p>
* This method iterates through all registered providers to see if any can source the requested resource. If a
* provider can handle the resource, a custom URL with protocol "remotefile://" is returned. If no provider can
* handle the resource, the request is delegated to the parent class loader.
*
* @param name the resource name
* @return a URL for reading the resource, or null if the resource could not be found
*/
@Override
public URL getResource(String name) {
RemoteFileSourceProvider provider = null;
for (RemoteFileSourceProvider candidate : providers) {
if (candidate.isActive() && candidate.canSourceResource(name)) {
provider = candidate;
break;
}
}

if (provider != null) {
try {
return new URL(null, "remotefile://" + name, new RemoteFileURLStreamHandler(provider, name));
} catch (MalformedURLException e) {
// Fall through to parent if URL creation fails
}
}

return super.getResource(name);
}

/**
* URLStreamHandler that delegates to a RemoteFileSourceProvider to fetch resource bytes.
*/
private static class RemoteFileURLStreamHandler extends URLStreamHandler {
private final RemoteFileSourceProvider provider;
private final String resourceName;

/**
* Constructs a new RemoteFileURLStreamHandler for the specified provider and resource.
*
* @param provider the provider that will source the resource
* @param resourceName the name of the resource to fetch
*/
RemoteFileURLStreamHandler(RemoteFileSourceProvider provider, String resourceName) {
this.provider = provider;
this.resourceName = resourceName;
}

/**
* Opens a connection to the resource referenced by this URL.
*
* @param url the URL to open a connection to
* @return a URLConnection to the specified URL
*/
@Override
protected URLConnection openConnection(URL url) {
return new RemoteFileURLConnection(url, provider, resourceName);
}
}

/**
* URLConnection that fetches resource bytes from a RemoteFileSourceProvider.
*/
private static class RemoteFileURLConnection extends URLConnection {
private final RemoteFileSourceProvider provider;
private final String resourceName;
private byte[] content;

/**
* Constructs a new RemoteFileURLConnection for the specified URL, provider, and resource.
*
* @param url the URL to connect to
* @param provider the provider that will source the resource
* @param resourceName the name of the resource to fetch
*/
RemoteFileURLConnection(URL url, RemoteFileSourceProvider provider, String resourceName) {
super(url);
this.provider = provider;
this.resourceName = resourceName;
}

/**
* Opens a connection to the resource by requesting it from the provider.
*
* <p>
* This method fetches the resource bytes from the provider with a timeout of {@value #RESOURCE_TIMEOUT_SECONDS}
* seconds. If already connected, this method does nothing.
*
* @throws IOException if the connection fails or times out
*/
@Override
public void connect() throws IOException {
if (!connected) {
try {
content = provider.requestResource(resourceName)
.orTimeout(RESOURCE_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.get();
Comment on lines +224 to +226
connected = true;
} catch (Exception e) {
throw new IOException("Failed to fetch remote resource: " + resourceName, e);
}
}
}

/**
* Returns an input stream that reads from this connection's resource.
*
* <p>
* This method calls {@link #connect()} to ensure the connection is established and resource bytes are fetched
* from the provider. The method then verifies that content has been successfully downloaded before creating the
* input stream.
*
* @return an input stream that reads from the fetched resource bytes
* @throws IOException if the connection or content download fails or if the resource has no content
*/
@Override
public InputStream getInputStream() throws IOException {
connect();
if (content == null || content.length == 0) {
throw new IOException("No content for resource: " + resourceName);
Comment on lines +248 to +249
}
return new ByteArrayInputStream(content);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// Copyright (c) 2016-2026 Deephaven Data Labs and Patent Pending
//
package io.deephaven.engine.util;

import java.util.concurrent.CompletableFuture;

/**
* Interface for providing remote resources to the ClassLoader. Plugins can implement this interface and register with
* RemoteFileSourceClassLoader to provide resources from remote sources.
*/
public interface RemoteFileSourceProvider {
/**
* Check if this provider can source the given resource.
*
* @param resourceName the name of the resource to check (e.g., "com/example/MyClass.groovy")
* @return true if this provider can handle the resource, false otherwise
*/
boolean canSourceResource(String resourceName);

/**
* Check if this provider is currently active and should be used for resource requests.
*
* @return true if this provider is active, false otherwise
*/
boolean isActive();

/**
* Check if this provider has any resource paths configured. A provider can be active (execution context set) but
* have no resource paths configured.
*
* @return true if this provider has resource paths configured, false otherwise
*/
boolean hasConfiguredResources();

/**
* Check if this provider's execution context is dirty, indicating that remote sources have changed and the cache
* should be cleared.
*
* @return true if this provider is active and dirty, false otherwise
*/
boolean isDirty();

/**
* Request a resource from the remote source.
*
* @param resourceName the name of the resource to fetch (e.g., "com/example/MyClass.groovy")
* @return a CompletableFuture containing the resource bytes, or null if not found
*/
CompletableFuture<byte[]> requestResource(String resourceName);
}

Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,21 @@ public ExecutionContext withClassLoader(ClassLoader classLoader) {
operationInitializer, classLoader);
}

/**
* Returns, or creates, an execution context with the given value for {@code queryCompiler} and existing values for
* the other members.
*
* @param queryCompiler the query compiler to use instead
* @return the execution context
*/
public ExecutionContext withQueryCompiler(QueryCompiler queryCompiler) {
if (queryCompiler == this.queryCompiler) {
return this;
}
return new ExecutionContext(isSystemic, authContext, queryLibrary, queryScope, queryCompiler, updateGraph,
operationInitializer);
}


/**
* Execute runnable within this execution context.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ private static void ensureDirectory(final Path directory) {
protected final File classCacheDirectory;
private final ScriptSessionQueryScope queryScope;

protected final ExecutionContext executionContext;
// DH-20578: Not final so it can be updated with fresh QueryCompiler when remote sources are detected
protected ExecutionContext executionContext;

private S lastSnapshot;

Expand All @@ -95,9 +96,26 @@ protected AbstractScriptSession(
this.classCacheDirectory = classCacheDirectory;

queryScope = new ScriptSessionQueryScope();

executionContext = createExecutionContext(updateGraph, operationInitializer, parentClassLoader);
}

/**
* DH-20578: Creates an ExecutionContext with a QueryCompiler based on the provided ClassLoader. This allows
* updating the ExecutionContext when fresh ClassLoaders are needed (e.g., for remote file sources).
*
* @param updateGraph the update graph for the context
* @param operationInitializer the operation initializer for the context
* @param parentClassLoader the ClassLoader to use for creating the QueryCompiler
* @return a new ExecutionContext with a QueryCompiler based on the provided ClassLoader
*/
protected ExecutionContext createExecutionContext(
final UpdateGraph updateGraph,
final OperationInitializer operationInitializer,
final ClassLoader parentClassLoader) {
final QueryCompiler compilerContext = QueryCompilerImpl.create(classCacheDirectory, parentClassLoader);

executionContext = ExecutionContext.newBuilder()
return ExecutionContext.newBuilder()
.markSystemic()
.newQueryLibrary()
.setQueryScope(queryScope)
Expand Down
Loading
Loading