[SPARK-58472][CORE] Add AutoCloseable lifecycle to CredentialProvider SPI - #57677
[SPARK-58472][CORE] Add AutoCloseable lifecycle to CredentialProvider SPI#57677sarutak wants to merge 3 commits into
AutoCloseable lifecycle to CredentialProvider SPI#57677Conversation
… 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.
| 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; | ||
| } | ||
| } | ||
| } |
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:
| 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; | |
| } | |
| } |
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.
| * Called by the credential management layer during shutdown. The default implementation | ||
| * is a no-op; providers that allocate long-lived resources in {@link #init(Map)} should | ||
| * override this method to clean them up. | ||
| * <p> |
There was a problem hiding this comment.
stop() calls shutdownNow() (interrupt only, no wait) and then closeAll() immediately, so close() can run while a renewal task is still inside resolve(). Since this is an SPI, that concurrency contract should be spelled out for implementors:
| * <p> | |
| * <p> | |
| * {@code close()} may be invoked while another thread is still executing | |
| * {@link #resolve(UserContext, URI)}: shutdown interrupts the renewal thread but does | |
| * not wait for in-flight calls to complete. Implementations must tolerate a concurrent | |
| * or subsequent {@code resolve()} failing after resources have been released, and | |
| * {@code close()} itself must not block indefinitely. | |
| * <p> |
| case NonFatal(e) => | ||
| logWarning(log"Error closing credential providers during shutdown.", e) |
There was a problem hiding this comment.
Scala's NonFatal does not match InterruptedException, so if a provider's close() throws it (plausible for HTTP client shutdown waits), it escapes stop() and can disrupt the caller's shutdown path. A shutdown hook should swallow it after restoring the interrupt flag.
| case NonFatal(e) => | |
| logWarning(log"Error closing credential providers during shutdown.", e) | |
| case e: InterruptedException => | |
| Thread.currentThread().interrupt() | |
| logWarning(log"Interrupted while closing credential providers during shutdown.", e) | |
| case NonFatal(e) => | |
| logWarning(log"Error closing credential providers during shutdown.", e) |
dongjoon-hyun
left a comment
There was a problem hiding this comment.
+1, LGTM except two parts.
- CredentialProvider Contract Documentation
- Exception Handling in UserCredentialManager
Thank you, @sarutak .
… handling - Document in CredentialProvider.close() Javadoc that close() may be invoked while resolve() is still executing on another thread, and that implementations must tolerate this and not block indefinitely. - Handle InterruptedException in UserCredentialManager.stop() by restoring the interrupt flag before logging, since Scala's NonFatal does not match InterruptedException. Addresses review feedback from dongjoon-hyun.
…der` SPI ### What changes were proposed in this pull request? This PR adds `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. - `CredentialProvider` now extends `AutoCloseable` with a default no-op `close()` - `CredentialProviderLoader.closeAll()` closes all initialized providers with exception suppression (first exception wins, others attached via `addSuppressed`) - `UserCredentialManager.stop()` calls `closeAll()` during shutdown ### Why are the changes needed? Unlike `HadoopDelegationTokenProvider` (which is stateless, and receives config on each `obtainDelegationTokens()` call), `CredentialProvider` uses an `init()` pattern where implementations construct long-lived resources. For example, `AwsStsCredentialProvider` (#57655) holds an `StsClient` with HTTP connection pools that must be closed. Without a lifecycle hook, these resources leak on application shutdown. Adding `AutoCloseable` (rather than a standalone `close()` method) follows the same pattern as `KVStore` and `DataWriter` in Spark . It serves as a lifecycle contract marker indicating that implementations may hold resources, and the framework is responsible for calling `close()` 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; existing `CredentialProvider` 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? Kiro CLI / Claude Closes #57677 from sarutak/oidc-propagation/credential-provider-autocloseable. Authored-by: Kousuke Saruta <sarutak@apache.org> Signed-off-by: Kousuke Saruta <sarutak@apache.org> (cherry picked from commit eb3b138) Signed-off-by: Kousuke Saruta <sarutak@apache.org>
|
Merged. Thank you @dongjoon-hyun for reviewing! |
…ntial-aws module scaffolding Implements the full AwsStsCredentialProvider that exchanges OIDC identity tokens for temporary AWS credentials via STS AssumeRoleWithWebIdentity. This builds on the credential-aws module scaffolding merged in SPARK-57897 (PRs apache#57677 and apache#57679). The diff only contains the provider implementation and its test suite; all build wiring (pom profile, SparkBuild.scala, modules.py, META-INF/services, CI scripts) is owned by the scaffolding. Key features: - Immutable ResolvedConfig behind a volatile for thread-safe publish - Config keys under spark.security.oidc.aws.* namespace - durationSeconds validated to STS range [900, 43200] - Re-initialization guard (IllegalStateException) - close() implementation shutting down the StsClient - Token-redaction in STS error messages - sanitizeSessionName with explicit [a-zA-Z0-9_+=,.@-] char class - Null-checks for target URI and user context - 37 unit tests covering all acceptance criteria Authored with assistance by Claude Opus 5
…ntial-aws module scaffolding Implements the full AwsStsCredentialProvider that exchanges OIDC identity tokens for temporary AWS credentials via STS AssumeRoleWithWebIdentity. This builds on the credential-aws module scaffolding merged in SPARK-57897 (PRs apache#57677 and apache#57679). The diff only contains the provider implementation and its test suite; all build wiring (pom profile, SparkBuild.scala, modules.py, META-INF/services, CI scripts) is owned by the scaffolding. Key features: - Immutable ResolvedConfig behind a volatile for thread-safe publish - Config keys under spark.security.oidc.aws.* namespace - durationSeconds validated to STS range [900, 43200] - Re-initialization guard (IllegalStateException) - close() implementation shutting down the StsClient - Token-redaction in STS error messages - sanitizeSessionName with explicit [a-zA-Z0-9_+=,.@-] char class - Null-checks for target URI and user context - 37 unit tests covering all acceptance criteria Authored with assistance by Claude Opus 5
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