Fix Cosmos Spark change feed offset discovery timeout - #50029
Fix Cosmos Spark change feed offset discovery timeout#50029Arnab Nandy (arnabnandy7) wants to merge 1 commit into
Conversation
|
Thank you for your contribution Arnab Nandy (@arnabnandy7)! We will review the pull request and get back to you soon. |
|
Azure Pipelines: Successfully started running 2 pipeline(s). 32 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Pull request overview
This PR addresses a reliability issue in the Cosmos DB Spark Structured Streaming change feed connector where initialOffset() / latestOffset() metadata discovery could block indefinitely (e.g., during DNS resolution failures). It introduces a configurable deadline and propagates it through nested metadata calls and transient retry/backoff, ensuring stalled operations are cancelled and surfaced as actionable timeouts.
Changes:
- Added
spark.cosmos.changeFeed.maxRetryDurationInSeconds(default 300s) with validation and documentation updates. - Introduced
OperationDeadlineand threaded it through change-feed offset discovery, including reactiveblock(...)and retry backoff sleeps. - Added unit tests for config parsing and for cancelling a non-completing reactive operation on deadline expiry.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| sdk/cosmos/azure-cosmos-spark_3/src/main/scala/com/azure/cosmos/spark/OperationDeadline.scala | New deadline utility used to bound blocking waits and backoff with a monotonic deadline. |
| sdk/cosmos/azure-cosmos-spark_3/src/main/scala/com/azure/cosmos/spark/TransientErrorsRetryPolicy.scala | Added optional OperationDeadline support to stop retries/backoff once the deadline expires. |
| sdk/cosmos/azure-cosmos-spark_3/src/main/scala/com/azure/cosmos/spark/CosmosPartitionPlanner.scala | Propagated the shared deadline through nested offset/metadata discovery and blocking points. |
| sdk/cosmos/azure-cosmos-spark_3/src/main/scala/com/azure/cosmos/spark/CosmosConfig.scala | Added the new config key and parsed it into CosmosChangeFeedConfig. |
| sdk/cosmos/azure-cosmos-spark_3-5/src/main/scala/com/azure/cosmos/spark/ChangeFeedMicroBatchStream.scala | Applied the deadline to initialOffset() and latestOffset() discovery in Spark 3.5 streaming. |
| sdk/cosmos/azure-cosmos-spark_3/src/test/scala/com/azure/cosmos/spark/OperationDeadlineSpec.scala | New unit test validating cancellation and input validation for OperationDeadline. |
| sdk/cosmos/azure-cosmos-spark_3/src/test/scala/com/azure/cosmos/spark/CosmosConfigSpec.scala | Added unit coverage for default/parsed/invalid max retry duration config. |
| sdk/cosmos/azure-cosmos-spark_3/docs/configuration-reference.md | Documented the new configuration option and semantics. |
| sdk/cosmos/azure-cosmos-spark_3-5_2-12/CHANGELOG.md | Documented the bug fix in the Spark 3.5 (Scala 2.12) connector changelog. |
| sdk/cosmos/azure-cosmos-spark_3-5_2-13/CHANGELOG.md | Documented the bug fix in the Spark 3.5 (Scala 2.13) connector changelog. |
2ebc163 to
683b52a
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (1)
sdk/cosmos/azure-cosmos-spark_3/src/main/scala/com/azure/cosmos/spark/OperationDeadline.scala:34
OperationDeadline.blockdetects a Reactor timeout by matching the prefix of anIllegalStateExceptionmessage ("Timeout on blocking read..."). This is brittle across Reactor versions/localization and can cause timeouts to surface as a less-actionableIllegalStateExceptioninstead of the intendedTimeoutExceptionwith the operation name/deadline context. Prefer usingMono.timeout(remaining)and unwrap the thrown exception to reliably detect timeouts without string matching.
case timeoutOnBlockingRead: IllegalStateException
if timeoutOnBlockingRead.getMessage != null &&
timeoutOnBlockingRead.getMessage.startsWith("Timeout on blocking read") =>
throw timeoutException(timeoutOnBlockingRead)
}
683b52a to
b174a5e
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (1)
sdk/cosmos/azure-cosmos-spark_3/src/main/scala/com/azure/cosmos/spark/OperationDeadline.scala:33
OperationDeadline.blockdetects Reactor blocking timeouts by checkingIllegalStateExceptionmessage withstartsWith("Timeout on blocking read"). This is brittle (message formatting can vary) and is inconsistent with other usages in this repo that match viacontains. Switching tocontainsreduces the risk of missing a timeout and leaking the less actionableIllegalStateExceptionto callers.
case timeoutOnBlockingRead: IllegalStateException
if timeoutOnBlockingRead.getMessage != null &&
timeoutOnBlockingRead.getMessage.startsWith("Timeout on blocking read") =>
throw timeoutException(timeoutOnBlockingRead)
}
|
Annie Liang (@xinlian12) - can you take a look at this? |
b174a5e to
900400f
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (1)
sdk/cosmos/azure-cosmos-spark_3/src/main/scala/com/azure/cosmos/spark/OperationDeadline.scala:46
OperationDeadline.sleepunconditionally throwsTimeoutExceptionwhen the requested sleep duration is >= the remaining budget, even thoughThread.sleepcan return before the deadline actually elapses (e.g., due to timer granularity). This can cause premature timeouts during retry backoff.
Consider re-checking the deadline after sleeping and only throwing if it has actually expired.
if (requested.compareTo(remaining) >= 0) {
throw timeoutException()
}
900400f to
96fe88e
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (1)
sdk/cosmos/azure-cosmos-spark_3/docs/configuration-reference.md:125
- The configuration reference documents
spark.cosmos.changeFeed.maxRetryDurationInSecondsas applying to change-feed offset discovery in general, but the deadline is only wired up in the Spark 3.5 streaming implementation (azure-cosmos-spark_3-5); the Spark 3.3/3.4 connectors'ChangeFeedMicroBatchStreamstill callCosmosPartitionPlannerwithout anOperationDeadline. This doc entry should clarify the current scope to avoid users of other Spark 3.x artifacts assuming it will prevent hangs.
| `spark.cosmos.changeFeed.maxRetryDurationInSeconds` | `300` | Maximum duration in seconds allowed for change-feed offset metadata discovery (`initialOffset` and `latestOffset`). The deadline includes metadata requests, transient-error retries, and retry backoff. When the deadline expires, the streaming query fails so Spark or an external orchestrator can retry it. The value must be greater than zero. |
96fe88e to
dc07b6a
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (2)
sdk/cosmos/azure-cosmos-spark_3/src/main/scala/com/azure/cosmos/spark/TransientErrorsRetryPolicy.scala:68
Thread.sleepclears the thread’s interrupted status when it throwsInterruptedException. Since this method doesn’t restore the interrupt flag, callers higher up may not be able to detect the interrupt. CatchInterruptedException, re-interrupt the thread, and rethrow to preserve expected interruption semantics during retry backoff (including whenOperationDeadline.sleepis used).
operationDeadline match {
case Some(deadline) => deadline.sleep(retryIntervalInMs)
case None => Thread.sleep(retryIntervalInMs)
}
sdk/cosmos/azure-cosmos-spark_3/src/main/scala/com/azure/cosmos/spark/OperationDeadline.scala:46
Thread.sleepclears the interrupted status when throwingInterruptedException. Re-interrupting the thread before rethrowing avoids losing the interrupt signal (which can lead to harder-to-debug shutdown/cancellation behavior when Spark is stopping a query).
Thread.sleep(millis, additionalNanos)
if (requested.compareTo(remaining) >= 0) {
remainingDuration
}
dc07b6a to
9bb7fc9
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.
Suppressed comments (1)
sdk/cosmos/azure-cosmos-spark_3/src/main/scala/com/azure/cosmos/spark/OperationDeadline.scala:24
remainingDurationcan overflow and become negative whendeadlineInNanosisLong.MaxValue(used when the configured timeout can’t be represented in nanos) andSystem.nanoTime()is negative, causing an immediate timeout even though the deadline is intended to be effectively unbounded. Consider special-casingLong.MaxValueto avoid subtractingnanoTime()in that case.
def remainingDuration: Duration = {
val remainingNanos = deadlineInNanos - System.nanoTime()
if (remainingNanos <= 0) {
throw timeoutException()
}
Signed-off-by: Arnab Nandy <arnab_nandy7@yahoo.com>
9bb7fc9 to
11afef9
Compare
Description
Fixes Azure Cosmos DB Spark change feed streaming queries potentially blocking indefinitely during offset metadata discovery when an endpoint cannot be resolved or another metadata request does not complete.
This pull request:
spark.cosmos.changeFeed.maxRetryDurationInSecondsconfiguration option, with a default of 300 seconds.initialOffset()andlatestOffset()metadata discovery.Fixes #50021
All SDK Contribution checklist:
General Guidelines and Best Practices
Testing Guidelines