Skip to content
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

Implement OSGi Service Loader Mediator Specification #295

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

HannesWell
Copy link
Member

In order to use bundles that use the Java ServiceLoader mechanism an implementation of the OSGi Service Loader specification is required at runtime. The reference implementation, which is also used by the Eclipse Platform at the moment, is Apache Aries SPI Fly, which uses class-file transformation in order to intercept calls to the ServiceLoader and needs to be configured as auto-started bundle.

This PR is a simple prototype with the intend to implement the OSGi Service Loader specification directly in org.eclipse.osgi.
The basic idea is to intercept ServiceLoader calls by using a ClassLoaderHook and searching for the ServiceLoader in the caller classes if the chances are high that the ClassLoader.getResource() and Class.forName() is called by the ServiceLoader.

This is currently a draft, that I have implemented naively without full knowledge of the entire OSGi Service Loader specification, so there are probably many things to work out. Nevertheless I think it already demonstrates the basic approach and at least for supplying a logging provider to the slf4j.api bundle this already works.

@laeubi FYI
@tjwatson what do you think?

@HannesWell
Copy link
Member Author

HannesWell commented Jul 31, 2023

The basic ideas could also be used to enable the usage of the ServiceLoader with less configuration in OSGi as part of the Core framework as discussed in osgi/osgi#372.
A framework would then just need to index the provided services in the bundle jars after resolution/installation and could supply the corresponding classes when it detect calls from the ServiceLoader in the same way as it is suggested in this PR.

@github-actions
Copy link

Test Results

     24 files  ±0       24 suites  ±0   11m 43s ⏱️ +28s
2 150 tests ±0  2 106 ✔️ +1  44 💤 ±0  0  - 1 
2 194 runs  ±0  2 150 ✔️ +1  44 💤 ±0  0  - 1 

Results for commit a9391e4. ± Comparison against base commit 3bd3dbd.

@laeubi
Copy link
Member

laeubi commented Aug 1, 2023

I think this is a very creative way to solve the problem but I'm not sure if the resource/classlaoding is always triggered, e.g what happens if the classes are already loaded? So the best would be to add a testcase that covers all possible call variants of service loader especially those that lookup the services multiple times (also using reset())...

@rotty3000 might can help on the details with the specification itself...

return null;
}

