Skip to content

Commit 0550f8b

Browse files
authored
[DTS-6979] Add more unit tests for async query (#922)
[DTS-6979] Add more unit tests for async query
1 parent 4e15100 commit 0550f8b

2 files changed

Lines changed: 141 additions & 8 deletions

File tree

server/src/main/scala/io/delta/sharing/server/DeltaSharingService.scala

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,10 @@ class DeltaSharingService(serverConfig: ServerConfig) {
192192

193193
private val rand = new scala.util.Random()
194194

195+
// Map to track poll count per queryId for testing async query behavior
196+
private val queryPollCounters =
197+
new java.util.concurrent.ConcurrentHashMap[String, java.util.concurrent.atomic.AtomicInteger]()
198+
195199
private val sharedTableManager = new SharedTableManager(serverConfig)
196200

197201
private val deltaSharedTableLoader = new DeltaSharedTableLoader(serverConfig)
@@ -375,22 +379,42 @@ class DeltaSharingService(serverConfig: ServerConfig) {
375379
throw new DeltaSharingIllegalArgumentException("expected error")
376380
}
377381

378-
// simulate async query with 50% chance of return
379-
// asynchronously for a specific table
380-
// client should be able to handle both cases and for server
381-
// test please use other table names.
382-
if(rand.nextInt(100) > 50 && table == "table2" && !request.pageToken.isDefined) {
382+
// Track poll count for this queryId and return results after 5 polls
383+
val pollCounter = queryPollCounters.computeIfAbsent(
384+
queryId, _ => new java.util.concurrent.atomic.AtomicInteger(0))
385+
val pollCount = pollCounter.incrementAndGet()
386+
387+
// Special handling for query ID change mid-query test
388+
val returnedQueryId = if (table.endsWith("_change_query_id_to_be_null")) {
389+
// Return null query ID to test client error handling
390+
null
391+
} else if (table.endsWith("_change_query_id")) {
392+
// Change query ID on the 2nd poll to simulate server-side query ID change
393+
s"${queryId}_modified"
394+
} else {
395+
queryId
396+
}
397+
398+
// Keep returning pending status until we've been polled more than 5 times
399+
if(pollCount <= 3 && !request.pageToken.isDefined) {
383400
streamingOutput(
384401
Some(0),
385402
"parquet",
386403
Seq(
387-
SingleAction(queryStatus = QueryStatus(queryId))
404+
SingleAction(queryStatus = QueryStatus(returnedQueryId))
388405
)
389406
)
390407
} else {
391408

409+
// Test case: Use a bad table name to trigger error during loadTable (on 2nd poll)
410+
val tableToLoad = if (table.endsWith("_bad_table") && pollCount == 2) {
411+
"nonexistent_bad_table"
412+
} else {
413+
table
414+
}
415+
392416
// we are reusing the table here to simulate a view query result
393-
val tableConfig = sharedTableManager.getTable(share, schema, table)
417+
val tableConfig = sharedTableManager.getTable(share, schema, tableToLoad)
394418
val capabilitiesMap = getDeltaSharingCapabilitiesMap(
395419
req.headers().get(DELTA_SHARING_CAPABILITIES_HEADER))
396420
val responseFormatSet = getResponseFormatSet(capabilitiesMap)
@@ -482,7 +506,10 @@ class DeltaSharingService(serverConfig: ServerConfig) {
482506

483507
val requestFileIdHash = getRequestFileIdHash(req)
484508
if(getAsyncQuery(capabilitiesMap)) {
485-
val queryId = s"${share}_${schema}_${table}"
509+
// Generate unique queryId and initialize poll counter
510+
val queryId = s"${share}_${schema}_${table}_${System.currentTimeMillis()}_" +
511+
s"${java.util.UUID.randomUUID().toString}"
512+
queryPollCounters.put(queryId, new java.util.concurrent.atomic.AtomicInteger(0))
486513

487514
streamingOutput(
488515
Some(0),

spark/src/test/scala/io/delta/sharing/spark/DeltaSharingSuite.scala

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,112 @@ class DeltaSharingSuite extends QueryTest with SharedSparkSession with DeltaShar
603603
.collect()
604604
assert(TestDeltaSharingClient.limits === Seq(1L))
605605
}
606+
607+
integrationTest("async query timeout") {
608+
// Test that async query times out when it exceeds the specified timeout
609+
withSQLConf(
610+
"spark.delta.sharing.network.useAsyncQuery" -> "true",
611+
"spark.delta.sharing.network.asyncQueryTimeout" -> "1", // 1ms timeout
612+
"spark.delta.sharing.network.asyncQueryPollIntervalMillis" -> "100"
613+
) {
614+
val tablePath = testProfileFile.getCanonicalPath + "#share_azure.default.table_wasb"
615+
616+
// Note: This test requires server-side support for async queries that take longer
617+
// than timeout In a real scenario, this would timeout if the server keeps returning
618+
// queryStatus
619+
val ex = intercept[IllegalStateException] {
620+
spark.read.format("deltaSharing").load(tablePath).collect()
621+
}
622+
assert(ex.getMessage.contains("Query is timed out after") ||
623+
ex.getMessage.contains("1000 ms"))
624+
}
625+
}
626+
627+
integrationTest("async query initial query table errors") {
628+
// Test error conditions in the initial query table request (DeltaSharingClient.scala:L650)
629+
withSQLConf("spark.delta.sharing.network.useAsyncQuery" -> "true") {
630+
val tablePath = testProfileFile.getCanonicalPath + "#share1.default.nonexistent_table"
631+
632+
// Test 1: Invalid table should throw UnexpectedHttpStatus
633+
val ex1 = intercept[io.delta.sharing.client.util.UnexpectedHttpStatus] {
634+
spark.read.format("deltaSharing").load(tablePath).collect()
635+
}
636+
assert(ex1.getMessage.contains("404") || ex1.getMessage.contains("Not Found"))
637+
}
638+
}
639+
640+
integrationTest("async query polling errors") {
641+
// Test error conditions during polling (DeltaSharingClient.scala:L1099)
642+
withSQLConf(
643+
"spark.delta.sharing.network.useAsyncQuery" -> "true",
644+
"spark.delta.sharing.network.asyncQueryPollIntervalMillis" -> "100"
645+
) {
646+
// Test 1: Inconsistent queryId in polling response
647+
// Server will change the queryId mid-query for tables ending with "_change_query_id"
648+
val tablePath1 = testProfileFile.getCanonicalPath +
649+
"#share_azure.default.table_wasb_change_query_id"
650+
val ex1 = intercept[IllegalStateException] {
651+
spark.read.format("deltaSharing").load(tablePath1).collect()
652+
}
653+
assert(ex1.getMessage.contains("QueryId is not consistent") ||
654+
ex1.getMessage.contains("query"))
655+
656+
// Test 2: Missing queryId when query is still pending
657+
// Server will return null queryId for tables ending with "_change_query_id_to_be_null"
658+
val tablePath2 = testProfileFile.getCanonicalPath +
659+
"#share_azure.default.table_wasb_change_query_id_to_be_null"
660+
val ex2 = intercept[IllegalStateException] {
661+
spark.read.format("deltaSharing").load(tablePath2).collect()
662+
}
663+
assert(ex2.getMessage.contains("QueryId is not returned") ||
664+
ex2.getMessage.contains("null"))
665+
}
666+
}
667+
668+
integrationTest("async query handles load table failures") {
669+
withSQLConf(
670+
"spark.delta.sharing.network.useAsyncQuery" -> "true",
671+
"spark.delta.sharing.network.asyncQueryPollIntervalMillis" -> "100"
672+
) {
673+
// Test: Server encounters error loading table after polling
674+
// Server will try to load a non-existent table on the 2nd poll for tables ending with
675+
// "_bad_table", which will cause loadTable to fail
676+
val tablePath3 = testProfileFile.getCanonicalPath +
677+
"#share_azure.default.table_wasb_bad_table"
678+
val ex3 = intercept[io.delta.sharing.client.util.UnexpectedHttpStatus] {
679+
spark.read.format("deltaSharing").load(tablePath3).collect()
680+
}
681+
assert(ex3.getMessage.contains("404") || ex3.getMessage.contains("400") ||
682+
ex3.getMessage.contains("does not exist"))
683+
}
684+
}
685+
686+
integrationTest("async query error handling in initial response") {
687+
// Test various error conditions that can occur in getNDJsonWithAsync
688+
withSQLConf("spark.delta.sharing.network.useAsyncQuery" -> "true") {
689+
// Test 1: Invalid version parameter
690+
val tablePath = testProfileFile.getCanonicalPath + "#share8.default.cdf_table_cdf_enabled"
691+
val ex1 = intercept[io.delta.sharing.client.util.UnexpectedHttpStatus] {
692+
spark.read
693+
.format("deltaSharing")
694+
.option("versionAsOf", "999999") // Non-existent version
695+
.load(tablePath)
696+
.collect()
697+
}
698+
assert(ex1.getMessage.contains("400") || ex1.getMessage.contains("404"))
699+
700+
// Test 2: Invalid timestamp parameter
701+
val ex2 = intercept[io.delta.sharing.client.util.UnexpectedHttpStatus] {
702+
spark.read
703+
.format("deltaSharing")
704+
.option("timestampAsOf", "1970-01-01 00:00:00")
705+
.load(tablePath)
706+
.collect()
707+
}
708+
assert(ex2.getMessage.contains("400") ||
709+
ex2.getMessage.contains("The provided timestamp"))
710+
}
711+
}
606712
}
607713

608714
class DeltaSharingWithParquetIOCacheEnabledSuite extends DeltaSharingSuite {

0 commit comments

Comments
 (0)