HyukjinKwon commented on code in PR #59313:
URL: https://github.com/apache/spark/pull/59313#discussion_r4237218425


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/ffi/NativeDataSourceRegistry.scala:
##########
@@ -0,0 +1,257 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.spark.sql.execution.datasources.v2.ffi
+
+import java.io.File
+import java.nio.file.Paths
+import java.util.Locale
+
+import org.apache.spark.{SparkEnv, SparkFiles}
+import org.apache.spark.internal.Logging
+import org.apache.spark.internal.LogKeys.PATH
+import org.apache.spark.sql.Artifact
+import org.apache.spark.sql.classic.SparkSession
+import org.apache.spark.sql.datasource.NativeBridge
+import org.apache.spark.sql.errors.{QueryCompilationErrors, 
QueryExecutionErrors}
+import org.apache.spark.sql.internal.{SQLConf, StaticSQLConf}
+import org.apache.spark.util.Utils
+
+/**
+ * Finds the native data sources on the driver. Like the Python data sources 
installed in the
+ * Python path, the native data source libraries installed in the native 
library path are found
+ * automatically, by their file name: the library `spark_datasource_<name>`, 
such as
+ * `libspark_datasource_my_source.so` on Linux, implements the data source 
`<name>`. The active
+ * session can also add packages with `spark.addArtifact`, or with
+ * `spark.sql.dataSource.native.paths`, which take precedence over the 
installed libraries.
+ */
+object NativeDataSourceRegistry extends Logging {
+
+  /** The prefix of the names of the installed native data source libraries. */
+  val LIBRARY_NAME_PREFIX = "spark_datasource_"
+
+  // The result of the last lookup by `DataSource.lookupDataSource` on this 
thread, if it found a
+  // native data source, by the name it looked up. The provider that the 
caller creates next for
+  // that name takes it, so that it does not look the name up again: see 
[[takeLookup]]. Each
+  // lookup clears it first, so a provider never takes the result of an 
earlier lookup.
+  private val lastLookup = new ThreadLocal[(String, (String, 
NativeLibraryLocation))]
+
+  /**
+   * Finds the package or the installed library that provides the native data 
source with the
+   * given name, which is case-insensitive. Returns the name of the data 
source, as listed in the
+   * manifest of the package or in lower case for an installed library, and 
where its library is.
+   */
+  def lookup(name: String, conf: SQLConf): Option[(String, 
NativeLibraryLocation)] = {
+    if (!conf.getConf(StaticSQLConf.NATIVE_DATA_SOURCE_ENABLED)) {
+      return None
+    }
+    val (invalid, valid) = (configuredPackageFiles(conf) ++ 
artifactPackageFiles())
+      .map(NativeDataSourcePackage.tryRead).partitionMap(identity)
+    // A package of a version of the interface that Spark does not support is 
unusable too.
+    val (unsupported, usable) = valid.partitionMap { pkg =>
+      if (pkg.manifest.abiVersion == NativeBridge.ABI_VERSION) Right(pkg) else 
Left(pkg)
+    }
+    lookupPackage(name, usable)
+      .orElse(lookupInstalledLibrary(name))
+      .orElse {
+        // An unusable package is only reported when no other package or 
installed library
+        // provides the data source: a stale copy does not hide a valid one. 
An invalid package
+        // whose manifest cannot be read is only logged, when it is read.
+        
unsupported.find(_.manifest.dataSources.exists(_.equalsIgnoreCase(name))).foreach
 { pkg =>
+          throw QueryExecutionErrors.invalidNativeDataSourcePackageError(
+            pkg.path,
+            "UNSUPPORTED_ABI_VERSION",
+            Map(
+              "version" -> pkg.manifest.abiVersion.toString,
+              "supported" -> NativeBridge.ABI_VERSION.toString))
+        }
+        invalid.find(_.dataSources.exists(_.equalsIgnoreCase(name))).foreach(p 
=> throw p.error)
+        None
+      }
+  }
+
+  /**
+   * Like [[lookup]], and keeps the result for the provider that the caller 
creates next for the
+   * given name, which takes it with [[takeLookup]].
+   */
+  def lookupForProvider(name: String, conf: SQLConf): Option[(String, 
NativeLibraryLocation)] = {
+    val result = lookup(name, conf)
+    result.foreach(found => lastLookup.set(name -> found))
+    result
+  }
+
+  /**
+   * Takes the result of the last [[lookupForProvider]] on this thread, if it 
was for the given
+   * name, and forgets it.
+   */
+  def takeLookup(name: String): Option[(String, NativeLibraryLocation)] = {
+    val last = Option(lastLookup.get())
+    lastLookup.remove()
+    last.collect { case (lookedUp, found) if lookedUp.equalsIgnoreCase(name) 
=> found }
+  }
+
+  /** Forgets the result of the last [[lookupForProvider]] on this thread. */
+  def clearLookup(): Unit = lastLookup.remove()
+
+  private def lookupPackage(
+      name: String,
+      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.
+      .groupBy(_._2.sha256).values.map(_.head).toSeq
+    found match {
+      case Seq() => None
+      case Seq(result) => Some(result)
+      case _ =>
+        throw QueryCompilationErrors.nativeDataSourcePackageConflictError(

Review Comment:
   Fixed in 9ac5a49 along the lines you suggested. A package that isn't already 
an artifact of the session (e.g. one under `spark.sql.dataSource.native.paths`) 
is now copied to the artifacts as `<fileName>.<sha256>`. That name doesn't end 
in `.sparkpkg`, so `getNativeDataSourcePackages`, and therefore the lookup, 
never counts it as a package of the session. 
`NativeDataSourcePackage.distributedFileName` tells the executors which name to 
look for. A package the user added with `addArtifact` is still shipped as it 
is. Since copies are named by checksum, different packages with the same file 
name no longer collide, so I removed 
`NATIVE_DATA_SOURCE_PACKAGE_FILE_NAME_CONFLICT` (unreleased). A new 
`ArtifactManager.hasFileArtifact` avoids re-copying, and the content 
comparison, on every query. The new test `replace a configured package in 
place` distributes a configured package, rewrites the file with a new version, 
and checks that the same session reads it with no conflict and gets a second
  copy under a different name.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/ffi/NativeDataSourceRegistry.scala:
##########
@@ -0,0 +1,257 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.spark.sql.execution.datasources.v2.ffi
+
+import java.io.File
+import java.nio.file.Paths
+import java.util.Locale
+
+import org.apache.spark.{SparkEnv, SparkFiles}
+import org.apache.spark.internal.Logging
+import org.apache.spark.internal.LogKeys.PATH
+import org.apache.spark.sql.Artifact
+import org.apache.spark.sql.classic.SparkSession
+import org.apache.spark.sql.datasource.NativeBridge
+import org.apache.spark.sql.errors.{QueryCompilationErrors, 
QueryExecutionErrors}
+import org.apache.spark.sql.internal.{SQLConf, StaticSQLConf}
+import org.apache.spark.util.Utils
+
+/**
+ * Finds the native data sources on the driver. Like the Python data sources 
installed in the
+ * Python path, the native data source libraries installed in the native 
library path are found
+ * automatically, by their file name: the library `spark_datasource_<name>`, 
such as
+ * `libspark_datasource_my_source.so` on Linux, implements the data source 
`<name>`. The active
+ * session can also add packages with `spark.addArtifact`, or with
+ * `spark.sql.dataSource.native.paths`, which take precedence over the 
installed libraries.
+ */
+object NativeDataSourceRegistry extends Logging {
+
+  /** The prefix of the names of the installed native data source libraries. */
+  val LIBRARY_NAME_PREFIX = "spark_datasource_"
+
+  // The result of the last lookup by `DataSource.lookupDataSource` on this 
thread, if it found a
+  // native data source, by the name it looked up. The provider that the 
caller creates next for
+  // that name takes it, so that it does not look the name up again: see 
[[takeLookup]]. Each
+  // lookup clears it first, so a provider never takes the result of an 
earlier lookup.
+  private val lastLookup = new ThreadLocal[(String, (String, 
NativeLibraryLocation))]
+
+  /**
+   * Finds the package or the installed library that provides the native data 
source with the
+   * given name, which is case-insensitive. Returns the name of the data 
source, as listed in the
+   * manifest of the package or in lower case for an installed library, and 
where its library is.
+   */
+  def lookup(name: String, conf: SQLConf): Option[(String, 
NativeLibraryLocation)] = {
+    if (!conf.getConf(StaticSQLConf.NATIVE_DATA_SOURCE_ENABLED)) {
+      return None
+    }
+    val (invalid, valid) = (configuredPackageFiles(conf) ++ 
artifactPackageFiles())
+      .map(NativeDataSourcePackage.tryRead).partitionMap(identity)
+    // A package of a version of the interface that Spark does not support is 
unusable too.
+    val (unsupported, usable) = valid.partitionMap { pkg =>
+      if (pkg.manifest.abiVersion == NativeBridge.ABI_VERSION) Right(pkg) else 
Left(pkg)
+    }
+    lookupPackage(name, usable)
+      .orElse(lookupInstalledLibrary(name))
+      .orElse {
+        // An unusable package is only reported when no other package or 
installed library
+        // provides the data source: a stale copy does not hide a valid one. 
An invalid package
+        // whose manifest cannot be read is only logged, when it is read.
+        
unsupported.find(_.manifest.dataSources.exists(_.equalsIgnoreCase(name))).foreach
 { pkg =>
+          throw QueryExecutionErrors.invalidNativeDataSourcePackageError(
+            pkg.path,
+            "UNSUPPORTED_ABI_VERSION",
+            Map(
+              "version" -> pkg.manifest.abiVersion.toString,
+              "supported" -> NativeBridge.ABI_VERSION.toString))
+        }
+        invalid.find(_.dataSources.exists(_.equalsIgnoreCase(name))).foreach(p 
=> throw p.error)
+        None
+      }
+  }
+
+  /**
+   * Like [[lookup]], and keeps the result for the provider that the caller 
creates next for the
+   * given name, which takes it with [[takeLookup]].
+   */
+  def lookupForProvider(name: String, conf: SQLConf): Option[(String, 
NativeLibraryLocation)] = {
+    val result = lookup(name, conf)
+    result.foreach(found => lastLookup.set(name -> found))
+    result
+  }
+
+  /**
+   * Takes the result of the last [[lookupForProvider]] on this thread, if it 
was for the given
+   * name, and forgets it.
+   */
+  def takeLookup(name: String): Option[(String, NativeLibraryLocation)] = {
+    val last = Option(lastLookup.get())
+    lastLookup.remove()
+    last.collect { case (lookedUp, found) if lookedUp.equalsIgnoreCase(name) 
=> found }
+  }
+
+  /** Forgets the result of the last [[lookupForProvider]] on this thread. */
+  def clearLookup(): Unit = lastLookup.remove()
+
+  private def lookupPackage(
+      name: String,
+      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.
+      .groupBy(_._2.sha256).values.map(_.head).toSeq
+    found match {
+      case Seq() => None
+      case Seq(result) => Some(result)
+      case _ =>
+        throw QueryCompilationErrors.nativeDataSourcePackageConflictError(
+          name, found.map(_._2.path).sorted)
+    }
+  }
+
+  private def lookupInstalledLibrary(name: String): Option[(String, 
InstalledNativeLibrary)] = {
+    // Other names, such as class names, are not the names of libraries.
+    if (!name.matches("[A-Za-z0-9_]+")) {
+      return None
+    }
+    val dataSource = name.toLowerCase(Locale.ROOT)
+    findInstalledLibrary(libraryFileName(dataSource))
+      .map(file => dataSource -> InstalledNativeLibrary(file.getPath))
+  }
+
+  /** Finds the library with the given file name in the native library path of 
this node. */
+  private[ffi] def findInstalledLibrary(fileName: String): Option[File] = {
+    librarySearchPath(System.getProperty("java.library.path"), 
System.getenv("PATH"))
+      .map(new File(_, fileName))
+      .find(_.isFile)
+  }
+
+  /**
+   * The file name of the installed library of the native data source with the 
given name, such
+   * as `libspark_datasource_my_source.so` on Linux, 
`libspark_datasource_my_source.dylib` on
+   * macOS and `spark_datasource_my_source.dll` on Windows.
+   */
+  def libraryFileName(name: String): String = {
+    System.mapLibraryName(LIBRARY_NAME_PREFIX + name.toLowerCase(Locale.ROOT))
+  }
+
+  /**
+   * The native library path: the directories where native data source 
libraries are installed,
+   * in the order they are searched. They are the directories of 
`java.library.path`, where the
+   * JVM finds native libraries, which include `LD_LIBRARY_PATH` on Linux, 
`DYLD_LIBRARY_PATH` on
+   * macOS and `PATH` on Windows, and then the `lib` directory of each 
installation prefix in
+   * `PATH`, such as `/usr/local/lib` for `/usr/local/bin`. Relative 
directories are ignored.
+   */
+  private[ffi] def librarySearchPath(javaLibraryPath: String, path: String): 
Seq[File] = {
+    def directories(value: String): Seq[File] = {
+      Option(value).toSeq.flatMap(_.split(File.pathSeparator)).map(new 
File(_)).filter(_.isAbsolute)
+    }
+    val prefixLibraries = directories(path)
+      .filter(_.getName == "bin")
+      .flatMap(bin => Option(bin.getParentFile))
+      .map(new File(_, "lib"))
+    (directories(javaLibraryPath) ++ prefixLibraries).distinct
+  }
+
+  /**
+   * Makes the library available to the executors of the active session, which 
runs the jobs of
+   * the scan or the write: for a streaming query, it is the session of the 
query, a clone of the
+   * session that started it. Returns where the executors find it. An 
installed library is already
+   * installed on every node.
+   */
+  def distribute(location: NativeLibraryLocation): NativeLibraryLocation = 
location match {
+    case pkg: NativeDataSourcePackage =>
+      SparkSession.getActiveSession match {
+        // In local mode, the executors run in the driver and read the package 
where it is.
+        case Some(session) if !session.sparkContext.isLocal => 
addToArtifacts(session, pkg)
+        case _ => pkg
+      }
+    case installed: InstalledNativeLibrary => installed
+  }
+
+  /** Adds the package as an artifact of the session, unless it already is 
one. */
+  private[ffi] def addToArtifacts(
+      session: SparkSession,
+      pkg: NativeDataSourcePackage): NativeDataSourcePackage = {
+    val artifactManager = session.artifactManager
+    val (added, artifactUUID) = artifactManager.getNativeDataSourcePackages
+    // The executors find the package by its file name, so the session cannot 
have another
+    // package with the same file name.
+    added.find(_.getName == pkg.fileName) match {
+      case Some(file) =>
+        val sha256 = 
NativeDataSourcePackage.tryRead(file).map(_.sha256).getOrElse("")
+        if (sha256 != pkg.sha256) {
+          throw 
QueryExecutionErrors.nativeDataSourcePackageFileNameConflictError(
+            pkg.path, pkg.fileName)
+        }
+      case None =>
+        artifactManager.addLocalArtifacts(Artifact.newFileArtifact(
+          Paths.get(pkg.fileName), new 
Artifact.LocalFile(Paths.get(pkg.path))) :: Nil)
+    }
+    pkg.copy(artifactUUID = artifactUUID)
+  }
+
+  /**
+   * The local copies, on an executor, of a package distributed with 
[[distribute]], in the order
+   * to check them. The files of an isolated session are stored in a directory 
named after the
+   * UUID of the session; the copies of other sessions are checked last, 
because the executors
+   * check the checksum of the package anyway.
+   */
+  def distributedCopies(pkg: NativeDataSourcePackage): Seq[File] = {
+    if (SparkEnv.get == null) {
+      Nil
+    } else {
+      val root = new File(SparkFiles.getRootDirectory())
+      val sessionCopies = Option(root.listFiles()).toSeq.flatten
+        .filter(_.isDirectory)
+        .sortBy(_.getName)
+        .map(new File(_, pkg.fileName))
+      (pkg.artifactUUID.map(uuid => new File(new File(root, uuid), 
pkg.fileName)).toSeq ++
+        Seq(new File(root, pkg.fileName)) ++ sessionCopies).distinct
+    }
+  }
+
+  private def configuredPackageFiles(conf: SQLConf): Seq[File] = {
+    conf.getConf(SQLConf.NATIVE_DATA_SOURCE_PATHS).flatMap { path =>
+      val uri = Utils.resolveURI(path)
+      val file = new File(uri.getPath)
+      if (uri.getScheme != null && uri.getScheme != "file") {
+        logWarning(log"Ignoring the native data source path ${MDC(PATH, 
path)}: only local " +
+          log"paths are supported. Add remote packages with spark.addArtifact 
instead.")
+        Nil
+      } else if (file.isDirectory) {
+        packageFilesIn(file)
+      } else if (file.isFile) {
+        Seq(file)
+      } else {
+        logWarning(log"The native data source path ${MDC(PATH, path)} does not 
exist.")
+        Nil
+      }
+    }
+  }
+
+  private def packageFilesIn(dir: File): Seq[File] = {
+    dir.listFiles()

Review Comment:
   Fixed in 9ac5a49: `packageFilesIn` now logs a warning naming the directory 
and skips it when `listFiles()` returns null. The new test `ignore configured 
directories that cannot be read` makes the directory unreadable and checks 
`DATA_SOURCE_NOT_FOUND` and `TABLE_OR_VIEW_NOT_FOUND`. It's skipped when the 
directory is still readable, e.g. when running as root.



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