private <T> Stream<T> searchProviders(String name, ModuleClassLoader classLoader,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of dynamically searching this list I would use BundleTracker in the Hook that collects bundles and read their META-INF/services in a datastructure that can then be used to lookup current active providers, that has several advantages:

  1. no need to query the wiring and iterate the bundles
  2. easier to debug as there is some current state to inspect
  3. instead of the latestServiceName we can inspect if the bundle tries to load a class that is an SPI of another bundle and then inspect the stack, requiring a certain call order seems a bit fragile if something changes in the implementation of service loader.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of dynamically searching this list I would use BundleTracker in the Hook that collects bundles and read their META-INF/services in a datastructure that can then be used to lookup current active providers, that has several advantages:

IIRC the service loader mediator specification requires the impl to look at the wiring to allow a bundle to "use" only the providers it is wired to.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of dynamically searching this list I would use BundleTracker in the Hook that collects bundles and read their META-INF/services in a datastructure that can then be used to lookup current active providers, that has several advantages:

IIRC the service loader mediator specification requires the impl to look at the wiring to allow a bundle to "use" only the providers it is wired to.

I think so as well. However having a state also allows to avoid the ThreadLocal.
In general I try to have a somewhat hybrid approach that maintains a state with all services, but when there is a query for services checks the current wires if that bundle is permitted to really obtain them.
The goal is also a bit to demonstrate how this could be done totally without (or at least with minimal) metadata as intended in osgi/osgi#372. If that issue is once resolved all the checks for wires could simply be removed and everything possible could be supplied.

@@ -113,6 +114,7 @@ public void initialize() {
addClassLoaderHook(new DevClassLoadingHook(container.getConfiguration()));
addClassLoaderHook(new EclipseLazyStarter(container));
addClassLoaderHook(new WeavingHookConfigurator(container));
addClassLoaderHook(new ServiceLoaderMediatorHook());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be provided as a fragment of org.eclipse.osgi like:

Fragment-Host: org.eclipse.osgi;bundle-version="[3.10.0,4.0.0)"

and use a hookconfigurators.properties to specify an implementation of org.eclipse.osgi.internal.hookregistry.HookConfigurator like

hook.configurators= \
org.eclipse.equinox.weaving.hooks.WeavingHook

Then you can add your ClassLoaderHook with HookConfigurator.addHooks(HookRegistry)

We should keep this out of the framework as long as service loader mediator specification is not part of the OSGi Core specification.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered this as well, mainly for the point you mentioned below that it would allow later Java versions than 8 (9 would already be a big win).
But the big drawback of this approach is that one has to list the fragment as framework-extension in a system-property of the runtime. For example in a launch config:
-Dosgi.framework.extensions=reference:file:${project_loc:/org.eclipse.equinox.spi}, in a product definition this would usually be -Dosgi.framework.extensions=reference:file:org.eclipse.equinox.spi, assuming the spi and o.e.osgi jar are colocated.
Or is there another way to load a framework-extension with the framework's classloader so that the hookconfigurators.properties are found?
Because actually another goal of this was to avoid the need (if possible) for any configuration that it just works out of the box.
For the build-time some p2-instructions could be added to the fragment, but PDE does not yet have any help for that as far as I know.

@tjwatson
Copy link
Contributor

tjwatson commented Aug 1, 2023

I think this is a very creative way to solve the problem but I'm not sure if the resource/classlaoding is always triggered, e.g what happens if the classes are already loaded? So the best would be to add a testcase that covers all possible call variants of service loader especially those that lookup the services multiple times (also using reset())...

Tests will be needed regardless. But handling cases where the class is already loaded should not be a concern if the class is loaded by a framework class loader. This is because the hook will be in place BEFORE any class is defined from any and all bundles installed in the framework. Say something is loaded by an OSGi connect class loader? I am fairly certain there is no solution that can implement the service loader mediator in that case. Only way that will be solved generally is if we can convince Java to add something to the service loader to allow us to hook into it instead of hacking around with class loaders to do this.

return false;
}
return requiredWires.stream().anyMatch(w -> w.getRequirement().getDirectives().getOrDefault("filter", "") //$NON-NLS-1$//$NON-NLS-2$
.contains("(osgi.extender=osgi.serviceloader.processor)") //$NON-NLS-1$
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More efficient and reliable to check the provider attribute osgi.extender equals osgi.serviceloader.processor I think and assume the filter matched it instead of trying to do a simple string compare here.

String filterDirective = requirement.getDirectives().get("filter"); //$NON-NLS-1$
Object serviceLoaderAttribute = wire.getCapability().getAttributes().get("osgi.serviceloader"); //$NON-NLS-1$
return serviceName.equals(serviceLoaderAttribute)
&& ("(osgi.serviceloader=" + serviceName + ")").equals(filterDirective); //$NON-NLS-1$ //$NON-NLS-2$
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this extra check on filter is needed.

return null;
}

private <T> Stream<T> searchProviders(String name, ModuleClassLoader classLoader,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of dynamically searching this list I would use BundleTracker in the Hook that collects bundles and read their META-INF/services in a datastructure that can then be used to lookup current active providers, that has several advantages:

IIRC the service loader mediator specification requires the impl to look at the wiring to allow a bundle to "use" only the providers it is wired to.

@laeubi
Copy link
Member

laeubi commented Aug 1, 2023

Only way that will be solved generally is if we can convince Java to add something to the service loader to allow us to hook into it instead of hacking around with class loaders to do this.

That would be great. Do you think it would be possible to suggest such think to the OpenJDK team and describe what would be required from framework POV?

But handling cases where the class is already loaded should not be a concern if the class is loaded by a framework class loader.

I think you are right, the only thing I can think of is if the bundle is "too late" then the user might need to reset the serviceloader to make this visible.

@tjwatson
Copy link
Contributor

tjwatson commented Aug 1, 2023

I think you are right, the only thing I can think of is if the bundle is "too late" then the user might need to reset the serviceloader to make this visible.

That is a limitation of Equinox framework hooks, they can only be added when the framework is initialized. They cannot be added dynamically later. If using Equinox Framework Hooks then the framework must be restarted to load up the mediator implementation.

@tjwatson
Copy link
Contributor

tjwatson commented Aug 1, 2023

That would be great. Do you think it would be possible to suggest such think to the OpenJDK team and describe what would be required from framework POV?

That is supposed to be what osgi/osgi#372 was about. But there has not been much activity on that issue.

@laeubi
Copy link
Member

laeubi commented Aug 1, 2023

I mean if an impl do severcieloaderlookup and get some classes and after that I install another bundle then there is no way to tell the consumer "here is another one" ... the hook should best be installed right away from the start of the framework.


@SuppressWarnings({ "unchecked", "rawtypes" })
private Supplier<Optional<Class<?>>> getCallerClassComputer() {
try { // The Java-9+ way
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you place this in a framework fragment then we can make it depend on a higher osgi.ee than Java 8.

@HannesWell
Copy link
Member Author

Thanks for all the feedback, I have already started to implemented some of the suggestions.
But I'm on vacation until mid next week and will update this PR when back and on a good new state.

I mean if an impl do severcieloaderlookup and get some classes and after that I install another bundle then there is no way to tell the consumer "here is another one" ... the hook should best be installed right away from the start of the framework.

Yes, but I assume that ServiceLoader objects are not kept for a long time. I guess that the usual usage pattern is that one looks for as many instances of a service as desired and collects them and if later interested in instances again creates a fresh instance.

@laeubi
Copy link
Member

laeubi commented Apr 21, 2024

@HannesWell any chance we can push this forward?

@HannesWell
Copy link
Member Author

@HannesWell any chance we can push this forward?

Yes. I will first complete my work on the native launcher build pipeline and will then continue with this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants