@@ -23,10 +23,12 @@ import java.time.temporal.ChronoUnit
23
23
import java .util .UUID
24
24
25
25
import scala .jdk .CollectionConverters ._
26
+ import scala .math .BigDecimal .RoundingMode
26
27
27
28
import org .json4s .jackson .JsonMethods ._
28
29
import org .scalatest .concurrent .Eventually
29
30
import org .scalatest .concurrent .PatienceConfiguration .Timeout
31
+ import org .scalatest .matchers .should .Matchers
30
32
import org .scalatest .time .SpanSugar ._
31
33
32
34
import org .apache .spark .sql .Row
@@ -40,7 +42,7 @@ import org.apache.spark.sql.streaming.util.StreamManualClock
40
42
import org .apache .spark .sql .types .StructType
41
43
import org .apache .spark .util .ArrayImplicits ._
42
44
43
- class StreamingQueryStatusAndProgressSuite extends StreamTest with Eventually {
45
+ class StreamingQueryStatusAndProgressSuite extends StreamTest with Eventually with Matchers {
44
46
test(" StreamingQueryProgress - prettyJson" ) {
45
47
val json1 = testProgress1.prettyJson
46
48
assertJson(
@@ -400,6 +402,40 @@ class StreamingQueryStatusAndProgressSuite extends StreamTest with Eventually {
400
402
assert(data(0 ).getAs[Timestamp ](0 ).equals(validValue))
401
403
}
402
404
405
+ test(" SPARK-53491: inputRowsPerSecond and processedRowsPerSecond " +
406
+ " should never be with scientific notation" ) {
407
+ val progress = testProgress4.jsonValue
408
+
409
+ // Actual values
410
+ val inputRowsPerSecond : Double = 6.923076923076923E8
411
+ val processedRowsPerSecond : Double = 2.923076923076923E8
412
+
413
+ // Get values from progress metrics JSON and cast back to Double
414
+ // for numeric comparison
415
+ val inputRowsPerSecondJSON = (progress \ " inputRowsPerSecond" ).values.toString
416
+ .toDouble
417
+ val processedRowsPerSecondJSON = (progress \ " processedRowsPerSecond" ).values.toString
418
+ .toDouble
419
+
420
+ // Get expected values after type casting
421
+ val inputRowsPerSecondExpected = BigDecimal (inputRowsPerSecond)
422
+ .setScale(1 , RoundingMode .HALF_UP ).toDouble
423
+ val processedRowsPerSecondExpected = BigDecimal (processedRowsPerSecond)
424
+ .setScale(1 , RoundingMode .HALF_UP ).toDouble
425
+
426
+ // This should fail if inputRowsPerSecond contains E notation
427
+ (progress \ " inputRowsPerSecond" ).values.toString should not include " E"
428
+
429
+ // This should fail if processedRowsPerSecond contains E notation
430
+ (progress \ " processedRowsPerSecond" ).values.toString should not include " E"
431
+
432
+ // Value in progress metrics should be equal to the Decimal conversion of the same
433
+ // Using epsilon to compare floating-point values
434
+ val epsilon = 1e-6
435
+ inputRowsPerSecondJSON shouldBe inputRowsPerSecondExpected +- epsilon
436
+ processedRowsPerSecondJSON shouldBe processedRowsPerSecondExpected +- epsilon
437
+ }
438
+
403
439
def waitUntilBatchProcessed : AssertOnQuery = Execute { q =>
404
440
eventually(Timeout (streamingTimeout)) {
405
441
if (q.exception.isEmpty) {
@@ -522,6 +558,44 @@ object StreamingQueryStatusAndProgressSuite {
522
558
observedMetrics = null
523
559
)
524
560
561
+ val testProgress4 = new StreamingQueryProgress (
562
+ id = UUID .randomUUID,
563
+ runId = UUID .randomUUID,
564
+ name = " myName" ,
565
+ timestamp = " 2025-09-05T20:54:20.827Z" ,
566
+ batchId = 2L ,
567
+ batchDuration = 0L ,
568
+ durationMs = new java.util.HashMap (Map (" total" -> 0L ).transform((_, v) => long2Long(v)).asJava),
569
+ eventTime = new java.util.HashMap (Map (
570
+ " max" -> " 2025-09-05T20:54:20.827Z" ,
571
+ " min" -> " 2025-09-05T20:54:20.827Z" ,
572
+ " avg" -> " 2025-09-05T20:54:20.827Z" ,
573
+ " watermark" -> " 2025-09-05T20:54:20.827Z" ).asJava),
574
+ stateOperators = Array (new StateOperatorProgress (operatorName = " op1" ,
575
+ numRowsTotal = 0 , numRowsUpdated = 1 , allUpdatesTimeMs = 1 , numRowsRemoved = 2 ,
576
+ allRemovalsTimeMs = 34 , commitTimeMs = 23 , memoryUsedBytes = 3 , numRowsDroppedByWatermark = 0 ,
577
+ numShufflePartitions = 2 , numStateStoreInstances = 2 ,
578
+ customMetrics = new java.util.HashMap (Map (" stateOnCurrentVersionSizeBytes" -> 2L ,
579
+ " loadedMapCacheHitCount" -> 1L , " loadedMapCacheMissCount" -> 0L )
580
+ .transform((_, v) => long2Long(v)).asJava)
581
+ )),
582
+ sources = Array (
583
+ new SourceProgress (
584
+ description = " source" ,
585
+ startOffset = " 123" ,
586
+ endOffset = " 456" ,
587
+ latestOffset = " 789" ,
588
+ numInputRows = 678 ,
589
+ inputRowsPerSecond = 6.923076923076923E8 , // Large double value having exponentials
590
+ processedRowsPerSecond = 2.923076923076923E8
591
+ )
592
+ ),
593
+ sink = SinkProgress (" sink" , None ),
594
+ observedMetrics = new java.util.HashMap (Map (
595
+ " event1" -> row(schema1, 1L , 3.0d ),
596
+ " event2" -> row(schema2, 1L , " hello" , " world" )).asJava)
597
+ )
598
+
525
599
val testStatus = new StreamingQueryStatus (" active" , true , false )
526
600
}
527
601
0 commit comments