-
Notifications
You must be signed in to change notification settings - Fork 13
fix(csharp): tear down in-flight CloudFetch on connection dispose and statement cancel/dispose #659
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
Open
eric-wang-1990
wants to merge
27
commits into
main
Choose a base branch
from
eric-wang/csharp-fix-cloudfetch-dispose-hang
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 16 commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
0829deb
fix(csharp): cancel in-flight CloudFetch pipeline on connection dispose
eric-wang-1990 472d2af
fix(csharp): address issue #659 (1 review thread)
peco-engineer-bot[bot] f20d690
feat(csharp): cancel CloudFetch on statement cancel/dispose (full cas…
eric-wang-1990 b2f10cd
fix(csharp): address issue #659 (1 review thread)
peco-engineer-bot[bot] 2a64b0f
fix(csharp): address issue #659 (2 review threads)
peco-engineer-bot[bot] 833f099
fix(csharp): address issue #659 (2 review threads)
peco-engineer-bot[bot] aca8f25
refactor(csharp): drop dead fallback in Thrift CloudFetch token wiring
eric-wang-1990 d6314d2
fix(csharp): address issue #659 (1 review thread)
peco-engineer-bot[bot] 9691fe6
fix(csharp): address issue #659 (1 review thread)
peco-engineer-bot[bot] 612d18a
fix(csharp): address issue #659 (1 review thread)
peco-engineer-bot[bot] 840d061
feat(csharp): extend CloudFetch cancel cascade to the SEA path
eric-wang-1990 4bf9cd7
fix(csharp): address issue #659 (1 review thread)
peco-engineer-bot[bot] 9ffde05
fix(csharp): address issue #659 (1 review thread)
peco-engineer-bot[bot] 3290755
fix(csharp): address issue #659 (1 review thread)
peco-engineer-bot[bot] e341068
fix(csharp): address issue #659 (1 review thread)
peco-engineer-bot[bot] 41e9465
fix(csharp): address issue #659 (1 review thread)
peco-engineer-bot[bot] 66fe26a
fix(csharp): address issue #659 (1 review thread)
peco-engineer-bot[bot] 4af9936
fix(csharp): address issue #659 (1 review thread)
peco-engineer-bot[bot] c6033c4
fix(csharp): address issue #659 (1 review thread)
peco-engineer-bot[bot] 6435a44
fix(csharp): address issue #659 (1 review thread)
peco-engineer-bot[bot] 587b090
feat(csharp): emit local traces for SEA statement execute + poll paths
eric-wang-1990 1af44a9
fix(csharp): simplify CloudFetch cancel — signal result in downloader…
eric-wang-1990 9a7b7c1
fix(csharp): address issue #659 (1 review thread)
peco-engineer-bot[bot] 3df7613
fix(csharp): drop redundant top-of-loop cancellation check in CloudFe…
eric-wang-1990 124121a
refactor(csharp): pass CloudFetch statement token to reader factory e…
eric-wang-1990 26a0ddc
Revert "feat(csharp): emit local traces for SEA statement execute + p…
eric-wang-1990 b3bfd8b
fix(csharp): address issue #659 (1 review thread)
peco-engineer-bot[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -105,9 +105,56 @@ internal class DatabricksStatement : SparkStatement, IHiveServer2Statement | |
|
|
||
| public override long BatchSize { get; protected set; } = DatabricksBatchSizeDefault; | ||
|
|
||
| // Statement-lifetime cancellation for the CloudFetch pipeline, linked to the connection's | ||
| // shutdown token. This gives the full connection ⊃ statement ⊃ cloudfetch cancel cascade: | ||
| // connection dispose cancels every statement's downloads (via the link), and Cancel()/Dispose() | ||
| // on a single statement stops just its downloads. It is distinct from the base | ||
| // HiveServer2Statement._executeTokenSource, which is disposed when ExecuteQuery() returns and so | ||
| // cannot cover the later CloudFetch result-fetch phase. | ||
| // Not readonly: refreshed at the start of each execution by RefreshCloudFetchStatementCts() | ||
| // so a Cancel() on a prior execution doesn't poison the next CloudFetch read (see that method). | ||
| // Guarded by _cloudFetchStatementCtsLock: Cancel() is explicitly supported from another thread | ||
| // and can race the field swap performed by a concurrent re-execute (RefreshCloudFetchStatementCts). | ||
| private CancellationTokenSource _cloudFetchStatementCts; | ||
|
|
||
| // Serializes the field swap in RefreshCloudFetchStatementCts() against the reads + | ||
| // Cancel()/Dispose() of _cloudFetchStatementCts. Without it, a cross-thread Cancel() issued | ||
| // around a re-execute boundary can act on the source the swap is replacing (cancelling the | ||
| // wrong pipeline) or on the source the swap just disposed (silently swallowed) — either of | ||
| // which drops the cancel this PR exists to deliver. | ||
| private readonly object _cloudFetchStatementCtsLock = new object(); | ||
|
|
||
| /// <summary> | ||
| /// Token cancelled when this statement is cancelled or disposed — and, via linkage to the | ||
| /// connection's shutdown token, when the connection is disposed. The CloudFetch download | ||
| /// manager links this into its pipeline source so any of those tears down in-flight downloads. | ||
| /// </summary> | ||
| internal CancellationToken CloudFetchStatementToken | ||
| { | ||
| get | ||
| { | ||
| lock (_cloudFetchStatementCtsLock) | ||
| { | ||
| // Defensive against a read after Dispose(bool) has disposed the source: return | ||
| // CancellationToken.None rather than throwing, matching the sibling | ||
| // CloudFetchDownloadManager.PipelineToken so the two tokens behave symmetrically. | ||
| try | ||
| { | ||
| return _cloudFetchStatementCts.Token; | ||
| } | ||
| catch (ObjectDisposedException) | ||
| { | ||
| return CancellationToken.None; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public DatabricksStatement(DatabricksConnection connection) | ||
| : base(connection) | ||
| { | ||
| _cloudFetchStatementCts = CancellationTokenSource.CreateLinkedTokenSource(connection.CloudFetchShutdownToken); | ||
|
|
||
| // set the catalog name for legacy compatibility | ||
| // TODO: use catalog and schema fields in hiveserver2 connection instead of DefaultNamespace so we don't need to cast | ||
| var defaultNamespace = ((DatabricksConnection)Connection).DefaultNamespace; | ||
|
|
@@ -137,6 +184,44 @@ public DatabricksStatement(DatabricksConnection connection) | |
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Recreates the statement-lifetime CloudFetch cancellation source (re-linked to the | ||
| /// connection's shutdown token) at the start of each execution. <see cref="AdbcStatement"/> | ||
| /// is reusable (settable <see cref="SqlQuery"/> + repeated Execute), and <see cref="Cancel"/>/ | ||
| /// <see cref="Dispose(bool)"/> cancel this source permanently; without a refresh a | ||
| /// cancel-then-reexecute would start the next CloudFetch read with an already-cancelled | ||
| /// token. This mirrors how the base <c>HiveServer2Statement._executeTokenSource</c> is | ||
| /// refreshed per-execute so a statement stays reusable after cancel. | ||
| /// </summary> | ||
| internal void RefreshCloudFetchStatementCts() | ||
|
Collaborator
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. This smells, why we need this? |
||
| { | ||
| // Swap the field under the lock so a concurrent cross-thread Cancel()/Dispose() either | ||
| // acts on the old source before the swap or on the new one after it — never on a torn read. | ||
| CancellationTokenSource previous; | ||
| lock (_cloudFetchStatementCtsLock) | ||
| { | ||
| previous = _cloudFetchStatementCts; | ||
| _cloudFetchStatementCts = CancellationTokenSource.CreateLinkedTokenSource( | ||
| ((DatabricksConnection)Connection).CloudFetchShutdownToken); | ||
| } | ||
| // Release the prior source's registration on the connection shutdown token. Under normal | ||
| // AdbcStatement usage the previous result set is fully consumed/disposed before the next | ||
| // Execute, so no pipeline is still linked to the old token here. We dispose (rather than | ||
| // leak) it so a reused statement (repeated Execute) doesn't accumulate one linked-CTS | ||
| // registration on the connection shutdown token per execution for the connection's lifetime. | ||
| // | ||
| // Constraint: the CloudFetch pipeline's cancellation source is CreateLinkedTokenSource of | ||
| // this statement token (CloudFetchDownloadManager), whose only link to the connection | ||
| // shutdown token runs THROUGH this source. If a caller re-executes while still holding an | ||
| // OPEN (undisposed) reader from a prior execution, disposing the old source detaches that | ||
| // reader's still-running pipeline from the connection-dispose cascade. That reader's own | ||
| // disposal (StopAsync/Dispose) still tears its pipeline down; the connection-shutdown safety | ||
|
peco-review-bot[bot] marked this conversation as resolved.
|
||
| // net only lapses for the narrow case of a reader that is never disposed AND whose | ||
| // connection is then disposed. Disposing a source does not cancel its already-created | ||
| // linked children. | ||
| previous?.Dispose(); | ||
| } | ||
|
|
||
| private StatementTelemetryContext? CreateTelemetryContext(Telemetry.Proto.Statement.Types.Type statementType) | ||
| { | ||
| var session = ((DatabricksConnection)Connection).TelemetrySession; | ||
|
|
@@ -270,6 +355,9 @@ private async Task EnsureCatalogScopedAsync() | |
|
|
||
| public override QueryResult ExecuteQuery() | ||
| { | ||
| // Refresh the CloudFetch cancellation source before base.ExecuteQuery() captures it into | ||
| // the reader, so a prior Cancel() doesn't leave the next read starting cancelled. | ||
| RefreshCloudFetchStatementCts(); | ||
| EnsureCatalogScopedAsync().GetAwaiter().GetResult(); | ||
| var ctx = IsMetadataCommand | ||
| ? CreateMetadataTelemetryContext() | ||
|
|
@@ -301,6 +389,9 @@ public override QueryResult ExecuteQuery() | |
|
|
||
| public override async ValueTask<QueryResult> ExecuteQueryAsync() | ||
| { | ||
| // Refresh the CloudFetch cancellation source before base.ExecuteQueryAsync() captures it | ||
| // into the reader, so a prior Cancel() doesn't leave the next read starting cancelled. | ||
| RefreshCloudFetchStatementCts(); | ||
| await EnsureCatalogScopedAsync().ConfigureAwait(false); | ||
| var ctx = IsMetadataCommand | ||
| ? CreateMetadataTelemetryContext() | ||
|
|
@@ -1428,6 +1519,42 @@ protected override void Dispose(bool disposing) | |
| { | ||
| if (disposing) | ||
| { | ||
| // Cancel this statement's CloudFetch pipeline before anything else so in-flight | ||
| // downloads stop promptly if the caller disposed the statement mid-stream. | ||
| // | ||
| // Cancel + dispose under the lock so this can't interleave with a concurrent | ||
| // re-execute's field swap (RefreshCloudFetchStatementCts) and act on a stale source. | ||
| lock (_cloudFetchStatementCtsLock) | ||
| { | ||
| // Best-effort: Cancel() runs cancellation callbacks synchronously and rethrows a | ||
| // faulting one wrapped in AggregateException (not ObjectDisposedException); letting | ||
| // that escape would skip the CTS Dispose and telemetry emission below. | ||
| try { _cloudFetchStatementCts.Cancel(); } | ||
| catch (ObjectDisposedException) | ||
| { | ||
| // Expected on a repeated Dispose(): the source was already disposed below on | ||
| // the first pass. Dispose(bool) has no idempotency guard, so this is a normal | ||
| // double-dispose, not an error — swallow silently (no error event), matching | ||
| // the CloudFetchStatementToken getter's ObjectDisposedException handling. | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| Activity.Current?.AddEvent(new ActivityEvent("cloudfetch.statement.cancel.error", | ||
| tags: new ActivityTagsCollection | ||
| { | ||
| { "error.type", ex.GetType().Name }, | ||
|
peco-review-bot[bot] marked this conversation as resolved.
|
||
| { "error.message", ex.Message } | ||
| })); | ||
| } | ||
|
|
||
| // Dispose the CloudFetch statement CTS before the telemetry emission below: | ||
| // it was already Cancel()ed just above and nothing in the telemetry blocks | ||
| // depends on it, so releasing it here guarantees the linked-token registration | ||
| // it holds on the connection's _cloudFetchShutdownCts is freed even if the | ||
| // telemetry calls below throw. Mirrors the ordering in DatabricksConnection.Dispose. | ||
| _cloudFetchStatementCts.Dispose(); | ||
| } | ||
|
|
||
| if (PendingTelemetryContext != null) | ||
| { | ||
| // Emit telemetry now that results have been consumed | ||
|
|
@@ -1489,6 +1616,32 @@ public override void Cancel() | |
| long startMs = _statementLifetimeStopwatch.ElapsedMilliseconds; | ||
| try | ||
| { | ||
| // Cancel the CloudFetch pipeline for this statement first. base.Cancel() only signals | ||
| // the per-execute token, which is already disposed once results are streaming, so | ||
| // without this a Cancel() during CloudFetch would leave the downloads running. Do it | ||
| // before base.Cancel() because base.Cancel() issues the remote CancelOperation RPC, | ||
| // which can throw on a network/transport failure and would otherwise skip this cleanup. | ||
| // | ||
| // Under the lock so a re-execute's field swap (RefreshCloudFetchStatementCts) can't | ||
| // race this read: we cancel whichever source is current, never a torn/half-swapped one. | ||
| // The lock scopes only the field access — base.Cancel()'s remote RPC runs outside it. | ||
| lock (_cloudFetchStatementCtsLock) | ||
| { | ||
| // Best-effort: don't let a faulting cancellation callback (surfaced as | ||
| // AggregateException, not ObjectDisposedException) skip base.Cancel() and the | ||
| // telemetry emission in the finally below. | ||
| try { _cloudFetchStatementCts.Cancel(); } | ||
| catch (Exception ex) | ||
| { | ||
| Activity.Current?.AddEvent(new ActivityEvent("cloudfetch.statement.cancel.error", | ||
| tags: new ActivityTagsCollection | ||
| { | ||
| { "error.type", ex.GetType().Name }, | ||
| { "error.message", ex.Message } | ||
| })); | ||
| } | ||
|
peco-review-bot[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| base.Cancel(); | ||
| } | ||
| catch (Exception ex) | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.