spark-test commented on code in PR #59947:
URL: https://github.com/apache/spark/pull/59947#discussion_r4420328181
##########
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 {
Review Comment:
**Nit (P3):** `checkAdded` goes through `parse`, which for a readable
manifest builds `NativeDataSourcePackage(path, sha256(file), _)` (line 236), so
every add of a `files/*.sparkpkg` artifact reads and hashes the whole package.
`checkAdded` never uses that checksum: it only looks at `manifest.abiVersion`,
and `unsupportedAbiVersionError` only reads the path and the manifest. The
validation itself only needs the zip's central directory, the manifest entry
and the `getEntry` lookups.
The cost scales with the package size, and a package bundles one library per
platform. On the Spark Connect path the staged file is then moved into place,
so this discarded hash is the only full read of the file during the add, and it
runs on the handler thread. The first lookup reads and hashes the artifact
again in `tryRead` before caching it. Query results and query-time work are not
affected; this is extra latency on `spark.addArtifact`.
One way to avoid it: move the zip, manifest and library-entry validation out
of `parse` into a helper that returns the manifest or the invalid-package
result, with the same `IOException` handling. `parse` keeps building the
package and its checksum for the lookup path, and `checkAdded` calls the helper
directly and builds the unsupported-version error from `path` and the manifest,
without constructing a package.
**Verification:**
- **Inspection:** The add-time check reads only the zip's central directory,
the manifest entry and the library-entry lookups, and no code path from it
computes the package checksum.
- **Regression:** Adding an unreadable package, a package missing a listed
library, and a package with a newer abiVersion still fails with the same error
conditions, path and parameters. A valid package is still added, listed and
used. Lookup-time errors for unusable configured packages are unchanged.
##########
docs/sql-native-data-sources.md:
##########
@@ -325,12 +325,15 @@ A session finds the packages in two places, and the
executors get them automatic
The packages of a session take precedence over the installed libraries, and
the package that a
session added last over its other packages, also the configured ones: a
session cannot remove an
-artifact, or add a file with the same name again, so it upgrades a package by
adding the new
-version under another file name, such as `rust_range-1.1.sparkpkg`. Distinct
file names are also
-needed because the sessions that are not isolated share the files they add. A
query fails if more
-than one configured package provides its data source. Spark ignores, and logs,
a package that it
-cannot read or whose version it does not support, unless its manifest lists
the data source that a
-query uses and no other package or installed library provides it.
+artifact, or add another file with the name of one that it added, so it
upgrades a package by adding
+the new version under another file name, such as `rust_range-1.1.sparkpkg`.
Distinct file names are
+also needed because the sessions that are not isolated share the files they
add. A query fails if
+more than one configured package provides its data source. `spark.addArtifact`
fails for a package
+that Spark cannot read or whose version it does not support, so that a session
does not keep using
Review Comment:
**Nit (P3):** This reads as if the failure itself stops the session from
using the package it added before. After a rejected add, though, the session
keeps using exactly the packages it had. `checkAdded` throws before the file is
transferred or registered, and the new `NativeDataSourceSuite` test asserts
after each rejection that queries still use the earlier package ("keeps using
the configured package"). What the failure changes is that the user learns the
new package was not added, instead of the session silently staying on the old
one while the add appears to succeed. The `checkAdded` Scaladoc is closer: it
is ignoring the package that would keep the session on the package that it
added before. As written, a reader could expect a failed upgrade to make the
session fall back to a configured package or an installed library.
For example: "`spark.addArtifact` fails for a package that Spark cannot read
or whose version it does not support, instead of adding it while the session
silently keeps using the package that it added before. After the failure, the
session keeps using the packages that it had."
**Verification:**
- **Inspection:** The reworded guide sentence matches the add-time behavior:
a rejected package never becomes an artifact, the session keeps the packages it
had, and the failure is what tells the user that the upgrade did not happen, as
the checkAdded Scaladoc already says.
##########
sql/connect/server/src/test/scala/org/apache/spark/sql/connect/NativeDataSourceConnectSuite.scala:
##########
@@ -92,6 +93,23 @@ class NativeDataSourceConnectSuite extends
SparkConnectServerTest {
}
}
+ nativeTest("reject a package that Spark cannot use when a session adds it") {
+ val invalid = new File(Utils.createTempDir(), "native_range.sparkpkg")
+ Files.writeString(invalid.toPath, "not a zip")
+ withNewSession { session =>
+ // The client gets the error of the server in the message of a
SparkException.
+ val e = intercept[SparkException](session.addArtifact(invalid.getPath))
+
assert(e.getMessage.contains("[INVALID_NATIVE_DATA_SOURCE_PACKAGE.INVALID_MANIFEST]"),
e)
+ // It names the artifact, rather than the file in which the server
staged it.
+ assert(e.getMessage.contains(s"package
files${File.separator}native_range.sparkpkg:"), e)
Review Comment:
**Nit (P3):** This assertion covers the artifact naming only for
`INVALID_MANIFEST`, whose path comes straight from `invalidManifestError(path,
...)`. A staged `UNSUPPORTED_ABI_VERSION` rejection takes its path one step
later, from the package that `parse` builds: `NativeDataSourcePackage(path,
sha256(file), _)` at line 236 of `NativeDataSourcePackage.scala`, which this PR
changed from `file.getPath`. No test exercises that step with a staged file.
The `NativeDataSourceSuite` unsupported-version case adds a local file, where
`path` and `file.getPath` are the same string, and the lookup-time cases read
configured packages by their canonical path. So if line 236 were reverted, a
Spark Connect client, or a classic `addArtifact(bytes, "x.sparkpkg")`, would
get the server's staging path for a package with a newer `abiVersion`, and CI
would still pass.
The mutation that the PR description mentions, naming the staged file in
`ArtifactManager`, is caught by this test, because both errors share that path.
The gap is only the separate step in `parse`. A staged add of a package with
`abiVersion = 2` that asserts the error names `files/<name>` would close it,
either here or as an in-memory add in `NativeDataSourceSuite`.
**Verification:**
- **Regression:** A test fails if the unsupported-version error for a staged
add takes its path from the staged file instead of the artifact path. For
example, it fails when the package that parse builds is named after the file it
reads.
--
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]