[improve][client] Enable configurable preemptive OAuth2 token refresh#25363
Open
lhotari wants to merge 18 commits intoapache:masterfrom
Open
[improve][client] Enable configurable preemptive OAuth2 token refresh#25363lhotari wants to merge 18 commits intoapache:masterfrom
lhotari wants to merge 18 commits intoapache:masterfrom
Conversation
…th2 token refresh scheduler - Resolve merge conflicts in AuthenticationFactoryOAuth2, ClientCredentialsFlow, AuthenticationOAuth2Test, TokenOauth2AuthenticatedProducerConsumerTest - Rename ClientCredentialsFlow field from keyFileUrl to privateKey to match origin/master's constructor parameter name - Update AuthenticationOAuth2Builder to use ClientCredentialsFlow.builder() instead of removed ClientCredentialsFlow(ClientCredentialsConfiguration) constructor - Fix AuthenticationFactoryOAuth2.ClientCredentialsBuilder.build() to use correct AuthenticationOAuth2 constructor - Accept deletion of site2/docs/security-oauth2.md (moved in origin/master) - Use daemon threads for internally created ScheduledThreadPoolExecutor to prevent resource leaks when close() is not called (addresses reviewer concern) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n refresh
Instead of creating a new ScheduledThreadPoolExecutor per AuthenticationOAuth2
instance (which creates a thread per client), use a single shared static
executor (INTERNAL_SCHEDULER) that:
- Uses a daemon thread so it does not block JVM shutdown
- Allows the core thread to time out after 10s of idle time, releasing the
OS thread when no OAuth2 instances are actively refreshing tokens
- Is shared across all AuthenticationOAuth2 instances, addressing the
concern about too many threads in systems with multiple clients
This directly addresses the reviewer concern from eolivelli ("Isn't creating
a new thread per each client instance too heavyweight?") and lhotari
("The lifecycle of the scheduler/executor will need to be managed").
When a caller supplies their own scheduler via AuthenticationOAuth2Builder,
that scheduler is used as-is and is never shut down by this class.
Also change scheduler type from ScheduledThreadPoolExecutor to the more
general ScheduledExecutorService in the builder API.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… remove unused field - Use io.netty.util.concurrent.DefaultThreadFactory to create named daemon threads for the shared internal scheduler - Remove unused ownsScheduler field Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Allows the early token refresh feature to be configured when the class is created via reflection (e.g., broker authentication plugins). The parameter "earlyRefreshPercent" in the JSON auth config accepts: - A decimal value (contains '.') used directly, e.g. "0.8" → 0.8 - An integer value divided by 100, e.g. "80" → 0.8, "100" → 1.0 (disabled) When the configured value is < 1, the shared internal scheduler is automatically activated if no external scheduler was provided. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rlyTokenRefreshPercent New tests cover the reflection-based usage pattern (default constructor + configure): - Default constructor leaves early refresh disabled (percent = 1) - Decimal value "0.8" is used directly - Integer value "80" is divided by 100 → 0.8 - Integer "100" → 1.0 (disabled) - Zero and non-numeric values throw IllegalArgumentException - When early refresh is enabled via configure(), the background scheduler is activated and triggers authenticate() before token expiry Also extract minimalCredentialsJson/credentialsJsonWithEarlyRefresh helpers to reduce duplication, and use CONFIG_PARAM_EARLY_TOKEN_REFRESH_PERCENT constant instead of the hardcoded string. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 task
merlimat
approved these changes
Mar 19, 2026
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #25363 +/- ##
=========================================
Coverage 72.73% 72.74%
- Complexity 34264 34279 +15
=========================================
Files 1954 1955 +1
Lines 154792 154872 +80
Branches 17731 17739 +8
=========================================
+ Hits 112586 112654 +68
- Misses 33170 33177 +7
- Partials 9036 9041 +5
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
…; add early refresh to ClientCredentialsBuilder ClientCredentialsFlow already has @builder, so a separate configuration class and dedicated builder class were unnecessary duplication. - Delete ClientCredentialsConfiguration.java - Delete AuthenticationOAuth2Builder.java - Remove class-level @deprecated from AuthenticationFactoryOAuth2 (keep @deprecated only on the individual deprecated static methods) - Add earlyTokenRefreshPercent(double) and scheduler(ScheduledExecutorService) to AuthenticationFactoryOAuth2.ClientCredentialsBuilder, making it the single builder for all client-credentials use cases - Remove stale javadoc reference to ClientCredentialsConfiguration in ClientCredentialsFlow Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
nodece
reviewed
Mar 20, 2026
pulsar-client/src/main/java/org/apache/pulsar/client/impl/auth/oauth2/AuthenticationOAuth2.java
Outdated
Show resolved
Hide resolved
nodece
approved these changes
Mar 20, 2026
…/oauth2/AuthenticationOAuth2.java Co-authored-by: Zixuan Liu <nodeces@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Continuation of #13951 (originally authored by @michaeljmarshall). Rebased onto master, merge conflicts resolved, and all reviewer feedback addressed.
Motivation
In some client use cases it is helpful to start refreshing the OAuth2 token in the background before it expires. This reduces latency spikes caused by blocking token-fetch calls and improves resilience when the Identity Provider is temporarily unavailable.
A key use case is reducing coupling to OAuth server availability. When the feature is enabled, the client continuously attempts to refresh the token in the background using exponential backoff, while continuing to serve requests with the existing valid token. This means a transient OAuth server outage does not immediately affect the Pulsar client — the client tolerates the outage for as long as the current token remains valid, which can be up to
(1 - earlyTokenRefreshPercent) * expires_inseconds after the first refresh failure.Modifications
Core feature (
AuthenticationOAuth2)earlyTokenRefreshPercent * expires_inmilliseconds after a successful fetch.1(100 %), which disables the feature and preserves existing behaviour.getAuthData()call if the token has since expired.ScheduledThreadPoolExecutor(raised as a reviewer concern) with a shared staticINTERNAL_SCHEDULERthat usesio.netty.util.concurrent.DefaultThreadFactory(daemon threads, namedoauth2-token-refresher) and allows the core thread to time out after 10 s of idle time — so no OS thread is held when no instances are actively refreshing.ScheduledExecutorService; this class never shuts it down.close()cancels the pending refresh task via the scheduler thread to avoid a race where a running refresh re-schedules itself; neither the shared nor the caller-supplied scheduler is shut down.earlyTokenRefreshPercentJSON configuration parameter, enabling the feature for broker-side / reflection-created instances. Accepts a decimal ("0.8"→ 0.8) or an integer percentage ("80"→ 0.8,"100"→ 1.0 = disabled).AuthenticationFactoryOAuth2(updated)@Deprecated; keep@Deprecatedonly on the individualclientCredentials(...)static methods.ClientCredentialsBuilderwith two new methods that cover the early refresh use case, making it the single builder for all client-credentials configurations:earlyTokenRefreshPercent(double)— fractional value in (0, 1) to enable preemptive refresh, ≥ 1 to disable (default).scheduler(ScheduledExecutorService)— optional external scheduler; if omitted the shared internal daemon scheduler is used automatically when early refresh is enabled.AuthenticationOAuth2StandardAuthzServer(updated)parseAuthParameters()to inject the RFC 8414 well-known metadata path, keeping theconfigure()refactor transparent to subclasses.Verifying this change
New and updated unit tests in
AuthenticationOAuth2Test:close().configure()via default constructor: decimal and integerearlyTokenRefreshPercentvalues, disabled (100), zero and non-numeric values throwIllegalArgumentException.parseEarlyRefreshPercentunit tests for all branches.authenticate()when enabled viaconfigure().Does this pull request potentially affect one of the following parts?
earlyTokenRefreshPercentdefaults to1(disabled).Documentation
doc—earlyTokenRefreshPercentparameter and early-refresh behaviour documented in javadoc onAuthenticationOAuth2andClientCredentialsBuilder.Closes #13951