HyukjinKwon commented on code in PR #59196:
URL: https://github.com/apache/spark/pull/59196#discussion_r4230814268
##########
common/utils/src/main/scala/org/apache/spark/util/MavenUtils.scala:
##########
@@ -62,32 +65,126 @@ private[spark] object MavenUtils extends Logging {
}
}
- private def acquireIvyLock(isCancelled: () => Boolean): Unit = {
- def checkCancelled(): Unit = {
- if (isCancelled() || Thread.currentThread().isInterrupted) {
- throw new CancellationException("Maven dependency resolution was
cancelled")
+ private class IvyResolutionCancellation {
+ private val requested = new AtomicBoolean(false)
+ private val httpHandler = new AtomicReference[HttpClientHandler]()
+
+ def isCancelled: Boolean = requested.get()
+
+ def installUrlHandler(): () => Unit = {
+ val previousHandler = URLHandlerRegistry.getDefault
+ val handler = new HttpClientHandler
Review Comment:
**Non-blocking (P2):** `installUrlHandler` routes http/https through a new
stock Ivy `HttpClientHandler` for every cancellable resolution, even when
cancellation is never requested. That covers every
`RuntimeDependencyResolver.resolve` call: classic
`SparkSession.addArtifact("ivy://...")` and Spark Connect AddArtifacts Maven
dependencies. Before this change, these resolutions used Ivy's default
`BasicURLHandler` (JDK `HttpURLConnection` plus `IvyAuthenticator`), which
`spark-submit --packages` still uses.
In Ivy 2.5.3 and HttpClient 4.5.14 the two transports differ in ways that
matter for real deployments:
- `HttpClientHandler` builds its client with `HttpClients.custom()` and an
explicit `PoolingHttpClientConnectionManager()`, without
`useSystemProperties()`. Its TLS context therefore comes from
`SSLContexts.createDefault()` with no key managers, so client certificates from
`javax.net.ssl.keyStore` are never presented.
- Authentication is enabled only when Ivy's `CredentialsStore` has
credentials for the repository host, and the credentials provider reads only
`CredentialsStore`. It ignores `http.proxyUser`/`http.proxyPassword` and a
JVM-wide `java.net.Authenticator`, which `IvyAuthenticator` honors.
- Only the Digest, Basic and NTLM schemes are registered, so
Negotiate/Kerberos proxies stop working. A custom default `URLHandler`
registered by the application is also bypassed for http/https.
A driver behind an authenticated proxy configured through JVM properties, or
a repository that needs a client certificate, can still resolve with
`spark-submit --packages` but now fails with an unresolved-dependency error
through `addArtifact(ivy://...)` or Connect AddArtifacts. The PR describes
prompt cancellation as the only user-facing change, and the new tests use only
unauthenticated plain-HTTP and file repositories, so nothing catches this
regression.
**Recommended change:** Replace the stock Ivy HttpClientHandler in
IvyResolutionCancellation with a Spark-owned, per-resolution closeable Ivy URL
handler whose Apache HttpClient is built from the JVM's network configuration.
Use HttpClients.custom().useSystemProperties(), with no explicit connection
manager that would discard the system SSL socket factory, so it gets
SSLContext.getDefault() with keystore and truststore properties,
https.protocols/cipherSuites, system proxy routing, and the default
SPNEGO/Kerberos schemes. Give it a credentials provider that consults Ivy's
CredentialsStore first and then the JVM (java.net.Authenticator and
http(s).proxyUser/proxyPassword through SystemDefaultCredentialsProvider), with
authentication always enabled. Take over http/https only when the current
default is Ivy's stock BasicURLHandler, so a caller-installed custom default is
not bypassed. Keep the existing install, restore and close-on-cancel lifecycle
unchanged. Add runtime resolver tes
ts that cover JVM-level proxy authentication.
**Why this works:** Cancellation still aborts blocked reads by closing the
per-resolution connection pool. Because the client now draws proxy credentials,
Authenticator responses and TLS key material from the same JVM inputs that
HttpURLConnection/IvyAuthenticator use, a resolution that is never cancelled
authenticates and negotiates TLS the way spark-submit --packages does.
**Scope:** Make the per-resolution cancellable HTTP handler honor the JVM's
network configuration, so runtime Maven resolution keeps spark-submit's auth
and TLS behavior while staying promptly cancellable. This may add a new
Spark-owned handler class under common/utils' util package.
**Compatibility:** Prompt abort of a resolution blocked on an HTTP read, and
the cooperative cancel-then-join lifecycle that releases ivyLock and restores
the previous URL handler before CancellationException reaches the caller.
**Risks:** HttpClient's SPNEGO and the JDK's Negotiate handle JAAS subjects
differently, so Kerberos-authenticated proxies need explicit verification
rather than assumed parity. A process-wide
HttpsURLConnection.setDefaultSSLSocketFactory customization is still not
honored by HttpClient; only SSLContext.getDefault() and javax.net.ssl.* inputs
are.
**Constraints:** Must not call Thread.stop directly or indirectly; keep the
cooperative cancel-and-join lifecycle. Must restore the previous
URLHandlerRegistry default and close the per-resolution client on every exit
path. The non-cancellable resolveMavenCoordinates path must keep Ivy's existing
default handler.
**Success:** With JVM proxy credentials (http(s).proxyUser/proxyPassword) or
a default java.net.Authenticator, a runtime resolution through
RuntimeDependencyResolver authenticates to the proxy or repository the same way
spark-submit --packages does. With a client certificate configured through
javax.net.ssl.keyStore, the runtime path presents it during the TLS handshake.
Repositories authenticated through ivysettings <credentials> and
unauthenticated repositories still resolve on the runtime path. Cancelling a
resolution blocked on an HTTP read still raises CancellationException promptly,
only after the worker has exited, with ivyLock released and the previous URL
handler restored. A custom default URLHandler installed before resolution is
not bypassed for http/https.
**Verification:**
- **Regression:** A runtime resolution through an HTTP proxy that requires
Basic proxy authentication, with credentials supplied only through JVM proxy
credential properties or a default java.net.Authenticator, succeeds where the
stock HttpClientHandler path receives 407.
- **Behavior:** The blocked-read cancellation and uncooperative-worker
scenarios still produce a prompt CancellationException after the worker exits,
with the lock released and the global handler restored.
- **Compatibility:** Repositories authenticated through ivysettings
<credentials>, and unauthenticated repositories, resolve identically through
the runtime path and the non-cancellable spark-submit path.
- **Inspection:** The per-resolution client is built from JVM system
properties (system-default SSL context, system credentials provider), and the
handler is installed only over Ivy's stock default handler.
##########
common/utils/src/test/scala/org/apache/spark/util/MavenUtilsSuite.scala:
##########
@@ -308,6 +323,135 @@ class MavenUtilsSuite
}
}
+ test("runtime dependency resolver cancels active Ivy resolution") {
+ val executor = Executors.newFixedThreadPool(2)
+ implicit val executionContext: ExecutionContext =
+ ExecutionContext.fromExecutorService(executor)
+ val server = new ServerSocket(0, 1, InetAddress.getByName("127.0.0.1"))
+ val requestAccepted = new CountDownLatch(1)
+ val releaseRequest = new CountDownLatch(1)
+ val serverFuture = Future {
+ val socket = server.accept()
+ try {
+ requestAccepted.countDown()
+ releaseRequest.await()
+ } finally {
+ socket.close()
+ }
+ }
+ try {
+ val settings = Paths.get(tempIvyPath, "cancellable-ivysettings.xml")
+ Files.writeString(
+ settings,
+ s"""<ivysettings>
+ | <settings defaultResolver="blocked"/>
+ | <resolvers>
+ | <ibiblio name="blocked" m2compatible="true"
+ | root="http://127.0.0.1:${server.getLocalPort}"/>
+ | </resolvers>
+ |</ivysettings>""".stripMargin)
+ val cancelled = new AtomicBoolean(false)
+ val resolver = new RuntimeDependencyResolver(
+ ivySettingsPath = Some(settings.toString),
+ ivyPath = Some(Paths.get(tempIvyPath, "cancelled").toString))
+ val previousUrlHandler = URLHandlerRegistry.getDefault
+ val resolution = Future {
+ resolver.resolve(
+ URI.create("ivy://my.runtime.active:mylib:0.1"),
+ resolverTimeoutMs,
+ resolverTimeoutMs,
+ isCancelled = () => cancelled.get())
+ }
+
+ assert(requestAccepted.await(5, TimeUnit.SECONDS))
+ cancelled.set(true)
+ intercept[CancellationException] {
+ SparkThreadUtils.awaitResultNoSparkExceptionConversion(resolution,
5.seconds)
+ }
+ assert(URLHandlerRegistry.getDefault eq previousUrlHandler)
+
+ val next = MavenCoordinate("my.runtime.after.cancel", "mylib", "0.1")
+ IvyTestUtils.withRepository(next, None, None) { repo =>
+ val resolved = MavenUtils.resolveMavenCoordinates(
+ next.toString,
+ MavenUtils.buildIvySettings(Some(repo), Some(tempIvyPath)),
+ transitive = true,
+ isTest = true)
+ assert(resolved.nonEmpty)
+ }
+ } finally {
+ releaseRequest.countDown()
+ server.close()
+ SparkThreadUtils.awaitResultNoSparkExceptionConversion(serverFuture,
5.seconds)
Review Comment:
**Nit (P3):** Suppose the resolution never reaches the fake server, for
example because it fails before connecting. Then `requestAccepted.await`
returns false and the assertion throws. `server.close()` makes the
still-pending `accept()` throw, and this
`awaitResultNoSparkExceptionConversion` rethrows that `SocketException` out of
the `finally`. That replaces the real assertion failure and skips
`executor.shutdownNow()`. A still-running resolution is then never interrupted
and can hold `ivyLock` until its 30 s network timeout, stalling or failing
later tests in the suite and making the original failure hard to diagnose.
**Verification:**
- **Regression:** When the blocked-read test's connection precondition
fails, the original assertion failure is reported and no executor thread or
held ivyLock remains.
- **Behavior:** On the normal path the test still proves prompt
cancellation, handler restoration and lock release.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]