-
Notifications
You must be signed in to change notification settings - Fork 29.4k
[SPARK-59169][CORE] Log a warning when KVStoreProtobufSerializer falls back to JSON SerDe #58467
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
a12f79c
6976b68
7dd4abe
cea4510
9912008
2b73e60
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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)}, " + | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think debug level is enough, so nothing noisy by default. We can enhance the display log level by modifying the configuration file.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's intended to log it at the warning level: JSON is the default option, and users likely switch to PROTOBUF for performance purposes, falling back means a performance drop. for noise concerns, it's a one-time warning that makes the gap visible without flooding the log. |
||
| log"falling back to the JSON SerDe.") | ||
| } | ||
| serializer | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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") { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: this assertion depends on the process-global
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a |
||
| 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) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please take a look at KVStoreProtobufSerializerSuite. No test covers the new warning behavior. The PR description asserts "Existing tests" suffice, but both KVStoreProtobufSerializerSuite suites (core and sql/core) exercise only known-registered classes and never reach the serializer.isEmpty branch. A test using withLogAppender should be added to assert (a) a warning IS logged on the first call with an unregistered class, and (b) is NOT logged again on a repeated call for the same class (the deduplication invariant). Without this, the "warn exactly once" semantics are entirely untested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a UT in
KVStoreProtobufSerializerSuite(core) usingwithLogAppender, covering both the warning content and the once-per-class dedup, in 7dd4abe.