ClickHouseDataSource: DisposeAsync() (and await using) does not dispose the owned ClickHouseClient/HttpClient
Maintainers usually reply within 2 days
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 78/100
Research direction
Start in ClickHouse.Driver/ADO/ClickHouseDataSource.cs around lines 123-132, where the Dispose(bool disposing) override releases the owned client and httpClient only when disposing is true. Add a DisposeAsyncCore() override that performs the same disposeClient-guarded cleanup, then run the regression test comparing Dispose() and DisposeAsync() with a tracking handler. Done when the owned HttpClient handler is disposed after DisposeAsync() and the caller's HttpClient is still left alone when disposeHttpClient is false.
Written by the indexing model from the issue text.
Description
Describe the bug
ClickHouseDataSource.DisposeAsync() does not dispose the ClickHouseClient and HttpClient that the data source owns. Only the synchronous Dispose() releases them.
This affects every async disposal path:
await using var dataSource = new ClickHouseDataSource(...)await dataSource.DisposeAsync()- a data source registered with
AddClickHouseDataSource(...)when the container is disposed withServiceProvider.DisposeAsync(). The ASP.NET Core / Generic Host root provider is disposed this way at shutdown.
After DisposeAsync(), the internal client is not marked disposed, and it still runs queries. The HTTP handler and its connection pool stay alive.
Root cause: ClickHouse.Driver/ADO/ClickHouseDataSource.cs:123-132 overrides only Dispose(bool disposing) and releases the owned objects only when disposing == true:
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing && disposeClient)
{
client?.Dispose();
httpClient?.Dispose();
}
}
DbDataSource.DisposeAsync() calls DisposeAsyncCore() and then Dispose(disposing: false) (dotnet/runtime DbDataSource.cs):
public async ValueTask DisposeAsync()
{
await DisposeAsyncCore().ConfigureAwait(false);
Dispose(disposing: false);
GC.SuppressFinalize(this);
}
protected virtual ValueTask DisposeAsyncCore()
=> default;
ClickHouseDataSource does not override DisposeAsyncCore(), and the Dispose(false) call skips the managed cleanup. So nothing disposes the owned objects. Connections from the data source do not own the client, so they do not dispose it either.
DbDataSource is different from DbConnection, DbCommand and DbDataReader here: their default DisposeAsync() calls Dispose(). So this problem is specific to the data source.
Steps to reproduce
- Create a
ClickHouseDataSourcethat owns its HTTP client: the default constructor, or a customHttpClientwithdisposeHttpClient: true. - Dispose it with
await dataSource.DisposeAsync()(orawait using). - Examine the HTTP handler, or run a query through
dataSource.GetClient().
Expected behaviour
DisposeAsync() releases the same resources as Dispose(). After it, the owned HttpClient/handler is disposed, and the internal client throws ObjectDisposedException, as it does after Dispose().
Code example
Run on net10.0 against a live server. Throwaway NUnit tests in ClickHouse.Driver.Tests; TrackingHandler is a DelegatingHandler over SocketsHttpHandler that records Dispose. "client.disposed" is the private ClickHouseClient.disposed field, read with reflection.
// Custom HttpClient, disposeHttpClient: true
var handler = new TrackingHandler();
var ds = new ClickHouseDataSource(cs, new HttpClient(handler), disposeHttpClient: true);
await ds.DisposeAsync();
// handler.Disposed == false (ds.Dispose() gives true)
// Default constructor (driver-created pooled handler)
var ds2 = new ClickHouseDataSource(cs);
await ds2.DisposeAsync();
await ds2.GetClient().ExecuteScalarAsync("SELECT 1"); // returns 1
// (after ds2.Dispose() the same call throws ObjectDisposedException)
// DI singleton
var sp = new ServiceCollection().AddClickHouseDataSource(cs, new HttpClient(handler)).BuildServiceProvider();
_ = sp.GetRequiredService<ClickHouseDataSource>();
await sp.DisposeAsync();
// handler.Disposed == false (sp.Dispose() gives true)
Results:
| Disposal path | Owned client / handler disposed? |
|---|---|
ds.Dispose(), custom HttpClient |
yes |
await ds.DisposeAsync(), custom HttpClient |
no |
await using (var ds = ...) { query }, custom HttpClient |
no |
ds.Dispose(), default constructor |
yes; a later query throws ObjectDisposedException |
await ds.DisposeAsync(), default constructor |
no; a later query returns 1 |
ServiceProvider.Dispose(), AddClickHouseDataSource(cs, httpClient) |
yes |
ServiceProvider.DisposeAsync(), AddClickHouseDataSource(cs, httpClient) |
no |
ServiceProvider.DisposeAsync(), AddClickHouseDataSource(cs) (DI creates the HttpClient) |
no; a later query returns 1 |
DisposeAsync() + Dispose(), disposeHttpClient: false |
not disposed, as expected (contrast case) |
Error log
No error. The resources are not released, without any message.
Suggested fix
Override DisposeAsyncCore() in ClickHouseDataSource so that the async path releases the same owned objects as Dispose(true), under the same disposeClient condition. Keep the current contrast case: with disposeHttpClient: false, neither path disposes the caller's HttpClient. A regression test can compare Dispose() and DisposeAsync() with a tracking handler, and also ServiceProvider.DisposeAsync() after AddClickHouseDataSource.
Provenance: found by automated analysis while working on #686. Verified by running the tests above against a live server, not only by reading the code.
Configuration
Environment
- Client version:
mainat 860fe1a (the same code is in 1.5.0) - Language version: C# (SDK default)
- .NET version: .NET 10.0.203 (net10.0 target; the code path is the same on every
NET7_0_OR_GREATERtarget) - OS: Linux (Docker)
ClickHouse server
- ClickHouse Server version: 26.9.1.1629
- ClickHouse Server non-default settings, if any: none
CREATE TABLEstatements for tables involved: none (SELECT 1only)
- Dominant language
- C#
- Stars
- 97
- Forks
- 24
- Avg merge
- 2d 12h
- Merged PRs (30d)
- 21
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from ClickHouse/clickhouse-cs
-
Every query fails with Code 164 (READONLY) for a readonly = 1 user: the driver always sends enable_http_compression and input_format_binary_read_json_as_string as settingsPossibly taken @polyglotAI-bot claimed this 9 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
ClickHouse/clickhouse-cs#659 · 1 comment ·
Maintainers usually reply within 2 days
-
ClickHouseDecimal: Floor() rounds negative non-integers toward zero (-1.5 → -1, server floor gives -2)Possibly taken @polyglotAI-bot claimed this 9 days ago. Openbug
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
ClickHouse/clickhouse-cs#645 · 1 assignee ·
Maintainers usually reply within 2 days
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
ClickHouse/clickhouse-cs#639 ·
Maintainers usually reply within 2 days
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
ClickHouse/clickhouse-cs#627 ·
Maintainers usually reply within 2 days
-
bug
Difficulty 1/5 Under an hour Newbie friendliness 85/100
ClickHouse/clickhouse-cs#545 ·
Maintainers usually reply within 2 days
All issues in ClickHouse/clickhouse-cs
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 60/100
Maintainers usually reply within 1 day
-
area: design documentation
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
eyalgolan/statecraft-israel#883 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Maintainers usually reply within 3 days
-
area/indexing bug package/core triage
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
Maintainers usually reply within 1 day
-
Bug
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
nunit/nunit-console#1913 ·
Maintainers usually reply within 1 day