This is an automated email from the ASF dual-hosted git repository.
curth pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-adbc.git
The following commit(s) were added to refs/heads/main by this push:
new a66d1e176 fix(csharp/test/Drivers/Databricks): Run token exchange in a
background task (#3188)
a66d1e176 is described below
commit a66d1e176fb1ca116a13bffc1101a55593631b2c
Author: Alex Guo <[email protected]>
AuthorDate: Tue Jul 22 15:24:55 2025 -0700
fix(csharp/test/Drivers/Databricks): Run token exchange in a background
task (#3188)
## Proposed Changes
- Currently, before we send an HttpRequest, we await
RenewTokenIfNeededAsync which blocks the HttpRequest on the token
refresh
- Make this run in a non-blocking background task, so that a token
refresh is triggered but the request will continue using the existing
token
- When the background task completes, use the new token
- This is to improve latency since requests don't wait on the token
refresh
- The risk is that there is a potential to use expired tokens if the
background task doesn't complete in time, however this can be mitigated
by setting TokenRenewLimit property to refresh X minutes before the
token expires
## Testing
`dotnet test --filter "FullyQualifiedName~TokenExchangeTests"`
```
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v3.1.1+bf6400fd51 (64-bit
.NET 8.0.7)
[xUnit.net 00:00:00.04] Discovering:
Apache.Arrow.Adbc.Tests.Drivers.Databricks
[xUnit.net 00:00:00.12] Discovered:
Apache.Arrow.Adbc.Tests.Drivers.Databricks
[xUnit.net 00:00:00.13] Starting:
Apache.Arrow.Adbc.Tests.Drivers.Databricks
[xUnit.net 00:00:11.02]
Apache.Arrow.Adbc.Tests.Drivers.Databricks.Auth.TokenExchangeTests.TokenExchangeHandler_WithValidTokenNotNearExpiry_UsesOriginalToken
[SKIP]
[xUnit.net 00:00:11.02] Access token is too close to expiration for
this test
[xUnit.net 00:00:11.34] Finished:
Apache.Arrow.Adbc.Tests.Drivers.Databricks
Apache.Arrow.Adbc.Tests.Drivers.Databricks test net8.0 succeeded (11.8s)
Test summary: total: 3, failed: 0, succeeded: 2, skipped: 1, duration: 11.7s
Build succeeded in 13.6s
```
`dotnet test --filter
"FullyQualifiedName~TokenExchangeDelegatingHandlerTests"`
```
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v3.1.1+bf6400fd51 (64-bit
.NET 8.0.7)
[xUnit.net 00:00:00.06] Discovering:
Apache.Arrow.Adbc.Tests.Drivers.Databricks
[xUnit.net 00:00:00.14] Discovered:
Apache.Arrow.Adbc.Tests.Drivers.Databricks
[xUnit.net 00:00:00.16] Starting:
Apache.Arrow.Adbc.Tests.Drivers.Databricks
[xUnit.net 00:00:01.76] Finished:
Apache.Arrow.Adbc.Tests.Drivers.Databricks
Apache.Arrow.Adbc.Tests.Drivers.Databricks test net8.0 succeeded (2.4s)
Test summary: total: 14, failed: 0, succeeded: 14, skipped: 0, duration:
2.4s
Build succeeded in 3.8s
```
---
.../Auth/TokenExchangeDelegatingHandler.cs | 77 +++++++++++-----
.../Databricks/E2E/Auth/TokenExchangeTests.cs | 101 ++++++++++++++++++---
.../Auth/TokenExchangeDelegatingHandlerTests.cs | 73 +++++++++++++--
3 files changed, 207 insertions(+), 44 deletions(-)
diff --git
a/csharp/src/Drivers/Databricks/Auth/TokenExchangeDelegatingHandler.cs
b/csharp/src/Drivers/Databricks/Auth/TokenExchangeDelegatingHandler.cs
index f24a9a90e..2748dc951 100644
--- a/csharp/src/Drivers/Databricks/Auth/TokenExchangeDelegatingHandler.cs
+++ b/csharp/src/Drivers/Databricks/Auth/TokenExchangeDelegatingHandler.cs
@@ -25,17 +25,19 @@ namespace Apache.Arrow.Adbc.Drivers.Databricks.Auth
{
/// <summary>
/// HTTP message handler that automatically refreshes OAuth tokens before
they expire.
+ /// Uses a non-blocking approach to refresh tokens in the background.
/// </summary>
internal class TokenExchangeDelegatingHandler : DelegatingHandler
{
private readonly string _initialToken;
private readonly int _tokenRenewLimitMinutes;
- private readonly SemaphoreSlim _tokenLock = new SemaphoreSlim(1, 1);
+ private readonly object _tokenLock = new object();
private readonly ITokenExchangeClient _tokenExchangeClient;
private string _currentToken;
private DateTime _tokenExpiryTime;
private bool _tokenExchangeAttempted = false;
+ private Task? _pendingTokenTask = null;
/// <summary>
/// Initializes a new instance of the <see
cref="TokenExchangeDelegatingHandler"/> class.
@@ -69,41 +71,54 @@ namespace Apache.Arrow.Adbc.Drivers.Databricks.Auth
// Only renew if:
// 1. We haven't already attempted token exchange (a token can
only be renewed once)
// 2. The token will expire within the renewal limit
+ // 3. We don't already have a pending refresh task
return !_tokenExchangeAttempted &&
- DateTime.UtcNow.AddMinutes(_tokenRenewLimitMinutes) >=
_tokenExpiryTime;
+ DateTime.UtcNow.AddMinutes(_tokenRenewLimitMinutes) >=
_tokenExpiryTime &&
+ _pendingTokenTask == null;
}
/// <summary>
- /// Renews the token if needed.
+ /// Starts token renewal in the background if needed.
/// </summary>
/// <param name="cancellationToken">A cancellation token.</param>
- /// <returns>A task representing the asynchronous operation.</returns>
- private async Task RenewTokenIfNeededAsync(CancellationToken
cancellationToken)
+ private void StartTokenRenewalIfNeeded(CancellationToken
cancellationToken)
{
if (!NeedsTokenRenewal())
{
return;
}
- // Acquire the lock to ensure only one thread attempts renewal
- await _tokenLock.WaitAsync(cancellationToken);
-
- try
+ bool needsRenewal;
+ lock (_tokenLock)
{
// Double-check pattern in case another thread renewed while
we were waiting
- if (!NeedsTokenRenewal())
+ needsRenewal = NeedsTokenRenewal();
+ if (needsRenewal)
{
- return;
+ // Mark that we've attempted token exchange to prevent
multiple attempts
+ // Specifically, NeedsTokenRenewal checks this flag
+ _tokenExchangeAttempted = true;
}
+ }
+
+ if (!needsRenewal)
+ {
+ return;
+ }
+ // Start token refresh in the background
+ _pendingTokenTask = Task.Run(async () =>
+ {
try
{
- _tokenExchangeAttempted = true;
-
TokenExchangeResponse response = await
_tokenExchangeClient.ExchangeTokenAsync(_initialToken, cancellationToken);
- _currentToken = response.AccessToken;
- _tokenExpiryTime = response.ExpiryTime;
+ // Update the token atomically when ready
+ lock (_tokenLock)
+ {
+ _currentToken = response.AccessToken;
+ _tokenExpiryTime = response.ExpiryTime;
+ }
}
catch (Exception ex)
{
@@ -111,11 +126,7 @@ namespace Apache.Arrow.Adbc.Drivers.Databricks.Auth
// This is to avoid interrupting the operation if token
exchange fails
System.Diagnostics.Debug.WriteLine($"Token exchange
failed: {ex.Message}");
}
- }
- finally
- {
- _tokenLock.Release();
- }
+ }, cancellationToken);
}
/// <summary>
@@ -126,8 +137,16 @@ namespace Apache.Arrow.Adbc.Drivers.Databricks.Auth
/// <returns>The HTTP response message.</returns>
protected override async Task<HttpResponseMessage>
SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
- await RenewTokenIfNeededAsync(cancellationToken);
- request.Headers.Authorization = new
AuthenticationHeaderValue("Bearer", _currentToken);
+ StartTokenRenewalIfNeeded(cancellationToken);
+
+ // Use the current token (which might be the old one while refresh
is in progress)
+ string tokenToUse;
+ lock (_tokenLock)
+ {
+ tokenToUse = _currentToken;
+ }
+
+ request.Headers.Authorization = new
AuthenticationHeaderValue("Bearer", tokenToUse);
return await base.SendAsync(request, cancellationToken);
}
@@ -135,7 +154,19 @@ namespace Apache.Arrow.Adbc.Drivers.Databricks.Auth
{
if (disposing)
{
- _tokenLock.Dispose();
+ // Wait for any pending token task to complete to avoid
leaking tasks
+ if (_pendingTokenTask != null)
+ {
+ try
+ {
+ // Try to wait for the task to complete, but don't
block indefinitely
+ _pendingTokenTask.Wait(TimeSpan.FromSeconds(10));
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Exception during
token task cleanup: {ex.Message}");
+ }
+ }
}
base.Dispose(disposing);
diff --git a/csharp/test/Drivers/Databricks/E2E/Auth/TokenExchangeTests.cs
b/csharp/test/Drivers/Databricks/E2E/Auth/TokenExchangeTests.cs
index b2e988be1..c574abe0c 100644
--- a/csharp/test/Drivers/Databricks/E2E/Auth/TokenExchangeTests.cs
+++ b/csharp/test/Drivers/Databricks/E2E/Auth/TokenExchangeTests.cs
@@ -16,6 +16,7 @@
*/
using System;
+using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
@@ -25,6 +26,28 @@ using Xunit.Abstractions;
namespace Apache.Arrow.Adbc.Tests.Drivers.Databricks.Auth
{
+ public class TokenCapturingHandler : DelegatingHandler
+ {
+ public List<string> CapturedTokens { get; } = new List<string>();
+ public List<DateTime> RequestTimes { get; } = new List<DateTime>();
+
+ public TokenCapturingHandler(HttpMessageHandler innerHandler) :
base(innerHandler)
+ {
+ }
+
+ protected override async Task<HttpResponseMessage>
SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ // Capture the authorization token
+ if (request.Headers.Authorization != null)
+ {
+ CapturedTokens.Add(request.Headers.Authorization.Parameter ??
string.Empty);
+ RequestTimes.Add(DateTime.UtcNow);
+ }
+
+ return await base.SendAsync(request, cancellationToken);
+ }
+ }
+
public class TokenExchangeTests : TestBase<DatabricksTestConfiguration,
DatabricksTestEnvironment>, IDisposable
{
private readonly HttpClient _httpClient;
@@ -80,7 +103,7 @@ namespace Apache.Arrow.Adbc.Tests.Drivers.Databricks.Auth
}
[SkippableFact]
- public async Task TokenExchangeHandler_WithValidToken_RefreshesToken()
+ public async Task
TokenExchangeHandler_WithValidToken_RefreshesTokenInBackgroundAcrossRequests()
{
Skip.IfNot(!string.IsNullOrEmpty(TestConfiguration.AccessToken),
"OAuth access token not configured");
@@ -93,8 +116,11 @@ namespace Apache.Arrow.Adbc.Tests.Drivers.Databricks.Auth
string host = GetHost();
var tokenExchangeClient = new TokenExchangeClient(_httpClient,
host);
+ // Create a token capturing handler to intercept the actual tokens
being sent
+ var tokenCapturingHandler = new TokenCapturingHandler(new
HttpClientHandler());
+
var handler = new TokenExchangeDelegatingHandler(
- new HttpClientHandler(),
+ tokenCapturingHandler,
tokenExchangeClient,
TestConfiguration.AccessToken,
nearFutureExpiry,
@@ -102,14 +128,40 @@ namespace Apache.Arrow.Adbc.Tests.Drivers.Databricks.Auth
var httpClient = new HttpClient(handler);
- var request = new HttpRequestMessage(HttpMethod.Get,
$"https://{host}/api/2.0/sql/config/warehouses");
- var response = await httpClient.SendAsync(request,
CancellationToken.None);
+ // First request - should trigger background token refresh but use
original token
+ var firstRequest = new HttpRequestMessage(HttpMethod.Get,
$"https://{host}/api/2.0/sql/config/warehouses");
+ var startTime = DateTime.UtcNow;
+ var firstResponse = await httpClient.SendAsync(firstRequest,
CancellationToken.None);
+ var firstRequestDuration = DateTime.UtcNow - startTime;
+
+ // The first request should succeed quickly (not waiting for token
refresh)
+ firstResponse.EnsureSuccessStatusCode();
+ string firstContent = await
firstResponse.Content.ReadAsStringAsync();
+ Assert.Contains("sql_configuration_parameters", firstContent);
+
+ // Verify the request completed quickly (token refresh happens in
background)
+ Assert.True(firstRequestDuration < TimeSpan.FromSeconds(5),
+ $"First request took
{firstRequestDuration.TotalMilliseconds}ms, which may indicate it waited for
token refresh");
+
+ // Verify the first request used the original token
+ Assert.Single(tokenCapturingHandler.CapturedTokens);
+
+ // Wait for background token refresh to complete
+ await Task.Delay(TimeSpan.FromSeconds(10));
- // The request should succeed with the refreshed token
- response.EnsureSuccessStatusCode();
+ // Second request - should use the refreshed token
+ var secondRequest = new HttpRequestMessage(HttpMethod.Get,
$"https://{host}/api/2.0/sql/config/warehouses");
+ var secondResponse = await httpClient.SendAsync(secondRequest,
CancellationToken.None);
- string content = await response.Content.ReadAsStringAsync();
- Assert.Contains("sql_configuration_parameters", content);
+ // The second request should also succeed (now with refreshed
token)
+ secondResponse.EnsureSuccessStatusCode();
+ string secondContent = await
secondResponse.Content.ReadAsStringAsync();
+ Assert.Contains("sql_configuration_parameters", secondContent);
+
+ // Verify we now have two different tokens
+ Assert.Equal(2, tokenCapturingHandler.CapturedTokens.Count);
+ Assert.Equal(TestConfiguration.AccessToken,
tokenCapturingHandler.CapturedTokens[0]);
+ Assert.NotEqual(TestConfiguration.AccessToken,
tokenCapturingHandler.CapturedTokens[1]);
}
[SkippableFact]
@@ -124,9 +176,12 @@ namespace Apache.Arrow.Adbc.Tests.Drivers.Databricks.Auth
string host = GetHost();
var tokenExchangeClient = new TokenExchangeClient(_httpClient,
host);
+ // Create a token capturing handler to verify no token refresh
occurs
+ var tokenCapturingHandler = new TokenCapturingHandler(new
HttpClientHandler());
+
// Create a handler that should not refresh the token (token not
near expiry)
var handler = new TokenExchangeDelegatingHandler(
- new HttpClientHandler(),
+ tokenCapturingHandler,
tokenExchangeClient,
TestConfiguration.AccessToken,
expiryTime,
@@ -134,14 +189,30 @@ namespace Apache.Arrow.Adbc.Tests.Drivers.Databricks.Auth
var httpClient = new HttpClient(handler);
- var request = new HttpRequestMessage(HttpMethod.Get,
$"https://{host}/api/2.0/sql/config/warehouses");
- var response = await httpClient.SendAsync(request,
CancellationToken.None);
+ // Make multiple requests to ensure no token refresh is triggered
+ var firstRequest = new HttpRequestMessage(HttpMethod.Get,
$"https://{host}/api/2.0/sql/config/warehouses");
+ var firstResponse = await httpClient.SendAsync(firstRequest,
CancellationToken.None);
+
+ // The first request should succeed with the original token
+ firstResponse.EnsureSuccessStatusCode();
+ string firstContent = await
firstResponse.Content.ReadAsStringAsync();
+ Assert.Contains("sql_configuration_parameters", firstContent);
+
+ // Similar wait as the token refresh case
+ await Task.Delay(TimeSpan.FromSeconds(10));
+
+ // Second request should also use original token (no refresh
needed)
+ var secondRequest = new HttpRequestMessage(HttpMethod.Get,
$"https://{host}/api/2.0/sql/config/warehouses");
+ var secondResponse = await httpClient.SendAsync(secondRequest,
CancellationToken.None);
- // The request should succeed with the original token
- response.EnsureSuccessStatusCode();
+ secondResponse.EnsureSuccessStatusCode();
+ string secondContent = await
secondResponse.Content.ReadAsStringAsync();
+ Assert.Contains("sql_configuration_parameters", secondContent);
- string content = await response.Content.ReadAsStringAsync();
- Assert.Contains("sql_configuration_parameters", content);
+ // Verify both requests used the same original token (no refresh
occurred)
+ Assert.Equal(2, tokenCapturingHandler.CapturedTokens.Count);
+ Assert.Equal(TestConfiguration.AccessToken,
tokenCapturingHandler.CapturedTokens[0]);
+ Assert.Equal(TestConfiguration.AccessToken,
tokenCapturingHandler.CapturedTokens[1]);
}
protected override void Dispose(bool disposing)
diff --git
a/csharp/test/Drivers/Databricks/Unit/Auth/TokenExchangeDelegatingHandlerTests.cs
b/csharp/test/Drivers/Databricks/Unit/Auth/TokenExchangeDelegatingHandlerTests.cs
index 1abf6dd7d..40b83c156 100644
---
a/csharp/test/Drivers/Databricks/Unit/Auth/TokenExchangeDelegatingHandlerTests.cs
+++
b/csharp/test/Drivers/Databricks/Unit/Auth/TokenExchangeDelegatingHandlerTests.cs
@@ -108,18 +108,22 @@ namespace Apache.Arrow.Adbc.Drivers.Databricks.Tests.Auth
Assert.Equal("Bearer",
capturedRequest.Headers.Authorization?.Scheme);
Assert.Equal(_initialToken,
capturedRequest.Headers.Authorization?.Parameter);
+ // Wait for background task to complete
+ await Task.Delay(100);
+
_mockTokenExchangeClient.Verify(
x => x.ExchangeTokenAsync(It.IsAny<string>(),
It.IsAny<CancellationToken>()),
Times.Never);
}
[Fact]
- public async Task
SendAsync_WithTokenNearExpiry_RenewsTokenBeforeRequest()
+ public async Task
SendAsync_WithTokenNearExpiry_StartsTokenRenewalInBackground()
{
// Arrange
var nearExpiryTime = DateTime.UtcNow.AddMinutes(5); // Within
renewal limit
var newToken = "new-renewed-token";
var newExpiry = DateTime.UtcNow.AddHours(1);
+ var tokenExchangeDelay = TimeSpan.FromMilliseconds(500);
var handler = new TokenExchangeDelegatingHandler(
_mockInnerHandler.Object,
@@ -141,7 +145,11 @@ namespace Apache.Arrow.Adbc.Drivers.Databricks.Tests.Auth
_mockTokenExchangeClient
.Setup(x => x.ExchangeTokenAsync(_initialToken,
It.IsAny<CancellationToken>()))
- .ReturnsAsync(tokenExchangeResponse);
+ .Returns(async (string token, CancellationToken ct) =>
+ {
+ await Task.Delay(tokenExchangeDelay, ct);
+ return tokenExchangeResponse;
+ });
HttpRequestMessage? capturedRequest = null;
@@ -154,12 +162,40 @@ namespace Apache.Arrow.Adbc.Drivers.Databricks.Tests.Auth
.ReturnsAsync(expectedResponse);
var httpClient = new HttpClient(handler);
+
+ // Make the first request - this should use the original token and
start background refresh
+ var startTime = DateTime.UtcNow;
var response = await httpClient.SendAsync(request);
+ var requestDuration = DateTime.UtcNow - startTime;
Assert.Equal(expectedResponse, response);
+ Assert.True(requestDuration < tokenExchangeDelay,
+ $"Request took {requestDuration.TotalMilliseconds}ms, which is
longer than the token refresh delay of
{tokenExchangeDelay.TotalMilliseconds}ms");
+
Assert.NotNull(capturedRequest);
Assert.Equal("Bearer",
capturedRequest.Headers.Authorization?.Scheme);
- Assert.Equal(newToken,
capturedRequest.Headers.Authorization?.Parameter);
+ Assert.Equal(_initialToken,
capturedRequest.Headers.Authorization?.Parameter); // First request uses
original token
+
+ // Wait a bit for the background task to complete
+ await Task.Delay(tokenExchangeDelay +
TimeSpan.FromMilliseconds(100));
+
+ // Make a second request - this should use the new token
+ var request2 = new HttpRequestMessage(HttpMethod.Get,
"https://example.com/2");
+ HttpRequestMessage? capturedRequest2 = null;
+
+ _mockInnerHandler.Protected()
+ .Setup<Task<HttpResponseMessage>>(
+ "SendAsync",
+ ItExpr.Is<HttpRequestMessage>(r =>
r.RequestUri!.PathAndQuery == "/2"),
+ ItExpr.IsAny<CancellationToken>())
+ .Callback<HttpRequestMessage, CancellationToken>((req, ct) =>
capturedRequest2 = req)
+ .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
+
+ await httpClient.SendAsync(request2);
+
+ Assert.NotNull(capturedRequest2);
+ Assert.Equal("Bearer",
capturedRequest2.Headers.Authorization?.Scheme);
+ Assert.Equal(newToken,
capturedRequest2.Headers.Authorization?.Parameter); // Second request uses new
token
_mockTokenExchangeClient.Verify(
x => x.ExchangeTokenAsync(_initialToken,
It.IsAny<CancellationToken>()),
@@ -204,6 +240,9 @@ namespace Apache.Arrow.Adbc.Drivers.Databricks.Tests.Auth
Assert.Equal("Bearer",
capturedRequest.Headers.Authorization?.Scheme);
Assert.Equal(_initialToken,
capturedRequest.Headers.Authorization?.Parameter); // Should still use original
token
+ // Wait for background task to complete
+ await Task.Delay(100);
+
// Verify token exchange was attempted
_mockTokenExchangeClient.Verify(
x => x.ExchangeTokenAsync(_initialToken,
It.IsAny<CancellationToken>()),
@@ -245,8 +284,13 @@ namespace Apache.Arrow.Adbc.Drivers.Databricks.Tests.Auth
var httpClient = new HttpClient(handler);
- // Make two requests
+ // Make first request to trigger token renewal
await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get,
"https://example.com/1"));
+
+ // Wait for background renewal to complete
+ await Task.Delay(100);
+
+ // Make second request
await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get,
"https://example.com/2"));
// Token exchange should only be called once (renewed tokens
cannot be renewed again)
@@ -282,7 +326,7 @@ namespace Apache.Arrow.Adbc.Drivers.Databricks.Tests.Auth
.Setup(x => x.ExchangeTokenAsync(_initialToken,
It.IsAny<CancellationToken>()))
.Returns(async () =>
{
- await Task.Delay(100);
+ await Task.Delay(200);
return tokenExchangeResponse;
});
@@ -305,6 +349,9 @@ namespace Apache.Arrow.Adbc.Drivers.Databricks.Tests.Auth
await Task.WhenAll(tasks);
+ // Wait for any background token renewal to complete
+ await Task.Delay(300);
+
// Token exchange should only be called once despite concurrent
requests
_mockTokenExchangeClient.Verify(
x => x.ExchangeTokenAsync(_initialToken,
It.IsAny<CancellationToken>()),
@@ -342,7 +389,7 @@ namespace Apache.Arrow.Adbc.Drivers.Databricks.Tests.Auth
}
[Fact]
- public async Task
SendAsync_WithTokenRenewalAndCancellation_PropagatesCancellation()
+ public async Task
SendAsync_WithTokenRenewalAndCancellation_HandlesCancellationGracefully()
{
var nearExpiryTime = DateTime.UtcNow.AddMinutes(5); // Within
renewal limit
var handler = new TokenExchangeDelegatingHandler(
@@ -369,6 +416,17 @@ namespace Apache.Arrow.Adbc.Drivers.Databricks.Tests.Auth
});
});
+ _mockInnerHandler.Protected()
+ .Setup<Task<HttpResponseMessage>>(
+ "SendAsync",
+ ItExpr.IsAny<HttpRequestMessage>(),
+ ItExpr.IsAny<CancellationToken>())
+ .Returns<HttpRequestMessage, CancellationToken>((req, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+ return Task.FromResult(new
HttpResponseMessage(HttpStatusCode.OK));
+ });
+
cts.Cancel();
var httpClient = new HttpClient(handler);
await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
@@ -425,6 +483,9 @@ namespace Apache.Arrow.Adbc.Drivers.Databricks.Tests.Auth
var httpClient = new HttpClient(handler);
await httpClient.SendAsync(request);
+ // Wait for background renewal to complete
+ await Task.Delay(100);
+
_mockTokenExchangeClient.Verify(
x => x.ExchangeTokenAsync(_initialToken,
It.IsAny<CancellationToken>()),
Times.Once);