This is an automated email from the ASF dual-hosted git repository.
ashrigondekar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/master by this push:
new 811482a79cbc [SPARK-54290][SS] Skip checksum creation if path already
exists without checksum
811482a79cbc is described below
commit 811482a79cbc9b26c1502cbaed114340e9a82379
Author: Dylan Wong <[email protected]>
AuthorDate: Tue Nov 11 10:20:29 2025 -0800
[SPARK-54290][SS] Skip checksum creation if path already exists without
checksum
### What changes were proposed in this pull request?
This PR modifies ChecksumCheckpointFileManager to fall back to the
underlying CheckpointFileManager when the target path exists but the checksum
file does not. This prevents failures in checkpoint recovery scenarios where
checksum validation cannot be performed.
### Why are the changes needed?
Consider the case using STATE_STORE_CHECKPOINT_FORMAT_VERSION = 1 when a
batch fails but state files are written. If on the next run, we try to upload
both a new state file and a file checksum, the file could fail to be uploaded
but the file checksum is uploaded successfully. This would lead to a situation
where the old file could be loaded and compared with the new file checksum,
which would fail the checksum verification. This issue does not happen when
STATE_STORE_CHECKPOINT_FORMAT [...]
### Does this PR introduce _any_ user-facing change?
### How was this patch tested?
- Added unit tests in ChecksumCheckpointFileManagerSuite to verify fallback
behavior
- Added a failure injection test in RocksDBCheckpointFailureInjectionSuite
to simulate how this error is caused
### Was this patch authored or co-authored using generative AI tooling?
No
Closes #52985 from dylanwong250/SPARK-54290.
Authored-by: Dylan Wong <[email protected]>
Signed-off-by: Anish Shrigondekar <[email protected]>
---
.../org/apache/spark/sql/internal/SQLConf.scala | 13 ++
.../ChecksumCheckpointFileManager.scala | 30 ++-
.../state/HDFSBackedStateStoreProvider.scala | 4 +-
.../sql/execution/streaming/state/RocksDB.scala | 15 +-
.../streaming/state/RocksDBFileManager.scala | 5 +-
.../execution/streaming/state/StateStoreConf.scala | 14 ++
.../ChecksumCheckpointFileManagerSuite.scala | 80 +++++--
.../FailureInjectionCheckpointFileManager.scala | 6 +-
.../RocksDBCheckpointFailureInjectionSuite.scala | 239 ++++++++++++++++++++-
9 files changed, 380 insertions(+), 26 deletions(-)
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala
index 36ded2bd7b63..8fbed6acc7d0 100644
--- a/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala
+++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala
@@ -3515,6 +3515,16 @@ object SQLConf {
.booleanConf
.createWithDefault(true)
+ val
STREAMING_CHECKPOINT_FILE_CHECKSUM_SKIP_CREATION_IF_FILE_MISSING_CHECKSUM =
+
buildConf("spark.sql.streaming.checkpoint.fileChecksum.skipCreationIfFileMissingChecksum")
+ .internal()
+ .doc("When true, if a microbatch is retried, if a file already exists
but its checksum " +
+ "file does not exist, the file checksum will not be created. This is
useful for " +
+ "compatibility with files created before file checksums were enabled.")
+ .version("4.2.0")
+ .booleanConf
+ .createWithDefault(true)
+
val PARALLEL_FILE_LISTING_IN_STATS_COMPUTATION =
buildConf("spark.sql.statistics.parallelFileListingInStatsComputation.enabled")
.internal()
@@ -6855,6 +6865,9 @@ class SQLConf extends Serializable with Logging with
SqlApiConf {
def checkpointFileChecksumEnabled: Boolean =
getConf(STREAMING_CHECKPOINT_FILE_CHECKSUM_ENABLED)
+ def checkpointFileChecksumSkipCreationIfFileMissingChecksum: Boolean =
+
getConf(STREAMING_CHECKPOINT_FILE_CHECKSUM_SKIP_CREATION_IF_FILE_MISSING_CHECKSUM)
+
def isUnsupportedOperationCheckEnabled: Boolean =
getConf(UNSUPPORTED_OPERATION_CHECK_ENABLED)
def useDeprecatedKafkaOffsetFetching: Boolean =
getConf(USE_DEPRECATED_KAFKA_OFFSET_FETCHING)
diff --git
a/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/checkpointing/ChecksumCheckpointFileManager.scala
b/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/checkpointing/ChecksumCheckpointFileManager.scala
index 7f801392c2f4..637d11ad890b 100644
---
a/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/checkpointing/ChecksumCheckpointFileManager.scala
+++
b/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/checkpointing/ChecksumCheckpointFileManager.scala
@@ -133,11 +133,21 @@ case class ChecksumFile(path: Path) {
* number of threads using file manager * 2.
* Setting this differently can lead to file operation being
blocked waiting for
* a free thread.
+ * @param skipCreationIfFileMissingChecksum (ES-1629547): If true, when a file
already exists
+ * but its checksum file does not exist, fall back to using
the underlying
+ * file manager directly instead of creating with checksum.
This is useful
+ * for compatibility with files created before checksums
were enabled. Consider
+ * the case when a batch fails but state files are written.
If on the next run,
+ * we try to upload both a new file and a checksum file, the
file could fail to be
+ * uploaded but the checksum file is uploaded successfully.
This would lead to a
+ * situation where the old file could be loaded and compared
with the new file
+ * checksum, which would fail the checksum verification.
*/
class ChecksumCheckpointFileManager(
private val underlyingFileMgr: CheckpointFileManager,
val allowConcurrentDelete: Boolean = false,
- val numThreads: Int)
+ val numThreads: Int,
+ val skipCreationIfFileMissingChecksum: Boolean)
extends CheckpointFileManager with Logging {
assert(numThreads % 2 == 0, "numThreads must be a multiple of 2, we need 1
for the main file" +
"and another for the checksum file")
@@ -160,9 +170,18 @@ class ChecksumCheckpointFileManager(
underlyingFileMgr.mkdirs(path)
}
+ private def shouldSkipChecksumCreation(path: Path): Boolean = {
+ skipCreationIfFileMissingChecksum &&
+ underlyingFileMgr.exists(path) &&
!underlyingFileMgr.exists(getChecksumPath(path))
+ }
+
override def createAtomic(path: Path,
overwriteIfPossible: Boolean): CancellableFSDataOutputStream = {
- createWithChecksum(path, underlyingFileMgr.createAtomic(_,
overwriteIfPossible))
+ if (shouldSkipChecksumCreation(path)) {
+ underlyingFileMgr.createAtomic(path, overwriteIfPossible)
+ } else {
+ createWithChecksum(path, underlyingFileMgr.createAtomic(_,
overwriteIfPossible))
+ }
}
private def createWithChecksum(path: Path,
@@ -327,8 +346,11 @@ class ChecksumFSDataInputStream(
override def close(): Unit = {
if (!closed) {
// We verify the checksum only when the client is done reading.
- verifyChecksum()
- closeInternal()
+ try {
+ verifyChecksum()
+ } finally {
+ closeInternal()
+ }
}
}
diff --git
a/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/state/HDFSBackedStateStoreProvider.scala
b/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/state/HDFSBackedStateStoreProvider.scala
index a0ace7976edd..3bbdc1fa6785 100644
---
a/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/state/HDFSBackedStateStoreProvider.scala
+++
b/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/state/HDFSBackedStateStoreProvider.scala
@@ -487,7 +487,9 @@ private[sql] class HDFSBackedStateStoreProvider extends
StateStoreProvider with
// (one for main file and another for checksum file).
// Since this fm is used by both query task and maintenance thread,
// then we need 2 * 2 = 4 threads.
- numThreads = 4)
+ numThreads = 4,
+ skipCreationIfFileMissingChecksum =
+ storeConf.checkpointFileChecksumSkipCreationIfFileMissingChecksum)
} else {
mgr
}
diff --git
a/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/state/RocksDB.scala
b/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/state/RocksDB.scala
index b1c9dee5a459..b8582d484538 100644
---
a/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/state/RocksDB.scala
+++
b/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/state/RocksDB.scala
@@ -150,20 +150,29 @@ class RocksDB(
localTempDir: File,
hadoopConf: Configuration,
codecName: String,
- loggingId: String): RocksDBFileManager = {
+ loggingId: String,
+ storeConf: StateStoreConf): RocksDBFileManager = {
new RocksDBFileManager(
dfsRootDir,
localTempDir,
hadoopConf,
codecName,
loggingId = loggingId,
+ storeConf,
fileChecksumEnabled = conf.fileChecksumEnabled,
fileChecksumThreadPoolSize = fileChecksumThreadPoolSize
)
}
- private[spark] val fileManager = createFileManager(dfsRootDir,
createTempDir("fileManager"),
- hadoopConf, conf.compressionCodec, loggingId = loggingId)
+ private[spark] val fileManager = createFileManager(
+ dfsRootDir,
+ createTempDir("fileManager"),
+ hadoopConf,
+ conf.compressionCodec,
+ loggingId = loggingId,
+ storeConf = conf.stateStoreConf
+ )
+
private val byteArrayPair = new ByteArrayPair()
private val commitLatencyMs = new mutable.HashMap[String, Long]()
diff --git
a/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/state/RocksDBFileManager.scala
b/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/state/RocksDBFileManager.scala
index 92fa5d0350fa..f67d80679d51 100644
---
a/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/state/RocksDBFileManager.scala
+++
b/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/state/RocksDBFileManager.scala
@@ -132,6 +132,7 @@ class RocksDBFileManager(
hadoopConf: Configuration,
codecName: String = CompressionCodec.ZSTD,
loggingId: String = "",
+ storeConf: StateStoreConf = StateStoreConf.empty,
fileChecksumEnabled: Boolean = false,
fileChecksumThreadPoolSize: Option[Int] = None)
extends Logging {
@@ -149,7 +150,9 @@ class RocksDBFileManager(
mgr,
// Allowing this for perf, since we do orphan checksum file cleanup in
maintenance anyway
allowConcurrentDelete = true,
- numThreads = fileChecksumThreadPoolSize.get)
+ numThreads = fileChecksumThreadPoolSize.get,
+ skipCreationIfFileMissingChecksum
+ = storeConf.checkpointFileChecksumSkipCreationIfFileMissingChecksum)
} else {
mgr
}
diff --git
a/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/state/StateStoreConf.scala
b/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/state/StateStoreConf.scala
index ebb212512ccb..3991f8d93f2c 100644
---
a/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/state/StateStoreConf.scala
+++
b/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/state/StateStoreConf.scala
@@ -125,6 +125,20 @@ class StateStoreConf(
val enableStateStoreCheckpointIds =
StatefulOperatorStateInfo.enableStateStoreCheckpointIds(sqlConf)
+ /**
+ * Whether to skip checksum creation if file missing checksum.
+ *
+ * Consider the case using STATE_STORE_CHECKPOINT_FORMAT_VERSION = 1 when a
batch fails but state
+ * files are written. If on the next run, we try to upload both a new state
file and a file
+ * checksum, the file could fail to be uploaded but the file checksum is
uploaded successfully.
+ * This would lead to a situation where the old file could be loaded and
compared with the new
+ * file checksum, which would fail the checksum verification. This issue
does not happen when
+ * STATE_STORE_CHECKPOINT_FORMAT_VERSION = 2 since each batch run unique ids
will be created.
+ */
+ val checkpointFileChecksumSkipCreationIfFileMissingChecksum: Boolean =
+ sqlConf.checkpointFileChecksumSkipCreationIfFileMissingChecksum &&
+ !enableStateStoreCheckpointIds
+
/**
* Whether the coordinator is reporting state stores trailing behind in
snapshot uploads.
*/
diff --git
a/sql/core/src/test/scala/org/apache/spark/sql/execution/streaming/ChecksumCheckpointFileManagerSuite.scala
b/sql/core/src/test/scala/org/apache/spark/sql/execution/streaming/ChecksumCheckpointFileManagerSuite.scala
index 29d09f5d52f9..b15e8f167db5 100644
---
a/sql/core/src/test/scala/org/apache/spark/sql/execution/streaming/ChecksumCheckpointFileManagerSuite.scala
+++
b/sql/core/src/test/scala/org/apache/spark/sql/execution/streaming/ChecksumCheckpointFileManagerSuite.scala
@@ -26,6 +26,7 @@ import org.apache.hadoop.fs._
import org.apache.spark.SparkException
import org.apache.spark.sql.execution.streaming.checkpointing._
+import org.apache.spark.sql.internal.SQLConf
/**
* This inherits tests for the [[CheckpointFileManager]] from
[[CheckpointFileManagerTests]].
@@ -57,9 +58,27 @@ abstract class ChecksumCheckpointFileManagerSuite extends
CheckpointFileManagerT
s"expected main files: $mainFilesForExistingChecksumFiles / actual
files: $files")
}
+ override def createManager(path: Path): CheckpointFileManager = {
+ createChecksumManager(
+ path,
+ skipCreationIfFileMissingChecksum =
+
SQLConf.STREAMING_CHECKPOINT_FILE_CHECKSUM_SKIP_CREATION_IF_FILE_MISSING_CHECKSUM
+ .defaultValue.get)
+ }
+
/** Create a normal CheckpointFileManager (not the checksum checkpoint
manager) */
protected def createNoChecksumManager(path: Path): CheckpointFileManager
+ protected def createChecksumManager(
+ path: Path,
+ skipCreationIfFileMissingChecksum: Boolean): CheckpointFileManager = {
+ new ChecksumCheckpointFileManager(
+ createNoChecksumManager(path),
+ allowConcurrentDelete = true,
+ numThreads = 4,
+ skipCreationIfFileMissingChecksum = skipCreationIfFileMissingChecksum)
+ }
+
private def makeDir(fm: CheckpointFileManager, dir: Path): Unit = {
assert(!fm.exists(dir))
fm.mkdirs(dir)
@@ -184,29 +203,62 @@ abstract class ChecksumCheckpointFileManagerSuite extends
CheckpointFileManagerT
assert(regularFm.open(path).readContent() == content)
}
}
-}
-class FileContextChecksumCheckpointFileManagerSuite extends
ChecksumCheckpointFileManagerSuite {
- override def createManager(path: Path): CheckpointFileManager = {
- new ChecksumCheckpointFileManager(
- createNoChecksumManager(path),
- allowConcurrentDelete = true,
- numThreads = 4)
+ test("skip checksum creation if file missing checksum") {
+ withTempHadoopPath { basePath =>
+ val regularFm = createNoChecksumManager(basePath)
+ // Mkdirs
+ val dir = new Path(s"$basePath/dir/subdir/subsubdir")
+ makeDir(regularFm, dir)
+
+ // Create a file using the regular file manager
+ val path = new Path(s"$dir/file")
+ regularFm.createAtomic(path, overwriteIfPossible =
true).writeContent(content).close()
+ assert(regularFm.exists(path))
+
+ // Now try to write and read the file with the checksum manager with
fallback.
+ val checksumFmWithFallback =
+ createChecksumManager(basePath, skipCreationIfFileMissingChecksum =
true)
+ // Overwrite the file with a different content.
+ checksumFmWithFallback.createAtomic(
+ path, overwriteIfPossible = true).writeContent(content + 1).close()
+ assert(checksumFmWithFallback.open(path).readContent() == content + 1)
+ // Checksum should not be created since we fallback to the underlying
file manager.
+ assert(!checksumFmWithFallback.exists(getChecksumPath(path)))
+
+ // Now try to write and read the file with the checksum manager without
fallback.
+ val checksumFmWithoutFallback =
+ createChecksumManager(basePath, skipCreationIfFileMissingChecksum =
false)
+ // Overwrite the file with a different content.
+ checksumFmWithoutFallback.createAtomic(
+ path, overwriteIfPossible = true).writeContent(content + 2).close()
+ assert(checksumFmWithoutFallback.open(path).readContent() == content + 2)
+ // Checksum should be created since we don't fallback to the underlying
file manager.
+ assert(checksumFmWithoutFallback.exists(getChecksumPath(path)))
+
+ // Try to write and read the file with the checksum manager with
fallback when the checksum
+ // file already exists.
+ checksumFmWithFallback.createAtomic(
+ path, overwriteIfPossible = true).writeContent(content + 3).close()
+ // This read should succeed since we do not fallback to the underlying
file manager, since
+ // the checksum file already exists.
+ assert(checksumFmWithFallback.open(path).readContent() == content + 3)
+ assert(checksumFmWithFallback.exists(getChecksumPath(path)))
+
+ regularFm.close()
+ checksumFmWithFallback.close()
+ checksumFmWithoutFallback.close()
+ }
}
+}
+class FileContextChecksumCheckpointFileManagerSuite extends
ChecksumCheckpointFileManagerSuite {
protected def createNoChecksumManager(path: Path): CheckpointFileManager = {
new FileContextBasedCheckpointFileManager(path, new Configuration())
}
}
class FileSystemChecksumCheckpointFileManagerSuite extends
ChecksumCheckpointFileManagerSuite {
- override def createManager(path: Path): CheckpointFileManager = {
- new ChecksumCheckpointFileManager(
- createNoChecksumManager(path),
- allowConcurrentDelete = true,
- numThreads = 4)
- }
-
protected def createNoChecksumManager(path: Path): CheckpointFileManager = {
new FileSystemBasedCheckpointFileManager(path, new Configuration())
}
diff --git
a/sql/core/src/test/scala/org/apache/spark/sql/execution/streaming/state/FailureInjectionCheckpointFileManager.scala
b/sql/core/src/test/scala/org/apache/spark/sql/execution/streaming/state/FailureInjectionCheckpointFileManager.scala
index fad207f97dd9..898e324a954e 100644
---
a/sql/core/src/test/scala/org/apache/spark/sql/execution/streaming/state/FailureInjectionCheckpointFileManager.scala
+++
b/sql/core/src/test/scala/org/apache/spark/sql/execution/streaming/state/FailureInjectionCheckpointFileManager.scala
@@ -307,7 +307,8 @@ object FailureInjectionRocksDBStateStoreProvider {
localTempDir: File,
hadoopConf: Configuration,
codecName: String,
- loggingId: String): RocksDBFileManager = {
+ loggingId: String,
+ storeConf: StateStoreConf): RocksDBFileManager = {
new RocksDBFileManager(
dfsRootDir,
localTempDir,
@@ -315,7 +316,8 @@ object FailureInjectionRocksDBStateStoreProvider {
codecName,
loggingId = loggingId,
fileChecksumEnabled = this.conf.fileChecksumEnabled,
- fileChecksumThreadPoolSize = this.fileChecksumThreadPoolSize) {
+ fileChecksumThreadPoolSize = this.fileChecksumThreadPoolSize,
+ storeConf = this.conf.stateStoreConf) {
override def getFileSystem(
myDfsRootDir: String,
myHadoopConf: Configuration): FileSystem = {
diff --git
a/sql/core/src/test/scala/org/apache/spark/sql/execution/streaming/state/RocksDBCheckpointFailureInjectionSuite.scala
b/sql/core/src/test/scala/org/apache/spark/sql/execution/streaming/state/RocksDBCheckpointFailureInjectionSuite.scala
index 30f1d77441bf..0b9690ee7277 100644
---
a/sql/core/src/test/scala/org/apache/spark/sql/execution/streaming/state/RocksDBCheckpointFailureInjectionSuite.scala
+++
b/sql/core/src/test/scala/org/apache/spark/sql/execution/streaming/state/RocksDBCheckpointFailureInjectionSuite.scala
@@ -25,7 +25,7 @@ import org.apache.hadoop.conf.Configuration
import org.apache.spark.{SparkConf, SparkException}
import org.apache.spark.sql.execution.streaming.runtime.MemoryStream
-import org.apache.spark.sql.functions.count
+import org.apache.spark.sql.functions.{count, udf}
import org.apache.spark.sql.internal.SQLConf
import
org.apache.spark.sql.internal.SQLConf.STREAMING_CHECKPOINT_FILE_MANAGER_CLASS
import org.apache.spark.sql.streaming._
@@ -587,6 +587,243 @@ class RocksDBCheckpointFailureInjectionSuite extends
StreamTest
}
}
+ case class FailureConf3(
+ skipCreationIfFileMissingChecksum: Boolean,
+ checkpointFormatVersion : String) {
+ override def toString: String = {
+ s"skipCreationIfFileMissingChecksum =
$skipCreationIfFileMissingChecksum, " +
+ s"checkpointFormatVersion = $checkpointFormatVersion"
+ }
+ }
+
+ private def versionsPresent(dir: File, suffix: String): Seq[(Long,
Option[String])] = {
+ dir.listFiles.filter(_.getName.endsWith(suffix))
+ .filter(!_.getName.startsWith("."))
+ .map(_.getName.stripSuffix(suffix).split("_"))
+ .map {
+ case Array(version, uniqueId) => (version.toLong, Some(uniqueId))
+ case Array(version) => (version.toLong, None)
+ }
+ .sorted
+ .distinct
+ .toSeq
+ }
+
+ /**
+ * Test that verifies upgrading from checksum disabled to checksum enabled
after state files are
+ * written but before batch commit completes. The important part of this
test is that files are
+ * not overwritten if they already exist. When checkpointFormatVersion is 2,
we will not run into
+ * the checksum verification failure because each batch run uses unique
changelog file names.
+ *
+ * Scenario:
+ * 1. Start with checksum verification disabled
+ * 2. Run batch 1 successfully (writes 1.changelog without .crc)
+ * 3. Start batch 2 - state store commits successfully (writes 2.changelog
without .crc) but the
+ * batch fails before the commit is complete (via UDF exception). This
leaves 2.changelog on
+ * disk without a corresponding commit log file
+ * 4. Restart query with checksum verification enabled and with a query
where the changelog file
+ * contents will change from batch 2
+ * 5. Run batch 2 again and it succeeds and writes 2.changelog (and
2.changelog.crc if
+ *
STREAMING_CHECKPOINT_FILE_CHECKSUM_SKIP_CREATION_IF_FILE_MISSING_CHECKSUM is
disabled)
+ * 6. Run batch 3 and it succeeds and writes 3.changelog and 3.changelog.crc
+ * 7. Query starts with
STREAMING_CHECKPOINT_FILE_CHECKSUM_SKIP_CREATION_IF_FILE_MISSING_CHECKSUM
+ * enabled/disabled and the different behavior is shown in the test
+ */
+
+ Seq(
+ FailureConf3(skipCreationIfFileMissingChecksum = false,
checkpointFormatVersion = "1"),
+ FailureConf3(skipCreationIfFileMissingChecksum = true,
checkpointFormatVersion = "1"),
+ FailureConf3(skipCreationIfFileMissingChecksum = false,
checkpointFormatVersion = "2"),
+ FailureConf3(skipCreationIfFileMissingChecksum = true,
checkpointFormatVersion = "2")
+ ).foreach { failureConf =>
+ test(s"Upgrading from file checksum disabled to enabled " +
+ "after state commits without batch commit " + failureConf.toString) {
+ val hadoopConf = new Configuration()
+ hadoopConf.set(STREAMING_CHECKPOINT_FILE_MANAGER_CLASS.parent.key,
fileManagerClassName)
+ val rocksdbChangelogCheckpointingConfKey =
+ RocksDBConf.ROCKSDB_SQL_CONF_NAME_PREFIX +
".changelogCheckpointing.enabled"
+
+ withTempDirAllowFailureInjection { (checkpointDir, injectionState) =>
+ var forceTaskFailure = false
+ val failUDF = udf((value: Int) => {
+ if (forceTaskFailure) {
+ // This will fail all close() call to trigger query failures in
execution phase.
+ throw new RuntimeException("Ingest task failure")
+ }
+ value
+ })
+
+ val inputData = MemoryStream[Int]
+ val aggregated =
+ inputData.toDF()
+ .groupBy($"value")
+ .agg(count("*").as("count"))
+ // would fail here after writing the changelog file for the agg
+ .select(failUDF($"value").as("value"), $"count")
+ .as[(Int, Long)]
+
+ val aggregated2 =
+ inputData.toDF()
+ .select($"value" + 1000 as "value") // This is to make the
changelog file different
+ .groupBy($"value")
+ .agg(count("*").as("count"))
+ // would fail here after writing the changelog file for the agg
+ .select(failUDF($"value").as("value"), $"count")
+ .as[(Int, Long)]
+
+ def getRunConf(checksumEnabled: Boolean) : Map[String, String] = {
+ Map(
+ rocksdbChangelogCheckpointingConfKey -> "true",
+ SQLConf.STATE_STORE_CHECKPOINT_FORMAT_VERSION.key ->
+ failureConf.checkpointFormatVersion,
+ SQLConf.STREAMING_CHECKPOINT_FILE_CHECKSUM_ENABLED.key ->
checksumEnabled.toString,
+
SQLConf.STREAMING_CHECKPOINT_FILE_CHECKSUM_SKIP_CREATION_IF_FILE_MISSING_CHECKSUM.key
->
+ failureConf.skipCreationIfFileMissingChecksum.toString,
+ STREAMING_CHECKPOINT_FILE_MANAGER_CLASS.parent.key ->
fileManagerClassName,
+ SQLConf.SHUFFLE_PARTITIONS.key -> "1")
+ }
+
+ // Verify that the changelog files exists for a version
+ def verifyChangelogFileExists(version: Long) : Boolean = {
+ versionsPresent(new File(checkpointDir, "state/0/0"),
".changelog").exists {
+ case (v, uniqueId) =>
+ if (failureConf.checkpointFormatVersion == "1") {
+ v == version && uniqueId.isEmpty
+ } else {
+ v == version && uniqueId.isDefined
+ }
+ }
+ }
+
+ // Verify that the changelog checksum files exists for a version
+ def verifyChangelogFileChecksumExists(version: Long) : Boolean = {
+ versionsPresent(new File(checkpointDir, "state/0/0"),
".changelog.crc").exists {
+ case (v, uniqueId) =>
+ if (failureConf.checkpointFormatVersion == "1") {
+ v == version && uniqueId.isEmpty
+ } else {
+ v == version && uniqueId.isDefined
+ }
+ }
+ }
+
+ // First run: file checksum disabled
+ val firstRunConfs = getRunConf(checksumEnabled = false)
+
+ testStream(aggregated, Update)(
+ StartStream(
+ checkpointLocation = checkpointDir.getAbsolutePath,
+ additionalConfs = firstRunConfs),
+ AddData(inputData, 3),
+ CheckLastBatch((3, 1)),
+ Execute { _ =>
+ forceTaskFailure = true
+ },
+ AddData(inputData, 3, 2),
+ ExpectFailure[SparkException] { ex =>
+ ex.getCause.getMessage.contains("FAILED_EXECUTE_UDF")
+ }
+ )
+
+ // Verify that the changelog file was written
+ assert(verifyChangelogFileExists(2))
+ // Verify that the changelog file checksum was NOT written since it
was disabled
+ assert(!verifyChangelogFileChecksumExists(2))
+
+ // Verify that the commit file was written
+ assert((new File(checkpointDir, "commits/0")).exists())
+ // Verify that the commit file was NOT written
+ assert(!(new File(checkpointDir, "commits/1")).exists())
+
+ // Second run: STREAMING_CHECKPOINT_FILE_CHECKSUM_ENABLED enabled with
+ // allowOverwriteInRename = false. This simulates an upgrade to a new
version where the
+ // file checksum is enabled. The allowOverwriteInRename is set to
false to test the case
+ // when overwriting the changelog file fails. This is to simulate the
case where the
+ // changelog file is not overwritten but the checksum file is written.
+ injectionState.allowOverwriteInRename = false
+ forceTaskFailure = false
+
+ val secondRunConfs = getRunConf(checksumEnabled = true)
+
+ inputData.addData(3, 1)
+
+ // The query should restart successfully and handle files without
checksums, whether
+ // skipCreationIfFileMissingChecksum is enabled or disabled. The
problem
+ // arises on the load after this run.
+ testStream(aggregated2, Update)(
+ StartStream(
+ checkpointLocation = checkpointDir.getAbsolutePath,
+ additionalConfs = secondRunConfs),
+ AddData(inputData, 4),
+ CheckLastBatch((1003, 2), (1001, 1), (1004, 1)),
+ StopStream
+ )
+
+ assert(verifyChangelogFileExists(3))
+ assert(verifyChangelogFileChecksumExists(3))
+
+ // Verify that the commit files were written
+ assert((new File(checkpointDir, "commits/1")).exists())
+ assert((new File(checkpointDir, "commits/2")).exists())
+
+ val failureCase =
+ !failureConf.skipCreationIfFileMissingChecksum &&
+ failureConf.checkpointFormatVersion == "1"
+
+ if (failureCase) {
+ assert(verifyChangelogFileChecksumExists(2))
+
+ // The query does not succeed, since we load the old changelog file
with the checksum from
+ // the new changelog file that did not overwrite the old one. This
will lead to a checksum
+ // verification failure when we try to load the old changelog file
with the checksum from
+ // the new changelog file that did not overwrite the old one.
+ testStream(aggregated2, Update)(
+ StartStream(
+ checkpointLocation = checkpointDir.getAbsolutePath,
+ additionalConfs = secondRunConfs),
+ AddData(inputData, 4),
+ ExpectFailure[SparkException] { ex =>
+
ex.getMessage.contains("CHECKPOINT_FILE_CHECKSUM_VERIFICATION_FAILED")
+ ex.getMessage.contains("2.changelog")
+ }
+ )
+
+ // Verify that the commit file was not written
+ assert(!(new File(checkpointDir, "commits/3")).exists())
+ } else {
+ if (failureConf.checkpointFormatVersion == "1") {
+ // With checkpointFormatVersion = 1, the changelog file checksum
should not be written
+ assert(!verifyChangelogFileChecksumExists(2))
+ } else {
+ // With checkpointFormatVersion = 2, the changelog file checksum
should be written
+ assert(verifyChangelogFileChecksumExists(2))
+ }
+
+ // The query should restart successfully
+ testStream(aggregated2, Update)(
+ StartStream(
+ checkpointLocation = checkpointDir.getAbsolutePath,
+ additionalConfs = secondRunConfs),
+ AddData(inputData, 4),
+ CheckLastBatch((1004, 2)),
+ StopStream
+ )
+
+ // Verify again the 2.changelog file checksum exists or not
+ if (failureConf.checkpointFormatVersion == "1") {
+ assert(!verifyChangelogFileChecksumExists(2))
+ } else {
+ assert(verifyChangelogFileChecksumExists(2))
+ }
+
+ assert(verifyChangelogFileExists(4))
+ assert(verifyChangelogFileChecksumExists(4))
+ assert((new File(checkpointDir, "commits/3")).exists())
+ }
+ }
+ }
+ }
+
def commitAndGetCheckpointId(db: RocksDB): Option[String] = {
val (v, ci) = db.commit()
ci.stateStoreCkptId
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]