Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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] =
Expand All @@ -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[_] = {
Expand All @@ -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]] = {

Copy link
Copy Markdown
Member

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.

Copy link
Copy Markdown
Member Author

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) using withLogAppender, covering both the warning content and the once-per-class dedup, in 7dd4abe.

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)}, " +

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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
Expand Up @@ -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
Expand All @@ -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") {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this assertion depends on the process-global missedClasses set never having seen FallbackTestData before. It works today because this test runs first and the class is unique to this suite, but it becomes order-dependent if another test above it (or a re-run in the same JVM) serializes the class first. Maybe worth a short comment, or a private[protobuf] reset hook for tests.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a private[protobuf] reset hook and the test now clears the set up front, so it no longer depends on ordering. Fixed in cea4510.

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",
Expand Down Expand Up @@ -1703,3 +1732,5 @@ class KVStoreProtobufSerializerSuite extends SparkFunSuite {
}
}
}

private[protobuf] case class FallbackTestData(value: String)