Skip to content

[feat][client] Embed GraalVM native image config#25883

Merged
lhotari merged 9 commits into
apache:masterfrom
david-streamlio:embed-native-image-config
Jun 8, 2026
Merged

[feat][client] Embed GraalVM native image config#25883
lhotari merged 9 commits into
apache:masterfrom
david-streamlio:embed-native-image-config

Conversation

@david-streamlio

Copy link
Copy Markdown
Contributor

Main Issue: quarkusio/quarkus#48776

Motivation

Applications using pulsar-client-original in GraalVM native-image builds currently fail unless the consuming framework (Quarkus, Micronaut, Spring Native, etc.) provides its own reflection and class initialization metadata for Pulsar client internals. This forces every framework to independently reverse-engineer and maintain configuration that tracks Pulsar's internal class layout — and when it drifts, native builds break silently.

A concrete example: Quarkus's Pulsar extension registered org.apache.pulsar.client.util.WithSNISslEngineFactory for runtime initialization, but that class was removed in Pulsar 4.x. Because the configuration lived downstream instead of in the client itself, the breakage went unnoticed for a year (quarkusio/quarkus#48776).

Embedding the configuration directly in pulsar-client-original following
the GraalVM embedded configuration convention means the native-image tool picks it up automatically from the classpath, keeping the config in sync with the code that owns the classes.

Modifications

Added three GraalVM native-image configuration files under
pulsar-client/src/main/resources/META-INF/native-image/org.apache.pulsar/pulsar-client-original/:

  • reflect-config.json — Registers 20 classes for reflective access:
    the three ConfigurationData classes (deserialized by Jackson), all six
    Authentication implementations (instantiated by name via
    AuthenticationUtil.create()), the OAuth2 protocol model classes, schema
    internals (ProtoBufParsingInfo, ProtobufNativeSchemaData, KeyValue),
    DataURLStreamHandler, and SecretsSerializer.

  • native-image.properties — Marks eight classes for runtime
    initialization that would otherwise fail during native-image build due to
    eager static initialization of Netty allocators, thread pools, or HTTP
    clients: PulsarByteBufAllocator, Commands, Backoff, TokenClient,
    GenericProtobufNativeSchema, ConnectionPool,
    ControlledClusterFailover, and HttpClient.

  • resource-config.json — Includes the async-http-client default
    properties files that are loaded via classloader at runtime.

Added NativeImageConfigTest that validates:

  • All classes referenced in reflect-config.json exist on the classpath
  • All classes referenced in native-image.properties exist on the classpath
  • All Authentication implementations shipped with the client are registered
  • Both JSON config files are well-formed

This test catches configuration drift early — if a class is renamed, removed, or a new Authentication plugin is added without updating the config, the test fails with a descriptive message.

