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


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/state/StateStore.scala:
##########
@@ -1625,13 +1628,69 @@ object StateStore extends Logging {
     if (version < 0) {
       throw QueryExecutionErrors.unexpectedStateStoreVersion(version)
     }
-    val (storeProvider, shouldForceSnapshotUpload) = 
getStateStoreProvider(storeProviderId,
-      keySchema, valueSchema, keyStateEncoderSpec, useColumnFamilies, 
storeConf, hadoopConf,
-      useMultipleValuesPerKey, stateSchemaBroadcast)
-    storeProvider.getStore(version, stateStoreCkptId, 
shouldForceSnapshotUpload)
+    withClosedProviderRetry(storeProviderId) { createUnpublishedProvider =>
+      getStateStoreProvider(storeProviderId,
+        keySchema, valueSchema, keyStateEncoderSpec, useColumnFamilies, 
storeConf, hadoopConf,
+        useMultipleValuesPerKey, stateSchemaBroadcast, 
createUnpublishedProvider)
+    } { (storeProvider, shouldForceSnapshotUpload) =>
+      storeProvider.getStore(version, stateStoreCkptId, 
shouldForceSnapshotUpload)
+    }
   }
   // scalastyle:on
 
+  private def withClosedProviderRetry[T](
+      storeProviderId: StateStoreProviderId)(
+      getProvider: Boolean => (StateStoreProvider, Boolean))(
+      loadStore: (StateStoreProvider, Boolean) => T): T = {
+    val (firstProvider, firstShouldForceSnapshotUpload) = getProvider(false)
+    try {
+      loadStore(firstProvider, firstShouldForceSnapshotUpload)
+    } catch {
+      case error: StateStoreInvalidStateMachineTransition
+        if isClosedProviderLoadTransition(error) =>
+        loadedProviders.synchronized {
+          if (loadedProviders.get(storeProviderId).exists(_ eq firstProvider)) 
{
+            loadedProviders.remove(storeProviderId)
+          } else {
+            None
+          }
+        }.foreach { provider =>
+          unloadedProvidersToClose.add(
+            (storeProviderId, provider, MaintenanceOpRequest.All))
+        }
+        // Create the replacement without publishing it in loadedProviders yet.
+        val (replacementProvider, replacementShouldForceSnapshotUpload) = 
getProvider(true)

Review Comment:
   Agreed, this is fine as is -- thanks for the detailed explanation. You're 
right that loadedProviders is not the sole holder of a provider for an id even 
today: the ownership-move path removes providers into unloadedProvidersToClose 
while tasks may still hold their stores, and close() blocks until the active 
store is released, so the transient second provider the retry can create is 
consistent with the existing lifecycle rather than a new invariant. With 
independent native RocksDB directories and first-writer-wins (or checkpoint-v2 
unique-id reporting) on the shared state-file commit, two instances committing 
the same version don't corrupt state. Publishing before acquisition would 
reopen the symmetric race you describe, so routing through the cache isn't 
clearly safer. Withdrawing this concern.
   
   <!-- SPARK_DEV_REVIEW_REPLY 
{"feedback_id":"inline:4140900374","thread_id":"inline:4140900374","verdict_sha256":"1c0bc543708058bf9bf8df9fbcfa699f5f085bc7a5c67bf3ef70d1f98bbd9457"}
 -->



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