-
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
Add package.json js-plugins support #4389
Add package.json js-plugins support #4389
Conversation
Here's how I tested: FROM node:lts-slim as build
ARG PACKAGE
RUN set -eux; \
npm pack ${PACKAGE}; \
tar --touch --extract --file *.tgz
FROM scratch
COPY --link --from=build /package / docker build --build-arg PACKAGE=@deephaven/js-plugin-table-example --output type=local,dest=/tmp/example .
docker build --build-arg PACKAGE=@deephaven/js-plugin-plotly --output type=local,dest=/tmp/plotly .
docker build --build-arg PACKAGE=@deephaven/js-plugin-matplotlib --output type=local,dest=/tmp/matplotlib . and then started the server up a variety of different configurations: START_OPTS="-Ddeephaven.jsPlugins.foo=/tmp/example -Ddeephaven.jsPlugins.bar=/tmp/plotly -Ddeephaven.jsPlugins.baz=/tmp/matplotlib" and then also mixed it in with the existing way of configuring plugins via |
The latest commit re-introduces |
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.
Approved the python. I did not review the rest.
@@ -170,6 +171,16 @@ public JettyBackedGrpcServer( | |||
// Wire up the provided grpc filter | |||
context.addFilter(new FilterHolder(filter), "/*", EnumSet.noneOf(DispatcherType.class)); | |||
|
|||
final JsPluginsZipFilesystem fs; | |||
try { | |||
fs = JsPlugins.initJsPlugins(Configuration.getInstance()); |
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.
this pattern (making the current class obtain the config instance rather than letting initJsPlugins call it) makes it look like you anticipate multiple instances of JsPlugins to be built from different configs, but I think by its nature we will only have one JsPlugins instance at a time (or at least per-server), since there can only be one absolute path called /js-plugins/manifest.json
?
It seems to push us towards a "better" pattern of injecting configuration, but our existing Configuration type inherently is global, and it requires a bit of boilerplate to achieve this, with no gain that i can see?
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.
In the latest version, I actually am injecting Configuration
.
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.
Right, I'm suggesting not bothering with that - more source churn, minimal payoff (since Configuration isn't mockable, can only have One True Instance, etc), and just referencing Configuration.getInstance() once where it is read
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.
Okay, will undo the Configuration injection.
server/jetty/src/main/java/io/deephaven/server/jetty/JsPlugins.java
Outdated
Show resolved
Hide resolved
server/jetty/src/main/java/io/deephaven/server/jetty/JsPlugins.java
Outdated
Show resolved
Hide resolved
import io.deephaven.plugin.Plugin; | ||
|
||
/** | ||
* A js plugin. |
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.
This doesn't really add much (except as a "there is javadoc" checkbox") - the linked items below are a little dry too, leading one to take apart the code or read the PR's comments rather than gain understanding of it directly. Can you beef these up, at least from the perspective of 'this is an interface that deliberately hides the implementation from you, here's how to use it, what to do with it, etc'?
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.
Let me know what you think about the latest. Kind of punting some specifics to the deephaven-plugins repo.
} | ||
|
||
private void init() throws IOException { | ||
try (final FileSystem fs = FileSystems.newFileSystem(filesystem, Map.of("create", "true"))) { |
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 think this method should be synchronized too, since it is going to modify the zip file in place. It isn't technically necessary, but it seems odd to me to let it be possible to call it again.
that said, why create a new filesystem each time? filesystems are threadsafe, or is there an issue with needing to explicitly close so jetty can read it?
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.
Will sync the init.
I think the zip filesystem does need to be closed to actually write out the contents. The reason being is that the whole .zip file is replaced with a new one; and you can imagine that if you are writing multiple files, anything else is very inefficient:
try (final FileSystem zipfs = FileSystems.newFileSystem(...)) {
for (int i = 0; i < 100; ++i) {
Files.writeString(fs.getPath("f" + i + ".txt"), "some content");
}
}
That said, I'm going to dig in more to verify and will add comment in code.
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.
Yep: verified FileSystem#close is necessary.
.distributionPath(srcEntry.main()); | ||
CopyHelper.copyRecursive(srcDist, destDist); | ||
entries.add(srcEntry); | ||
writeManifest(fs); |
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.
If I understand this correctly, each copyFrom call re-opens the zip file, appends new files to it, then appends a new manifest central directory entry to it, and writes a new toc at the end of the file, and closes it.
The reason for doing this is that we then pass the path to this zip directly to jetty, so it can open it to use as its resource base, and serve files from it. Jetty prefers to not serve from zips, but we disable that to avoid unpacking to /tmp and risk something cleaning out that directory. So we're finding already-unpacked contents on disk so we can pack it and tell jetty to server it still-packed...
Tradeoffs:
- the contents are immutable (changes won't break the server), but the contents are immutable (harder to develop live)
- extra copy on disk (deleted on exit, assuming non-crash exit)
- contents served from zip (shouldn't be a big deal, assuming good caching, small files)
- contents ostensibly filtered to only what is expected to be served
We don't support reading zips/tarballs (i.e. jars, wheels, npm packages), and so always already have an unpacked structure on disk somewhere, and we require (i think) that the unpacked structure matches what we will zip (though we only copy/serve a subset of what was in that directory). Why not point to those existing unpacked dirs, with some filtering for "you only need to see these files"?
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.
Jetty prefers to not serve from zips, but we disable that
Is this documented somewhere? I don't think we need to disable anything to make jetty work with zips...
Why not point to those existing unpacked dirs, with some filtering for "you only need to see these files"?
This might be possible, but it's somewhat dependent on being able to configure jetty later in our startup lifecycle (or, get the plugin information earlier in our lifecycle) than we currently are.
Right now, the zip represents the single-point of entry that we can later add to via plugins:
// Wire up /js-plugins/*
context.addServlet(zipFsServlet, "/js-plugins/*");
If we were to allow plugins to add to them, it could look something like:
WebAppContext context = new WebAppContext(null, "/js-plugins/<pluginName>", null, null, null, new ErrorPageErrorHandler(), NO_SESSIONS);
context.setBaseResource(createResource(<pluginPath>));
addHandler.accept(context);
And then we'd need to create an immutable manifest.json resource as well.
I think it would be a tricky change to the order of when things are created during startup, ala io.deephaven.server.runner.DeephavenApiServer#run
.
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.
Of course, there may be ways we could use a Servlet
and add later w/ with uncopied plugin Resources, but I don't really know the specific jetty machinations to see if that would be possible.
* @return the distribution path | ||
*/ | ||
public final Path distributionPath(String main) { | ||
return path().resolve(main).getParent(); |
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 at all sure that this is correct, except for projects that follow the convention that you're used to. That is, two cases this doesn't do what I think you want (but docs are light, so I can't be sure):
- nested directories, like
dist/js/index.js
(unsure how common this is) - the other files in the dist/ output dir will be missed this way (for example in this contrived case, adist/css/main.css
could exist) - no dist dir, like
mylibrary.min.js
(this is decently common, where the .min.js file is generated, and just lives adjacent to the mylibrary.js file - now the whole dir, including package.json, possibly .git/ dir will be copied/served
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.
Okay; I think we can copy the full contents over as is, and remove this assumption.
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.
Approved python. Have not reviewed the rest.
server/jetty/src/main/java/io/deephaven/server/jetty/JsPlugins.java
Outdated
Show resolved
Hide resolved
server/src/main/java/io/deephaven/server/runner/DeephavenApiServerComponent.java
Outdated
Show resolved
Hide resolved
server/src/main/java/io/deephaven/server/plugin/js/JsPluginModule.java
Outdated
Show resolved
Hide resolved
server/src/test/java/io/deephaven/server/runner/DeephavenApiServerTestBase.java
Outdated
Show resolved
Hide resolved
server/src/test/java/io/deephaven/server/runner/DeephavenApiServerTestBase.java
Outdated
Show resolved
Hide resolved
3a51bc8
.stream() | ||
.map(Registration.class::cast) | ||
.collect(Collectors.toSet()); | ||
return Set.copyOf(jsPluginsPackageRoots()); |
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.
why not apply to the other unnecessary streams above too?
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.
The above aren't streams, but Optional
s that turn into Set.of(Element)
or Set.of()
.
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.
providesConfigDirRegistration and providesResourceBaseRegistration, the two methods above this?
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.
Correct. @IntoSet
might be more appropriate if there is a way to ignore null or optional return values, but I don't suspect that would work...
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.
Ah got it, thanks for clarifying.
Labels indicate documentation is required. Issues for documentation have been opened: How-to: https://github.com/deephaven/deephaven.io/issues/3272 |
This allows the additional configuration of js-plugins via the configuration properties
deephaven.jsPlugins.<x>=<packageRoot>
, with the expectation that<packageRoot>/package.json
exists. This should make some use cases easier, as it's now possible to configure js-plugins without the need to create amanifest.json
.This re-introduces some code that was originally part of #2908; namely, an on-disk zip filesystem is used as the installable state for js-plugins.
In support of #4405