EvaluatedApplications.HoloDb.Client
1.5.2
See the version list below for details.
dotnet add package EvaluatedApplications.HoloDb.Client --version 1.5.2
NuGet\Install-Package EvaluatedApplications.HoloDb.Client -Version 1.5.2
<PackageReference Include="EvaluatedApplications.HoloDb.Client" Version="1.5.2" />
<PackageVersion Include="EvaluatedApplications.HoloDb.Client" Version="1.5.2" />
<PackageReference Include="EvaluatedApplications.HoloDb.Client" />
paket add EvaluatedApplications.HoloDb.Client --version 1.5.2
#r "nuget: EvaluatedApplications.HoloDb.Client, 1.5.2"
#:package EvaluatedApplications.HoloDb.Client@1.5.2
#addin nuget:?package=EvaluatedApplications.HoloDb.Client&version=1.5.2
#tool nuget:?package=EvaluatedApplications.HoloDb.Client&version=1.5.2
HoloDb.Client
Beta. The public API may change between versions while we improve it. Breaking changes always come with a new major version, and the release notes say what changed.
A thin TCP+TLS .NET client for a remote HoloDb server. It mirrors the in-process HoloDbService surface exactly — Execute / ExecuteAsync / BulkLoadAsync and the same QueryResult type — so moving from embedded to networked changes only how you get the handle. Columnar results stay columnar over the wire (typed arrays, no JSON). One connection = one session; requests serialize.
Quick start
using HoloDb.Client;
// Connect with TLS, auth token, and pinned certificate
await using var db = await HoloDbClient.ConnectAsync("your-host", 5432, new HoloDbClientOptions
{
AuthToken = "s3cret",
PinnedThumbprint = "ABCD...", // Server prints this on startup
});
// Execute a query — get columnar results (no boxing)
var result = await db.ExecuteAsync("SELECT id, name FROM users WHERE active = true");
for (var i = 0; i < result.RowCount; i++)
{
var id = result.GetLong(i, 0);
var name = result.GetText(i, 1);
Console.WriteLine($"User {id}: {name}");
}
// Bulk-load data (column-native, no SQL parsing)
await db.BulkLoadAsync("users", new[] { "id", "age", "name" },
new Array[] {
new long[] { 2, 3 },
new long[] { 25, 41 },
new[] { "Ben", "Cara" }
},
count: 2);
// Get server stats
var stats = await db.GetStatsAsync();
Console.WriteLine($"Uptime: {stats["uptime_seconds"]}, Queries: {stats["query_count"]}");
// Ping the server (throws on drop, returns true if alive)
await db.PingAsync();
Transactions are per-session — send SQL BEGIN/COMMIT/ROLLBACK as plain text. For pooled connections, bounded timeouts, or auto-reconnect-on-drop, see the Resilience & Pooling section.
Table of Contents
- Core Client
- Pooling
- Resilience & Retry
- Exceptions
- Connection Lifecycle & Gotchas
- Thread Safety & Concurrency
- Server Discovery & Deployment
Core Client
HoloDbClientOptions
Connection configuration passed to ConnectAsync. All properties are optional; default is to enable TLS with no cert pinning (so you must set AllowUntrustedCertificate = true for dev, or pin a thumbprint for production).
| Property | Purpose |
|---|---|
AuthToken : string? |
Shared secret presented during TLS handshake. Server-side set via --token. Sent over TLS, never unencrypted. Defaults to empty string if not set. |
UseTls : bool (default true) |
Enable TLS encryption. Must match the server's config. |
TlsHost : string? |
SNI host name for TLS validation. Defaults to the host passed to ConnectAsync. Set this if your DNS CNAME differs from the cert's CN. |
PinnedThumbprint : string? |
Pin the server's certificate by thumbprint (recommended for self-hosted/self-signed servers). When set, the connection accepts the cert iff its SHA1 thumbprint matches exactly, ignoring CA chain. Overrides AllowUntrustedCertificate. Server prints its own thumbprint on startup. |
AllowUntrustedCertificate : bool |
Dev-only: accept any certificate (no MITM protection). Ignored if PinnedThumbprint is set. Never use in production. |
Example:
var options = new HoloDbClientOptions
{
AuthToken = "my-secret",
UseTls = true,
TlsHost = "db.example.com", // Custom SNI name
PinnedThumbprint = "1A2B3C...", // Pin server cert
AllowUntrustedCertificate = false
};
await using var db = await HoloDbClient.ConnectAsync("db.internal", 5432, options);
HoloDbClient
The core stateful connection. One instance = one TCP+TLS session. Requests serialize through an internal mutex (no concurrent requests on one client). Transactions (BEGIN/COMMIT/ROLLBACK) are scoped to this session; a dropped connection loses mid-transaction state.
Connecting
static Task<HoloDbClient> ConnectAsync(string host, int port, CancellationToken ct = default)
Connect with default options (TLS on, no pinning). Requires AllowUntrustedCertificate = true for self-signed certs, or set a PinnedThumbprint in the next overload.
await using var db = await HoloDbClient.ConnectAsync("localhost", 5432);
static Task<HoloDbClient> ConnectAsync(string host, int port, HoloDbClientOptions options, CancellationToken ct = default)
Connect with explicit options (auth token, pinning, SNI, etc).
var opts = new HoloDbClientOptions { AuthToken = "secret", PinnedThumbprint = "ABC..." };
await using var db = await HoloDbClient.ConnectAsync("db.example.com", 5432, opts);
static Task<HoloDbClient> ConnectAsync(string host, int port, TimeSpan connectTimeout, CancellationToken ct = default)
Connect with a bounded timeout (TLS + handshake must complete within connectTimeout). Throws TimeoutException if deadline is missed, or OperationCanceledException if ct is cancelled.
try
{
await using var db = await HoloDbClient.ConnectAsync(
"db.example.com", 5432,
TimeSpan.FromSeconds(5));
}
catch (TimeoutException) { Console.WriteLine("Connect took too long"); }
catch (OperationCanceledException) { Console.WriteLine("Caller cancelled"); }
static Task<HoloDbClient> ConnectAsync(string host, int port, HoloDbClientOptions options, TimeSpan connectTimeout, CancellationToken ct = default)
Connect with both options and a timeout.
var opts = new HoloDbClientOptions { AuthToken = "secret" };
await using var db = await HoloDbClient.ConnectAsync(
"db.example.com", 5432, opts,
TimeSpan.FromSeconds(5));
Properties & Info
string ServerInfo { get; }
Handshake info from the server (usually version/capabilities string). Set at connect time, does not change. Read after a successful connect.
var db = await HoloDbClient.ConnectAsync("localhost", 5432);
Console.WriteLine($"Connected to: {db.ServerInfo}");
Querying
Task<QueryResult> ExecuteAsync(string sql, CancellationToken ct = default)
Execute a SQL statement asynchronously. Returns a QueryResult (columnar typed arrays). Throws HoloRemoteException on app-level errors (bad SQL, auth failure) and HoloProtocolException on connection failure.
var result = await db.ExecuteAsync(
"SELECT id, name, age FROM users WHERE age > 18");
Console.WriteLine($"Rows: {result.RowCount}");
for (var i = 0; i < result.RowCount; i++)
{
var id = result.GetLong(i, 0);
var name = result.GetText(i, 1);
var age = result.GetLong(i, 2);
Console.WriteLine($" {id}: {name} ({age})");
}
QueryResult Execute(string sql)
Synchronous wrapper over ExecuteAsync (blocking). Avoid on UI threads. Prefer ExecuteAsync in async code.
var result = db.Execute("SELECT 1 AS n");
Console.WriteLine($"Result: {result.GetLong(0, 0)}");
Bulk Loading
Task<int> BulkLoadAsync(string table, IReadOnlyList<string> cols, IReadOnlyList<Array> data, int count, CancellationToken ct = default)
Column-native bulk insert (no SQL parsing on either side). Each element of data is a typed array for one column (e.g. long[], string[], double[], etc). count is the number of rows. Returns number of rows inserted.
var rowsInserted = await db.BulkLoadAsync(
table: "products",
cols: new[] { "id", "name", "price" },
data: new Array[]
{
new long[] { 1, 2, 3 }, // id column
new[] { "Widget", "Gadget", "Doohickey" }, // name column
new double[] { 9.99, 19.99, 14.99 } // price column
},
count: 3);
Console.WriteLine($"Inserted {rowsInserted} rows");
Server Stats
Task<IReadOnlyDictionary<string, long>> GetStatsAsync(CancellationToken ct = default)
Fetch server performance counters (uptime, connection count, query count, transaction commits/rollbacks, aborts, WAL size, etc). Keys and values depend on the server version.
var stats = await db.GetStatsAsync();
Console.WriteLine($"Connections: {stats["connection_count"]}");
Console.WriteLine($"Queries: {stats["query_count"]}");
Console.WriteLine($"WAL size: {stats["wal_bytes"]}");
Health Check
Task<bool> PingAsync(CancellationToken ct = default)
Send a keep-alive Ping frame. Returns true if the server replies with Pong (normal case). Throws HoloProtocolException if the connection is dropped before a reply arrives (matching ExecuteAsync, BulkLoadAsync, GetStatsAsync — all fail the same way). Returns false only if a live connection sends a non-Pong reply (not seen in practice today).
try
{
var alive = await db.PingAsync();
if (alive) Console.WriteLine("Server is up");
}
catch (HoloProtocolException ex)
{
Console.WriteLine($"Connection dropped: {ex.Message}");
// Reconnect here if desired, or use ResilientHoloDbClient
}
Cleanup
ValueTask DisposeAsync()
Closes the TLS stream and TCP socket. Called automatically by await using. Once disposed, all methods throw ObjectDisposedException.
await using (var db = await HoloDbClient.ConnectAsync("localhost", 5432))
{
// ... use db ...
} // DisposeAsync() called here
Pooling
HoloDbClientPool
Owns a fixed-size set of pre-connected HoloDbClient instances and rents them out exclusively — one client per concurrent worker, with no multiplexing. Useful to avoid hand-rolling "one client per worker" bookkeeping; does not add reconnect/retry (that is the job of ResilientHoloDbClient).
Sizing Guidelines
Each client is a single-session, request-serialized connection — no multiplexing or contention inside it. Size the pool to your expected concurrent worker count, not by HikariCP rules:
- If you have 8 worker threads, size the pool to 8.
- Check server limits (HoloDb default: 100 open connections) and stay below.
- Start small and measure: 4–16 typical. Watch lease-wait latency and idle slots.
- Undersized → workers queue for leases → latency climbs.
- Oversized → wastes memory/file descriptors, no throughput gain (each query still serializes through its client's mutex).
Connecting
static Task<HoloDbClientPool> ConnectAsync(string host, int port, int size, HoloDbClientOptions? options = null, CancellationToken ct = default)
Connect size clients up front, all with the same options. Throws if any connection fails; all connected clients so far are disposed. size must be positive.
// Connect 8 clients, reusing them across workers
await using var pool = await HoloDbClientPool.ConnectAsync(
"db.example.com", 5432,
size: 8,
options: new HoloDbClientOptions { AuthToken = "secret" });
// ... do work, then dispose all 8
Properties
int Size { get; }
Number of clients in the pool (fixed at construction).
Console.WriteLine($"Pool size: {pool.Size}");
Renting
Task<Lease> RentAsync(CancellationToken ct = default)
Rent an exclusive client. Blocks until one is free. Dispose the returned Lease (not the client itself) to return it to the pool.
using (var lease = await pool.RentAsync())
{
var result = await lease.Client.ExecuteAsync("SELECT COUNT(*) FROM users");
Console.WriteLine($"User count: {result.GetLong(0, 0)}");
} // Lease disposed here; client returned to pool
Throws ObjectDisposedException if the pool has been disposed.
Cleanup
ValueTask DisposeAsync()
Closes all clients in the pool. After disposal, RentAsync throws ObjectDisposedException.
await pool.DisposeAsync();
Lease
A lightweight struct wrapping a rented client. Dispose to return it to the pool (does not close the underlying connection).
Properties
HoloDbClient Client { get; }
The rented client instance. Valid until the lease is disposed.
using (var lease = await pool.RentAsync())
{
await lease.Client.ExecuteAsync("INSERT INTO log VALUES (1, 'msg')");
}
Resilience & Retry
ResilientHoloDbClient
An opt-in decorator around HoloDbClient that adds reconnect-and-retry on transient transport failures. HoloDbClient itself is reconnect-free by design; use this when you want auto-recovery instead of handling reconnects yourself.
Key invariant: Reconnects create a brand-new session (fresh TCP+TLS handshake). Any mid-transaction state in the old session is lost. Only retry idempotent/self-contained operations — never ones assuming transaction state survived the reconnect.
Connecting
static Task<ResilientHoloDbClient> ConnectAsync(string host, int port, HoloDbClientOptions? options = null, int maxAttempts = 3, TimeSpan? retryDelay = null, TimeSpan? maxRetryDelay = null, int? breakerFailureThreshold = null, TimeSpan? breakerCooldown = null, CancellationToken ct = default)
Connect with opt-in retry/backoff/circuit-breaker settings. All parameters except the first three are optional and additive.
| Parameter | Purpose | Default |
|---|---|---|
maxAttempts |
Total tries per call (1 initial + 2 retries = 3 total). Must be ≥ 1. | 3 |
retryDelay |
Base delay for exponential backoff between attempts. See Backoff Tuning. | 200 ms |
maxRetryDelay |
Cap on backed-off delay. Grows exponentially up to this, then clamps. | 5 seconds |
breakerFailureThreshold |
Circuit breaker threshold: after this many consecutive whole calls fail with transient errors, the breaker opens. null = disabled (previous behavior). |
null (off) |
breakerCooldown |
How long the breaker stays open before admitting a probe. Only used if threshold is set. | 30 seconds |
// Bare resilience: retry up to 3 times with exponential backoff
await using var resilient = await ResilientHoloDbClient.ConnectAsync(
"db.example.com", 5432);
// With circuit breaker: open after 5 consecutive call failures, cooldown 30s
await using var resilient = await ResilientHoloDbClient.ConnectAsync(
"db.example.com", 5432,
maxAttempts: 3,
breakerFailureThreshold: 5,
breakerCooldown: TimeSpan.FromSeconds(30));
// Custom backoff: 100ms base, 10s max
await using var resilient = await ResilientHoloDbClient.ConnectAsync(
"db.example.com", 5432,
retryDelay: TimeSpan.FromMilliseconds(100),
maxRetryDelay: TimeSpan.FromSeconds(10));
Properties
string ServerInfo { get; }
Handshake info from the current session. Changes when a reconnect happens.
Console.WriteLine($"Server: {resilient.ServerInfo}");
Operations
All methods mirror HoloDbClient's surface, plus an optional operationTimeout parameter (deadline for the whole call, not one attempt — see Gotchas for composition rules).
Task<QueryResult> ExecuteAsync(string sql, TimeSpan? operationTimeout = null, CancellationToken ct = default)
Execute SQL with auto-retry on transient failures.
try
{
var result = await resilient.ExecuteAsync(
"SELECT 1",
operationTimeout: TimeSpan.FromSeconds(10)); // Whole call must finish in 10s
Console.WriteLine($"Rows: {result.RowCount}");
}
catch (HoloRemoteException ex)
{
// App-level error (bad SQL, auth). Not retried.
Console.WriteLine($"Query failed: {ex.Message}");
}
catch (TimeoutException)
{
// operationTimeout deadline exceeded
Console.WriteLine("Operation took too long");
}
catch (CircuitBreakerOpenException ex)
{
// Breaker is open; retry after ex.RetryAfter
Console.WriteLine($"Breaker open, retry after {ex.RetryAfter}");
}
QueryResult Execute(string sql, TimeSpan? operationTimeout = null)
Synchronous wrapper (blocking). Prefer ExecuteAsync.
var result = resilient.Execute("SELECT 42");
Task<int> BulkLoadAsync(string table, IReadOnlyList<string> cols, IReadOnlyList<Array> data, int count, TimeSpan? operationTimeout = null, CancellationToken ct = default)
Bulk insert with auto-retry.
var inserted = await resilient.BulkLoadAsync(
"users",
new[] { "id", "name" },
new Array[] { new long[] { 1 }, new[] { "Alice" } },
count: 1,
operationTimeout: TimeSpan.FromSeconds(5));
Task<IReadOnlyDictionary<string, long>> GetStatsAsync(TimeSpan? operationTimeout = null, CancellationToken ct = default)
Fetch server stats with auto-retry.
var stats = await resilient.GetStatsAsync(
operationTimeout: TimeSpan.FromSeconds(5));
Task<bool> PingAsync(TimeSpan? operationTimeout = null, CancellationToken ct = default)
Send keep-alive with auto-retry. Throws HoloProtocolException on drop (retried), or CircuitBreakerOpenException if the breaker is open.
try
{
await resilient.PingAsync();
Console.WriteLine("Ping succeeded");
}
catch (CircuitBreakerOpenException)
{
Console.WriteLine("Server is down (breaker open)");
}
Cleanup
ValueTask DisposeAsync()
Closes the current session and cleanup resources.
await resilient.DisposeAsync();
Circuit Breaker
Layered on top of retry/backoff. Disabled by default (breakerFailureThreshold = null). Opt in to prevent hammering a down server.
State machine:
- Closed (normal): calls go through with normal retry/backoff.
- Open: every call fails fast with
CircuitBreakerOpenException(no connection attempted) until cooldown elapses. - HalfOpen: cooldown expired; exactly one call admitted as a probe. Success → Closed + reset counters. Transient failure → re-Open + restart cooldown.
Opening rule: After breakerFailureThreshold consecutive whole calls (not individual retry attempts) end in a transient failure, the breaker opens.
Transient failures (retried by default, counted by breaker):
IOException(network/stream errors)SocketException(connection lost)ObjectDisposedException(connection disposed mid-call)HoloProtocolException(framing error)
Application-level errors (never retried, treated as success by breaker):
HoloRemoteException(bad SQL, auth failure, etc) — connection is alive, no breaker penalty.CircuitBreakerOpenException— breaker only (not a transient network error).
Cancellation — neither a caller-cancelled ct nor an expired operationTimeout count as pass/fail for the breaker; they're "the caller's own budget", not a server verdict.
Example:
// Open breaker after 3 consecutive call failures, cooldown 10s
await using var resilient = await ResilientHoloDbClient.ConnectAsync(
"db.example.com", 5432,
breakerFailureThreshold: 3,
breakerCooldown: TimeSpan.FromSeconds(10));
// If the server is down, this will fail immediately (no retry)
// after it accumulates 3 failures
try
{
await resilient.ExecuteAsync("SELECT 1");
}
catch (CircuitBreakerOpenException ex)
{
if (ex.RetryAfter > TimeSpan.Zero)
Console.WriteLine($"Breaker open, retry after {ex.RetryAfter}");
else
Console.WriteLine("Breaker half-open; another call already probing");
}
Backoff Tuning
Retry delays grow exponentially with full jitter (AWS "full jitter" strategy):
Formula: delay_before_retry_N = full_jitter( min(baseDelay * 2^N, maxRetryDelay) )
Where:
N= retry number (0-based; first retry is N=0)baseDelay=retryDelayparam (default 200 ms)maxRetryDelay= max cap (default 5 s)full_jitter= uniformly random in[0, cap](not[0.5*cap, cap]or other variants)
Example delays (baseDelay=200ms, maxRetryDelay=5s):
| Retry | Min cap | Full-jitter range |
|---|---|---|
| 0 (1st retry) | 200 ms | [0, 200 ms] |
| 1 (2nd retry) | 400 ms | [0, 400 ms] |
| 2 (3rd retry) | 800 ms | [0, 800 ms] |
| 3+ | 5 s | [0, 5 s] |
Why full jitter? If all workers retry in lockstep, they hammer the recovering server in a thundering herd. Full jitter spreads them out randomly.
Tuning:
// Aggressive: retry fast (50ms base, 1s max) — good for flaky local networks
await using var resilient = await ResilientHoloDbClient.ConnectAsync(
"db.local", 5432,
retryDelay: TimeSpan.FromMilliseconds(50),
maxRetryDelay: TimeSpan.FromSeconds(1));
// Conservative: slow backoff (1s base, 30s max) — good when server recovery is slow
await using var resilient = await ResilientHoloDbClient.ConnectAsync(
"db.example.com", 5432,
retryDelay: TimeSpan.FromSeconds(1),
maxRetryDelay: TimeSpan.FromSeconds(30));
Exceptions
HoloProtocolException
Framing, handshake, or low-level connection error (not an application-level SQL error). Thrown by any operation when the wire protocol state is unexpected (server closes mid-handshake, frame format is corrupt, connection drops before a reply arrives, etc). Retried by ResilientHoloDbClient (transient).
try
{
await db.ExecuteAsync("SELECT 1");
}
catch (HoloProtocolException ex)
{
Console.WriteLine($"Protocol error: {ex.Message}");
// Connection is likely dead. Reconnect if desired.
}
HoloRemoteException
Application-level error reported by the server (bad SQL, auth failure, constraint violation, etc). Sent over a live connection and decoded successfully. Never retried by ResilientHoloDbClient (proves the connection works; the problem is the request, not the network).
try
{
await db.ExecuteAsync("INSERT INTO users (id) VALUES (null)"); // id is NOT NULL
}
catch (HoloRemoteException ex)
{
Console.WriteLine($"Query error: {ex.Message}");
// Not a transient error; check your SQL/data.
}
CircuitBreakerOpenException
The circuit breaker rejected the call outright (Open state, cooldown not yet elapsed, or HalfOpen with a probe already in flight). No connection attempt was made. A RetryAfter property says when to retry (or Zero if a concurrent probe is already checking).
Not retried by ResilientHoloDbClient (retrying would defeat the breaker). A caller wanting to keep working should back off (e.g., skip this worker's turn) rather than retry immediately.
try
{
await resilient.ExecuteAsync("SELECT 1");
}
catch (CircuitBreakerOpenException ex)
{
if (ex.RetryAfter > TimeSpan.Zero)
{
Console.WriteLine($"Breaker open. Retry after {ex.RetryAfter}.");
await Task.Delay(ex.RetryAfter);
// ... try again on next request, or skip this turn
}
else
{
Console.WriteLine("Breaker half-open; another caller is already probing.");
}
}
TimeoutException
Operation deadline exceeded (either HoloDbClient.ConnectAsync(..., connectTimeout) or ResilientHoloDbClient.ExecuteAsync(..., operationTimeout)). Distinct from OperationCanceledException (caller-driven cancellation via CancellationToken). Neither is retried by resilient client (both indicate "our own budget ran out", not a server failure).
try
{
await db.ExecuteAsync("SELECT 1", operationTimeout: TimeSpan.FromSeconds(2));
}
catch (TimeoutException)
{
Console.WriteLine("Operation took too long");
}
catch (OperationCanceledException)
{
Console.WriteLine("Caller cancelled via CancellationToken");
}
ObjectDisposedException
Thrown when a method is called on a disposed client/pool. Retried by ResilientHoloDbClient (if the underlying session was disposed and a fresh one is created, the retry may succeed).
var db = await HoloDbClient.ConnectAsync("localhost", 5432);
await db.DisposeAsync();
try
{
await db.ExecuteAsync("SELECT 1");
}
catch (ObjectDisposedException)
{
Console.WriteLine("Client is disposed");
}
Connection Lifecycle & Gotchas
One Session Per Connection
Each HoloDbClient is one TCP+TLS session. Transactions (BEGIN/COMMIT/ROLLBACK) are scoped to it:
await db.ExecuteAsync("BEGIN");
await db.ExecuteAsync("INSERT INTO t VALUES (1)");
// If the connection drops here, the transaction is lost.
await db.ExecuteAsync("COMMIT");
If the connection drops mid-transaction and you reconnect, that transaction is rolled back on the server, and the new session starts fresh. Only ResilientHoloDbClient can reconnect automatically; HoloDbClient leaves it to you.
Requests Serialize
Concurrent callers on the same HoloDbClient are queued (internal SemaphoreSlim(1, 1)). They do not parallelize:
// DON'T DO THIS on a single client:
var t1 = db.ExecuteAsync("SELECT ... "); // waits for mutex
var t2 = db.ExecuteAsync("SELECT ... "); // waits in queue
await Task.WhenAll(t1, t2); // t2 only runs after t1 completes
// DO THIS instead: one client per concurrent worker (use HoloDbClientPool)
TLS Certificate Validation
TLS is on by default (UseTls = true). Trust model:
Pinned thumbprint (recommended): set
PinnedThumbprintto the server's cert SHA1. Server prints it on startup. Ignores CA chain and hostname; accepts the cert iff thumbprints match. Safest for self-hosted/self-signed.var opts = new HoloDbClientOptions { PinnedThumbprint = "1A2B3C..." };AllowUntrustedCertificate (dev only): accept any cert (no MITM protection).
var opts = new HoloDbClientOptions { AllowUntrustedCertificate = true };CA chain (production): neither pinning nor allowing untrusted → standard CA chain validation (cert must be signed by a trusted root).
Wire-Compatibility Version
The client and server exchange a version byte during handshake (HoloProtocol.Version). Currently NOT enforced — both sides send and receive it, but neither validates. A version mismatch causes opaque decode errors downstream, not a clean rejection. Stay in sync by using the same HoloDb.Protocol package version on client and server.
Reconnect Loses Transaction State
A dropped connection (or an intentional reconnect via ResilientHoloDbClient) means a brand-new session:
var resilient = await ResilientHoloDbClient.ConnectAsync("db.example.com", 5432);
await resilient.ExecuteAsync("BEGIN");
// Connection drops; auto-retry creates a fresh session
await resilient.ExecuteAsync("INSERT INTO t VALUES (1)");
// This runs in a NEW transaction context; the original BEGIN is lost.
Only wrap idempotent operations (or ones safe to re-run) in resilient retry. Don't rely on mid-transaction state surviving a reconnect.
Cancel & Timeout Composition (ResilientHoloDbClient)
When both a caller CancellationToken and an operationTimeout deadline can expire, ordering matters:
- A cancelled
ctalways wins first (checked on entry and each retry-loop iteration). - An expired
operationTimeoutdeadline fails fast before the circuit breaker is consulted (prevents hammering an Open breaker while out of time). - Neither cancellation nor a deadline timeout counts as a circuit-breaker pass/fail (both are "caller's budget", not server health).
- A retry is never started if its backoff cap alone would exceed remaining budget (prevents sleeping for a delay that would exceed the deadline).
- A deadline only gates decision points, not in-flight I/O — a hung read inside one attempt is bounded by
ct, notoperationTimeout. Pair a real boundedctwithoperationTimeoutif you need both.
Example:
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); // caller budget
try
{
await resilient.ExecuteAsync(
"SELECT 1",
operationTimeout: TimeSpan.FromSeconds(15), // operation budget
ct: cts.Token);
// If caller's 10s expires, OperationCanceledException is thrown.
// If operation's 15s expires (and caller's 10s hasn't), TimeoutException is thrown.
}
catch (OperationCanceledException) { Console.WriteLine("Caller cancelled"); }
catch (TimeoutException) { Console.WriteLine("Operation deadline exceeded"); }
Thread Safety & Concurrency
HoloDbClient & ResilientHoloDbClient
Not thread-safe for concurrent calls. Use one per concurrent worker (or share via HoloDbClientPool). Concurrent callers on one instance queue through an internal mutex — they serialize anyway, so contention is high and you lose throughput.
// Share a pool, not a single client:
await using var pool = await HoloDbClientPool.ConnectAsync("db.example.com", 5432, size: 8);
var t1 = Task.Run(async () =>
{
using (var lease = await pool.RentAsync())
await lease.Client.ExecuteAsync("SELECT 1");
});
var t2 = Task.Run(async () =>
{
using (var lease = await pool.RentAsync())
await lease.Client.ExecuteAsync("SELECT 2");
});
await Task.WhenAll(t1, t2);
HoloDbClientPool
Thread-safe. Multiple threads can call RentAsync concurrently.
Lease
Not thread-safe (it's a readonly struct). Don't share a rented client across threads. Dispose the lease in the thread that rented it.
using (var lease = await pool.RentAsync())
{
// Don't pass lease.Client to another thread
await lease.Client.ExecuteAsync("SELECT 1");
} // Lease disposed in same thread
Server Discovery & Deployment
Server Setup
Run a HoloDb.Server instance (NuGet package EvaluatedApplications.HoloDb.Server):
# Start with default options (TCP 5432, no TLS, no token)
dotnet HoloDb.Server.dll
# With TLS and auth
dotnet HoloDb.Server.dll --useTls --cert-path /path/to/cert.pfx --cert-password secret --token mysecret
# Server prints its cert thumbprint on startup:
# [INFO] Certificate thumbprint: 1A2B3C...
Docker
A Docker image is available (EvaluatedApplications.HoloDb.Server):
docker run -p 5432:5432 \
-e HOLO_AUTH_TOKEN=mysecret \
-e HOLO_USE_TLS=true \
evaluated-applications/holo-db-server
Connection Strings
This package does not use connection strings. Configure via HoloDbClientOptions or ConnectAsync params:
// Not supported:
// var connStr = "Server=db.example.com;Port=5432;Token=secret;TlsThumbprint=ABC...";
// Do this instead:
var opts = new HoloDbClientOptions
{
AuthToken = "secret",
PinnedThumbprint = "ABC..."
};
await using var db = await HoloDbClient.ConnectAsync("db.example.com", 5432, opts);
Advanced: Combining Pool + Resilience
For high reliability with many workers, combine pooling and resilience:
// Create a pool of resilient clients
var clients = new List<ResilientHoloDbClient>();
for (var i = 0; i < 8; i++)
{
clients.Add(await ResilientHoloDbClient.ConnectAsync(
"db.example.com", 5432,
maxAttempts: 3,
breakerFailureThreshold: 5,
breakerCooldown: TimeSpan.FromSeconds(30)));
}
// Distribute across workers
var tasks = clients.Select(async c =>
{
try
{
var result = await c.ExecuteAsync(
"SELECT COUNT(*) FROM large_table",
operationTimeout: TimeSpan.FromSeconds(30));
Console.WriteLine($"Count: {result.GetLong(0, 0)}");
}
catch (CircuitBreakerOpenException)
{
Console.WriteLine("Server is down; skipping this worker's turn");
}
finally
{
await c.DisposeAsync();
}
});
await Task.WhenAll(tasks);
License
Proprietary — this package ships the compiled library only; see LICENSE for the full terms. Installing and using unmodified copies for internal evaluation and non-commercial development or testing is licensed; commercial, production, or revenue-generating use requires a separate commercial licence from the owner. No feature on this page is gated behind a tier or a licence key — every capability described works identically with or without one. Copyright (c) 2026 Dongyang Stephen Chen, trading as Evaluated Applications. All rights reserved.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- EvaluatedApplications.HoloDb.Protocol (>= 1.1.2)
-
net8.0
- EvaluatedApplications.HoloDb.Protocol (>= 1.1.2)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.