spark-test commented on code in PR #59890:
URL: https://github.com/apache/spark/pull/59890#discussion_r4419772209
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/CacheManager.scala:
##########
@@ -649,11 +666,13 @@ class CacheManager extends Logging with
AdaptiveSparkPlanHelper {
plan: LogicalPlan,
fs: FileSystem,
qualifiedPath: Path,
- includeTimeTravel: Boolean): Boolean = {
+ includeTimeTravel: Boolean,
+ refreshedIndexes: JSet[FileIndex]): Boolean = {
Review Comment:
**Nit (P3):** The ScalaDoc just above this method (lines 659-664) predates
this change and no longer describes it. With the new `refreshedIndexes`
parameter, a matching index that was already refreshed earlier in the same
`recacheByPath` call makes this method return `true` without calling
`refresh()` (`refreshFileIndexIfNecessary`, lines 708-710). The
`first.union(second).union(first)` shared-index test hits exactly this case.
The doc still says "If found, we refresh the metadata and return true". It also
says the method traverses the plan, which the caller now does with `foreach`,
and it names only `HadoopFsRelation`, although the V2 `FileTable` arm is
handled too. A reader of this method's contract would expect every match to
re-list its index, and the deduplication rule is documented only on the private
callee.
`refreshFileIndexIfNecessary`'s doc was already updated for this change, so
something along the same lines would work here, for example:
```scala
/**
* Checks whether `plan` is a V1 `HadoopFsRelation` or V2 `FileTable` read
whose file index
* has a root path under `qualifiedPath`; time-travel snapshots are
skipped unless
* `includeTimeTravel` is true. A matching index is refreshed only the
first time it is seen
* during the current recacheByPath call, as tracked by `refreshedIndexes`.
* @return whether the read matches, including one whose index was already
refreshed.
*/
```
**Verification:**
- **Inspection:** The lookupAndRefresh documentation no longer claims that
every match refreshes its index. It describes the single-node check, both the
V1 and V2 file-relation arms, the per-call set of already refreshed indexes,
and the true result for an already refreshed match, in terms consistent with
the refreshFileIndexIfNecessary documentation.
##########
sql/core/src/test/scala/org/apache/spark/sql/CachedTableSuite.scala:
##########
@@ -855,6 +856,133 @@ class CachedTableSuite extends SharedSparkSession
}
}
+ private def appendParquetWithoutRefresh(path: String, value: Long): Unit = {
+ withTempDir { staging =>
+ val output = new File(staging, "data")
+ spark.range(value, value + 1).write.parquet(output.getCanonicalPath)
+ // Move files externally so Spark does not refresh the destination
before refreshByPath.
+ output.listFiles().filter(_.getName.endsWith(".parquet")).foreach { file
=>
+ Files.move(file.toPath, new File(path, file.getName).toPath)
+ }
+ }
+ }
+
+ for {
+ useV1SourceList <- Seq("parquet", "")
+ relationCount <- Seq(1, 2, 3)
+ } {
+ val sourceVersion = if (useV1SourceList.nonEmpty) "V1" else "V2"
+ test(s"refreshByPath refreshes all $relationCount file indexes in one
cached plan " +
+ s"($sourceVersion)") {
+ withSQLConf(SQLConf.USE_V1_SOURCE_LIST.key -> useV1SourceList) {
+ withTempDir { dir =>
+ val paths = (0 until relationCount).map(i => new File(dir,
s"part$i").getCanonicalPath)
+ paths.foreach(path => spark.range(1).write.parquet(path))
+ val cached =
paths.map(spark.read.parquet(_)).reduce(_.union(_)).cache()
+ try {
+ // Check the analyzed relations so a V2 fallback cannot silently
test only V1.
+ val fileSources = cached.queryExecution.analyzed.collect {
+ case LogicalRelationWithTable(_: HadoopFsRelation, _) => "V1"
+ case DataSourceV2Relation(_: FileTable, _, _, _, _, _) => "V2"
+ }
+ assert(fileSources == Seq.fill(relationCount)(sourceVersion))
+ assert(cached.count() == relationCount)
+ paths.foreach(path => appendParquetWithoutRefresh(path, 1L))
+ assert(cached.count() == relationCount)
+ spark.catalog.refreshByPath(dir.getCanonicalPath)
Review Comment:
**Nit (P3):** Every plan these tests cache ends its pre-order walk on a read
under the refreshed path. That covers the one- to three-read unions here,
`first.union(second).union(first)`, and the cross-entry unions, and it also
holds for the existing `refreshByPath`/`recacheByPath` tests, including the
pinned-then-live union in `V1TimeTravelCacheSuite`. So nothing checks that
entry selection in the new `foreach` loop (CacheManager.scala:647-655) is
independent of node order, which the replaced `exists` guaranteed by
construction. If the `if` were later rewritten as `refreshed =
lookupAndRefresh(...)`, the flag would reflect only the last node visited and
every test would still pass. A cached
`spark.read.parquet("/d/a").union(spark.read.parquet("/e/b"))` refreshed with
`refreshByPath("/d")` would then not be re-cached and would keep serving stale
`/d/a` rows. One case whose first read is under the refreshed directory and
whose last read is outside it would pin this.
**Verification:**
- **Regression:** Cache a union whose first read is under the refreshed
directory and whose last read is outside it, add files under both outside
Spark's writer, and call refreshByPath on the first directory. The cached query
must then return the first read's new rows, and the test must fail if entry
selection reflects only the last visited node.
- **Behavior:** In the same partially matching plan, the matching index is
refreshed exactly once per refreshByPath call and the index of the trailing
read outside the refreshed directory is not refreshed.
--
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]