ProxyDataSource
High-availability multi-source proxy with intelligent routing, circuit breaking,
caching, watchdog promotion and cluster-tier scaling — all behind the standard
IDataSource interface.
On This Page
Overview
ProxyDataSource wraps one or more concrete IDataSource instances
behind a single, transparent facade that the rest of the application treats as an ordinary
data source. It adds the following capabilities without any changes to calling code:
Smart Routing
Four strategies: Weighted Latency, Least Outstanding Requests, Round Robin, Health Weighted. Reads can be load-balanced across replicas; writes are pinned to the Primary role.
Circuit Breaker
Per-source three-state (Closed / HalfOpen / Open) breaker with severity-weighted failure accumulation and configurable reset windows.
Retry & Failover
Automatic retry with exponential back-off and jitter. Seamless failover across candidates on transient errors. Non-idempotent writes execute exactly once.
Connection Pool
Per-source bounded pool (MaxPoolSize=10). Idle connections expire after ConnectionTimeout (5 min). TTL-based eviction runs on the health-check timer.
Caching
Stale-while-revalidate entity cache backed by CacheScope. Write-through invalidation keeps cache consistent on every insert/update/delete.
Watchdog
Tight-cycle background loop probes the write-primary independently of the health-check timer. Promotes a replica automatically when the primary fails, demotes it when the primary recovers.
Observability
Per-source DataSourceMetrics counters and a rolling 500-sample latency buffer that yields p50/p95/p99 SLO snapshots on demand.
Audit Trail
Every proxied operation writes a ProxyAuditEntry to a swappable IProxyAuditSink. Ships with a file-based rolling JSON-lines sink out of the box.
ProxyDataSource implements both
IProxyDataSource and IDataSource, so you can pass it anywhere a
normal data source is expected. All resilience features are invisible to the caller.
Architecture & Phase Map
The implementation is split across partial classes by concern, each corresponding to a numbered phase of the design plan:
| Phase | File | Responsibility |
|---|---|---|
| 1 | ProxyDataSource.cs | Core shell — fields, constructors, IDataSource properties, events, policy bootstrap |
| 2 | ProxyotherClasses.cs | Error taxonomy (ProxyErrorCategory, ProxyErrorSeverity), resilience profiles, enums, DataSourceMetrics |
| 3 | ProxyDataSource.Routing.cs | Candidate selection — WeightedLatency, RoundRobin, LeastOutstanding, HealthWeighted |
| 4 | ProxyDataSource.ExecutionHelpers.cs | Read/write execution loops — retry, jitter, saturation back-off, audit emission |
| 5 | ProxyDataSource.Caching.cs | Stale-while-revalidate entity cache, invalidation, cache key generation |
| 6 | ProxyDataSource.Observability.cs | Latency sample buffer, SLO snapshot, runtime ApplyPolicy |
| 7 | ProxyDataSource.Audit.cs | IProxyAuditSink, NullProxyAuditSink, FileProxyAuditSink, ProxyAuditEntry |
| 8 | ProxyDataSource.CircuitState.cs | ICircuitStateStore, InProcessCircuitStateStore |
| 9 | ProxyDataSource.FanOut.cs | Fan-out / scatter-gather operations across multiple nodes |
| 10 | ProxyDataSource.Transactions.cs | Distributed transaction coordination |
| 11 | ProxyDataSource.Watchdog.cs | Tight-cycle liveness probe, role promotion/demotion |
| — | ProxyDataSource.RedisCircuitStateStore.cs | Redis-backed distributed circuit state |
| 11+ | ProxyCluster.cs et al. | Cluster tier — node management, cluster routing, hedging, fault injection |
| — | Remote/ | HTTP transport for remote/cross-process proxy calls |
| — | EntityAffinityMap.cs | Entity-name → datasource affinity routing |
Request lifecycle
caller → ProxyDataSource.GetEntity(...)
│
├─ SelectCandidates() [Routing]
│ filter healthy + closed circuits
│ order by routing strategy
│
├─ for each candidate:
│ GetPooledConnection()
│ for 1..MaxRetries:
│ execute operation
│ ├─ success → RecordSuccess, emit audit, return
│ └─ failure → ProxyErrorClassifier.Classify()
│ ├─ non-retryable → throw immediately
│ ├─ saturation → 2× back-off, retry
│ └─ transient → exp back-off+jitter, retry
│
└─ all candidates exhausted → log + throw last exception
Core Types & Enums
ProxyDataSourceRole
| Value | Description |
|---|---|
Primary | Receives all writes; eligible for reads |
Replica | Read-only — never receives writes |
Standby | No normal traffic; activated only on failover |
ProxyRoutingStrategy
| Value | Description |
|---|---|
WeightedLatency | Prefer high-weight, low-latency sources (default) |
LeastOutstandingRequests | Route to the source with fewest in-flight operations |
RoundRobin | Simple sequential rotation across healthy sources |
HealthWeighted | Weight inversely proportional to observed failure rate |
ProxyOperationSafety
| Value | Retry behaviour |
|---|---|
ReadSafe | May be retried freely — reads and queries |
IdempotentWrite | May be retried with the same idempotency key |
NonIdempotentWrite | Single-execute only; no automatic retry |
DataSourceMetrics
Thread-safe counters maintained per backend datasource. Available via GetMetrics().
| Property | Type | Description |
|---|---|---|
TotalRequests | long | All proxied calls (reads + writes) |
SuccessfulRequests | long | Calls that returned successfully |
FailedRequests | long | Calls that threw an exception |
AverageResponseTime | double | Rolling average response time (ms) |
CircuitBreaks | long | Number of times the circuit opened for this source |
LastRequested | DateTime | UTC timestamp of last attempted call |
LastSuccessful | DateTime | UTC timestamp of last successful call |
ProxySloSnapshot
Point-in-time SLO report returned by GetSloSnapshot(dsName).
| Property | Description |
|---|---|
P50LatencyMs | Median latency (ms) over last 500 samples |
P95LatencyMs | 95th-percentile latency (ms) |
P99LatencyMs | 99th-percentile latency (ms) |
ErrorRatePercent | Failure rate as a percentage of total requests |
TotalRequests | Lifetime request count for this source |
CacheHitRatio | Cache hit ratio (0–1); 0 when cache is disabled |
SnapshotTime | UTC time of snapshot generation |
PooledConnection
A lease on a connection inside the per-source pool.
| Property | Description |
|---|---|
DataSource | The underlying IDataSource instance |
LastUsed | Timestamp used by idle-eviction to expire stale connections |
Constructors
Preferred constructor — policy-based
public ProxyDataSource(
IDMEEditor dmeEditor,
List<string> dataSourceNames,
ProxyPolicy policy = null,
ICircuitStateStore circuitStateStore = null,
IProxyAuditSink auditSink = null)
| Parameter | Description |
|---|---|
dmeEditor | Active IDMEEditor — used to resolve datasource instances and logging |
dataSourceNames | Names of the pre-configured datasources to pool (must exist in ConfigEditor.DataConnections) |
policy | Full routing + resilience + cache policy. Defaults to ProxyPolicy.Default (Balanced profile, WeightedLatency) |
circuitStateStore | Swappable circuit state backend. Defaults to InProcessCircuitStateStore (in-process, no dependency) |
auditSink | Audit write destination. Defaults to NullProxyAuditSink (no-op) |
Backwards-compatible constructor
public ProxyDataSource(
IDMEEditor dmeEditor,
List<string> dataSourceNames,
int? maxRetries = null,
int? retryDelayMilliseconds = null,
int? healthCheckIntervalMs = null)
Wraps the scalar parameters into a ProxyPolicy internally.
Provided for compatibility with older call sites; prefer the policy overload for new code.
InitializeFromPolicy() which starts the health-check timer,
seeds per-source circuit breakers, and initialises the cache provider if enabled.
ProxyPolicy
ProxyPolicy is the single source of truth for every tunable behaviour.
It is applied at construction time and can be swapped at runtime via
ApplyPolicy(ProxyPolicy) without recreating the proxy.
Top-level properties
| Property | Type | Default | Description |
|---|---|---|---|
Name | string | "Default" | Human-readable policy name used in log messages |
Version | int | 1 | Monotonic version for change tracking |
RoutingStrategy | ProxyRoutingStrategy | WeightedLatency | Algorithm used to order candidate sources for each request |
HealthCheckIntervalMs | int | 30 000 | Interval between background health checks (ms) |
Resilience | ProxyResilienceProfile | Balanced preset | Retry / circuit-breaker parameters |
Cache | ProxyCacheProfile | disabled | Entity cache configuration |
Runtime hot-swap
// Change to aggressive failover without recreating the proxy
var aggressivePolicy = new ProxyPolicy
{
Name = "HighAvailability",
RoutingStrategy = ProxyRoutingStrategy.HealthWeighted,
Resilience = ProxyResilienceProfile.AggressiveFailover,
HealthCheckIntervalMs = 10_000
};
proxy.ApplyPolicy(aggressivePolicy); // thread-safe; stops old timer, reinitialises circuits
ApplyPolicy stops the old health-check timer before swapping the policy
reference to prevent a duplicate timer leak. Always use ApplyPolicy rather than
mutating _policy directly.
Resilience Profiles
ProxyResilienceProfile bundles all retry and circuit-breaker knobs.
Three built-in presets cover most workloads; use Custom for fine tuning.
| Property | Conservative | Balanced (default) | AggressiveFailover |
|---|---|---|---|
MaxRetries | 5 | 3 | 1 |
RetryBaseDelayMs | 1 000 | 500 | 100 |
RetryMaxDelayMs | 60 000 | 30 000 | 5 000 |
UseExponentialBackoff | true | true | true |
UseJitter | true | true | true |
FailureThreshold | 3 | 5 | 2 |
CircuitResetTimeout | 10 min | 5 min | 30 s |
ConsecutiveSuccessesToClose | 3 | 2 | 1 |
// Use a preset
var policy = new ProxyPolicy { Resilience = ProxyResilienceProfile.Conservative };
// Or inline-custom
var policy = new ProxyPolicy
{
Resilience = new ProxyResilienceProfile
{
ProfileType = ProxyResilienceProfileType.Custom,
MaxRetries = 4,
RetryBaseDelayMs = 300,
FailureThreshold = 7,
CircuitResetTimeout = TimeSpan.FromMinutes(2),
ConsecutiveSuccessesToClose = 2
}
};
Routing Strategies
On every read operation, SelectCandidates() builds an ordered list of
healthy (non-open-circuit) sources and passes it to the active routing algorithm.
The execution loop tries candidates in that order with retries.
Writes are pre-filtered to Primary-role sources only.
| Strategy | Algorithm | Best for |
|---|---|---|
WeightedLatency |
Sort by weight descending, then average latency ascending | Mixed-performance backends where weight and latency both matter |
RoundRobin |
Monotonic counter mod candidate count; rotates list so selected index is first | Homogeneous backends, simple equal distribution |
LeastOutstandingRequests |
Sort by number of in-flight connections ascending | Backends with variable concurrency limits or connection pools |
HealthWeighted |
Weight = 1 / (failure rate + ε); source with lowest failure rate gets most traffic | Sources that flip between healthy and degraded frequently |
Circuit Breaker
Each registered datasource gets its own CircuitBreaker instance managed by the
active ICircuitStateStore. The breaker uses three states:
Normal operation
All calls rejected
Probe calls allowed
⟵ failure → Open
Severity-weighted failure accumulation
| Severity | Weight | Example exceptions |
|---|---|---|
Low | 1 | OperationCanceledException, generic connection errors |
Medium | 1 | TimeoutException, deadlock, transient errors |
High | 2 | UnauthorizedAccessException, persistent errors |
Critical | Immediate open | OutOfMemoryException (saturation) |
Swappable state store
The ICircuitStateStore interface allows replacing the default in-process store
with a distributed backend so circuit state is shared across multiple proxy instances:
public interface ICircuitStateStore
{
bool CanExecute(string dsName);
void RecordSuccess(string dsName);
void RecordFailure(string dsName, ProxyErrorSeverity severity = ProxyErrorSeverity.Medium);
void Initialize(string dsName, int failureThreshold, TimeSpan resetTimeout, int successThreshold = 2);
void Remove(string dsName);
}
Use the provided ProxyRedisCircuitStateStore for clusters that share state across
processes, or implement the interface against any storage backend.
Connection Pool
Each registered datasource has its own bounded ConcurrentQueue<PooledConnection>.
Connections are leased from the pool before each operation and returned after.
Idle eviction runs on the health-check timer interval.
| Knob | Default | Description |
|---|---|---|
MaxPoolSize | 10 | Maximum number of connections per datasource |
ConnectionTimeout | 5 min | Idle threshold; connections older than this are closed on next eviction pass |
Pool API
// Internal — used by execution helpers
IDataSource lease = proxy.GetPooledConnection(dsName); // borrow from pool (or open new)
proxy.ReturnConnection(dsName, lease); // return lease after use
IDataSource direct = proxy.GetConnection(dsName); // bypass pool — new connection always
Caching
Enabled via ProxyCacheProfile inside ProxyPolicy.
The cache is backed by a CacheScope from the BeepDM caching subsystem.
The proxy uses a stale-while-revalidate strategy: a soft TTL (the visible
expiry) and a hard TTL (stale extension window). When a key is between soft and hard
TTL the stale value is returned immediately while a background refresh populates the cache.
| Property | Default | Description |
|---|---|---|
Enabled | false | Must be explicitly set to true to activate the cache |
DefaultExpiration | — | Soft TTL for cached entries |
StaleWindow | — | Additional window during which stale data is served while revalidating |
MaxItems | — | Upper bound on cached entries before LRU eviction |
Tier | EntityProfile | Cache provider type (InMemory by default) |
Consistency | — | WriteThrough triggers automatic invalidation on write operations |
Cache API
// Read with cache (respects stale-while-revalidate)
var rows = proxy.GetEntityWithCache("Orders", filters, expiration: TimeSpan.FromMinutes(5));
// Manual invalidation
proxy.InvalidateCache("Orders"); // invalidate one entity
proxy.InvalidateCache(); // clear all cached entries
CacheConsistency = WriteThrough any call to
InsertEntity, UpdateEntity, or DeleteEntity
automatically calls InvalidateCache(entityName) before returning.
Watchdog
The watchdog runs as a background Task on a tight cycle (default 5 s) and probes
every registered datasource with a lightweight liveness call capped at
WatchdogProbeTimeoutMs (default 2 s).
It maintains independent consecutive failure/success counters per source, separate from the
main circuit breaker counters.
Role management
- When a Primary source fails
WatchdogFailureThresholdconsecutive probes, the watchdog promotes the highest-weighted healthy Replica to Primary. - The promotion is recorded in
_originalRolesso the role can be reversed. - When the original Primary recovers (
WatchdogRecoveryThresholdconsecutive successes), the watchdog demotes it back to Replica and restores the promoted Replica.
Knobs & events
| Member | Default | Description |
|---|---|---|
WatchdogIntervalMs | 5 000 | Probe loop delay (ms) |
WatchdogProbeTimeoutMs | 2 000 | Max ms for a single probe call |
WatchdogFailureThreshold | 2 | Consecutive failures before promotion |
WatchdogRecoveryThreshold | 3 | Consecutive successes before demotion |
OnRolePromoted | — | Fired when a Replica is promoted to Primary |
OnRoleDemoted | — | Fired when the original Primary is restored |
Usage
proxy.WatchdogIntervalMs = 3_000; // tighter probe on critical DB
proxy.WatchdogFailureThreshold = 3;
proxy.OnRolePromoted += (_, e) =>
Console.WriteLine($"{e.DataSourceName} promoted to {e.NewRole}");
proxy.StartWatchdog();
// ... application lifetime ...
proxy.StopWatchdog(); // graceful — waits up to 5 s for loop exit
Observability & SLO
The observability layer maintains a bounded 500-sample circular latency buffer per datasource and exposes p50/p95/p99 percentile summaries through the SLO snapshot API. Samples are recorded automatically by the execution helpers on every successful call.
SLO snapshot API
// Single source snapshot
ProxySloSnapshot snap = proxy.GetSloSnapshot("orders-primary");
Console.WriteLine($"p99={snap.P99LatencyMs:F1}ms err={snap.ErrorRatePercent:F2}% cache={snap.CacheHitRatio:P}");
// All sources at once
IReadOnlyList<ProxySloSnapshot> all = proxy.GetAllSloSnapshots();
foreach (var s in all)
Console.WriteLine($"{s.DataSourceName}: p95={s.P95LatencyMs:F1}ms");
Raw metrics access
IDictionary<string, DataSourceMetrics> metrics = proxy.GetMetrics();
foreach (var (name, m) in metrics)
Console.WriteLine($"{name}: total={m.TotalRequests} failed={m.FailedRequests} breaks={m.CircuitBreaks}");
Audit
Every proxied operation emits a ProxyAuditEntry to the configured
IProxyAuditSink after the operation completes — whether it succeeded or failed.
ProxyAuditEntry fields
| Field | Description |
|---|---|
CorrelationId | AsyncLocal GUID propagated across await boundaries |
OperationName | Caller-supplied operation label (e.g. "GetEntity") |
SelectedSource | Name of the datasource that ultimately served the request |
Succeeded | Final outcome |
TotalAttempts | Number of attempts across all candidates |
ElapsedMs | End-to-end wall-clock duration |
Safety | ProxyOperationSafety of this call |
Attempts | Per-attempt detail: datasource, number, success, duration, error |
Provided sinks
| Class | Description |
|---|---|
NullProxyAuditSink | Singleton no-op — default when no sink is supplied. Zero allocations. |
FileProxyAuditSink | Rolling daily JSON-lines file: {dir}/proxy-audit-YYYYMMDD.jsonl. Uses a bounded background queue so the data path is never blocked. |
// Enable file audit sink at construction
var sink = new FileProxyAuditSink(directory: @"C:\Logs\ProxyAudit", queueCapacity: 10_000);
var proxy = new ProxyDataSource(
dmeEditor : editor,
dataSourceNames : new List<string> { "db1", "db2" },
auditSink : sink);
// ... use proxy ...
sink.Dispose(); // flushes background queue, waits up to 5 s
IProxyAuditSink to write entries to a database, telemetry provider
(Application Insights, OpenTelemetry), or any other destination. The sink's
Write method must not throw and must complete quickly.
Error Taxonomy
ProxyErrorClassifier.Classify(Exception) maps every exception to a category +
severity pair that drives retry eligibility and circuit-breaker weight:
| Category | Retry? | Severity | Trigger |
|---|---|---|---|
Transient | ✅ Yes | Low–Medium | OperationCanceledException, IOException, "connection" in message |
Timeout | ✅ Yes | Medium | TimeoutException, "timeout" in message |
AuthFailure | ❌ No | High | UnauthorizedAccessException |
Saturation | ⏳ Back-off 2× | Critical | OutOfMemoryException, "connection pool full" |
Persistent | ❌ No | High | Schema errors, bad SQL, all other exceptions |
Unknown | ❌ No | Medium | Unmatched exceptions |
Saturation is special: the proxy backs off with a delay proportional to
attempt × 2 (double the normal back-off) to reduce load on an overwhelmed
backend rather than hammering it with rapid retries.
ProxyCluster
ProxyCluster implements IProxyCluster : IProxyDataSource and
represents a cluster of ProxyNode instances behind a single
IDataSource facade. Use it when you need a second tier of abstraction:
clusters of proxies rather than clusters of bare datasources.
Node management
var cluster = new ProxyCluster(
clusterPolicy : new ProxyPolicy { RoutingStrategy = ProxyRoutingStrategy.LeastOutstandingRequests },
auditSink : new FileProxyAuditSink(@"C:\Logs\Cluster"));
// Add nodes
cluster.AddNode(new ProxyNode("primary", primaryProxy, weight: 2, role: ProxyDataSourceRole.Primary));
cluster.AddNode(new ProxyNode("replica1", replicaProxy1, weight: 1, role: ProxyDataSourceRole.Replica));
cluster.AddNode(new ProxyNode("replica2", replicaProxy2, weight: 1, role: ProxyDataSourceRole.Replica));
// Remove a node gracefully (drains in-flight requests)
cluster.RemoveNode("replica2");
// Inspect nodes
IReadOnlyList<IProxyNode> nodes = cluster.GetNodes();
foreach (var n in nodes)
Console.WriteLine($"{n.NodeId}: alive={n.IsAlive} weight={n.Weight} inflight={n.InFlightCount}");
ProxyNode properties
| Property | Description |
|---|---|
NodeId | Unique identifier within the cluster |
Proxy | The underlying IProxyDataSource handling actual I/O |
Weight | Routing weight (mutable; ramp via slow-start) |
NodeRole | Primary, Replica, or Standby |
IsAlive | Last probe result |
InFlightCount | Atomic in-flight operation counter |
Metrics | Aggregated node-level DataSourceMetrics |
NodeConfig | ConnectionProperties for persistence across restarts |
Cluster routing strategies (ProxyNodeRoutingStrategy)
| Value | Description |
|---|---|
LeastConnections | Pick the node with fewest in-flight operations |
WeightedRoundRobin | Rotate proportional to node Weight |
LowestLatency | Always route to the node with the lowest rolling average response time |
PrimaryWithStandby | All traffic to Primary; Replicas only receive overflow/reads |
ConsistentHash | Ring hash keyed on CorrelationId for session affinity |
Cluster events
| Event | Fired when |
|---|---|
OnNodeDown | A probe determines a node is unreachable |
OnNodeRestored | A previously down node passes a probe |
OnNodePromoted | Watchdog promotes a Replica to Primary role |
OnNodeDemoted | Original Primary recovers; promoted node is restored to Replica |
OnClusterPolicyChanged | ApplyPolicy is called on the cluster |
OnAffinityRebalanced | Session affinity map is rebuilt after a topology change |
Usage Examples
Example 1 — Basic read with default policy
var proxy = new ProxyDataSource(
dmeEditor : editor,
dataSourceNames : new List<string> { "orders-primary", "orders-replica" });
proxy.Openconnection();
var rows = proxy.GetEntity("Orders", new List<AppFilter>
{
new AppFilter { FieldName = "Status", Operator = "=", FilterValue = "Open" }
});
foreach (var row in rows)
Console.WriteLine(row);
proxy.Closeconnection();
proxy.Dispose();
Example 2 — Custom policy with caching
var policy = new ProxyPolicy
{
Name = "ProductsPolicy",
RoutingStrategy = ProxyRoutingStrategy.WeightedLatency,
Resilience = new ProxyResilienceProfile
{
ProfileType = ProxyResilienceProfileType.Custom,
MaxRetries = 3,
RetryBaseDelayMs = 200,
FailureThreshold = 5,
CircuitResetTimeout = TimeSpan.FromSeconds(30)
},
Cache = new ProxyCacheProfile
{
Enabled = true,
DefaultExpiration = TimeSpan.FromSeconds(60),
Consistency = ProxyCacheConsistency.WriteThrough
}
};
var proxy = new ProxyDataSource(
dmeEditor : editor,
dataSourceNames : new List<string> { "products-db1", "products-db2" },
policy : policy);
proxy.Openconnection();
var result = proxy.RunQuery("SELECT COUNT(*) FROM Products");
proxy.Dispose();
Example 3 — Read/write role separation
var proxy = new ProxyDataSource(
dmeEditor : editor,
dataSourceNames : new List<string> { "sales-primary", "sales-replica-1", "sales-replica-2" });
// Assign roles
proxy.SetRole("sales-primary", ProxyDataSourceRole.Primary);
proxy.SetRole("sales-replica-1", ProxyDataSourceRole.Replica);
proxy.SetRole("sales-replica-2", ProxyDataSourceRole.Replica);
proxy.Openconnection();
// Read — may land on any healthy node
var items = proxy.GetEntity("SaleItems", null);
// Write — guaranteed to go to Primary-role source only
proxy.InsertEntity("Orders", new { OrderId = Guid.NewGuid(), Total = 99.99 });
proxy.Dispose();
Example 4 — Watchdog with role promotion events
var proxy = new ProxyDataSource(
dmeEditor : editor,
dataSourceNames : new List<string> { "db-primary", "db-replica" });
proxy.SetRole("db-primary", ProxyDataSourceRole.Primary);
proxy.SetRole("db-replica", ProxyDataSourceRole.Replica);
proxy.WatchdogIntervalMs = 3_000;
proxy.WatchdogFailureThreshold = 2;
proxy.WatchdogRecoveryThreshold = 4;
proxy.OnRolePromoted += (_, e) =>
logger.LogWarning($"PROMOTED: {e.DataSourceName} is now {e.NewRole}");
proxy.OnRoleDemoted += (_, e) =>
logger.LogInformation($"DEMOTED: {e.DataSourceName} restored to {e.NewRole}");
proxy.Openconnection();
proxy.StartWatchdog();
// ... application lifetime ...
proxy.StopWatchdog();
proxy.Dispose();
Example 5 — Audit + observability
using var sink = new FileProxyAuditSink(@"C:\Logs\ProxyAudit");
var proxy = new ProxyDataSource(
dmeEditor : editor,
dataSourceNames : new List<string> { "analytics-db1", "analytics-db2" },
auditSink : sink);
proxy.Openconnection();
// ... run operations ...
// Check SLO
foreach (var snap in proxy.GetAllSloSnapshots())
{
Console.WriteLine(
$"{snap.DataSourceName}: p50={snap.P50LatencyMs:F1}ms " +
$"p99={snap.P99LatencyMs:F1}ms " +
$"err={snap.ErrorRatePercent:F2}% " +
$"cache={snap.CacheHitRatio:P}");
}
proxy.Dispose();
// sink.Dispose() called by using — flushes queue
Example 6 — ProxyCluster
// Build individual proxies (one per physical server)
var primary = new ProxyDataSource(editor, new List<string> { "server-a" });
var replica = new ProxyDataSource(editor, new List<string> { "server-b" });
// Compose into a cluster
var cluster = new ProxyCluster(
clusterPolicy : new ProxyPolicy { RoutingStrategy = ProxyRoutingStrategy.LeastOutstandingRequests });
cluster.AddNode(new ProxyNode("primary", primary, weight: 2, role: ProxyDataSourceRole.Primary));
cluster.AddNode(new ProxyNode("replica1", replica, weight: 1, role: ProxyDataSourceRole.Replica));
cluster.OnNodeDown += (_, e) => Console.WriteLine($"Node down: {e.NodeId}");
cluster.OnNodeRestored += (_, e) => Console.WriteLine($"Node up: {e.NodeId}");
// Use the cluster exactly like a plain IDataSource
IDataSource ds = cluster;
var data = ds.GetEntity("Products", null);
Example 7 — Hot policy swap
// Start with balanced policy
var proxy = new ProxyDataSource(editor, new List<string> { "db1", "db2" });
// During an incident: switch to aggressive failover in place
proxy.ApplyPolicy(new ProxyPolicy
{
Name = "Incident-2024",
RoutingStrategy = ProxyRoutingStrategy.HealthWeighted,
Resilience = ProxyResilienceProfile.AggressiveFailover,
HealthCheckIntervalMs = 5_000
});
// After the incident: restore normal policy
proxy.ApplyPolicy(new ProxyPolicy { Resilience = ProxyResilienceProfile.Balanced });
Full API Reference
IProxyDataSource (complete interface)
public interface IProxyDataSource : IDataSource, IDisposable
{
// Events
event EventHandler<FailoverEventArgs> OnFailover;
event EventHandler<RecoveryEventArgs> OnRecovery;
// Policy
void ApplyPolicy(ProxyPolicy policy);
// Backward-compat knobs
int MaxRetries { get; set; }
int RetryDelayMilliseconds { get; set; }
int HealthCheckIntervalMilliseconds { get; set; }
// Load-balanced execution
Task<T> ExecuteWithLoadBalancing<T>(Func<IDataSource, Task<T>> operation, bool isWrite = false, CancellationToken cancellationToken = default);
// Connection pool
IDataSource GetPooledConnection(string dsName);
void ReturnConnection(string dsName, IDataSource connection);
IDataSource GetConnection(string dsName);
// Cache
object GetEntityWithCache(string entityName, List<AppFilter> filter, TimeSpan? expiration = null);
void InvalidateCache(string entityName = null);
// Metrics & SLO
IDictionary<string, DataSourceMetrics> GetMetrics();
ProxySloSnapshot GetSloSnapshot(string dsName);
IReadOnlyList<ProxySloSnapshot> GetAllSloSnapshots();
// Datasource membership
void AddDataSource(string dsName, int weight = 1);
void RemoveDataSource(string dsName);
void SetRole(string dsName, ProxyDataSourceRole role);
// Watchdog
int WatchdogIntervalMs { get; set; }
int WatchdogProbeTimeoutMs { get; set; }
int WatchdogFailureThreshold { get; set; }
int WatchdogRecoveryThreshold { get; set; }
event EventHandler<RoleChangeEventArgs> OnRolePromoted;
event EventHandler<RoleChangeEventArgs> OnRoleDemoted;
void StartWatchdog();
void StopWatchdog();
IReadOnlyList<WatchdogNodeStatus> GetWatchdogStatus();
// Audit
IProxyAuditSink AuditSink { get; set; }
}
IProxyCluster (complete interface)
public interface IProxyCluster : IProxyDataSource
{
// Node management
void AddNode(IProxyNode node);
void RemoveNode(string nodeId);
IReadOnlyList<IProxyNode> GetNodes();
// Drain / rolling restart
void DrainNode(string nodeId, int timeoutMs = 30_000);
Task DrainNodeAsync(string nodeId, int timeoutMs = 30_000, CancellationToken ct = default);
// Policy propagation
void ApplyClusterPolicy(ProxyPolicy policy);
// Aggregate observability
IDictionary<string, DataSourceMetrics> GetClusterMetrics();
IReadOnlyList<ProxySloSnapshot> GetClusterSloSnapshots();
// Cluster events
event EventHandler<NodeStatusEventArgs> OnNodeDown;
event EventHandler<NodeStatusEventArgs> OnNodeRestored;
event EventHandler<RoleChangeEventArgs> OnNodePromoted;
event EventHandler<RoleChangeEventArgs> OnNodeDemoted;
event EventHandler<ClusterPolicyChangedEventArgs> OnClusterPolicyChanged;
event EventHandler<AffinityRebalancedEventArgs> OnAffinityRebalanced;
// Distributed transaction
bool SupportsTwoPhaseCommit => false;
}
IProxyNode (Phase 11)
public interface IProxyNode
{
string NodeId { get; }
int Weight { get; set; }
ProxyDataSourceRole NodeRole { get; set; }
bool IsAlive { get; }
int InFlightCount { get; }
DataSourceMetrics Metrics { get; }
ConnectionProperties NodeConfig { get; set; }
}
ProxyPolicy — Complete Properties
| Property | Type | Default | Description |
|---|---|---|---|
Name | string | "default" | Human-readable policy name |
Version | string | "1.0" | Semantic version for change tracking |
Environment | string | "production" | Environment label (production, staging, dev) |
Resilience | ProxyResilienceProfile | Balanced | Retry + circuit-breaker params |
Cache | ProxyCacheProfile | new() | Cache configuration |
RoutingStrategy | ProxyRoutingStrategy | WeightedLatency | Candidate selection algorithm |
HealthCheckIntervalMs | int | 30 000 | Health-check timer interval |
HealthCheckTimeoutSecs | int | 5 | Timeout per health check |
HealthyThresholdCount | int | 2 | Consecutive successes to mark healthy |
UnhealthyThresholdCount | int | 2 | Consecutive failures to mark unhealthy |
EnableLogRedaction | bool | true | Redact PII in log messages via ProxyLogRedactor |
WriteMode | ProxyWriteMode | SinglePrimary | SinglePrimary, FanOut, or QuorumWrite |
WriteFanOutQuorum | int | 1 | Minimum Primaries for QuorumWrite success |
NodeRoutingStrategy | ProxyNodeRoutingStrategy | LeastConnections | Cluster-tier node selection |
NodeUnhealthyThreshold | int | 2 | Probes before node marked down |
NodeHealthyThreshold | int | 1 | Probes before node restored |
NodeProbeIntervalMs | int | 5 000 | Cluster node probe interval |
NodeProbeTimeoutMs | int | 2 000 | Timeout per node probe |
EnableNodeAffinity | bool | false | Session pinning to nodes |
NodeAffinityTtlSeconds | int | 300 | Session binding expiry (seconds) |
SlowStartDurationMs | int | 0 | Ramp-up for new nodes (0 = disabled) |
MaxReplicaLagMs | long | 0 | Max replication lag for replica reads |
EntityAffinity | EntityAffinityMap? | null | Entity-to-node affinity mapping |
AffinityFallback | EntityAffinityFallback | RouteToAny | Fallback when affinity node down |
RoutingRules | IReadOnlyList<ProxyRoutingRule> | empty | Declarative routing rules (first match wins) |
TrafficSplits | IReadOnlyList<TrafficSplitRule> | empty | Canary deployment traffic splits |
OutlierDetection | OutlierDetectionPolicy? | null | Envoy-style outlier ejection (null = disabled) |
EnableHedging | bool | false | Hedged requests for slow nodes |
HedgingThresholdMs | int | 100 | Delay before issuing hedge request |
MaxHedgeRequests | int | 2 | Max concurrent copies incl. original |
ReadMode | ProxyReadMode | SingleNode | SingleNode or ScatterGather |
FaultInjection | FaultInjectionPolicy? | null | Chaos engineering config (null = disabled) |
NodeRateLimits | IReadOnlyList<NodeRateLimit> | empty | Per-node RPS caps |
ClusterMaxBackendConnections | int | 0 | Global connection cap (0 = unlimited) |
ClusterMaxQueueDepth | int | 100 | Max queued requests when capped |
ClusterQueueTimeoutMs | int | 5 000 | Queued request timeout |
ICircuitStateStore (complete)
public interface ICircuitStateStore
{
bool CanExecute(string dsName);
void RecordSuccess(string dsName);
void RecordFailure(string dsName, ProxyErrorSeverity severity = ProxyErrorSeverity.Medium);
void Initialize(string dsName, int failureThreshold, TimeSpan resetTimeout, int successThreshold = 2);
void Remove(string dsName);
}
IProxyAuditSink & IProxyAuditEntry
public interface IProxyAuditSink
{
/// Must not throw and must complete quickly
void Write(IProxyAuditEntry entry);
}
public interface IProxyAuditEntry
{
string CorrelationId { get; }
string OperationName { get; }
string SelectedSource { get; }
bool Succeeded { get; }
int TotalAttempts { get; }
long ElapsedMs { get; }
ProxyOperationSafety Safety { get; }
IReadOnlyList<ProxyAttemptRecord> Attempts { get; }
}
IProxyNode
public interface IProxyNode
{
string NodeId { get; }
int Weight { get; set; }
ProxyDataSourceRole NodeRole { get; set; }
bool IsAlive { get; }
int InFlightCount { get; }
DataSourceMetrics Metrics { get; }
ConnectionProperties NodeConfig { get; set; }
}
EntityAffinityMap
File: EntityAffinityMap.cs. Binds entity names to specific cluster nodes.
public class EntityAffinityMap
{
public Dictionary<string, string> Affinities { get; init; } = new();
}
Fallback behavior: RouteToAny | ThrowException | WaitForOwner.
ProxyLogRedactor
File: ProxyLogRedactor.cs. Scrubs PII (passwords, tokens, connection strings) from proxy log messages when EnableLogRedaction = true.
All Enums Reference
| Enum | Values |
|---|---|
ProxyDataSourceRole | Primary, Replica, Standby |
ProxyRoutingStrategy | WeightedLatency, LeastOutstandingRequests, RoundRobin, HealthWeighted |
ProxyNodeRoutingStrategy | LeastConnections, WeightedRoundRobin, LowestLatency, PrimaryWithStandby, ConsistentHash |
ProxyOperationSafety | ReadSafe, IdempotentWrite, NonIdempotentWrite |
ProxyErrorCategory | Transient, Timeout, AuthFailure, Saturation, Persistent, Unknown |
ProxyErrorSeverity | Low, Medium, High, Critical |
ProxyResilienceProfileType | Conservative, Balanced, AggressiveFailover, Custom |
ProxyCacheTier | None, RequestScope, ShortLived, EntityProfile |
ProxyCacheConsistency | Eventual, StaleWhileRevalidate, WriteThrough |
ProxyWriteMode | SinglePrimary, FanOut, QuorumWrite |
ProxyReadMode | SingleNode, ScatterGather |
ProxyRoutingOverrideStrategy | RouteToReplica, RouteToPrimary, RouteToAny |
ProxySplitScope | All, ReadsOnly, WritesOnly |
EntityAffinityFallback | RouteToAny, ThrowException, WaitForOwner |
RateLimitAction | RouteElsewhere, Queue, Reject |
CircuitBreakerState | Closed, Open, HalfOpen |
Class: CircuitBreaker
Thread-safe three-state circuit breaker with severity-weighted failure accumulation.
Critical severity immediately breaches the threshold;
High counts as 2 failures.
Uses separate _successThreshold for HalfOpen → Closed transitions.
public class CircuitBreaker
{
public CircuitBreakerState State { get; }
public int FailureCount { get; }
public DateTime LastStateChange { get; }
public CircuitBreaker(int failureThreshold, TimeSpan resetTimeout, int successThreshold = 2);
public bool CanExecute(); // Test + transition Open→HalfOpen
public void RecordSuccess(); // HalfOpen successes → Closed
public void RecordFailure(ProxyErrorSeverity severity = Medium);
public void Reset(); // Force Closed
}
Source Files Reference
| File | Responsibility |
|---|---|
ProxyDataSource.cs | Core: fields, constructors, IDataSource props, policy bootstrap |
ProxyotherClasses.cs | Enums, error taxonomy, resilience profiles, cache profiles, ProxyPolicy, events, SLO snapshot |
CircuitBreaker.cs | Three-state breaker with severity-weighted failure accumulation |
ProxyDataSource.Routing.cs | Candidate selection: WeightedLatency, RoundRobin, LeastOutstanding, HealthWeighted |
ProxyDataSource.ExecutionHelpers.cs | Read/write loops with retry, jitter, back-off, audit emission |
ProxyDataSource.Caching.cs | Stale-while-revalidate entity cache, invalidation, CacheScope integration |
ProxyDataSource.Observability.cs | 500-sample latency buffer, SLO snapshots, runtime ApplyPolicy |
ProxyDataSource.Audit.cs | IProxyAuditSink, NullProxyAuditSink, FileProxyAuditSink, ProxyAuditEntry |
ProxyDataSource.CircuitState.cs | ICircuitStateStore, InProcessCircuitStateStore |
ProxyDataSource.RedisCircuitStateStore.cs | Redis-backed distributed circuit state |
ProxyDataSource.FanOut.cs | Concurrent write fan-out to all Primary nodes |
ProxyDataSource.Transactions.cs | Distributed transaction coordination (2PC/Saga) |
ProxyDataSource.Watchdog.cs | Liveness probe loop, role promotion/demotion auto-recovery |
EntityAffinityMap.cs | Entity-name → datasource affinity routing |
ProxyLogRedactor.cs | PII scrubbing for proxy log messages |
IProxyDataSource.cs | IProxyDataSource + IProxyCluster + IProxyNode interfaces |
ProxyCluster.cs | Multi-node cluster: routing, failover, affinity |
ProxyCluster.NodeManagement.cs | Add/remove/drain nodes, slow-start weight ramp |
ProxyCluster.NodeRouting.cs | Node routing strategies + ConsistentHash ring |
ProxyCluster.NodeProbing.cs | Background liveness probe loop |
ProxyCluster.ScatterGather.cs | Parallel read scatter-gather across nodes |
ProxyCluster.Hedging.cs | Hedged request dispatch with deadline cancellation |
ProxyCluster.RoutingRules.cs | Declarative routing rules + TrafficSplit canary |
ProxyCluster.FaultInjection.cs | Chaos engineering fault + latency injection |
ProxyClusterEvents.cs | Cluster event arg types |
ProxyNode.cs | Cluster node data class |
Remote/HttpProxyTransport.cs | HTTP transport for cross-process proxy calls |
Remote/ProxyRemoteRequestDispatcher.cs | Remote request serialization/deserialization |
Remote/RemoteProxyDataSource.cs | Remote proxy client (sends ops over wire) |
Remote/ProxyRequestSigner.cs | Request signing for secure cross-process calls |
Remote/ProxyRemoteProtocol.cs | Wire protocol definitions |
Remote/IProxyTransport.cs | Transport abstraction interface |
Examples/ProxyDataSourceExamples.cs | ProxyDataSource usage examples |
Examples/ProxyClusterExamples.cs | ProxyCluster usage examples |