This is an automated email from the ASF dual-hosted git repository.

pan3793 pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.x by this push:
     new 9de7379b30b6 [SPARK-59169][CORE] Log a warning when 
KVStoreProtobufSerializer falls back to JSON SerDe
9de7379b30b6 is described below

commit 9de7379b30b66a5a668d9c7f0f771d19de733d49
Author: Cheng Pan <[email protected]>
AuthorDate: Fri Oct 9 16:45:34 2026 +0800

    [SPARK-59169][CORE] Log a warning when KVStoreProtobufSerializer falls back 
to JSON SerDe
    
    ### What changes were proposed in this pull request?
    
    `KVStoreProtobufSerializer.getSerializer` now logs a warning when no 
`ProtobufSerDe` is registered for a class and it silently falls back to the 
JSON SerDe. Each missed class is recorded in a concurrent set so the warning is 
logged at most once per class, avoiding log flood.
    
    ### Why are the changes needed?
    
    The fallback is currently silent, so a missing `ProtobufSerDe` registration 
(e.g. a new UI store class without a corresponding serializer) is easy to miss 
and quietly loses the performance benefit of Protobuf serialization. A one-time 
warning makes the gap visible without flooding the log.
    
    ### Does this PR introduce _any_ user-facing change?
    
    No, only a new warning log message.
    
    ### How was this patch tested?
    
    New UT in `KVStoreProtobufSerializerSuite` (core) verifies the warning 
content and the once-per-class dedup via `withLogAppender`. Existing 
`KVStoreProtobufSerializerSuite` suites in `core` and `sql/core` also pass.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Fable 5
    
    Closes #58467 from pan3793/SPARK-59169.
    
    Authored-by: Cheng Pan <[email protected]>
    Signed-off-by: Cheng Pan <[email protected]>
    (cherry picked from commit fe185b7f63c8c8f61006bcf4f7396978d8473e9b)
    Signed-off-by: Cheng Pan <[email protected]>
---
 .../protobuf/KVStoreProtobufSerializer.scala       | 27 ++++++++++++++++---
 .../protobuf/KVStoreProtobufSerializerSuite.scala  | 31 ++++++++++++++++++++++
 2 files changed, 55 insertions(+), 3 deletions(-)

diff --git 
a/core/src/main/scala/org/apache/spark/status/protobuf/KVStoreProtobufSerializer.scala
 
b/core/src/main/scala/org/apache/spark/status/protobuf/KVStoreProtobufSerializer.scala
index d87c9e6d59a7..d8bb13e316f6 100644
--- 
a/core/src/main/scala/org/apache/spark/status/protobuf/KVStoreProtobufSerializer.scala
+++ 
b/core/src/main/scala/org/apache/spark/status/protobuf/KVStoreProtobufSerializer.scala
@@ -19,10 +19,14 @@ package org.apache.spark.status.protobuf
 
 import java.lang.reflect.ParameterizedType
 import java.util.ServiceLoader
+import java.util.concurrent.ConcurrentHashMap
 
 import scala.jdk.CollectionConverters._
 
+import org.apache.spark.internal.Logging
+import org.apache.spark.internal.LogKeys.CLASS_NAME
 import org.apache.spark.status.KVUtils.KVStoreScalaSerializer
+import org.apache.spark.util.kvstore.{LevelDB, RocksDB}
 
 private[spark] class KVStoreProtobufSerializer extends KVStoreScalaSerializer {
   override def serialize(o: Object): Array[Byte] =
@@ -39,7 +43,7 @@ private[spark] class KVStoreProtobufSerializer extends 
KVStoreScalaSerializer {
     }
 }
 
