sarutak commented on code in PR #59947:
URL: https://github.com/apache/spark/pull/59947#discussion_r4422638341


##########
sql/core/src/main/scala/org/apache/spark/sql/artifact/ArtifactManager.scala:
##########
@@ -285,6 +290,19 @@ class ArtifactManager(session: SparkSession) extends 
AutoCloseable with Logging
           Map("normalizedRemoteRelativePath" -> 
normalizedRemoteRelativePath.toString)
         )
       }
+      // A package is checked before it becomes an artifact of the session: 
see checkAdded. A
+      // file that does not exist fails to transfer instead. Errors name the 
file of the user, or
+      // the artifact if the file was staged for it, such as by a Spark 
Connect client.
+      if (isNativeDataSourcePackage(normalizedRemoteRelativePath) &&
+          session.conf.get(StaticSQLConf.NATIVE_DATA_SOURCE_ENABLED) &&
+          Files.exists(serverLocalStagingPath)) {
+        val path = if (deleteStagedFile) {
+          normalizedRemoteRelativePath.toString

Review Comment:
   Fixed in 3c422f37c57: the `finallyBlock` of the in-memory case now deletes 
the temporary directory, which also covers `ARTIFACT_ALREADY_EXISTS`. No test 
checks it: the directory is created under `java.io.tmpdir`, which the other 
tests of the suite share.



##########
sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/v2/ffi/NativeDataSourceSuite.scala:
##########
@@ -1555,6 +1555,70 @@ class NativeDataSourceSuite extends QueryTest with 
SharedSparkSession {
     assert(logged.forall(_.contains("for the data source native_range")), 
logged)
   }
 
+  nativeTest("reject a package that a session adds when Spark cannot use it") {

Review Comment:
   They do run in the PR builds: the job "Java 25 build with Maven" in 
`build_and_test.yml` has a step "Test native data sources", which runs 
`NativeDataSourceSuite`, `NativeDataSourceClusterSuite` and 
`NativeDataSourceConnectSuite` on Java 25 with the C++ compiler of the runner. 
On the previous head, 40d50ada443, it ran "reject a package that a session adds 
when Spark cannot use it" and "reject a package that Spark cannot use when a 
session adds it", and both passed 
(https://github.com/sarutak/spark/actions/runs/40969344226/job/123798271572). 
So I kept them as `nativeTest`s, like the other tests of these suites.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/ffi/NativeDataSourceRegistry.scala:
##########
@@ -146,8 +142,8 @@ object NativeDataSourceRegistry extends Logging {
       packages: Seq[NativeDataSourcePackage]): Option[(String, 
NativeDataSourcePackage)] = {
     val found = packages
       .flatMap(pkg => 
pkg.manifest.dataSources.find(_.equalsIgnoreCase(name)).map(_ -> pkg))
-      // The same package can be found more than once, for example when it is 
in a configured
-      // directory and was also added as an artifact.
+      // The same package can be found more than once, for example when it is 
listed in
+      // spark.sql.dataSource.native.paths both as a file and through its 
directory.

Review Comment:
   Fixed in 3c422f37c57. The test now lists a package in 
`spark.sql.dataSource.native.paths` both as a file and through its directory, 
which contains only that package, and the comment says so. It fails without the 
deduplication by `sha256` in `lookupPackage`. The part that adds a configured 
package as an artifact too is kept, with a comment that says that the same 
package, configured and also added by the session, is not a conflict either. 
(That the package of the session takes precedence is tested by "the package 
that a session added last replaces the other packages".)



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/ffi/NativeDataSourcePackage.scala:
##########
@@ -132,6 +132,33 @@ object NativeDataSourcePackage extends Logging {
   /** Reads the package in the given file. The result is cached until the file 
changes. */
   def read(file: File): NativeDataSourcePackage = tryRead(file).fold(p => 
throw p.error, identity)
 
+  /**
+   * Checks a package that a session adds with `spark.addArtifact`, in the 
given file, before the
+   * session gets it: fails if Spark cannot read the package, or does not 
support its version of
+   * the interface. The package that a session added last takes precedence, 
and a session cannot
+   * remove an artifact, so Spark rejects such a package rather than ignore 
it, as it ignores the
+   * configured ones, which would keep the session on the package that it 
added before. The file
+   * is not the artifact itself, so the result is not cached, and errors call 
it `path`.
+   */
+  def checkAdded(file: File, path: String): Unit = {

Review Comment:
   Agreed, there was no good reason to accept it: the driver loads the library 
to plan any query on its data sources (`capabilities` at analysis, then the 
planner), so such a package can never be used from that driver, and the session 
could not go back to the package it added before. In 3c422f37c57 `checkAdded` 
also rejects a package without a library for `NativePlatform.current`, with 
`INVALID_NATIVE_DATA_SOURCE_PACKAGE.UNSUPPORTED_PLATFORM`, the error that a 
query got before. The check still does not load the library, so a library that 
fails to load fails the queries that use it, which the doc of `checkAdded` and 
the docs now say. The executors are not affected: they check their own platform 
when they load the library, as before.



##########
sql/core/src/main/scala/org/apache/spark/sql/artifact/ArtifactManager.scala:
##########
@@ -285,6 +290,19 @@ class ArtifactManager(session: SparkSession) extends 
AutoCloseable with Logging
           Map("normalizedRemoteRelativePath" -> 
normalizedRemoteRelativePath.toString)
         )
       }
+      // A package is checked before it becomes an artifact of the session: 
see checkAdded. A
+      // file that does not exist fails to transfer instead. Errors name the 
file of the user, or
+      // the artifact if the file was staged for it, such as by a Spark 
Connect client.
+      if (isNativeDataSourcePackage(normalizedRemoteRelativePath) &&
+          session.conf.get(StaticSQLConf.NATIVE_DATA_SOURCE_ENABLED) &&
+          Files.exists(serverLocalStagingPath)) {
+        val path = if (deleteStagedFile) {

Review Comment:
   Fixed in 3c422f37c57. `addLocalArtifacts` passes the name of the file of the 
user to a private overload of `addArtifact`, as `sourceName: Option[String]`, 
and the errors name the artifact when it is not given. The public `addArtifact` 
keeps its signature and passes `None`.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/ffi/NativeDataSourcePackage.scala:
##########
@@ -205,7 +233,7 @@ object NativeDataSourcePackage extends Logging {
             }
         }
       }
-      manifest.map(NativeDataSourcePackage(file.getPath, sha256(file), _))
+      manifest.map(NativeDataSourcePackage(path, sha256(file), _))

Review Comment:
   Fixed in 3c422f37c57. `readManifest` now reads the manifest and checks the 
entries of the libraries, and `parse` uses it and then computes the checksum 
for the lookup. `checkAdded` only calls `readManifest`, so it does not read the 
whole file. With the copy from item 4, adding a local package reads it once, by 
the copy, instead of twice; a staged package, as from a Spark Connect client, 
is only moved. The first lookup still hashes the artifact in `tryRead`, which 
caches the result.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/ffi/NativeDataSourcePackage.scala:
##########
@@ -132,6 +132,33 @@ object NativeDataSourcePackage extends Logging {
   /** Reads the package in the given file. The result is cached until the file 
changes. */
   def read(file: File): NativeDataSourcePackage = tryRead(file).fold(p => 
throw p.error, identity)
 
+  /**
+   * Checks a package that a session adds with `spark.addArtifact`, in the 
given file, before the
+   * session gets it: fails if Spark cannot read the package, or does not 
support its version of
+   * the interface. The package that a session added last takes precedence, 
and a session cannot
+   * remove an artifact, so Spark rejects such a package rather than ignore 
it, as it ignores the
+   * configured ones, which would keep the session on the package that it 
added before. The file
+   * is not the artifact itself, so the result is not cached, and errors call 
it `path`.
+   */
+  def checkAdded(file: File, path: String): Unit = {
+    parse(file, path) match {
+      case Left(invalid) => throw invalid.error
+      case Right(pkg) if pkg.manifest.abiVersion > NativeLibrary.ABI_VERSION =>

Review Comment:
   Fixed in 3c422f37c57: `NativeDataSourcePackage.isSupported(manifest)` is 
used by `checkAdded`, `tryRead` and `NativeDataSourceRegistry.lookup`.



-- 
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]

Reply via email to