Skip to content

[SPARK-58472][CORE] Add AutoCloseable lifecycle to CredentialProvider SPI - #57677

Closed
sarutak wants to merge 3 commits into
apache:masterfrom
sarutak:oidc-propagation/credential-provider-autocloseable
Closed

[SPARK-58472][CORE] Add AutoCloseable lifecycle to CredentialProvider SPI#57677
sarutak wants to merge 3 commits into
apache:masterfrom
sarutak:oidc-propagation/credential-provider-autocloseable

Conversation

@sarutak

@sarutak sarutak commented Jul 31, 2026

Copy link
Copy Markdown
Member

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

… 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.
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;
}
}
}

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.

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;
}
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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>

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.

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:

Suggested change
* <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>

Comment on lines +140 to +141
case NonFatal(e) =>
logWarning(log"Error closing credential providers during shutdown.", e)

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.

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.

Suggested change
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 dongjoon-hyun 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.

+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.
@sarutak sarutak closed this in eb3b138 Aug 2, 2026
sarutak added a commit that referenced this pull request Aug 2, 2026
…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>
@sarutak

sarutak commented Aug 2, 2026

Copy link
Copy Markdown
Member Author

Merge Summary:

Posted by merge_spark_pr.py

@sarutak

sarutak commented Aug 2, 2026

Copy link
Copy Markdown
Member Author

Merged. Thank you @dongjoon-hyun for reviewing!

yadavay-amzn added a commit to yadavay-amzn/spark that referenced this pull request Aug 3, 2026
…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
yadavay-amzn added a commit to yadavay-amzn/spark that referenced this pull request Aug 7, 2026
…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
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