Mark micrometer OSGi imports as optional and add bundle resolution test#1982
Mark micrometer OSGi imports as optional and add bundle resolution test#1982rozza wants to merge 1 commit into
Conversation
c15546a to
5d82a4f
Compare
The micrometer-observation dependency is optional but its packages were imported as required in the OSGi metadata, causing bundle resolution failure in OSGi containers. Adds io.micrometer.*;resolution:=optional to driver-core Import-Package. Also adds a new testing/osgi-test module that uses Apache Felix to verify all driver bundles resolve without optional dependencies present. This runs in CI via static-checks.sh to prevent future regressions. JAVA-6215
There was a problem hiding this comment.
Pull request overview
This PR fixes OSGi bundle resolution when Micrometer is not present by marking Micrometer package imports as optional in the driver’s OSGi metadata, and introduces an automated OSGi resolution test module (Apache Felix-based) to prevent regressions in CI.
Changes:
- Mark
io.micrometer.*imports asresolution:=optionalindriver-coreOSGiImport-Package. - Add
:testing:osgi-testmodule with a JUnit test that installs/starts driver bundles in an embedded Felix framework to verify resolution without optional deps. - Run the new OSGi test module in Evergreen static checks.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
testing/osgi-test/src/test/java/com/mongodb/osgi/OsgiBundleResolutionTest.java |
New Felix-based test that installs driver bundles and asserts they resolve/start without optional dependencies. |
testing/osgi-test/build.gradle.kts |
New Gradle module wiring + dependencies for Felix/JUnit/AssertJ and jar prerequisites. |
settings.gradle.kts |
Includes the new :testing:osgi-test subproject. |
gradle/libs.versions.toml |
Adds version-catalog entry for org.apache.felix.framework. |
driver-core/build.gradle.kts |
Marks Micrometer packages as optional in OSGi Import-Package. |
.evergreen/static-checks.sh |
Executes the new OSGi module checks in CI. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "driver-core", | ||
| "driver-sync", | ||
| "driver-reactive-streams" |
| try (JarFile jar = new JarFile(file)) { | ||
| String version = jar.getManifest().getMainAttributes().getValue("Bundle-Version"); | ||
| if (version == null) { | ||
| version = "0.0.0"; | ||
| } |
There was a problem hiding this comment.
Good catch 🎉 minor things... the only concerning points are the one reported by Claude Review & Copilot (the NPE & the bundle coverage see below). Note: if we update the bundles we need to update the task dependency in https://github.com/mongodb/mongo-java-driver/pull/1982/changes#diff-2f25ca21826808ef1b48096651d4c012fa373323c328cb630b57fc904edf60edR39
AI review (/driver-code-review with Opus 4.7 1M)
Findings
driver-core/build.gradle.kts
- 🎉
[praise]L105: The fix is precise and consistent with the existing pattern for optional packages (io.netty.*,com.amazonaws.*,org.slf4j.*, etc.). Addingio.micrometer.*;resolution:=optional
is the correct minimal change. Verified: onlydriver-coresource actually referencesio.micrometer.*, sodriver-syncanddriver-reactive-streams(which also declare
optionalImplementation(libs.micrometer.observation)but never import the package) don't need the same change — bnd only emits Import-Package entries for packages actually referenced in compiled bytecode.
testing/osgi-test/src/test/java/com/mongodb/osgi/OsgiBundleResolutionTest.java
-
🟡
[important]L51-56 (BUNDLE_MODULES): Coverage is narrower than the PR description claims. The PR says the test verifies "all driver bundles resolve," but the array only includesbson,
driver-core,driver-sync,driver-reactive-streams. The repo publishes ~10 OSGi bundles —bson-record-codec,bson-scala,bson-kotlin,bson-kotlinx,driver-kotlin-sync,driver-kotlin-coroutine,
driver-kotlin-extensions,driver-scala,mongodb-crypt,driver-legacy— and none are covered. Several of these are likely the most regression-prone (Kotlin/Scala bundles, since their compiled output
references language runtimes). This is the same gap Copilot flagged and that remains unaddressed. Either expand the coverage or amend the PR description to scope the goal to "core publishable bundles" and file
a follow-up issue for the rest. -
🟡
[important]L181:JarFile#getManifest()may returnnull; the chainjar.getManifest().getMainAttributes()...is a latent NPE. The surroundingtry/catchonly catchesIOException, so an NPE
would propagate and abort framework startup. In practice bothreactive-streamsandreactor-coreship valid manifests so this is theoretical, but it's a one-line defensive fix and Copilot already raised it:Manifest manifest = jar.getManifest(); String version = manifest != null ? manifest.getMainAttributes().getValue("Bundle-Version") : null; if (version == null) { version = "0.0.0"; }
-
🟢
[nit]L197: The catch block silently swallowsIOException(// Skip JARs that can't be read). ASystem.err.printlnorLogger.warnwould help future maintainers diagnose a degraded
extraPackagesstring if the cause ever isn't intentional. -
🟢
[nit]L95-100 (bundlesResolveWithoutOptionalDependencies):Assertions.failthrowsAssertionError, so the loop terminates on the first bundle failure. If multiple bundles regress, only the
first is reported. A soft-assertion pattern (collect all failures, report at end) would give more complete diagnostics, but this is fine for now. -
💡
[suggestion]L84-100: Callingbundle.start()does trigger resolution and will throwBundleException(RESOLVE_ERROR)on failure, so the current approach catches real regressions. An alternative
that surfaces issues earlier and more explicitly isframework.adapt(FrameworkWiring.class).resolveBundles(installed)afterinstallBundleand beforestart()— it returnsfalseif any bundle fails to
resolve and lets you inspect bundle state without depending on activator side-effects. Not blocking. -
💡
[suggestion]L66 (felix.log.level = "1"): Errors-only is fine for CI but unhelpful when debugging locally. Consider gating verbosity on a system property (e.g.,-Dfelix.log.level=4) so
developers can crank it up without modifying source. -
🎉
[praise]L107-129 (formatBundleFailure): Excellent developer-UX touch. Parsingosgi.wiring.package=out of the exception message and printing an actionableFIX:line directly in the test
failure output is far better than a raw Felix stack trace. This will save real debugging time on future regressions. -
🎉
[praise]L141-178 (buildSystemPackagesFromClasspath): Dynamically discovering reactor-core / reactive-streams packages from the test classpath (rather than hardcoding versions and package lists)
is a clean approach that keeps the test in sync with the version catalog automatically. Good design.
testing/osgi-test/build.gradle.kts
-
🟢
[nit]L22-24: HardcodingJavaLanguageVersion.of(17)works today sinceproject.DEFAULT_JAVA_VERSION = 17(buildSrc/.../project/Companion.kt), but it'll silently diverge if the project bumps
that constant. Importing and usingDEFAULT_JAVA_VERSION(as other modules do viaproject.java) would keep them locked together. -
🟢
[nit]L38:dependsOn(":bson:jar", ":driver-core:jar", ":driver-sync:jar", ":driver-reactive-streams:jar")duplicates theBUNDLE_MODULESarray in the test class. If one is updated and the other
isn't, the test will either fail to find a JAR or skip a module silently. A comment cross-referencing the test class, or generating the list from a single source, would prevent drift. -
💡
[suggestion]Test source layout: Usessrc/test/java/instead of the project'ssrc/test/unit//src/test/functional/convention (established inconventions.testing-junit). Minor
inconsistency; understandable given this module doesn't applyconventions.testing-junit.
gradle/libs.versions.toml
- 🟢
[nit]L42 and L178:felix-frameworkis inserted out of alphabetical order in both the[versions]# Testblock (currently betweenassertjandaws-lambda-core) and the[libraries]block
(currently betweenclassgraphandfindbugs-jsr). The file is mostly alphabetical;felix-frameworkwould naturally sort betweenfindbugs-jsrandgroovy.
.evergreen/static-checks.sh
- 💡
[suggestion]L17-18: This adds a second./gradlewinvocation, which incurs a fresh daemon startup and re-configures the project. Folding:testing:osgi-test:checkinto the main invocation above
(it already runsjar, which the test depends on) would shave CI time. The standalone invocation is fine if the goal is isolated, clearly attributed CI output.
Prior Review Comments
Both Copilot comments remain unaddressed:
- L57
BUNDLE_MODULEScoverage — Merged into the [important] finding above. - L181
getManifest()NPE — Merged into the [important] finding above.
Summary
The core fix is correct and minimal, and the new OSGi resolution test is a thoughtful addition with strong diagnostic output and a clever auto-syncing system-packages mechanism. The two concerns worth
addressing before merge are the unguarded getManifest() dereference (a one-line defensive fix) and the gap between the PR description's "all driver bundles" claim and the actual 4-bundle scope — either widen
coverage to include Kotlin/Scala/record-codec/crypt bundles, or narrow the description and file a follow-up.
| private Framework framework; | ||
|
|
||
| @BeforeEach | ||
| void startFramework() throws BundleException, IOException { |
There was a problem hiding this comment.
IOException not thrown in the method
| } | ||
|
|
||
| @Test | ||
| void bundlesReportCorrectSymbolicNames() throws Exception { |
There was a problem hiding this comment.
We can reduce duplication in these tests method by extracting a helper
private List<Bundle> installAllBundles() throws IOException, BundleException {
BundleContext ctx = framework.getBundleContext();
List<Bundle> installed = new ArrayList<>();
for (String module : BUNDLE_MODULES) {
File jar = findBundleJar(module);
try (InputStream is = Files.newInputStream(jar.toPath())) {
installed.add(ctx.installBundle("file:" + jar.getAbsolutePath(), is));
}
}
return installed;
}
@Test
void bundlesResolveWithoutOptionalDependencies() throws Exception {
for (Bundle bundle : installAllBundles()) {
try {
bundle.start();
} catch (BundleException e) {
fail(formatBundleFailure(bundle, e));
}
}
}
@Test
void bundlesReportCorrectSymbolicNames() throws Exception {
assertThat(installAllBundles())
.extracting(Bundle::getSymbolicName)
.containsExactly(
"org.mongodb.bson",
"org.mongodb.driver-core",
"org.mongodb.driver-sync",
"org.mongodb.driver-reactivestreams");
}| testImplementation(libs.junit.jupiter.platform.launcher) | ||
| testImplementation(libs.assertj) | ||
| testImplementation(libs.felix.framework) | ||
| testImplementation(libs.reactive.streams) |
There was a problem hiding this comment.
I disabled this deps (libs.reactive.streams) and the test still passes?
The micrometer-observation dependency is optional but its packages were imported as required in the OSGi metadata, causing bundle resolution failure in OSGi containers.
Adds io.micrometer.*;resolution:=optional to driver-core Import-Package.
Also adds a new testing/osgi-test module that uses Apache Felix to verify all driver bundles resolve without optional dependencies present. This runs in CI via static-checks.sh to prevent future regressions.
JAVA-6215
JAVA-5062