Inconsistency with service loading by layer or by class loader
David Lloyd
david.lloyd at redhat.com
Sat Dec 14 17:10:54 UTC 2024
On Sat, Dec 14, 2024 at 1:57 AM Alan Bateman <alan.bateman at oracle.com>
wrote:
> On 14/12/2024 00:02, David Lloyd wrote:
>
> :
>
> The problem with having one (or a few) broad layer for all named modules
> is twofold: first, that every module that *might* be needed for an
> application must be found and loaded before *any* module is able to be
> loaded. This works in some simpler packaging scenarios but is too
> startup-heavy in cases with very large numbers of modules. The
> "--add-modules" switch on the Java launcher is a direct result of
> forbidding late binding of modules. From a usability perspective, this is
> already far from ideal, and if you start talking about hundreds or
> thousands of modules, it becomes completely unworkable. The second problem
> is that it makes it very difficult to support any kind of dynamicity (for
> example adding additional plugins/service implementations at run time)
> since an outsized amount of static analysis must be done to categorize the
> layers, whereas lazily loading layers solves the problem easily and
> elegantly.
>
> If the real issue here is "too startup-heavy" then that might be something
> to focus on.
>
> The --add-modules command line option serves many cases where additional
> modules may be needed. The intention with `--add-modules ALL-DEFAULT` was
> to help container like applications that in turn load other applications at
> run-time. The container can of course create a module layer before creating
> layers for applications and the only real challenge there is the JDK
> "platform modules", cue the requirement for "dynamic augmentation of
> platform modules". We only got so far on this topic, but enough for needs
> such as allow the JMX agent or a Java agent be loaded into a running VM
> when the modules required to support that are not in the boot layer.
>
> Module layers works well for plugins and services and are of course
> created on-demand. I don't think I understand what you mean by "outsized
> amount of static analysis must be done to categorize the layers", is this a
> reference to your exploration into multi-parent configurations and
> one-module-per-layer?
>
The reason to lazily load and link modules is basically analogous to the
reason that we lazily load and link classes. In fact there is very little
difference. While requiring all classes to be preloaded may have some
benefits in certain circumstances (take GraalVM for example), it also
significantly restricts flexibility in a few important ways, and of course
has a heavy performance cost, and so is not generally considered to be a
good strategy for application runtimes or even the Java launcher itself.
For modules, we face the same issue. I can, for example, create a single
application layer with a thousand modules in it. Before the application can
start, every JAR has to be opened and every module has to be loaded, which
entails parsing or dynamically creating descriptors (generally a
combination of these things), each with dozens of support objects for
things like dependencies, exports, and services, and then their internal
graphs have to be resolved and wired and checked for consistency. So there
is a significant performance cost there. But, that is not the only problem.
A user application is often a combination of many modules in addition to
our own basic modules, plus a significant amount of generated code. The
resultant module graph might even have internal consistency problems (for
example, having multiple versions of a module, or cyclical dependencies)
that can't realistically be resolved socially because many of these modules
are going to be from third parties that we may or may not have any control
over, and might even be dependencies brought in by the user which we know
nothing about. These graphs could be the result of very complex resolution
of Maven artifacts for example.
Now I could be clever and break this application up into different layers
that I can lazily resolve as a unit. As designed, layers are strictly
hierarchical (i.e. non cyclical). So I need a way to analyze my thousand
JARs and find islands of interdependence that can work as a layer (which is
to say, that do not need to be encapsulated from "lower" layers or can work
as independent layers), for example by analyzing the service graphs of
every module. This is not an easy problem. Sometimes an application is
logically layered, but usually it is not. Most often, the layers themselves
end up being quite large anyway once all the interdependencies are worked
out.
Layering (or more generally, arranging program modules into a DAG) has
always been, in my opinion, a poor way to model a complex program; it
creates an arbitrary constraint that never quite pays off in terms of
performance, usability, or maintainability (one symptom of the flaws in
this design is that services found via multiple routes to a single parent
are reported multiple times; another is the parent-first/child-first
dichotomy which has subtle implications). We moved from layered class
loading to parentless, arbitrary class loader graphs for the JBoss
application service about 15 years ago for this reason (one class loader
per (pre-JDK9) module), and this move essentially saw the end of "JAR hell"
for us and brought many benefits (such as a massive reduction in
maintenance costs, better startup time, and much stronger encapsulation)
besides. The improvement was so dramatic in fact that it is now my belief
that the parent delegation model was essentially a design error. So from my
view, the question isn't "why can you not conform to the design of the
module system" but rather "why doesn't the module system design conform to
the requirements of those who seek to use it". The only way to bring our
desired model to the JPMS appears to be by using a layer per module (and a
corresponding class loader per module). This way, we don't have to do any
analysis of the module graph beyond a single module, and only on demand,
which makes things much simpler and faster for us at startup and as the
application runs. The application can start up quickly regardless of
whether there are only a handful or modules or many thousands of them. Many
inconsistencies that don't affect the integrity of the application can be
handled gracefully.
The service loader problem is the first, and so far only, insurmountable
road block for me in pursuing this model, because with separate layers per
module, no module can find services from any other module under any
circumstances (unless they use a different service loading API that we
provide, or use some other provided API to find the layers to search, which
as I said before isn't likely to ever happen).
One workaround I was playing with was defining a special kind of "reverse
dependency" which causes the layers of all modules containing
implementations of a service to be parents of the module layers that
require it. Obviously this is fragile in the face of cycles though, and
service requirements may commonly be cyclical, so it's not really a viable
solution for us.
Another workaround is to put anything which provides a service in an
unnamed module. This actually works fairly well, because then my class
loader implementation can reroute services just like we did in the old
days. In this case though, the service provider won't be able to use the
`provider` method mechanism for service loading. But, any library which
expects to function correctly as an unnamed module (i.e. most of them)
generally cannot take advantage of this mechanism anyway in most
circumstances. We also lose some encapsulation features, and finding the
name of a class's module is a bit trickier. We also lose some nice stack
trace stuff, but I work around this by naming the class loader with the
module name and version, but wrapped in brackets `[` `]` so that we know at
a glance that the module is unnamed.
My current experiment is to instrument all loaded code and intercept all
attempts to load services, and mediate these requests through our own API.
I had a problem with the classfile API that held me up last week but I
expect that I'll have some results from this next week. That said, it's not
a realistic long term solution, because instrumenting classes at run time
with the classfile API has too high a cost.
What I would really like to see is a way for us to gain control over
service loading when using custom layers or class loaders. One idea would
be for class loaders to gain an API which is used by the service loader to
find all services for a given service type e.g. `protected List<String>
findServiceProviders(Class<?> serviceType)`. The default implementation
might find all `META-INF/services/*` resources and read the module
descriptor of the layer for example. But custom run time systems would be
able to find services based on other kinds of dependencies that are not
known to the JPMS.
--
- DML • he/him
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/jigsaw-dev/attachments/20241214/ed784e02/attachment-0001.htm>
More information about the jigsaw-dev
mailing list