-
Notifications
You must be signed in to change notification settings - Fork 80
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enhance Groovy classloader experience #4596
Conversation
engine/table/src/main/java/io/deephaven/engine/util/GroovyDeephavenSession.java
Outdated
Show resolved
Hide resolved
engine/table/src/main/java/io/deephaven/engine/util/GroovyDeephavenSession.java
Outdated
Show resolved
Hide resolved
engine/table/src/test/java/io/deephaven/engine/util/scripts/TestGroovyDeephavenSession.java
Show resolved
Hide resolved
private static final boolean ALLOW_UNKNOWN_GROOVY_PACKAGE_IMPORTS = Configuration.getInstance() | ||
.getBooleanForClassWithDefault(GroovyDeephavenSession.class, "allowUnknownGroovyPackageImports", false); | ||
|
||
private static final ClassLoader STATIC_LOADER = | ||
new URLClassLoader(new URL[0], GroovyDeephavenSession.class.getClassLoader()) { | ||
new URLClassLoader(new URL[0], Thread.currentThread().getContextClassLoader()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note for reviewers (or future me, who forgot this fact again): SomeClass.class.getClassLoader()
is only the correct answer when trying to say "load a thing from the same classloader that this class came from", usually for something like getting a resource known to be in the same jar as this class. The context classloader is almost always preferred for "load with the classloader currently being used", since this lets a caller replace the current classloader with a more specific one (that probably points to getClass().getClassLoader()
as a parent or other ancestor, so the same class can still be loaded).
engine/table/src/main/java/io/deephaven/engine/util/GroovyDeephavenSession.java
Outdated
Show resolved
Hide resolved
try { | ||
return Class.forName(className, false, GroovyDeephavenSession.class.getClassLoader()); | ||
return Class.forName(className, false, this.groovyShell.getClassLoader()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As above, a custom classloader installed by the app before calling this wouldn't necessarily be called when a class needs to be loaded in this way. Between this and the above change, not only does groovy get a chance to compile if needed, but a custom classloader can also be used.
@@ -288,7 +384,7 @@ private static Class<?> loadClass(String className) throws ClassNotFoundExceptio | |||
} | |||
} | |||
|
|||
private static boolean classExists(String className) { | |||
private boolean classExists(String className) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewer note: these could be made static again if the classloader was passed as an argument, it is needed in loadClass
.
private void addDefaultImports(ImportCustomizer imports) { | ||
// TODO (core#230): Remove large list of manual text-based consoleImports | ||
// NOTE: Don't add to this list without a compelling reason!!! Use the user script import if possible. | ||
imports.addImports( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewer note: I made this a list of class literals to assist in future refactoring, but can revert to just putting a list of strings for each of these.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could introduce a helper method to invoke getName automatically, but this doesn't look like it will be gross to maintain as is =P.
engine/table/src/main/java/io/deephaven/engine/util/GroovyDeephavenSession.java
Outdated
Show resolved
Hide resolved
|
||
@After | ||
public void tearDown() { | ||
executionContext.close(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure how we got away with not closing the exec context before, are we possibly just opening other context on top of it and never checking that none were leaked?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing is helpful to ensure that we are setting it before every test that needs it. It is otherwise not an error to not "close" an open execution at this time. Thanks for finding that it was missing here!
engine/table/src/test/java/io/deephaven/engine/util/scripts/TestGroovyDeephavenSession.java
Outdated
Show resolved
Hide resolved
engine/table/src/main/java/io/deephaven/engine/util/GroovyDeephavenSession.java
Outdated
Show resolved
Hide resolved
engine/table/src/main/java/io/deephaven/engine/util/GroovyDeephavenSession.java
Outdated
Show resolved
Hide resolved
Here's an extremely hacky example classloader that produces groovy files on the fly, which could easily be a blocking call to read network/table data. Set this up before DHC has started, so that it will be used to load groovy files as requested by the DHE groovy IDE. final Path dir = Files.createTempDirectory("dhc");
Thread.currentThread().setContextClassLoader(new ClassLoader(Thread.currentThread().getContextClassLoader()) {
@Nullable
@Override
public URL getResource(String name) {
// if this is a groovy file, pretend to load contents remotely, and return a file: URL
// to it, otherwise groovy won't parse it.
if (name.startsWith("notebook/")) {// be sure not to take over deephaven/groovy/java files
Path path = Path.of(name);
String packageString = StreamSupport.stream(path.getParent().spliterator(), false)
.map(Path::toString).collect(Collectors.joining("."));
Path fileName = path.getParent().relativize(path);
if (fileName.endsWith("Script.groovy")) {// this check doesn't make sense for a real use case, but
// we're just going to make a fake file
Path result = dir.resolve(path);
try {
if (!Files.exists(result)) {
Files.createDirectories(result.getParent());
System.out.println(result);
Files.writeString(result, "package " + packageString + "\nprintln 'hello, world'\n");
}
return result.toFile().toURL();
} catch (IOException e) {
// instead of failing, possibly log, then pass the buck to the next link in the chain
}
}
}
return super.getResource(name);
}
}); |
|
||
@After | ||
public void tearDown() { | ||
executionContext.close(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing is helpful to ensure that we are setting it before every test that needs it. It is otherwise not an error to not "close" an open execution at this time. Thanks for finding that it was missing here!
// import is not optional, ; is optional; | ||
"static io.deephaven.engine.util.scripts.TestGroovyDeephavenSession.StaticClass", | ||
// groovy is not cool with non-star package imports for packages that don't exist | ||
"import com.illumon.foo.bar;", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
io.deephaven? we probably don't want to reference illumon in these tests
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, but the point is that the package does not exist...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically, io.deephaven.foo.bar
does not exist. I'm just considering all of the effort we went through to remove references to illumon and db, for example.
|
||
c = "t = emptyTable(1).updateView(\"Y=" + Y + "\", \"Z=max(Y, d)\")\n"; | ||
try { | ||
QueryScope.addParam("z", z); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand why you're addParam
on each test - in this particular case you're setting z
but looks unused. Why not set all parameters once at the top of the method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, this seems a bit off, but is part of the original test from DHE. I tried to keep my changes here limited, ensuring that except where I explicitly changed something, the same things that pass for DHE passes in DHC.
} | ||
} | ||
} else { | ||
okToImport = classExists(body); | ||
if (classExists(body)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd collapse this with the line above to reduce a level of indentation:
} else if (classExists(body)) {
// ...
} else {
// ...
}
if (isWildcard) { | ||
result = Optional.of(imports -> imports.addStaticStars(body)); | ||
} else { | ||
if (alias != null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would collapse this if
with else
above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or drop the else entirely and just return once? no opinions offered either way, so I'm going to apply that change and your suggestions...
private void addDefaultImports(ImportCustomizer imports) { | ||
// TODO (core#230): Remove large list of manual text-based consoleImports | ||
// NOTE: Don't add to this list without a compelling reason!!! Use the user script import if possible. | ||
imports.addImports( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could introduce a helper method to invoke getName automatically, but this doesn't look like it will be gross to maintain as is =P.
@@ -68,6 +68,7 @@ dependencies { | |||
testImplementation project(':extensions-parquet-table') | |||
testImplementation project(':extensions-source-support') | |||
testImplementation project(':Numerics') | |||
testImplementation project(':extensions-suanshu') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we still need this? thought it was deprecated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added for test classpaths only, as an example used in DHE tests for groovy parameter disambiguation hit one of these methods.
* Adds the default imports that Groovy users assume to be present. | ||
*/ | ||
private void addDefaultImports(ImportCustomizer imports) { | ||
// TODO (core#230): Remove large list of manual text-based consoleImports |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this PR close #230?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, just moves it to here rather than a config entry.
engine/table/src/main/java/io/deephaven/engine/util/GroovyDeephavenSession.java
Show resolved
Hide resolved
engine/table/src/main/java/io/deephaven/engine/util/GroovyDeephavenSession.java
Outdated
Show resolved
Hide resolved
engine/table/src/main/java/io/deephaven/engine/util/GroovyDeephavenSession.java
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, leaving approval for others.
engine/table/src/main/java/io/deephaven/engine/util/GroovyDeephavenSession.java
Outdated
Show resolved
Hide resolved
engine/table/src/main/java/io/deephaven/engine/util/GroovyDeephavenSession.java
Outdated
Show resolved
Hide resolved
7c9b700
Labels indicate documentation is required. Issues for documentation have been opened: How-to: https://github.com/deephaven/deephaven.io/issues/3304 |
Introduces new features when using Groovy as a console language. This
should not remove or change existing features at all, but only serves
to backfill the removed source() and sourceOnce() functions with an
approach that encourages users to write real groovy scripts.
Makes it possible to load .groovy files from the classpath (or a custom
classloader) as classes. Standalone groovy scripts provide a static
main() method (and an instance run() method) to invoke one or more
times, and classes can be reimported to read fresh from disk. Includes
a feature where groovy files loaded from the classpath can use
Deephaven default imports, and can inherit imports from the console,
both disabled by default.
Replaces a list of import statements with the Groovy ImportCustomizer,
so that evaluated code doesn't have a variable number of prefix lines.
This has the advantage of also being applicable to scripts loaded from
the classpath.
Restored unit tests lost when migrating to the deephaven-core codebase,
which helped to validate some of these cases, and verify
console<->querylanguage classloader interactions.