Skip to content

Mark micrometer OSGi imports as optional and add bundle resolution test#1982

Open
rozza wants to merge 1 commit into
mongodb:mainfrom
rozza:JAVA-6215
Open

Mark micrometer OSGi imports as optional and add bundle resolution test#1982
rozza wants to merge 1 commit into
mongodb:mainfrom
rozza:JAVA-6215

Conversation

@rozza
Copy link
Copy Markdown
Member

@rozza rozza commented May 21, 2026

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

@rozza rozza force-pushed the JAVA-6215 branch 2 times, most recently from c15546a to 5d82a4f Compare May 21, 2026 11:26
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
@rozza rozza marked this pull request as ready for review May 21, 2026 12:40
@rozza rozza requested a review from a team as a code owner May 21, 2026 12:40
@rozza rozza requested a review from strogiyotec May 21, 2026 12:40
@rozza rozza requested a review from nhachicha May 21, 2026 15:21
@nhachicha nhachicha requested a review from Copilot May 21, 2026 23:20
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 as resolution:=optional in driver-core OSGi Import-Package.
  • Add :testing:osgi-test module 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.

Comment on lines +55 to +57
"driver-core",
"driver-sync",
"driver-reactive-streams"
Comment on lines +177 to +181
try (JarFile jar = new JarFile(file)) {
String version = jar.getManifest().getMainAttributes().getValue("Bundle-Version");
if (version == null) {
version = "0.0.0";
}
Copy link
Copy Markdown
Collaborator

@nhachicha nhachicha left a comment

Choose a reason for hiding this comment

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

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.). Adding io.micrometer.*;resolution:=optional
    is the correct minimal change. Verified: only driver-core source actually references io.micrometer.*, so driver-sync and driver-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 includes bson,
    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 return null; the chain jar.getManifest().getMainAttributes()... is a latent NPE. The surrounding try/catch only catches IOException, so an NPE
    would propagate and abort framework startup. In practice both reactive-streams and reactor-core ship 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 swallows IOException (// Skip JARs that can't be read). A System.err.println or Logger.warn would help future maintainers diagnose a degraded
    extraPackages string if the cause ever isn't intentional.

  • 🟢 [nit] L95-100 (bundlesResolveWithoutOptionalDependencies): Assertions.fail throws AssertionError, 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: Calling bundle.start() does trigger resolution and will throw BundleException(RESOLVE_ERROR) on failure, so the current approach catches real regressions. An alternative
    that surfaces issues earlier and more explicitly is framework.adapt(FrameworkWiring.class).resolveBundles(installed) after installBundle and before start() — it returns false if 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. Parsing osgi.wiring.package= out of the exception message and printing an actionable FIX: 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: Hardcoding JavaLanguageVersion.of(17) works today since project.DEFAULT_JAVA_VERSION = 17 (buildSrc/.../project/Companion.kt), but it'll silently diverge if the project bumps
    that constant. Importing and using DEFAULT_JAVA_VERSION (as other modules do via project.java) would keep them locked together.

  • 🟢 [nit] L38: dependsOn(":bson:jar", ":driver-core:jar", ":driver-sync:jar", ":driver-reactive-streams:jar") duplicates the BUNDLE_MODULES array 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: Uses src/test/java/ instead of the project's src/test/unit/ / src/test/functional/ convention (established in conventions.testing-junit). Minor
    inconsistency; understandable given this module doesn't apply conventions.testing-junit.

gradle/libs.versions.toml

  • 🟢 [nit] L42 and L178: felix-framework is inserted out of alphabetical order in both the [versions] # Test block (currently between assertj and aws-lambda-core) and the [libraries] block
    (currently between classgraph and findbugs-jsr). The file is mostly alphabetical; felix-framework would naturally sort between findbugs-jsr and groovy.

.evergreen/static-checks.sh

  • 💡 [suggestion] L17-18: This adds a second ./gradlew invocation, which incurs a fresh daemon startup and re-configures the project. Folding :testing:osgi-test:check into the main invocation above
    (it already runs jar, 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_MODULES coverage — 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 {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

IOException not thrown in the method

}

@Test
void bundlesReportCorrectSymbolicNames() throws Exception {
Copy link
Copy Markdown
Collaborator

@nhachicha nhachicha May 22, 2026

Choose a reason for hiding this comment

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

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)
Copy link
Copy Markdown
Collaborator

@nhachicha nhachicha May 22, 2026

Choose a reason for hiding this comment

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

I disabled this deps (libs.reactive.streams) and the test still passes?

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