[SPARK-58472][CORE][FOLLOWUP] Scope credential providers to manager lifecycle - #57954
[SPARK-58472][CORE][FOLLOWUP] Scope credential providers to manager lifecycle#57954cloud-fan wants to merge 1 commit into
Conversation
…ifecycle Scope CredentialProviderLoader state to each UserCredentialManager so stopping one manager cannot leave a later manager reusing closed providers. Prevent provider reinitialization after shutdown and cover sequential managers plus concurrent renewal shutdown.
|
cc @sarutak |
|
Thank you @cloud-fan! |
| synchronized (this) { | ||
| // Copy and clear under the lock to prevent double-close if closeAll() is called | ||
| // again concurrently, and to avoid ConcurrentModificationException. | ||
| providersClosed = true; |
There was a problem hiding this comment.
Thanks for the followup, @cloud-fan. I agree that the closeAll() cache inconsistency is a real issue in that after one manager closes providers, the stale instances remain in cachedProviders and could be reinitialized by a later SparkContext.
However, I think the root cause is simpler than what this PR addresses: closeAll() clears initializedProviders but does not invalidate cachedProviders. I think the following minimal fix is sufficient.
public static void closeAll() throws Exception {
synchronized (CredentialProviderLoader.class) {
providersClosed = true;
cachedProviders = null; // force fresh ServiceLoader discovery on next use
toClose = new ArrayList<>(initializedProviders);
initializedProviders.clear();
}
// ... close logic
}This forces the next providerFor() call to re-run ServiceLoader discovery, producing fresh instances. The providersClosed flag prevents accidental reuse between close and the next SparkContext.
There was a problem hiding this comment.
Thanks for taking a look. I agree that clearing cachedProviders addresses the immediate stale-instance issue. However, the proposed snippet sets providersClosed = true without a safe point to reset it for the next manager. Resetting global state during the next discovery would also leave lifecycle races where one manager can invalidate or close providers used by another.
Scoping the loader to UserCredentialManager makes provider ownership and shutdown state explicit: stopping one manager closes only its providers, while a later manager receives fresh instances. For that reason, I’d prefer to keep the instance-scoped approach.
There was a problem hiding this comment.
Thanks. On the two concerns:
-
Reset point for
providersClosed: We can simplify by dropping the flag entirely. SettingcachedProviders = nullincloseAll()is sufficient. The null cache naturally means "rediscover on next use." No separate closed/open state is needed. -
Lifecycle races between managers: Spark enforces a single active SparkContext per JVM via SparkContext.assertNoOtherContextIsRunning. Two
UserCredentialManagerinstances cannot coexist. The lifecycle is strictly sequential (manager A stops, then manager B starts). There is no window for one manager to invalidate providers used by another.
I intentionally chose a JVM-scoped loader because classpath-based ServiceLoader discovery is a JVM-level invariant (the discovered providers don't change between SparkContexts). Additionally, if we extend this to Spark Connect multi-session in the future, a shared server-level loader would be more appropriate. CredentialProvider.resolve() takes UserContext as an argument, so session isolation is achieved at the call site without per-session loaders. Instance-per-manager would mean redundant ServiceLoader discovery and separate HTTP connection pools per session.
What changes were proposed in this pull request?
Followup to #57677.
This PR scopes
CredentialProviderLoaderstate to eachUserCredentialManager. It convertsprovider discovery and initialization tracking from static state to loader-instance state, prevents
provider reinitialization after
closeAll(), and gives each manager its own loader while retainingthe existing constructor for callers.
It also adds coverage for sequential managers and for shutdown while credential renewal is in
flight.
Why are the changes needed?
The original change stores providers in global static state. After one manager closes those
providers, a later manager in the same JVM can rediscover and reinitialize the released provider
instances. Instance-scoped loaders align provider ownership with the manager lifecycle and allow a
later manager to discover fresh providers. Marking a loader closed also prevents accidental reuse
after its resources have been released.
Does this PR introduce any user-facing change?
No.
How was this patch tested?
Added tests for closed-loader reuse, sequential manager lifecycles, and concurrent renewal shutdown.
All 28
CredentialProviderLoaderSuitetests and all 22UserCredentialManagerSuitetests passed.Was this patch authored or co-authored using generative AI tooling?
Generated-by: OpenAI Codex (GPT-5)