Skip to content

Latest commit

 

History

History
156 lines (116 loc) · 5.55 KB

File metadata and controls

156 lines (116 loc) · 5.55 KB

HttpClientFactory Optimization - Changes Summary

Overview

This change addresses critical HttpClient lifetime and DNS staleness issues in the custom HttpClientFactory implementation.

Problem Statement

The original implementation had several critical issues:

  1. Socket Exhaustion Risk: Each HttpClient created its own HttpClientHandler with disposeHandler: true, leading to separate connection pools per client.

  2. DNS Staleness: No connection lifetime management meant DNS entries never refreshed, causing stale connections to persist indefinitely.

  3. Dispose Race Condition: The Dispose() method disposed all HttpClient instances, potentially affecting active clients still in use.

  4. No Handler Pooling: The factory didn't leverage SocketsHttpHandler pooling for efficient connection reuse.

Solution Implemented

1. Added PooledConnectionLifetime Configuration

File: src/GrpcWebBridge/Integration/HttpClientFactory.cs

Added new property to HttpClientFactoryOptions:

/// <summary>
/// Gets or sets the pooled connection lifetime in milliseconds.
/// After this duration, the underlying SocketsHttpHandler will be rotated to
/// prevent DNS staleness and ensure connection freshness.
/// Default is 2 minutes (120000 ms).
/// </summary>
public int PooledConnectionLifetimeMs { get; set; } = 120000;

2. Implemented Handler Pooling with SocketsHttpHandler

  • Created internal PooledHandler class that manages shared SocketsHttpHandler instances
  • Each handler is shared across multiple HttpClient instances with the same name
  • Handlers are disposed only when the factory is disposed, not when clients are disposed
  • Clients use disposeHandler: false to prevent premature handler disposal

3. Added Automatic Handler Rotation

Implemented CheckHandlerRotation() method that:

  • Monitors elapsed time since last handler rotation
  • Rotates all pooled handlers when PooledConnectionLifetimeMs is exceeded
  • Uses thread-safe locking to prevent race conditions during rotation
  • Logs rotation events for observability

4. Fixed Dispose Implementation

Updated Dispose() to:

  • Dispose pooled handlers first (shared resources)
  • Clear handler pool
  • Dispose client instances managed by the factory
  • Avoid disposing clients that might still be in use

5. Added Input Validation

Added ArgumentNullException.ThrowIfNull(logger) to constructor for better error handling.

Key Benefits

  1. Prevents Socket Exhaustion: Shared handlers mean one connection pool per client name, not per client instance
  2. Eliminates DNS Staleness: Handlers rotate on configurable schedule, forcing DNS refresh
  3. Safe Disposal: No race conditions between disposed handlers and active clients
  4. Better Performance: Connection pooling reduces overhead of creating new handlers
  5. Configurable: Connection lifetime can be tuned per application needs

Configuration

Default Settings

  • PooledConnectionLifetimeMs: 120,000 ms (2 minutes)
  • MaxConnectionsPerServer: 10 (unchanged)
  • RequestTimeoutMs: 30,000 ms (unchanged)

Customization Example

var options = new HttpClientFactoryOptions
{
    PooledConnectionLifetimeMs = 300000, // 5 minutes
    MaxConnectionsPerServer = 20,
    RequestTimeoutMs = 15000
};
var factory = new HttpClientFactory(logger, options);

Testing

Added comprehensive tests in tests/grpc-web-bridge.Tests/HttpClientFactoryTests.cs:

  1. GetClient_WithSameName_ReusesSameHandler - Verifies handler sharing
  2. GetClient_WithDifferentNames_CreatesDifferentHandlers - Verifies isolation
  3. PooledConnectionLifetime_WithDefaultValue_IsSetCorrectly - Verifies defaults
  4. PooledConnectionLifetime_WithCustomValue_IsSetCorrectly - Verifies customization
  5. Dispose_WithMultipleClients_DisposesHandlersNotClients - Verifies safe disposal
  6. HandlerRotation_OccursAfterConfiguredLifetime - Verifies rotation mechanism
  7. SocketsHttpHandler_ConfiguredWithProperSettings - Verifies configuration

All 47 HttpClientFactory tests pass successfully.

Backward Compatibility

Fully backward compatible

  • Existing code continues to work without changes
  • Default PooledConnectionLifetimeMs of 2 minutes is reasonable for most use cases
  • No breaking changes to public API
  • All existing tests continue to pass

Performance Impact

Positive:

  • Reduced memory usage (fewer handler instances)
  • Better connection reuse (shared pools)
  • Automatic DNS refresh prevents stale connections
  • Lower GC pressure (fewer allocations)

Minimal overhead:

  • Handler rotation check is O(1) and only runs on client creation
  • No impact on existing connections
  • No additional network calls

Migration Guide

No migration needed! The changes are transparent to existing code.

For applications requiring longer connection lifetimes:

var options = new HttpClientFactoryOptions
{
    PooledConnectionLifetimeMs = 3600000 // 1 hour for stable services
};

For applications needing frequent DNS updates:

var options = new HttpClientFactoryOptions
{
    PooledConnectionLifetimeMs = 60000 // 1 minute
};

Files Modified

  1. src/GrpcWebBridge/Integration/HttpClientFactory.cs - Main implementation
  2. tests/grpc-web-bridge.Tests/HttpClientFactoryTests.cs - Added tests

Verification

  • ✅ All tests pass (47/47 HttpClientFactory tests)
  • ✅ Solution builds successfully
  • ✅ No breaking changes
  • ✅ Backward compatible
  • ✅ Follows C# best practices
  • ✅ Proper XML documentation
  • ✅ Thread-safe implementation
  • ✅ Proper error handling with ArgumentNullException.ThrowIfNull