The Apache IoTDB C# client library provides comprehensive exception handling and health monitoring capabilities for SessionPool operations. This document explains how to handle pool depletion scenarios, monitor pool health, and implement recovery strategies.
SessionPoolDepletedException is a specialized exception thrown when the SessionPool cannot provide a client connection. This indicates that:
The exception provides detailed diagnostic information through the following properties:
| Property | Type | Description |
|---|---|---|
DepletionReason | string | A human-readable description of why the pool was depleted |
AvailableClients | int | Number of currently available clients in the pool at the time of exception |
TotalPoolSize | int | The total configured size of the session pool |
FailedReconnections | int | Number of failed reconnection attempts since the pool was opened |
using Apache.IoTDB; using System; try { var sessionPool = new SessionPool.Builder() .SetHost("127.0.0.1") .SetPort(6667) .SetPoolSize(4) .Build(); await sessionPool.Open(); // Perform operations... await sessionPool.InsertRecordAsync("root.sg.d1", record); } catch (SessionPoolDepletedException ex) { Console.WriteLine($"Pool depleted: {ex.DepletionReason}"); Console.WriteLine($"Available clients: {ex.AvailableClients}/{ex.TotalPoolSize}"); Console.WriteLine($"Failed reconnections: {ex.FailedReconnections}"); // Implement recovery strategy (see below) }
CheckHealthAsync vs IsOpenSessionPool.IsOpen() reports whether you have opened the pool and not yet closed it. It is a lifecycle flag, not a connectivity probe:
true after a successful Open() and only returns to false when you call Close().false. Reconnection happens lazily, on the next operation.This makes the following common guard a trap - it short-circuits forever, so the pool is never rebuilt:
// Anti-pattern: IsOpen() stays true even while every connection is dead if (_pool != null && _pool.IsOpen()) return;
Use CheckHealthAsync when you need to know whether the server is actually reachable. It issues one lightweight request on an idle pooled connection and returns a SessionPoolHealth snapshot:
var health = await sessionPool.CheckHealthAsync(); if (!health.IsHealthy) { Console.WriteLine(health); // e.g. "Unhealthy: The server did not answer the probe: ..." Console.WriteLine(health.Status); // NotOpen | Healthy | Degraded | Unhealthy Console.WriteLine(health.Error); // the underlying exception, when Status is Unhealthy }
| Status | Meaning |
|---|---|
NotOpen | The pool has not been opened yet, or has already been closed. No network call is attempted. |
Healthy | The server answered the probe. |
Degraded | The pool is open but no connection was idle to probe with. Says nothing about the server. |
Unhealthy | A connection was available but the server did not answer. See Error for the cause. |
Degraded immediately instead of queueing behind ordinary work, so a health endpoint cannot be starved by application load. It is also unaffected by the pool wait timeout described below.Degraded is not a server verdict. Under high concurrency it simply means the pool was saturated at that instant. Treat a persistent Degraded as a sizing signal, not an outage.using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2)); var health = await sessionPool.CheckHealthAsync(cts.Token);
TableSessionPool exposes the same CheckHealthAsync method with identical semantics.
Two independent timeouts govern a pool operation:
| Setting | Unit | Default | Controls |
|---|---|---|---|
SetConnectionTimeoutInMs(int) | ms | 500 | Socket-level send/receive timeout of an individual connection |
SetPoolWaitTimeoutInMs(int) | ms | 10000 | How long an operation waits for a free client before the pool gives up |
var sessionPool = new SessionPool.Builder() .SetHost("127.0.0.1") .SetPort(6667) .SetPoolSize(8) .SetConnectionTimeoutInMs(500) // socket timeout .SetPoolWaitTimeoutInMs(10_000) // give up after 10s of waiting for a free client .Build();
When the wait budget is exhausted, the operation throws SessionPoolDepletedException with the reason Connection pool is empty and wait time out(...ms). Raise SetPoolWaitTimeoutInMs if your workload legitimately queues behind long operations; lower it if you would rather fail fast and retry. The budget is a single deadline for the whole call: waiters woken by a returned connection that they lose the race for do not restart it.
Note: before this setting existed, the wait budget was derived from the connection timeout and then misinterpreted as seconds, which turned the 500 ms default into a ~41 minute block. If you are upgrading from an older version and relied on that (unintended) long wait, set
SetPoolWaitTimeoutInMsexplicitly.
The SessionPool class exposes real-time health metrics that can be used for monitoring and alerting:
var sessionPool = new SessionPool.Builder() .SetHost("127.0.0.1") .SetPort(6667) .SetPoolSize(8) .Build(); await sessionPool.Open(); // Check pool health Console.WriteLine($"Available Clients: {sessionPool.AvailableClients}"); Console.WriteLine($"Total Pool Size: {sessionPool.TotalPoolSize}"); Console.WriteLine($"Unrealized Capacity: {sessionPool.UnrealizedCapacity}"); Console.WriteLine($"Failed Reconnections: {sessionPool.FailedReconnections}");
| Metric | Property | Description | Recommended Threshold |
|---|---|---|---|
| Available Clients | AvailableClients | Number of idle clients ready for use | Alert if < 25% of pool size |
| Total Pool Size | TotalPoolSize | Configured maximum pool size | N/A (constant) |
| Unrealized Capacity | UnrealizedCapacity | Configured capacity currently holding no connection, refilled on demand | Not an alert signal on its own - see below |
| Failed Reconnections | FailedReconnections | Cumulative count of failed reconnection attempts | Alert if > 0 and increasing |
When an operation fails and reconnection also fails, the dead connection is discarded but its capacity is retained rather than lost. UnrealizedCapacity counts the capacity left without a connection, and an acquisition that finds no idle client materializes one connection before falling back to waiting. Consequences:
Close() + Open() cycle is required.UnrealizedCapacity legitimately stays above zero long after the server has fully recovered. It measures how much of the configured pool has not been materialized, not server availability.UnrealizedCapacity alone. Use FailedReconnections to reason about server reachability: it only increases when a reconnection actually fails.Symptoms:
SessionPoolDepletedException with reason “Connection pool is empty and wait time out”AvailableClients = 0FailedReconnections = 0 or lowRoot Cause: Application workload exceeds pool capacity
Recovery Strategies:
var sessionPool = new SessionPool.Builder() .SetHost("127.0.0.1") .SetPort(6667) .SetPoolSize(16) // Increased from 8 .Build();
int maxRetries = 3; int retryDelayMs = 1000; for (int i = 0; i < maxRetries; i++) { try { await sessionPool.InsertRecordAsync(deviceId, record); break; // Success } catch (SessionPoolDepletedException ex) when (i < maxRetries - 1) { await Task.Delay(retryDelayMs * (i + 1)); // Exponential backoff } }
Symptoms:
SessionPoolDepletedException with reason “Reconnection failed”AvailableClients drops toward 0 while UnrealizedCapacity risesFailedReconnections > 0 and increasing (this, not UnrealizedCapacity, is the outage signal)Root Cause: IoTDB server unreachable or network issues
Recovery Strategies:
catch (SessionPoolDepletedException ex) { // Capacity is retained and refilled on demand - just back off and try again await Task.Delay(2000); }
catch (SessionPoolDepletedException ex) when (ex.FailedReconnections > 5) { Console.WriteLine($"Critical: {ex.FailedReconnections} failed reconnections"); // Close existing pool await sessionPool.Close(); // Wait for network recovery await Task.Delay(5000); // Create new pool sessionPool = new SessionPool.Builder() .SetHost("127.0.0.1") .SetPort(6667) .SetPoolSize(8) .Build(); await sessionPool.Open(); }
public class SessionPoolCircuitBreaker { private SessionPool _pool; private int _failureCount = 0; private const int FailureThreshold = 5; private bool _circuitOpen = false; private DateTime _lastFailureTime; public async Task<T> ExecuteAsync<T>(Func<SessionPool, Task<T>> operation) { if (_circuitOpen && DateTime.Now - _lastFailureTime < TimeSpan.FromMinutes(1)) { throw new Exception("Circuit breaker is open"); } try { var result = await operation(_pool); _failureCount = 0; // Reset on success _circuitOpen = false; return result; } catch (SessionPoolDepletedException ex) { _failureCount++; _lastFailureTime = DateTime.Now; if (_failureCount >= FailureThreshold) { _circuitOpen = true; Console.WriteLine("Circuit breaker opened - too many failures"); } throw; } } }
Symptoms:
SessionPoolDepletedExceptionRoot Cause: IoTDB server is overloaded
Recovery Strategies:
using System.Threading; private SemaphoreSlim _rateLimiter = new SemaphoreSlim(10, 10); // Max 10 concurrent operations public async Task RateLimitedInsert(string deviceId, RowRecord record) { await _rateLimiter.WaitAsync(); try { await sessionPool.InsertRecordAsync(deviceId, record); } finally { _rateLimiter.Release(); } }
var sessionPool = new SessionPool.Builder() .SetHost("127.0.0.1") .SetPort(6667) .SetConnectionTimeoutInMs(5000) // Increased socket timeout for a slow server .Build();
The simplest health check delegates to the built-in probe and only adds your own policy on top:
public class SessionPoolHealthCheck { private readonly SessionPool _pool; public SessionPoolHealthCheck(SessionPool pool) { _pool = pool; } public async Task<HealthStatus> CheckHealthAsync() { using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2)); var health = await _pool.CheckHealthAsync(cts.Token); if (health.Status == SessionPoolHealthStatus.Unhealthy) { return new HealthStatus { Status = "Critical", Message = health.Message, Recommendation = "Check IoTDB server availability" }; } if (health.Status == SessionPoolHealthStatus.Degraded) { return new HealthStatus { Status = "Warning", Message = $"No idle connection to probe ({health.AvailableClients}/{health.TotalPoolSize} available)", Recommendation = "Consider increasing pool size" }; } return new HealthStatus { Status = "Healthy", Message = health.Message }; } } public class HealthStatus { public string Status { get; set; } public string Message { get; set; } public string Recommendation { get; set; } }
If you would rather not issue a network call on every scrape, the metrics below can be sampled on their own - just remember they describe the pool, not the server.
// Example: Export metrics to Prometheus, StatsD, or similar public class SessionPoolMetricsCollector { private readonly SessionPool _pool; public void CollectMetrics() { // Gauge: Current available clients MetricsCollector.Set("iotdb_pool_available_clients", _pool.AvailableClients); // Gauge: Total pool size MetricsCollector.Set("iotdb_pool_total_size", _pool.TotalPoolSize); // Counter: Failed reconnections MetricsCollector.Set("iotdb_pool_failed_reconnections", _pool.FailedReconnections); // Calculated: Pool utilization percentage var utilization = (1.0 - (double)_pool.AvailableClients / _pool.TotalPoolSize) * 100; MetricsCollector.Set("iotdb_pool_utilization_percent", utilization); } }
Critical Alerts:
FailedReconnections > 10: Server connectivity issuesAvailableClients == 0 for > 30 seconds: Complete pool exhaustionWarning Alerts:
AvailableClients < TotalPoolSize * 0.25: Pool under pressureFailedReconnections > 0 and increasing: Network instabilityInfo Alerts:
Pool Sizing:
Error Handling:
SessionPoolDepletedException specificallyMonitoring:
AvailableClients metricFailedReconnections as a leading indicator of problemsResource Management:
sessionPool.Close() when doneusing statements or try-finally blocks for proper cleanupusing Apache.IoTDB; using System; using System.Threading.Tasks; public class ProductionSessionPoolManager { private SessionPool _pool; private readonly object _lock = new object(); public async Task Initialize() { _pool = new SessionPool.Builder() .SetHost("127.0.0.1") .SetPort(6667) .SetPoolSize(8) .SetConnectionTimeoutInMs(5000) .Build(); await _pool.Open(); // Start health monitoring _ = Task.Run(MonitorHealth); } public async Task<T> ExecuteWithRetry<T>(Func<SessionPool, Task<T>> operation) { const int maxRetries = 3; const int baseDelayMs = 1000; for (int attempt = 0; attempt < maxRetries; attempt++) { try { return await operation(_pool); } catch (SessionPoolDepletedException ex) { Console.WriteLine($"Attempt {attempt + 1} failed: {ex.Message}"); Console.WriteLine($"Pool state - Available: {ex.AvailableClients}/{ex.TotalPoolSize}, Failed reconnections: {ex.FailedReconnections}"); if (attempt == maxRetries - 1) { // Last attempt failed if (ex.FailedReconnections > 5) { // Reinitialize pool await ReinitializePool(); } throw; } // Exponential backoff await Task.Delay(baseDelayMs * (int)Math.Pow(2, attempt)); } } throw new InvalidOperationException("Should not reach here"); } private async Task ReinitializePool() { lock (_lock) { try { _pool?.Close().Wait(); } catch { } } await Task.Delay(5000); // Wait for server recovery await Initialize(); } private async Task MonitorHealth() { while (true) { await Task.Delay(10000); // Check every 10 seconds try { var availableRatio = (double)_pool.AvailableClients / _pool.TotalPoolSize; if (_pool.FailedReconnections > 10) { Console.WriteLine($"CRITICAL: {_pool.FailedReconnections} failed reconnections"); } else if (availableRatio < 0.25) { Console.WriteLine($"WARNING: Low available clients - {_pool.AvailableClients}/{_pool.TotalPoolSize}"); } } catch (Exception ex) { Console.WriteLine($"Health check failed: {ex.Message}"); } } } public async Task Cleanup() { await _pool?.Close(); } }
The SessionPool exception handling and health monitoring features provide comprehensive tools for building robust IoTDB applications:
SessionPoolDepletedException to understand and react to pool issuesCheckHealthAsync for connectivity checks; IsOpen() is a lifecycle flag onlySetPoolWaitTimeoutInMs separately from SetConnectionTimeoutInMsAvailableClients, TotalPoolSize, and FailedReconnections; read UnrealizedCapacity as capacity not yet materialized rather than as an outage signal