You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Apache.Arrow.Adbc.Client is the ADO.NET wrapper over ADBC. It never calls the asynchronous methods that the layers above and below it already provide. An awaiting caller reaches the wire through blocking calls, two of them .Result.
Three defects, all in csharp/src/Client/, verified against main at b6fcd135.
1. AdbcDataReader.ReadNextRecordBatchAsync blocks on an already-async stream.
AdbcDataReader.cs:392 declares a private method returning ValueTask<RecordBatch?> whose name ends in Async. Line 396 is its body:
QueryResult.Stream is an IArrowArrayStream (Results.cs:51). Its ReadNextRecordBatchAsync(CancellationToken) is asynchronous by contract and takes a token. The reader blocks on it, and discards the token, once per record batch.
2. AdbcDataReader does not override ReadAsync.
AdbcDataReader.cs:325 overrides Read() only. DbDataReader.ReadAsync falls back to the base implementation, which runs the synchronous body and reaches the .Result at line 339.
3. AdbcCommand does not override ExecuteDbDataReaderAsync.
AdbcCommand.cs:207 overrides ExecuteDbDataReader only. ExecuteReaderAsync falls back to the synchronous body, which calls AdbcStatement.ExecuteQuery(). AdbcStatement.ExecuteQueryAsync() already exists at AdbcStatement.cs:86 and is virtual. Nothing in the Client calls it.
There is a fourth one at AdbcConnection.cs:554, in the schema-loading path. Same defect, different code path.
The deadlock
.Result blocks the calling thread until the task completes. The task completes on a continuation. Without ConfigureAwait(false) that continuation is posted back to the captured SynchronizationContext, which is the thread already blocked in .Result. Neither side can proceed.
The probe drives the real AdbcConnection, AdbcCommand, and AdbcDataReader from the shipped Apache.Arrow.Adbc.Client through their public API. Only the IArrowArrayStream is a fake, and its one distinguishing feature is an await without ConfigureAwait(false), matching FlightSqlResult.cs:54. Each scenario has a five-second timeout. Its source is in the reproduction section.
Scenario
Expected
Observed
A
reader.Read() on a UI-style SynchronizationContext
hang
HUNG
B
await reader.ReadAsync(ct) on the same
hang
HUNG
C
reader.Read() with no SynchronizationContext
complete
completed
D
as A, but the stream uses ConfigureAwait(false)
complete
completed
C is the control: the same code completes with no context, so the probe works. A and D differ only by ConfigureAwait(false), which pins the cause. The deadlock needs the context capture below the wrapper and the .Result inside it.
B is the one that matters. The caller wrote await. DbDataReader.ReadAsync has no override, so the base implementation runs Read() inline on the calling thread and deadlocks anyway. Writing asynchronous code does not avoid this.
Nothing on this path calls ConfigureAwait(false):
Directory
await
ConfigureAwait(false)
csharp/src/Client
0
0
csharp/src/Drivers/FlightSql
4
0
csharp/src/Apache.Arrow.Adbc
6
0
Other parts of the tree use it heavily. 16 calls in BigQueryStatement.cs, plus the Databricks CloudFetch, Thrift and Telemetry code, 70 in total. That reads as an oversight on this path rather than a policy.
Apache.Arrow.Adbc.Client targets netstandard2.0, so it ships to .NET Framework hosts. WinForms, WPF, and classic ASP.NET each install a single-threaded SynchronizationContext. Any of them reaches scenario A or B.
On ASP.NET Core and console hosts no SynchronizationContext exists, so the call returns, as scenario C shows. It still parks a thread-pool thread for every record batch, which starves the pool under concurrency.
Bug or enhancement
Item 1 stands on its own terms. A private method named ReadNextRecordBatchAsync, returning ValueTask<RecordBatch?>, taking a CancellationToken, whose body calls .Result on an asynchronous method, is wrong whatever the caller wants. It blocks a thread and drops the token.
On their own, items 2 and 3 are enhancements. DbDataReader and DbCommand ship working base implementations, and a provider that declines to override them breaks no contract.
Together they produce a defect. Items 2 and 3 route an awaiting caller into item 1, and the result is the deterministic hang shown above. A fallback path that hangs is a bug.
Drivers that are already asynchronous
Two in-tree drivers implement ExecuteQueryAsync for real: Flight SQL at FlightSqlStatement.cs:37, and HiveServer2/Databricks at HiveServer2Statement.cs:166.
Flight SQL then inverts itself to satisfy the synchronous abstract member. FlightSqlStatement.cs:47-50 is ExecuteQuery() => ExecuteQueryAsync().Result. Composed through the wrapper, an awaiting caller runs: BCL synchronous fallback, ExecuteQuery(), .Result, then the genuinely asynchronous method. A thread blocks for a gRPC round trip that was already asynchronous.
Item 1 affects every driver, because IArrowArrayStream is an Apache Arrow contract that every driver's stream satisfies asynchronously.
This is not #1843. That issue asked for a broader asynchronous surface; #1865 answered it with the AdbcStatement11 family, which is asynchronous-primitive and carries a CancellationToken throughout. That design is sound and this report does not question it.
This defect sits below it. AdbcStatement.ExecuteQueryAsync() and AdbcStatement11.ExecuteQueryAsync(CancellationToken) both return the same QueryResult, holding the same IArrowArrayStream. The .Result at AdbcDataReader.cs:396 is downstream of that join, so moving a driver to the 1.1 family does not remove it. Meanwhile every driver shipping today reaches users through AdbcStatement and this wrapper.
Proposed fix
Four additive changes in csharp/src/Client/, three files. No public contract change and no driver change.
Make ReadNextRecordBatchAsync genuinely async and await the stream. The method is private, so this changes no contract. Read() keeps its .Result on it, which is the cost of the synchronous path.
Add an AdbcCommand.ExecuteDbDataReaderAsync override that awaits AdbcStatement.ExecuteQueryAsync(), sharing the body of ExecuteReader(CommandBehavior).
Where the synchronous APIs must still block, use AsTask() rather than .Result. A driver's stream is genuinely asynchronous, so reading .Result on the ValueTask it returns is unsupported. This covers Read() and the AdbcConnection schema loop.
Drivers that override only ExecuteQuery keep the existing base behaviour, Task.Run(() => ExecuteQuery()), and are unaffected.
Adding ConfigureAwait(false) to the driver and core await sites would also help, and is a separate change.
Not fixed here
AdbcStatement.ExecuteQueryAsync() takes no CancellationToken, so change 3 can only observe the token at the command boundary. The initial query call stays uncancellable, exactly as today. Per-batch fetches become cancellable, and that is where the repeated round trips happen.
Adding a token overload to AdbcStatement would be new public API on the core package and would re-open the design settled in #1865. This report does not propose it.
Stack Trace
No exception is thrown. This is the managed stack of the deadlocked thread, captured from a live process with dotnet-stack report -p <pid> while the probe in the next field was hung. The thread shown is the one pumping the message loop, the same role a WPF Dispatcher or a WinForms message loop plays.
Thread (0x145D401B):
[Native Frames]
System.Private.CoreLib!System.Threading.WaitSubsystem+ThreadWaitInfo.Wait(int32,bool,bool,value class LockHolder&)
System.Private.CoreLib!System.Threading.WaitSubsystem.Wait(class IWaitableObject,class ThreadWaitInfo,int32,bool)
System.Private.CoreLib!System.Threading.WaitHandle.WaitOneNoCheck(int32,bool,class System.Object,value class WaitHandleWaitSourceMap)
System.Private.CoreLib!System.Threading.Condition.Wait(int32,class System.Object)
System.Private.CoreLib!System.Threading.ManualResetEventSlim.Wait(int32,value class System.Threading.CancellationToken)
System.Private.CoreLib!System.Threading.Tasks.Task.SpinThenBlockingWait(int32,value class System.Threading.CancellationToken)
System.Private.CoreLib!System.Threading.Tasks.Task.InternalWaitCore(int32,value class System.Threading.CancellationToken)
System.Private.CoreLib!System.Threading.Tasks.Task.InternalWait(int32,value class System.Threading.CancellationToken)
System.Private.CoreLib!System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(class System.Threading.Tasks.Task,value class System.Threading.Tasks.ConfigureAwaitOptions)
System.Private.CoreLib!System.Threading.Tasks.ValueTask`1[System.__Canon].get_Result()
Apache.Arrow.Adbc.Client!Apache.Arrow.Adbc.Client.AdbcDataReader.ReadNextRecordBatchAsync(value class System.Threading.CancellationToken)
Apache.Arrow.Adbc.Client!Apache.Arrow.Adbc.Client.AdbcDataReader.Read()
hang!PumpSyncContext.Pump()
System.Private.CoreLib!System.Threading.Thread.StartCallback(class System.Threading.Thread*)
Read it bottom-up. The pumping thread is inside AdbcDataReader.Read(), inside AdbcDataReader.ReadNextRecordBatchAsync, inside ValueTask<T>.get_Result(), blocked in Task.InternalWait. Two frames of Apache.Arrow.Adbc.Client sit directly above get_Result.
The cycle it is stuck in:
┌───────────────────────┐ ┌────────────────────────┐
│ │ │ continuation of the │
│ UI thread parked in │ waits for │ await at │
│ .Result at ├───────────────►│ FlightSqlResult.cs:54, │
│ AdbcDataReader.cs:396 │ │ queued on the │
│ │ │ context │
└───────────────────────┘ └────────────┬───────────┘
▲can only run on │
│ │
╰─────────────────────────────────────────╯
What a user writes, in a WPF or WinForms event handler:
privateasyncvoidOnLoadClick(objectsender,RoutedEventArgse){usingvarcommand=connection.CreateCommand();command.CommandText="SELECT * FROM some_large_table";usingvarreader=awaitcommand.ExecuteReaderAsync(ct);while(awaitreader.ReadAsync(ct))// never returns{
...}}
The UI freezes. Nothing throws and no timeout fires, because the token never reaches an overridden ReadAsync.
Runnable probe
Save as probe.cs outside the repository, adjust the #:project path, then run with .NET SDK 10 or later:
dotnet run probe.cs
Observed on macOS, .NET SDK 10.0.301, main at b6fcd135:
[A] reader.Read() on a UI-style SynchronizationContext
expected: HUNG observed: HUNG OK
[B] await reader.ReadAsync(ct) on a UI-style SynchronizationContext
expected: HUNG observed: HUNG OK
[C] reader.Read() with no SynchronizationContext (console / ASP.NET Core)
expected: completed observed: completed OK
[D] reader.Read() on a UI-style SynchronizationContext, stream uses ConfigureAwait(false)
expected: completed observed: completed OK
RESULT: all scenarios matched expectations.
probe.cs
#:property PublishAot=false
#:property Nullable=disable
#:property TreatWarningsAsErrors=false
#:project ../arrow-adbc/csharp/src/Client/Apache.Arrow.Adbc.Client.csproj// Everything below the stream fake is the real shipped code, reached through its// fully public surface. No internals, no reflection, no patched build.
using System;using System.Collections.Concurrent;using System.Collections.Generic;using System.Threading;using System.Threading.Tasks;using Apache.Arrow;using Apache.Arrow.Ipc;using Apache.Arrow.Types;usingAdbc=Apache.Arrow.Adbc;usingClient=Apache.Arrow.Adbc.Client;constintTimeoutMs=5000;booluseConfigureAwait=false;StreamMode.UseConfigureAwait=()=>useConfigureAwait;intfailures=0;failures+=Report("A","reader.Read() on a UI-style SynchronizationContext",expectHang:true,RunWithSyncContext(reader =>reader.Read()));failures+=Report("B","await reader.ReadAsync(ct) on a UI-style SynchronizationContext",expectHang:true,RunWithSyncContext(reader =>reader.ReadAsync(CancellationToken.None).GetAwaiter().GetResult()));failures+=Report("C","reader.Read() with no SynchronizationContext (console / ASP.NET Core)",expectHang:false,RunWithoutSyncContext(reader =>reader.Read()));useConfigureAwait=true;failures+=Report("D","reader.Read() on a UI-style SynchronizationContext, stream uses ConfigureAwait(false)",expectHang:false,RunWithSyncContext(reader =>reader.Read()));useConfigureAwait=false;Console.WriteLine();Console.WriteLine(failures==0?"RESULT: all scenarios matched expectations.":$"RESULT: {failures} scenario(s) did NOT match expectations.");returnfailures==0?0:1;staticintReport(stringid,stringwhat,boolexpectHang,boolhung){stringgot=hung?"HUNG":"completed";stringwant=expectHang?"HUNG":"completed";Console.WriteLine($"[{id}] {what}");Console.WriteLine($" expected: {want,-9} observed: {got,-9}{(hung==expectHang?"OK":"MISMATCH")}");returnhung==expectHang?0:1;}// Runs body(reader) on a thread that installs a single-threaded SynchronizationContext// and pumps it, as a WPF Dispatcher or a WinForms message loop does.staticboolRunWithSyncContext(Func<Client.AdbcDataReader,bool>body){varfinished=newManualResetEventSlim(false);vart=newThread(()=>{varctx=newPumpSyncContext();SynchronizationContext.SetSynchronizationContext(ctx);ctx.Post(_ =>{try{body(NewReader());}catch{}finished.Set();ctx.Complete();},null);ctx.Pump();});t.IsBackground=true;t.Start();return!finished.Wait(TimeoutMs);}staticboolRunWithoutSyncContext(Func<Client.AdbcDataReader,bool>body){varfinished=newManualResetEventSlim(false);vart=newThread(()=>{SynchronizationContext.SetSynchronizationContext(null);try{body(NewReader());}catch{}finished.Set();});t.IsBackground=true;t.Start();return!finished.Wait(TimeoutMs);}staticClient.AdbcDataReaderNewReader(){varconnection=newClient.AdbcConnection(newProbeDriver(),newDictionary<string,string>{{"probe","1"}},newDictionary<string,string>());connection.Open();varcommand=connection.CreateCommand();command.CommandText="SELECT 1";returncommand.ExecuteReader();}sealedclassPumpSyncContext:SynchronizationContext{readonlyBlockingCollection<KeyValuePair<SendOrPostCallback,object>>queue=new();publicoverridevoidPost(SendOrPostCallbackd,objectstate){try{queue.Add(newKeyValuePair<SendOrPostCallback,object>(d,state));}catch(InvalidOperationException){}}publicoverridevoidSend(SendOrPostCallbackd,objectstate)=>d(state);publicvoidPump(){foreach(variteminqueue.GetConsumingEnumerable())item.Key(item.Value);}publicvoidComplete()=>queue.CompleteAdding();}// The only fake. Its one distinguishing feature is the await below:// no ConfigureAwait(false), matching Drivers/FlightSql/FlightSqlResult.cs:54.sealedclassAsyncCapturingStream:IArrowArrayStream{readonlySchemaschema;readonlyRecordBatch[]batches;intindex=-1;publicAsyncCapturingStream(){schema=newSchema(newList<Field>{newField("n",Int32Type.Default,true)},null);varbuilder=newInt32Array.Builder();builder.AppendRange(newList<int>{1,2,3});Int32Arrayarray=builder.Build();batches=new[]{newRecordBatch(schema,newList<IArrowArray>{array},array.Length)};}publicSchemaSchema=>schema;publicasyncValueTask<RecordBatch>ReadNextRecordBatchAsync(CancellationTokencancellationToken=default){if(StreamMode.UseConfigureAwait())awaitTask.Delay(25,cancellationToken).ConfigureAwait(false);elseawaitTask.Delay(25,cancellationToken);index++;returnindex<batches.Length?batches[index]:null;}publicvoidDispose(){}}staticclassStreamMode{publicstaticFunc<bool>UseConfigureAwait=()=>false;}sealedclassProbeStatement:Adbc.AdbcStatement{publicoverrideAdbc.QueryResultExecuteQuery()=>newAdbc.QueryResult(3,newAsyncCapturingStream());publicoverrideAdbc.UpdateResultExecuteUpdate()=>thrownewNotImplementedException();}sealedclassProbeConnection:Adbc.AdbcConnection{publicoverrideAdbc.AdbcStatementCreateStatement()=>newProbeStatement();publicoverrideIArrowArrayStreamGetObjects(GetObjectsDepthdepth,stringcatalogPattern,stringdbSchemaPattern,stringtableNamePattern,IReadOnlyList<string>tableTypes,stringcolumnNamePattern)=>thrownewNotImplementedException();publicoverrideSchemaGetTableSchema(stringcatalog,stringdbSchema,stringtableName)=>thrownewNotImplementedException();publicoverrideIArrowArrayStreamGetTableTypes()=>thrownewNotImplementedException();}sealedclassProbeDatabase:Adbc.AdbcDatabase{publicoverrideAdbc.AdbcConnectionConnect(IReadOnlyDictionary<string,string>options)=>newProbeConnection();}sealedclassProbeDriver:Adbc.AdbcDriver{publicoverrideAdbc.AdbcDatabaseOpen(IReadOnlyDictionary<string,string>parameters)=>newProbeDatabase();}
Environment/Setup
Repository: apache/arrow-adbc, main at b6fcd135
Package: Apache.Arrow.Adbc.Client, version prefix 0.25.0-SNAPSHOT (csharp/Directory.Build.props:32)
Target frameworks: netstandard2.0;net8.0
Driver used to trace the chain: Apache.Arrow.Adbc.Drivers.FlightSql
OS: macOS. The defect is a property of the source and is platform independent, but the deadlock requires a host that installs a SynchronizationContext.
What happened?
Apache.Arrow.Adbc.Clientis the ADO.NET wrapper over ADBC. It never calls the asynchronous methods that the layers above and below it already provide. Anawaiting caller reaches the wire through blocking calls, two of them.Result.Three defects, all in
csharp/src/Client/, verified againstmainatb6fcd135.1.
AdbcDataReader.ReadNextRecordBatchAsyncblocks on an already-async stream.AdbcDataReader.cs:392declares a private method returningValueTask<RecordBatch?>whose name ends inAsync. Line 396 is its body:QueryResult.Streamis anIArrowArrayStream(Results.cs:51). ItsReadNextRecordBatchAsync(CancellationToken)is asynchronous by contract and takes a token. The reader blocks on it, and discards the token, once per record batch.2.
AdbcDataReaderdoes not overrideReadAsync.AdbcDataReader.cs:325overridesRead()only.DbDataReader.ReadAsyncfalls back to the base implementation, which runs the synchronous body and reaches the.Resultat line 339.3.
AdbcCommanddoes not overrideExecuteDbDataReaderAsync.AdbcCommand.cs:207overridesExecuteDbDataReaderonly.ExecuteReaderAsyncfalls back to the synchronous body, which callsAdbcStatement.ExecuteQuery().AdbcStatement.ExecuteQueryAsync()already exists atAdbcStatement.cs:86and isvirtual. Nothing in the Client calls it.There is a fourth one at
AdbcConnection.cs:554, in the schema-loading path. Same defect, different code path.The deadlock
.Resultblocks the calling thread until the task completes. The task completes on a continuation. WithoutConfigureAwait(false)that continuation is posted back to the capturedSynchronizationContext, which is the thread already blocked in.Result. Neither side can proceed.The probe drives the real
AdbcConnection,AdbcCommand, andAdbcDataReaderfrom the shippedApache.Arrow.Adbc.Clientthrough their public API. Only theIArrowArrayStreamis a fake, and its one distinguishing feature is anawaitwithoutConfigureAwait(false), matchingFlightSqlResult.cs:54. Each scenario has a five-second timeout. Its source is in the reproduction section.reader.Read()on a UI-styleSynchronizationContextawait reader.ReadAsync(ct)on the samereader.Read()with noSynchronizationContextConfigureAwait(false)C is the control: the same code completes with no context, so the probe works. A and D differ only by
ConfigureAwait(false), which pins the cause. The deadlock needs the context capture below the wrapper and the.Resultinside it.B is the one that matters. The caller wrote
await.DbDataReader.ReadAsynchas no override, so the base implementation runsRead()inline on the calling thread and deadlocks anyway. Writing asynchronous code does not avoid this.Nothing on this path calls
ConfigureAwait(false):awaitConfigureAwait(false)csharp/src/Clientcsharp/src/Drivers/FlightSqlcsharp/src/Apache.Arrow.AdbcOther parts of the tree use it heavily. 16 calls in
BigQueryStatement.cs, plus the Databricks CloudFetch, Thrift and Telemetry code, 70 in total. That reads as an oversight on this path rather than a policy.Apache.Arrow.Adbc.Clienttargetsnetstandard2.0, so it ships to .NET Framework hosts. WinForms, WPF, and classic ASP.NET each install a single-threadedSynchronizationContext. Any of them reaches scenario A or B.On ASP.NET Core and console hosts no
SynchronizationContextexists, so the call returns, as scenario C shows. It still parks a thread-pool thread for every record batch, which starves the pool under concurrency.Bug or enhancement
Item 1 stands on its own terms. A private method named
ReadNextRecordBatchAsync, returningValueTask<RecordBatch?>, taking aCancellationToken, whose body calls.Resulton an asynchronous method, is wrong whatever the caller wants. It blocks a thread and drops the token.On their own, items 2 and 3 are enhancements.
DbDataReaderandDbCommandship working base implementations, and a provider that declines to override them breaks no contract.Together they produce a defect. Items 2 and 3 route an awaiting caller into item 1, and the result is the deterministic hang shown above. A fallback path that hangs is a bug.
Drivers that are already asynchronous
Two in-tree drivers implement
ExecuteQueryAsyncfor real: Flight SQL atFlightSqlStatement.cs:37, and HiveServer2/Databricks atHiveServer2Statement.cs:166.Flight SQL then inverts itself to satisfy the synchronous abstract member.
FlightSqlStatement.cs:47-50isExecuteQuery() => ExecuteQueryAsync().Result. Composed through the wrapper, anawaiting caller runs: BCL synchronous fallback,ExecuteQuery(),.Result, then the genuinely asynchronous method. A thread blocks for a gRPC round trip that was already asynchronous.Item 1 affects every driver, because
IArrowArrayStreamis an Apache Arrow contract that every driver's stream satisfies asynchronously.Relationship to #1843 and #1865
This is not #1843. That issue asked for a broader asynchronous surface; #1865 answered it with the
AdbcStatement11family, which is asynchronous-primitive and carries aCancellationTokenthroughout. That design is sound and this report does not question it.This defect sits below it.
AdbcStatement.ExecuteQueryAsync()andAdbcStatement11.ExecuteQueryAsync(CancellationToken)both return the sameQueryResult, holding the sameIArrowArrayStream. The.ResultatAdbcDataReader.cs:396is downstream of that join, so moving a driver to the 1.1 family does not remove it. Meanwhile every driver shipping today reaches users throughAdbcStatementand this wrapper.Proposed fix
Four additive changes in
csharp/src/Client/, three files. No public contract change and no driver change.ReadNextRecordBatchAsyncgenuinelyasyncand await the stream. The method is private, so this changes no contract.Read()keeps its.Resulton it, which is the cost of the synchronous path.AdbcDataReader.ReadAsync(CancellationToken)override that awaits the batch fetch. Preserve the intra-batch fast path and the dispose-before-fetch ordering added by fix(csharp/src/Client): clear cached record batch in Read() to prevent stale data on exception #4133, so a caller retrying after a mid-stream error sees the exception again instead of stale rows.AdbcCommand.ExecuteDbDataReaderAsyncoverride that awaitsAdbcStatement.ExecuteQueryAsync(), sharing the body ofExecuteReader(CommandBehavior).AsTask()rather than.Result. A driver's stream is genuinely asynchronous, so reading.Resulton theValueTaskit returns is unsupported. This coversRead()and theAdbcConnectionschema loop.Drivers that override only
ExecuteQuerykeep the existing base behaviour,Task.Run(() => ExecuteQuery()), and are unaffected.Adding
ConfigureAwait(false)to the driver and coreawaitsites would also help, and is a separate change.Not fixed here
AdbcStatement.ExecuteQueryAsync()takes noCancellationToken, so change 3 can only observe the token at the command boundary. The initial query call stays uncancellable, exactly as today. Per-batch fetches become cancellable, and that is where the repeated round trips happen.Adding a token overload to
AdbcStatementwould be new public API on the core package and would re-open the design settled in #1865. This report does not propose it.Stack Trace
No exception is thrown. This is the managed stack of the deadlocked thread, captured from a live process with
dotnet-stack report -p <pid>while the probe in the next field was hung. The thread shown is the one pumping the message loop, the same role a WPF Dispatcher or a WinForms message loop plays.Read it bottom-up. The pumping thread is inside
AdbcDataReader.Read(), insideAdbcDataReader.ReadNextRecordBatchAsync, insideValueTask<T>.get_Result(), blocked inTask.InternalWait. Two frames ofApache.Arrow.Adbc.Clientsit directly aboveget_Result.The cycle it is stuck in:
For reference, the same path traced by line:
How can we reproduce the bug?
What a user writes, in a WPF or WinForms event handler:
The UI freezes. Nothing throws and no timeout fires, because the token never reaches an overridden
ReadAsync.Runnable probe
Save as
probe.csoutside the repository, adjust the#:projectpath, then run with .NET SDK 10 or later:Observed on macOS, .NET SDK 10.0.301,
mainatb6fcd135:probe.cs
Environment/Setup
apache/arrow-adbc,mainatb6fcd135Apache.Arrow.Adbc.Client, version prefix0.25.0-SNAPSHOT(csharp/Directory.Build.props:32)netstandard2.0;net8.0Apache.Arrow.Adbc.Drivers.FlightSqlSynchronizationContext.