Note: the shaded pulsar-client JAR intentionally strips
META-INF/native-image/** in its shade plugin filters, which is correct since shading relocates packages and invalidates the original class names. Users building native images should depend on pulsar-client-original.

Verifying this change

This change added tests and can be verified as follows:

  • Added NativeImageConfigTest that validates all 20 reflection entries and
    8 runtime-initialized classes resolve on the classpath, and that all
    Authentication implementations are registered.
  • Every class in the configuration was verified against the Pulsar 4.2.1
    source tree.
  • The equivalent configuration (derived from Quarkus's
    SmallRyeReactiveMessagingPulsarProcessor) was validated by successfully
    compiling six native-image integration tests in the Quarkus
    integration-tests/reactive-messaging-pulsar module against Pulsar 4.2.1.

Does this pull request potentially affect one of the following parts:

  • Dependencies (add or upgrade a dependency)
  • The public API
  • The schema
  • The default values of configurations
  • The threading model
  • The binary protocol
  • The REST endpoints
  • The admin CLI options
  • The metrics
  • Anything that affects deployment

Documentation

  • doc-not-needed

@david-streamlio david-streamlio requested a review from lhotari May 27, 2026 21:52
@lhotari lhotari changed the title Embed native image config [feat][client] Embed GraalVM native image config Jun 3, 2026

@lhotari lhotari left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The changes look good so far. However, we'd need an integration test to verify that a Pulsar client application can be compiled to a native image. Besides this, I'd assume that there would have to be a smoke test to cover at least basic functionality (produce/consume).

Would it be possible to add the integration test to tests/pulsar-client-native-image directory? It would be great if the test could be written in TestNG and call the native application with java.lang.ProcessBuilder or similar. The native application could have different cli options to perform the actual logic of each test case. I guess there could also be other ways to add test coverage. Perhaps the TestNG class could be directly native compiled without the need for a separate native application, at least junit is supported in the Gradle GraalVM plugin, however TestNG support is most likely missing. We could add Junit Jupiter dependencies to Pulsar alongside TestNG if that makes things easier. There has been plans to start migrating to Junit Jupiter in Pulsar anyways.

GraalVM Gradle plugin docs: https://graalvm.github.io/native-build-tools/latest/gradle-plugin.html

david-streamlio added a commit to david-streamlio/pulsar that referenced this pull request Jun 3, 2026
…nfig

Address review feedback on PR apache#25883 (native-image support):

- Add tests/pulsar-client-native-image integration module: a TestNG
  smoke test (NativeImageSmokeTest) that drives the natively-compiled
  NativeImageTesterApp via java.lang.ProcessBuilder, covering native
  compilation plus basic produce/consume against a Testcontainers
  Pulsar broker. Registered in tests/pom.xml under the
  nativeImageTests profile.

- Add native-image metadata for pulsar-client-admin-original
  (reflect-config.json, resource-config.json, native-image.properties)
  covering the Jackson-serialized admin model DTOs and the
  runtime-initialized AsyncHttpConnector, with a NativeImageConfigAdminTest
  static validator.

- Add ASF license header to the pulsar-client native-image.properties
  (pre-existing RAT violation from the prior commit).

- Add ci-pulsar-native-image.yaml GitHub Actions workflow (nightly +
  workflow_dispatch) building with GraalVM community JDK 17.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@david-streamlio

Copy link
Copy Markdown
Contributor Author

Thanks for the review, @lhotari! I've added the integration test coverage you asked for. It follows the approach you suggested exactly.

New module: tests/pulsar-client-native-image

  • NativeImageTesterApp — a small CLI application (the "native application") with subcommands that perform the actual logic of each test case (e.g. produce, consume, produce-consume). It's the binary that gets compiled to a native image via the native-maven-plugin.
  • NativeImageSmokeTest — a TestNG driver that invokes the compiled native binary through java.lang.ProcessBuilder, passing the relevant CLI options per case. It first verifies the client compiles to a native image, then runs a basic produce/consume smoke test against a Pulsar broker started with Testcontainers (PulsarContainer).
  • Wired into tests/pom.xml behind a nativeImageTests profile so it's opt-in and doesn't slow the default build. Added a nightly + workflow_dispatch GitHub Actions workflow (ci-pulsar-native-image.yaml) that runs it on a GraalVM community JDK.

I went with the separate-native-application + ProcessBuilder design rather than native-compiling the TestNG class directly, since (as you noted) TestNG native support is missing today. Happy to revisit that toward JUnit Jupiter if/when the migration lands.

I also added native-image metadata for pulsar-client-admin (reflect-config.json / resource-config.json / native-image.properties) covering the Jackson-serialized admin model classes and the runtime-initialized AsyncHttpConnector, with a static validator test — so admin-based client apps compile too.

Let me know if you'd like any changes to the test structure or CLI surface.

Comment thread tests/pom.xml Outdated
Comment thread tests/pulsar-client-native-image/pom.xml Outdated
Comment thread .github/workflows/ci-pulsar-native-image.yaml Outdated
david-streamlio and others added 3 commits June 5, 2026 07:27
Add META-INF/native-image configuration files (reflect-config.json,
resource-config.json, native-image.properties) so that GraalVM's
native-image tool can build applications using the Pulsar client
without framework-specific configuration.

Includes a test (NativeImageConfigTest) that validates all referenced
classes exist on the classpath and all Authentication implementations
are registered, so the config does not drift silently as the codebase
evolves.

This eliminates the need for downstream frameworks (Quarkus, Micronaut,
Spring Native) to independently maintain reflection and class
initialization metadata for Pulsar client internals.
…nfig

Address review feedback on PR apache#25883 (native-image support). Reworked
for the Gradle build (master migrated from Maven to Gradle):

- Add tests/pulsar-client-native-image integration module with a Gradle
  build.gradle.kts using the GraalVM native-build-tools plugin
  (org.graalvm.buildtools.native). A TestNG smoke test
  (NativeImageSmokeTest) drives the natively-compiled NativeImageTesterApp
  via java.lang.ProcessBuilder, covering native compilation plus basic
  produce/consume against a Testcontainers Pulsar broker. Quick build
  mode (quickBuild = true) keeps CI compilation within the integration
  test budget. Registered in settings.gradle.kts.

- Add native-image metadata for pulsar-client-admin-original
  (reflect-config.json, resource-config.json, native-image.properties)
  covering the Jackson-serialized admin model DTOs and the
  runtime-initialized AsyncHttpConnector, with a NativeImageConfigAdminTest
  static validator.

- Add ASF license header to the pulsar-client native-image.properties
  (pre-existing RAT violation from the prior commit).

- Wire native-image tests into the existing Pulsar CI integration test
  matrix (pulsar-ci.yaml NATIVE_IMAGE group + a test_group_native_image
  entry in run_integration_group_gradle.sh) instead of a standalone
  workflow, with a conditional GraalVM JDK setup step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@david-streamlio david-streamlio force-pushed the embed-native-image-config branch from 2efd53b to 071ea20 Compare June 5, 2026 05:40
Comment thread .github/workflows/pulsar-ci.yaml Outdated
Comment thread .github/workflows/pulsar-ci.yaml Outdated
Comment thread gradle/libs.versions.toml Outdated
lhotari and others added 6 commits June 5, 2026 11:42
Co-authored-by: Lari Hotari <lhotari@users.noreply.github.com>
Remove blank line between static and regular imports to comply with
the ImportOrder checkstyle rule.

Assisted-by: Claude Code
Resolve this error:
The configured GraalVM reachability metadata repository at /home/runner/.gradle/native-build-tools/repositories/c8a0af0125b1a60c42de4ada4a5284b8cc6826d3/exploded provides a reachability-metadata schema, but your GraalVM installation at /opt/hostedtoolcache/graalvm-community-jdk-21.0.2_linux-x64_bin/21.0.2/x64/graalvm-community-openjdk-21.0.2+13.1 does not. Please update your GraalVM installation to a newer version. Update to the latest available release in your line. For example: Update your GraalVM version from 25.0.0 to the latest 25 release.

GraalVM gradle plugin 1.1.1 requires GraalVM 25
The CI - Unit - Other group runs a broad `./gradlew ... test` across all
modules on the Corretto JDK. The :tests:pulsar-client-native-image:test
task depends on :nativeCompile (GraalVM native-build-tools), so it was
swept into the unit run and failed with "native-image wasn't found ...
JDK isn't a GraalVM distribution" on the non-GraalVM runner.

Exclude :tests:pulsar-client-native-image:test from the OTHER group, the
same way the shaded client test modules are already excluded. The native
image build/test still runs in the dedicated NATIVE_IMAGE integration
group, which sets up a GraalVM JDK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@lhotari lhotari left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

@lhotari lhotari merged commit f0d2bcc into apache:master Jun 8, 2026
47 of 48 checks passed
@lhotari lhotari added this to the 5.0.0-M1 milestone Jun 8, 2026
@david-streamlio david-streamlio deleted the embed-native-image-config branch June 10, 2026 14:08
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.

2 participants