[SPARK-58472][CORE] Add AutoCloseable lifecycle to CredentialProvider SPI - #57677
Open
sarutak wants to merge 2 commits into
Open
[SPARK-58472][CORE] Add AutoCloseable lifecycle to CredentialProvider SPI#57677sarutak wants to merge 2 commits into
AutoCloseable lifecycle to CredentialProvider SPI#57677sarutak wants to merge 2 commits into
Conversation
… SPI Add AutoCloseable to the CredentialProvider interface so that implementations holding long-lived resources (e.g., HTTP connection pools in StsClient) can be properly cleaned up on shutdown. Changes: - CredentialProvider now extends AutoCloseable with a default no-op close() method (fully backward compatible for existing implementations) - CredentialProviderLoader.closeAll() closes all initialized providers with exception suppression (first exception wins, others suppressed) - UserCredentialManager.stop() calls closeAll() during shutdown - FakeCredentialProvider updated with close tracking (AtomicInteger) - Tests added for closeAll() (normal, exception suppression, empty) and stop()-closes-providers integration ### What changes were proposed in this pull request? Add AutoCloseable lifecycle management to the CredentialProvider SPI, enabling proper resource cleanup for stateful provider implementations. ### Why are the changes needed? Unlike HadoopDelegationTokenProvider (stateless), CredentialProvider uses an init() pattern where implementations construct long-lived resources. AwsStsCredentialProvider holds an StsClient with HTTP connection pools that must be closed. Without this change, there is no lifecycle hook for cleanup. ### Does this PR introduce _any_ user-facing change? No. The default close() is a no-op; existing implementations are unaffected. ### How was this patch tested? - CredentialProviderLoaderSuite: 3 new tests for closeAll() - UserCredentialManagerSuite: 1 new test for stop() closing providers - All 47 tests pass ### Was this patch authored or co-authored using generative AI tooling? Yes.
dongjoon-hyun
requested changes
Jul 31, 2026
Comment on lines
+273
to
+297
| public static void closeAll() throws Exception { | ||
| synchronized (CredentialProviderLoader.class) { | ||
| // Copy and clear first to prevent double-close if closeAll() is called again | ||
| // concurrently or re-entrantly, and to avoid ConcurrentModificationException | ||
| // if a close() implementation were to interact with this class. | ||
| List<CredentialProvider> toClose = new ArrayList<>(initializedProviders); | ||
| initializedProviders.clear(); | ||
|
|
||
| Exception firstException = null; | ||
| for (CredentialProvider provider : toClose) { | ||
| try { | ||
| provider.close(); | ||
| } catch (Exception e) { | ||
| if (firstException == null) { | ||
| firstException = e; | ||
| } else { | ||
| firstException.addSuppressed(e); | ||
| } | ||
| } | ||
| } | ||
| if (firstException != null) { | ||
| throw firstException; | ||
| } | ||
| } | ||
| } |
Member
There was a problem hiding this comment.
closeAll() invokes third-party close() implementations while holding the class lock. Since providerFor() takes the same lock on every call, a close() that blocks (e.g., draining an HTTP connection pool) stalls all credential resolutions — and if a close() implementation waits on a thread that is calling providerFor(), we get a deadlock. The javadoc contract ("must not call back into CredentialProviderLoader") only covers direct re-entry, not indirect waiting.
Since the copy-and-clear already happens under the lock, moving just the close loop outside the lock removes the risk:
Suggested change
| public static void closeAll() throws Exception { | |
| synchronized (CredentialProviderLoader.class) { | |
| // Copy and clear first to prevent double-close if closeAll() is called again | |
| // concurrently or re-entrantly, and to avoid ConcurrentModificationException | |
| // if a close() implementation were to interact with this class. | |
| List<CredentialProvider> toClose = new ArrayList<>(initializedProviders); | |
| initializedProviders.clear(); | |
| Exception firstException = null; | |
| for (CredentialProvider provider : toClose) { | |
| try { | |
| provider.close(); | |
| } catch (Exception e) { | |
| if (firstException == null) { | |
| firstException = e; | |
| } else { | |
| firstException.addSuppressed(e); | |
| } | |
| } | |
| } | |
| if (firstException != null) { | |
| throw firstException; | |
| } | |
| } | |
| } | |
| public static void closeAll() throws Exception { | |
| List<CredentialProvider> toClose; | |
| synchronized (CredentialProviderLoader.class) { | |
| // Copy and clear first to prevent double-close if closeAll() is called again, | |
| // and to avoid ConcurrentModificationException if a close() implementation | |
| // were to interact with this class. | |
| toClose = new ArrayList<>(initializedProviders); | |
| initializedProviders.clear(); | |
| } | |
| // Close outside the lock so a slow or blocking close() cannot stall | |
| // providerFor() callers or deadlock against them. | |
| Exception firstException = null; | |
| for (CredentialProvider provider : toClose) { | |
| try { | |
| provider.close(); | |
| } catch (Exception e) { | |
| if (firstException == null) { | |
| firstException = e; | |
| } else { | |
| firstException.addSuppressed(e); | |
| } | |
| } | |
| } | |
| if (firstException != null) { | |
| throw firstException; | |
| } | |
| } |
Member
Author
There was a problem hiding this comment.
Thank you, @dongjoon-hyun. I've applied suggested change.
Move the provider close loop outside the class lock to prevent a slow or blocking close() implementation from stalling providerFor() callers. The copy-and-clear still happens under the lock to prevent double-close, but third-party close() calls now execute without holding the lock. Addresses review feedback from dongjoon-hyun.
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.
What changes were proposed in this pull request?
This PR adds
AutoCloseableto theCredentialProviderinterface so that implementations holding long-lived resources (e.g., HTTP connection pools inStsClient) can be properly cleaned up on shutdown.CredentialProvidernow extendsAutoCloseablewith a default no-opclose()CredentialProviderLoader.closeAll()closes all initialized providers with exception suppression (first exception wins, others attached viaaddSuppressed)UserCredentialManager.stop()callscloseAll()during shutdownWhy are the changes needed?
Unlike
HadoopDelegationTokenProvider(which is stateless, and receives config on eachobtainDelegationTokens()call),CredentialProvideruses aninit()pattern where implementations construct long-lived resources. For example,AwsStsCredentialProvider(#57655) holds anStsClientwith HTTP connection pools that must be closed. Without a lifecycle hook, these resources leak on application shutdown.Adding
AutoCloseable(rather than a standaloneclose()method) follows the same pattern asKVStoreandDataWriterin Spark . It serves as a lifecycle contract marker indicating that implementations may hold resources, and the framework is responsible for callingclose()at shutdown. It is not intended for use with try-with-resources.Does this PR introduce any user-facing change?
No. The default
close()is a no-op; existingCredentialProviderimplementations are unaffected.How was this patch tested?
Was this patch authored or co-authored using generative AI tooling?
Kiro CLI / Claude