44using GreptimeDB . Ingester . Internal ;
55using Grpc . Core ;
66using Grpc . Net . Client ;
7+ using Grpc . Net . Client . Balancer ;
8+ using Grpc . Net . Client . Configuration ;
9+ using Microsoft . Extensions . DependencyInjection ;
710using Microsoft . Extensions . Logging ;
811using Microsoft . Extensions . Logging . Abstractions ;
912
@@ -18,6 +21,7 @@ public sealed partial class GreptimeClient : IAsyncDisposable, IDisposable
1821 private readonly ILoggerFactory _loggerFactory ;
1922 private readonly ILogger _logger ;
2023 private readonly GrpcChannel _channel ;
24+ private readonly ServiceProvider ? _channelServices ;
2125 private readonly GreptimeDatabase . GreptimeDatabaseClient _client ;
2226 private readonly HealthCheck . HealthCheckClient _healthClient ;
2327 private readonly Lazy < FlightClient > _flightClient ;
@@ -35,14 +39,13 @@ public GreptimeClient(GreptimeClientOptions options, ILoggerFactory? loggerFacto
3539 _loggerFactory = loggerFactory ?? NullLoggerFactory . Instance ;
3640 _logger = _loggerFactory . CreateLogger < GreptimeClient > ( ) ;
3741
38- var channelOptions = new GrpcChannelOptions ( ) ;
39-
40- _channel = GrpcChannel . ForAddress ( options . Endpoint , channelOptions ) ;
42+ var endpoints = options . ResolveEndpoints ( ) ;
43+ ( _channel , _channelServices ) = BuildChannel ( endpoints , options . LoadBalancing ) ;
4144 _client = new GreptimeDatabase . GreptimeDatabaseClient ( _channel ) ;
4245 _healthClient = new HealthCheck . HealthCheckClient ( _channel ) ;
4346 _flightClient = new Lazy < FlightClient > ( ( ) => new FlightClient ( _channel ) ) ;
4447
45- LogClientCreated ( _logger , options . Endpoint ) ;
48+ LogClientCreated ( _logger , endpoints . Count , endpoints [ 0 ] ) ;
4649 }
4750
4851 /// <summary>
@@ -265,6 +268,11 @@ public async Task CloseAsync()
265268 _disposed = true ;
266269 await DisposeFlightClientAsync ( ) . ConfigureAwait ( false ) ;
267270 await _channel . ShutdownAsync ( ) . ConfigureAwait ( false ) ;
271+ _channel . Dispose ( ) ;
272+ if ( _channelServices is not null )
273+ {
274+ await _channelServices . DisposeAsync ( ) . ConfigureAwait ( false ) ;
275+ }
268276 LogClientClosed ( _logger ) ;
269277 }
270278
@@ -285,6 +293,7 @@ public void Dispose()
285293 _disposed = true ;
286294 DisposeFlightClient ( ) ;
287295 _channel . Dispose ( ) ;
296+ _channelServices ? . Dispose ( ) ;
288297 LogClientDisposed ( _logger ) ;
289298 }
290299
@@ -349,6 +358,59 @@ private CallOptions CreateCallOptions(CancellationToken cancellationToken)
349358 cancellationToken : cancellationToken ) ;
350359 }
351360
361+ private static ( GrpcChannel Channel , ServiceProvider ? Services ) BuildChannel (
362+ IReadOnlyList < string > endpoints ,
363+ LoadBalancingStrategy strategy )
364+ {
365+ // Single endpoint: skip the balancer entirely. This preserves the original
366+ // direct-channel behavior, including default TLS authority/SNI handling for
367+ // https endpoints (the balancer path uses a synthetic channel authority,
368+ // which can break certificate hostname validation).
369+ if ( endpoints . Count == 1 )
370+ {
371+ return ( GrpcChannel . ForAddress ( endpoints [ 0 ] ) , null ) ;
372+ }
373+
374+ var addresses = new List < BalancerAddress > ( endpoints . Count ) ;
375+ string ? scheme = null ;
376+ foreach ( var endpoint in endpoints )
377+ {
378+ var uri = new Uri ( endpoint , UriKind . Absolute ) ;
379+ scheme ??= uri . Scheme ;
380+ addresses . Add ( new BalancerAddress ( uri . DnsSafeHost , uri . Port ) ) ;
381+ }
382+
383+ var services = new ServiceCollection ( ) ;
384+ services . AddSingleton < ResolverFactory > ( new StaticResolverFactory ( addresses ) ) ;
385+ if ( strategy == LoadBalancingStrategy . Random )
386+ {
387+ services . AddSingleton < LoadBalancerFactory > ( RandomBalancerFactory . Instance ) ;
388+ }
389+ var serviceProvider = services . BuildServiceProvider ( ) ;
390+
391+ LoadBalancingConfig lbConfig = strategy switch
392+ {
393+ LoadBalancingStrategy . Random => new RandomConfig ( ) ,
394+ LoadBalancingStrategy . RoundRobin => new RoundRobinConfig ( ) ,
395+ _ => throw new ArgumentOutOfRangeException ( nameof ( strategy ) , strategy , "Unsupported load-balancing strategy." ) ,
396+ } ;
397+
398+ var channelOptions = new GrpcChannelOptions
399+ {
400+ ServiceProvider = serviceProvider ,
401+ ServiceConfig = new ServiceConfig
402+ {
403+ LoadBalancingConfigs = { lbConfig }
404+ } ,
405+ Credentials = scheme == Uri . UriSchemeHttps
406+ ? ChannelCredentials . SecureSsl
407+ : ChannelCredentials . Insecure
408+ } ;
409+
410+ var channel = GrpcChannel . ForAddress ( "static:///greptime" , channelOptions ) ;
411+ return ( channel , serviceProvider ) ;
412+ }
413+
352414 private static void CheckResponse ( GreptimeResponse response )
353415 {
354416 var header = response . Header ;
@@ -361,20 +423,13 @@ private static void CheckResponse(GreptimeResponse response)
361423
362424 private void ThrowIfDisposed ( )
363425 {
364- #if NET7_0_OR_GREATER
365426 ObjectDisposedException . ThrowIf ( _disposed , this ) ;
366- #else
367- if ( _disposed )
368- {
369- throw new ObjectDisposedException ( nameof ( GreptimeClient ) ) ;
370- }
371- #endif
372427 }
373428
374429 #region Logging
375430
376- [ LoggerMessage ( Level = LogLevel . Debug , Message = "GreptimeClient created for endpoint {Endpoint }" ) ]
377- private static partial void LogClientCreated ( ILogger logger , string endpoint ) ;
431+ [ LoggerMessage ( Level = LogLevel . Debug , Message = "GreptimeClient created with {EndpointCount} endpoint(s); first: {FirstEndpoint }" ) ]
432+ private static partial void LogClientCreated ( ILogger logger , int endpointCount , string firstEndpoint ) ;
378433
379434 [ LoggerMessage ( Level = LogLevel . Debug , Message = "Writing {TableCount} tables with {RowCount} total rows" ) ]
380435 private static partial void LogWriteStarted ( ILogger logger , int tableCount , int rowCount ) ;
0 commit comments