@@ -327,18 +327,131 @@ public async Task UpdateAddresses_ConnectIsInProgress_InProgressConnectIsCancele
327
327
Assert . AreEqual ( 81 , connectAddress2 . Port ) ;
328
328
}
329
329
330
+ [ Test ]
331
+ public async Task PickAsync_DoesNotDeadlockAfterReconnect_WithResolverError ( )
332
+ {
333
+ // Arrange
334
+ var services = new ServiceCollection ( ) ;
335
+ services . AddNUnitLogger ( ) ;
336
+ await using var serviceProvider = services . BuildServiceProvider ( ) ;
337
+ var loggerFactory = serviceProvider . GetRequiredService < ILoggerFactory > ( ) ;
338
+
339
+ var resolver = new TestResolver ( loggerFactory ) ;
340
+
341
+ GrpcChannelOptions channelOptions = new GrpcChannelOptions ( ) ;
342
+ channelOptions . ServiceConfig = new ServiceConfig ( )
343
+ {
344
+ LoadBalancingConfigs = { new RoundRobinConfig ( ) }
345
+ } ;
346
+
347
+ var transportFactory = new TestSubchannelTransportFactory ( ) ;
348
+ var clientChannel = CreateConnectionManager ( loggerFactory , resolver , transportFactory , new [ ] { new RoundRobinBalancerFactory ( ) } ) ;
349
+ // Configure balancer similar to how GrpcChannel constructor does it
350
+ clientChannel . ConfigureBalancer ( c => new ChildHandlerLoadBalancer (
351
+ c ,
352
+ channelOptions . ServiceConfig ,
353
+ clientChannel ) ) ;
354
+
355
+ // Act
356
+ var connectTask = clientChannel . ConnectAsync ( waitForReady : true , cancellationToken : CancellationToken . None ) ;
357
+ var pickTask = clientChannel . PickAsync (
358
+ new PickContext { Request = new HttpRequestMessage ( ) } ,
359
+ waitForReady : true ,
360
+ CancellationToken . None ) . AsTask ( ) ;
361
+
362
+ resolver . UpdateAddresses ( new List < BalancerAddress >
363
+ {
364
+ new BalancerAddress ( "localhost" , 80 )
365
+ } ) ;
366
+ await Task . WhenAll ( connectTask , pickTask ) . DefaultTimeout ( ) ;
367
+
368
+ // Simulate transport/network issue
369
+ transportFactory . Transports . ForEach ( t => t . Disconnect ( ) ) ;
370
+ resolver . UpdateError ( new Status ( StatusCode . Unavailable , "Test error" ) ) ;
371
+
372
+ pickTask = clientChannel . PickAsync (
373
+ new PickContext { Request = new HttpRequestMessage ( ) } ,
374
+ waitForReady : true ,
375
+ CancellationToken . None ) . AsTask ( ) ;
376
+ resolver . UpdateAddresses ( new List < BalancerAddress >
377
+ {
378
+ new BalancerAddress ( "localhost" , 80 )
379
+ } ) ;
380
+
381
+ // Assert
382
+ // Should not timeout (deadlock)
383
+ await pickTask . DefaultTimeout ( ) ;
384
+ }
385
+
386
+ [ Test ]
387
+ public async Task PickAsync_DoesNotDeadlockAfterReconnect_WithZeroAddressResolved ( )
388
+ {
389
+ // Arrange
390
+ var services = new ServiceCollection ( ) ;
391
+ services . AddNUnitLogger ( ) ;
392
+ await using var serviceProvider = services . BuildServiceProvider ( ) ;
393
+ var loggerFactory = serviceProvider . GetRequiredService < ILoggerFactory > ( ) ;
394
+
395
+ var resolver = new TestResolver ( loggerFactory ) ;
396
+
397
+ GrpcChannelOptions channelOptions = new GrpcChannelOptions ( ) ;
398
+ channelOptions . ServiceConfig = new ServiceConfig ( )
399
+ {
400
+ LoadBalancingConfigs = { new RoundRobinConfig ( ) }
401
+ } ;
402
+
403
+ var transportFactory = new TestSubchannelTransportFactory ( ) ;
404
+ var clientChannel = CreateConnectionManager ( loggerFactory , resolver , transportFactory , new [ ] { new RoundRobinBalancerFactory ( ) } ) ;
405
+ // Configure balancer similar to how GrpcChannel constructor does it
406
+ clientChannel . ConfigureBalancer ( c => new ChildHandlerLoadBalancer (
407
+ c ,
408
+ channelOptions . ServiceConfig ,
409
+ clientChannel ) ) ;
410
+
411
+ // Act
412
+ var connectTask = clientChannel . ConnectAsync ( waitForReady : true , cancellationToken : CancellationToken . None ) ;
413
+ var pickTask = clientChannel . PickAsync (
414
+ new PickContext { Request = new HttpRequestMessage ( ) } ,
415
+ waitForReady : true ,
416
+ CancellationToken . None ) . AsTask ( ) ;
417
+
418
+ resolver . UpdateAddresses ( new List < BalancerAddress >
419
+ {
420
+ new BalancerAddress ( "localhost" , 80 )
421
+ } ) ;
422
+ await Task . WhenAll ( connectTask , pickTask ) . DefaultTimeout ( ) ;
423
+
424
+ // Simulate transport/network issue (with resolver reporting no addresses)
425
+ transportFactory . Transports . ForEach ( t => t . Disconnect ( ) ) ;
426
+ resolver . UpdateAddresses ( new List < BalancerAddress > ( ) ) ;
427
+
428
+ pickTask = clientChannel . PickAsync (
429
+ new PickContext { Request = new HttpRequestMessage ( ) } ,
430
+ waitForReady : true ,
431
+ CancellationToken . None ) . AsTask ( ) ;
432
+ resolver . UpdateAddresses ( new List < BalancerAddress >
433
+ {
434
+ new BalancerAddress ( "localhost" , 80 )
435
+ } ) ;
436
+
437
+ // Assert
438
+ // Should not timeout (deadlock)
439
+ await pickTask . DefaultTimeout ( ) ;
440
+ }
441
+
330
442
private static ConnectionManager CreateConnectionManager (
331
443
ILoggerFactory loggerFactory ,
332
444
Resolver resolver ,
333
- TestSubchannelTransportFactory transportFactory )
445
+ TestSubchannelTransportFactory transportFactory ,
446
+ LoadBalancerFactory [ ] ? loadBalancerFactories = null )
334
447
{
335
448
return new ConnectionManager (
336
449
resolver ,
337
450
disableResolverServiceConfig : false ,
338
451
loggerFactory ,
339
452
new TestBackoffPolicyFactory ( ) ,
340
453
transportFactory ,
341
- Array . Empty < LoadBalancerFactory > ( ) ) ;
454
+ loadBalancerFactories ?? Array . Empty < LoadBalancerFactory > ( ) ) ;
342
455
}
343
456
344
457
private class DropLoadBalancer : LoadBalancer
0 commit comments