-private[spark] object KVStoreProtobufSerializer {
+private[spark] object KVStoreProtobufSerializer extends Logging {
 
   private[this] lazy val serializerMap: Map[Class[_], ProtobufSerDe[Any]] = {
     def getGenericsType(klass: Class[_]): Class[_] = {
@@ -51,6 +55,23 @@ private[spark] object KVStoreProtobufSerializer {
     }.toMap
   }
 
-  def getSerializer(klass: Class[_]): Option[ProtobufSerDe[Any]] =
-    serializerMap.get(klass)
+  private[this] val missedClasses = ConcurrentHashMap.newKeySet[Class[_]]()
+
+  // The KVStore backends' own bookkeeping values fall back to the JSON SerDe 
by design:
+  // they are internals of the kvstore library, which the ProtobufSerDe SPI 
does not cover.
+  // Skip warning for them.
+  private[this] val jsonByDesignClasses: Set[Class[_]] = Set(
+    classOf[LevelDB.TypeAliases],
+    classOf[RocksDB.TypeAliases])
+
+  private[protobuf] def resetMissedClassesForTesting(): Unit = 
missedClasses.clear()
+
+  def getSerializer(klass: Class[_]): Option[ProtobufSerDe[Any]] = {
+    val serializer = serializerMap.get(klass)
+    if (serializer.isEmpty && !jsonByDesignClasses.contains(klass) && 
missedClasses.add(klass)) {
+      logWarning(log"No Protobuf SerDe found for class ${MDC(CLASS_NAME, 
klass.getName)}, " +
+        log"falling back to the JSON SerDe.")
+    }
+    serializer
+  }
 }
diff --git 
a/core/src/test/scala/org/apache/spark/status/protobuf/KVStoreProtobufSerializerSuite.scala
 
b/core/src/test/scala/org/apache/spark/status/protobuf/KVStoreProtobufSerializerSuite.scala
index 23cb99bce2ff..ef983e1b2b6d 100644
--- 
a/core/src/test/scala/org/apache/spark/status/protobuf/KVStoreProtobufSerializerSuite.scala
+++ 
b/core/src/test/scala/org/apache/spark/status/protobuf/KVStoreProtobufSerializerSuite.scala
@@ -22,6 +22,8 @@ import java.util.Date
 import scala.collection.mutable
 import scala.io.Source
 
+import org.apache.logging.log4j.Level
+
 import org.apache.spark.{JobExecutionStatus, SparkFunSuite}
 import org.apache.spark.executor.ExecutorMetrics
 import org.apache.spark.metrics.ExecutorMetricType
@@ -31,10 +33,37 @@ import org.apache.spark.status._
 import org.apache.spark.status.api.v1._
 import org.apache.spark.ui.scope.{RDDOperationEdge, RDDOperationNode}
 import org.apache.spark.util.Utils.tryWithResource
+import org.apache.spark.util.kvstore.{LevelDB, RocksDB}
 
 class KVStoreProtobufSerializerSuite extends SparkFunSuite {
   private val serializer = new KVStoreProtobufSerializer()
 
+  test("SPARK-59169: log a warning once per class when no ProtobufSerDe is 
found") {
+    KVStoreProtobufSerializer.resetMissedClassesForTesting()
+    val appender = new LogAppender("KVStoreProtobufSerializer fallback 
warning")
+    withLogAppender(appender, loggerNames = 
Seq(classOf[KVStoreProtobufSerializer].getName)) {
+      serializer.serialize(FallbackTestData("a"))
+      serializer.serialize(FallbackTestData("b"))
+    }
+    val warnings = appender.loggingEvents
+      .filter(_.getLevel == Level.WARN)
+      .map(_.getMessage.getFormattedMessage)
+      .filter(_.contains(classOf[FallbackTestData].getName))
+    assert(warnings.size === 1)
+    assert(warnings.head.contains("No Protobuf SerDe found for class"))
+  }
+
+  test("SPARK-59169: no warning for KVStore bookkeeping classes without 
ProtobufSerDe") {
+    KVStoreProtobufSerializer.resetMissedClassesForTesting()
+    val appender = new LogAppender("KVStoreProtobufSerializer by-design 
fallback")
+    withLogAppender(appender, loggerNames = 
Seq(classOf[KVStoreProtobufSerializer].getName)) {
+      
assert(KVStoreProtobufSerializer.getSerializer(classOf[RocksDB.TypeAliases]).isEmpty)
+      
assert(KVStoreProtobufSerializer.getSerializer(classOf[LevelDB.TypeAliases]).isEmpty)
+    }
+    val warnings = appender.loggingEvents.filter(_.getLevel == Level.WARN)
+    assert(warnings.isEmpty)
+  }
+
   test("All the string fields must be optional to avoid NPE") {
     val protoFile = getWorkspaceFilePath(
       "core", "src", "main", "protobuf", "org", "apache", "spark", "status", 
"protobuf",
@@ -1703,3 +1732,5 @@ class KVStoreProtobufSerializerSuite extends 
SparkFunSuite {
     }
   }
 }
+
+private[protobuf] case class FallbackTestData(value: String)


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to