TsgcWSCircuitBreaker

TsgcWSCircuitBreaker implements the Circuit Breaker resilience pattern for client-side calls to HTTP APIs.

Introduction

TsgcWSCircuitBreaker implements the Circuit Breaker resilience pattern for client-side calls to HTTP APIs. It prevents cascading failures when a remote endpoint becomes unavailable by tracking failures in a rolling window, opening the circuit once configurable thresholds are reached, and rejecting calls immediately during the cooldown period. It supports failure-count and failure-rate thresholds, slow-call detection, half-open trial calls, exception classification (record / ignore), per-endpoint overrides, fallback responses, and real-time metrics.

 

The circuit breaker integrates automatically with any descendant of TsgcHTTPAPI_client (OpenAI, Anthropic, Gemini, Grok, DeepSeek, Mistral, Ollama, Stripe, Binance, Bybit, Coinbase, Kraken and the other API wrappers) through the CircuitBreaker property. Once assigned, every Get / Post / Put / Patch / Delete call is wrapped automatically. One independent circuit is maintained per unique hostname.

 

Quick Start

 

1. Drop a TsgcWSCircuitBreaker component on the form.

 

2. Configure Thresholds (when to open) and Recovery (how to recover):

 

sgcWSCircuitBreaker1.Thresholds.FailureCount := 5;
sgcWSCircuitBreaker1.Recovery.CooldownSec := 30;

 

3. Assign the circuit breaker to an HTTP API client (any TsgcHTTPAPI_client descendant):

 

sgcAI_OpenAI1.CircuitBreaker := sgcWSCircuitBreaker1;

 

4. Call Get / Post / Delete / Patch / Put on the client. The breaker records successes and failures automatically and rejects new calls while the circuit is Open.

 

States

 

What it does: The circuit breaker is a three-state machine. Each host (key) has its own independent state that controls whether outbound calls are allowed, rejected, or sent as trials.

 

StateBehaviorTransition
csClosedAll calls pass through. Failures are counted in the rolling window.Moves to csOpen when FailureCount, FailureRatePercent or SlowCallRatePercent is reached.
csOpenEvery call is rejected immediately. OnCallRejected fires for each attempt.Moves to csHalfOpen after Recovery.CooldownSec seconds elapsed since opening.
csHalfOpenUp to Recovery.HalfOpenTrialCalls trial calls are allowed to probe recovery.Moves to csClosed when the trials succeed; moves back to csOpen if any trial fails.

 

Top-level Properties

 

What it does: Master switch for the component plus the two key names used when no per-call key is supplied. The client-side integration uses the target hostname as the key automatically — DefaultKey and ServerKey only apply to manual Execute/RecordSuccess/RecordFailure calls and to the optional server-integration helpers.

 

When to use it: Disable the breaker at runtime during a controlled load test or during a maintenance window without unassigning it from the HTTP client. Set a meaningful DefaultKey (for example 'openai-api') when you drive Execute manually inside custom code.

 

PropertyTypeDefaultDescription
EnabledBooleanTrueMaster switch. When False, every public method short-circuits to "allowed" and no counters are updated.
DefaultKeystring‘default’Key used by the parameterless overloads of Execute, RecordSuccess and RecordFailure.
ServerKeystring‘server’Key used by the optional server-integration hooks (IsConnectionAllowed, IsMessageAllowed, RecordMessageError).

 


    sgcWSCircuitBreaker1.Enabled := True;
    sgcWSCircuitBreaker1.DefaultKey := 'openai-api';
    sgcWSCircuitBreaker1.ServerKey := 'server';
    

 

Thresholds

 

What it does: Defines the conditions that move the circuit from Closed to Open. The breaker opens when FailureCount raw failures are reached, or when FailureRatePercent of calls in the window fail (after MinCalls), or when SlowCallRatePercent of calls exceed SlowCallDurationMs.

 

When to use it: Your server calls OpenAI's API. When 5 failures occur within 60 seconds (or 50% of calls fail once at least 10 calls have been observed), open the circuit and stop hammering OpenAI — let the remote recover instead of piling on retries.

 

PropertyTypeDefaultDescription
EnabledBooleanTrueEnables threshold-based opening. When False the circuit never opens automatically.
FailureCountInteger5Absolute number of failures in the rolling window that trips the breaker.
FailureRatePercentInteger50Failure percentage (0-100) that trips the breaker once MinCalls is reached.
SlowCallDurationMsInteger3000Threshold (ms) above which a successful call is also counted as "slow".
SlowCallRatePercentInteger0Percentage (0-100) of slow calls that trips the breaker. 0 disables slow-call detection.
MinCallsInteger10Minimum calls observed before rate thresholds are evaluated (avoids tripping on small samples).

 


    // Open after 5 failures, or 50% failure rate on at least 10 calls
    sgcWSCircuitBreaker1.Thresholds.Enabled := True;
    sgcWSCircuitBreaker1.Thresholds.FailureCount := 5;
    sgcWSCircuitBreaker1.Thresholds.FailureRatePercent := 50;
    sgcWSCircuitBreaker1.Thresholds.MinCalls := 10;
    // Also treat any call slower than 3 seconds as a "slow" call
    sgcWSCircuitBreaker1.Thresholds.SlowCallDurationMs := 3000;
    sgcWSCircuitBreaker1.Thresholds.SlowCallRatePercent := 60;
    

 

TimeWindow

 

What it does: Defines the rolling-window used for the threshold evaluation. The window is divided into BucketCount sub-buckets so old counters age out smoothly as time advances. Thresholds are always evaluated against the sum of live buckets.

 

When to use it: Use a 30-second rolling window with 6 buckets so transient spikes don't trip the circuit but sustained failure does. Longer windows smooth noise; shorter windows react faster to outages. More buckets give smoother aging but consume slightly more memory per host.

 

PropertyTypeDefaultDescription
RollingWindowSecInteger60Total width of the rolling window in seconds. Older samples are discarded.
BucketCountInteger10Number of sub-buckets the window is divided into. Each bucket holds success / failure / slow counters.

 


    // 30-second window split into 6 buckets (5 seconds each)
    sgcWSCircuitBreaker1.TimeWindow.RollingWindowSec := 30;
    sgcWSCircuitBreaker1.TimeWindow.BucketCount := 6;
    

 

Recovery

 

What it does: Controls how the circuit recovers from the Open state. After CooldownSec elapsed, the breaker transitions to HalfOpen and allows HalfOpenTrialCalls probe calls through. If the trials succeed the breaker closes; if they fail it re-opens and the cooldown timer restarts.

 

When to use it: After opening, wait 30 seconds then let one trial call through to test recovery before re-closing. A longer cooldown protects a struggling upstream from another wave of traffic; a short cooldown reopens quickly for services that only flicker.

 

PropertyTypeDefaultDescription
CooldownSecInteger30Seconds to wait in the Open state before transitioning to HalfOpen.
HalfOpenTrialCallsInteger1Number of trial calls permitted in HalfOpen before deciding to close or re-open.
AutoResetBooleanTrueWhen True, counters are cleared automatically on state transitions.
MaxRetriesInteger3Informational ceiling consumed by retry helpers. No automatic retrying is done by the breaker itself.

 


    // After opening: wait 30s, then allow 1 trial call, then decide
    sgcWSCircuitBreaker1.Recovery.CooldownSec := 30;
    sgcWSCircuitBreaker1.Recovery.HalfOpenTrialCalls := 1;
    sgcWSCircuitBreaker1.Recovery.AutoReset := True;
    sgcWSCircuitBreaker1.Recovery.MaxRetries := 3;
    

 

Fallback

 

What it does: Configures an alternative payload served while the circuit is Open. When Fallback.Enabled is True and Execute / ExecuteWithResult rejects a call, the OnFallback event fires with a response initialized from CachedResponse (or CustomMessage). Handlers may replace this string before it is returned to the caller.

 

When to use it: Return a cached last-known-good response when OpenAI is down so users see degraded-but-functional behavior (for example the previously generated summary) instead of an error. Use CustomMessage for a static "maintenance mode" JSON body.

 

PropertyTypeDefaultDescription
EnabledBooleanFalseEnables the fallback path. When False, rejected calls just return without firing OnFallback.
CachedResponsestring‘’Static payload returned while the circuit is Open. Takes precedence over CustomMessage.
CustomMessagestring‘’Fallback payload used when CachedResponse is empty.
UseLastSuccessBooleanFalseWhen True, the last successful response observed for the key is served as the fallback.

 


    // When api.openai.com is down, serve a safe JSON body instead of erroring
    sgcWSCircuitBreaker1.Fallback.Enabled := True;
    sgcWSCircuitBreaker1.Fallback.CachedResponse :=
      '{"error":"service unavailable","retry_after":30}';
    sgcWSCircuitBreaker1.Fallback.CustomMessage :=
      '{"error":"ai service temporarily offline"}';
    sgcWSCircuitBreaker1.Fallback.UseLastSuccess := True;
    

 

Classification

 

What it does: Classifies raised exceptions as either "recorded as failure" or "ignored". Only recorded exceptions advance the failure counters. MatchMode selects how the strings in RecordAsFailure / IgnoreExceptions are compared to the exception text (ClassName: Message).

 

When to use it: Treat EIdHTTPProtocolException with 500/502/503/504 as failures, but ignore EIdHTTPProtocolException 400/401/404 (business errors should not trip the circuit). Leaving RecordAsFailure empty means every non-ignored exception is recorded.

 

PropertyTypeDefaultDescription
RecordAsFailureTStringListemptyPatterns matched against exception text; matching exceptions count as failures. Empty list = record everything not ignored.
IgnoreExceptionsTStringListemptyPatterns matched against exception text; matching exceptions are dropped (neither success nor failure).
MatchModeTsgcCircuitBreakerExceptionMatchcemContainsComparison mode: cemExact, cemContains, cemStartsWith or cemWildcard (* / ?).

 


    // Record HTTP 5xx and timeouts as failures; ignore client-side 4xx
    sgcWSCircuitBreaker1.Classification.MatchMode := cemContains;
    sgcWSCircuitBreaker1.Classification.RecordAsFailure.Add('HTTP/1.1 500');
    sgcWSCircuitBreaker1.Classification.RecordAsFailure.Add('HTTP/1.1 502');
    sgcWSCircuitBreaker1.Classification.RecordAsFailure.Add('HTTP/1.1 503');
    sgcWSCircuitBreaker1.Classification.RecordAsFailure.Add('HTTP/1.1 504');
    sgcWSCircuitBreaker1.Classification.RecordAsFailure.Add('timeout');
    sgcWSCircuitBreaker1.Classification.IgnoreExceptions.Add('HTTP/1.1 400');
    sgcWSCircuitBreaker1.Classification.IgnoreExceptions.Add('HTTP/1.1 401');
    sgcWSCircuitBreaker1.Classification.IgnoreExceptions.Add('HTTP/1.1 404');
    

 

PerEndpoint

 

What it does: Collection of pattern-based overrides. Each TsgcCircuitBreakerEndpointItem has its own Pattern (wildcards * and ? supported), an OverrideThresholds block and an OverrideRecovery block. When the active key (hostname) matches an enabled item, that item's settings replace the top-level Thresholds / Recovery for the duration of the call.

 

When to use it: Separate circuits per API: api.openai.com has lenient thresholds (it occasionally slows under load), but api.stripe.com is payment-critical and should open faster to fail closed. Add one endpoint item per host with tuned thresholds.

 

PropertyTypeDefaultDescription
EnabledBooleanFalseEnables the endpoint-override collection. When False no items are evaluated.
ItemsTCollectionemptyCollection of TsgcCircuitBreakerEndpointItem. First enabled match wins.

 

Each TsgcCircuitBreakerEndpointItem exposes:

 

PropertyTypeDefaultDescription
Patternstring‘’Wildcard pattern matched against the key (hostname). Supports * and ?.
EnabledBooleanTrueDisables this item without deleting it.
OverrideThresholdsTsgcCircuitBreakerThresholdsdefaultsThresholds applied when the pattern matches. Active only when its own Enabled is True.
OverrideRecoveryTsgcCircuitBreakerRecoverydefaultsRecovery settings applied when the pattern matches.

 


    sgcWSCircuitBreaker1.PerEndpoint.Enabled := True;

    // OpenAI - lenient: 10 failures, 60s cooldown
    with sgcWSCircuitBreaker1.PerEndpoint.Items.Add as TsgcCircuitBreakerEndpointItem do
    begin
      Pattern := 'api.openai.com';
      Enabled := True;
      OverrideThresholds.Enabled := True;
      OverrideThresholds.FailureCount := 10;
      OverrideThresholds.FailureRatePercent := 70;
      OverrideRecovery.CooldownSec := 60;
    end;

    // Stripe - payment critical: open fast, recover slow
    with sgcWSCircuitBreaker1.PerEndpoint.Items.Add as TsgcCircuitBreakerEndpointItem do
    begin
      Pattern := 'api.stripe.com';
      Enabled := True;
      OverrideThresholds.Enabled := True;
      OverrideThresholds.FailureCount := 3;
      OverrideThresholds.FailureRatePercent := 30;
      OverrideRecovery.CooldownSec := 120;
    end;
    

 

Metrics

 

What it does: Read-only counters updated atomically as calls flow through the breaker. Aggregated across all keys / hostnames. CurrentOpenBreakers is refreshed after every state transition; AverageLatencyMs is a running average of the durations observed by Execute.

 

When to use it: Live dashboard showing per-host circuit states, failure rates and rejected counts. Poll Metrics from a TTimer to paint gauges for operators, or emit them to Prometheus / StatsD for alerting.

 

PropertyTypeDescription
TotalCallsInt64Total calls processed (successes + recorded failures).
TotalFailuresInt64Total failures recorded across all keys.
TotalSuccessesInt64Total successful calls recorded across all keys.
TotalRejectedInt64Total calls rejected because the circuit was Open.
CurrentOpenBreakersIntegerNumber of keys currently in the Open state.
AverageLatencyMsDoubleRunning average latency (ms) measured around Execute / ExecuteWithResult calls.

 


    // Poll from a TTimer and update a dashboard
    procedure TForm1.MetricsTimer(Sender: TObject);
    begin
      LabelCalls.Caption := Format('Calls: %d (ok %d / fail %d / rejected %d)',
        [sgcWSCircuitBreaker1.Metrics.TotalCalls,
         sgcWSCircuitBreaker1.Metrics.TotalSuccesses,
         sgcWSCircuitBreaker1.Metrics.TotalFailures,
         sgcWSCircuitBreaker1.Metrics.TotalRejected]);
      LabelOpen.Caption := Format('Open breakers: %d',
        [sgcWSCircuitBreaker1.Metrics.CurrentOpenBreakers]);
      LabelLatency.Caption := Format('Avg latency: %.1f ms',
        [sgcWSCircuitBreaker1.Metrics.AverageLatencyMs]);
    end;
    

 

Events

 

OnStateChange: Fired when a circuit transitions between states. Provides the key, the old state and the new state (csClosed, csOpen or csHalfOpen). Useful for logging, alerting and updating live UI.

 

OnCallRejected: Fired when a call is rejected because the circuit is Open or the HalfOpen trial budget is exhausted. Provides the key and a reason string (for example Circuit Open).

 

OnFallback: Fired just before a fallback response is returned. The aResponse parameter (var string) is initialized from Fallback.CachedResponse / CustomMessage and may be replaced by the handler.

 

OnFailureRecorded: Fired every time a failure is recorded. Provides the key and the exception text (ClassName: Message). Use for structured logging or to correlate with upstream incident tracking.

 

OnSlowCall: Fired when a successful call exceeded Thresholds.SlowCallDurationMs. Provides the key and the measured duration in milliseconds.

 

Public Methods

 

MethodDescription
Execute(Key, Action)Runs the TProc under the given key. Checks IsCallAllowed first, records success / failure / slow automatically and returns True if the action executed.
Execute(Action)Overload using DefaultKey.
ExecuteWithResult(Key, Action, out Result)(D2009+) Runs a TFunc<TObject> and returns its result via the out parameter; same accounting as Execute.
RecordSuccess(Key)Records a successful call. Advances HalfOpen -> Closed when trials succeed.
RecordSuccessOverload using DefaultKey.
RecordFailure(Key, Exception)Records a failure with optional exception text; triggers transition evaluation.
RecordFailure(Exception)Overload using DefaultKey.
ForceOpen(Key)Manually forces the circuit into the Open state.
ForceClose(Key)Manually forces the circuit into the Closed state.
Reset(Key)Clears counters and state for the given key.
ResetAllClears every tracked circuit and resets Metrics.
GetState(Key)Returns the current TsgcCircuitState for the key.
IsCallAllowed(Key)Returns True if a new call is currently allowed. Called automatically by the HTTP API client before every request.
GetFailureRate(Key)Returns the current failure percentage (0.0 - 100.0) observed in the rolling window.
GetTotalCalls(Key)Returns the number of calls observed in the rolling window for the key.
GetLastStateChange(Key)Returns the TDateTime of the last state transition.
GetOpenBreakersReturns TArray<string> of keys currently in the Open state.
SaveStateToFile(FileName)Persists every circuit's state and counters to a file.
LoadStateFromFile(FileName)Restores circuit states from a previously saved file.
IsConnectionAllowed(IP)Server-side self-protection hook. Secondary to the primary client-side use. Returns True if the ServerKey circuit is not Open.
IsMessageAllowed(IP, Message)Server-side self-protection hook. Returns True if the ServerKey circuit is not Open.
RegisterConnection(IP)Server-side hook reserved for future per-IP metrics (currently no-op).
UnregisterConnection(IP)Server-side hook reserved for future per-IP metrics (currently no-op).
RecordMessageError(IP, Exception)Server-side self-protection hook. Records a failure on ServerKey.
RecordMessageSuccess(IP)Server-side self-protection hook. Records a success on ServerKey.

 

Automatic Integration

 

When assigned to a TsgcHTTPAPI_client descendant via the CircuitBreaker property, the client automatically:

 

Complete Example

 


    // Enable the circuit breaker
    sgcWSCircuitBreaker1.Enabled := True;

    // Thresholds: open after 5 failures, or 50% failure rate on 10+ calls
    sgcWSCircuitBreaker1.Thresholds.Enabled := True;
    sgcWSCircuitBreaker1.Thresholds.FailureCount := 5;
    sgcWSCircuitBreaker1.Thresholds.FailureRatePercent := 50;
    sgcWSCircuitBreaker1.Thresholds.SlowCallDurationMs := 3000;
    sgcWSCircuitBreaker1.Thresholds.MinCalls := 10;

    // Rolling window: 60 seconds / 10 buckets
    sgcWSCircuitBreaker1.TimeWindow.RollingWindowSec := 60;
    sgcWSCircuitBreaker1.TimeWindow.BucketCount := 10;

    // Recovery: wait 30s in Open, then 1 trial call in HalfOpen
    sgcWSCircuitBreaker1.Recovery.CooldownSec := 30;
    sgcWSCircuitBreaker1.Recovery.HalfOpenTrialCalls := 1;

    // Classification: record HTTP 5xx and timeouts, ignore 4xx
    sgcWSCircuitBreaker1.Classification.MatchMode := cemContains;
    sgcWSCircuitBreaker1.Classification.RecordAsFailure.Add('HTTP/1.1 5');
    sgcWSCircuitBreaker1.Classification.RecordAsFailure.Add('timeout');
    sgcWSCircuitBreaker1.Classification.IgnoreExceptions.Add('HTTP/1.1 404');

    // Fallback: serve a cached payload while the circuit is Open
    sgcWSCircuitBreaker1.Fallback.Enabled := True;
    sgcWSCircuitBreaker1.Fallback.CustomMessage :=
      '{"error":"service unavailable"}';

    // Per-endpoint overrides
    sgcWSCircuitBreaker1.PerEndpoint.Enabled := True;
    with sgcWSCircuitBreaker1.PerEndpoint.Items.Add as TsgcCircuitBreakerEndpointItem do
    begin
      Pattern := 'api.stripe.com';
      OverrideThresholds.Enabled := True;
      OverrideThresholds.FailureCount := 3;
      OverrideRecovery.CooldownSec := 120;
    end;

    // Persist circuit state across restarts
    sgcWSCircuitBreaker1.LoadStateFromFile('circuit.dat');

    // Attach to an HTTP API client (any TsgcHTTPAPI_client descendant)
    sgcAI_OpenAI1.CircuitBreaker := sgcWSCircuitBreaker1;

    // Use the client normally; the breaker intercepts Get / Post / etc.
    try
      Response := sgcAI_OpenAI1.Get('https://api.openai.com/v1/models');
    except
      on E: Exception do
        if Pos('Circuit breaker open', E.Message) > 0 then
          Log('Circuit open for api.openai.com - using fallback')
        else
          Log('Call failed: ' + E.Message);
    end;

    // Handle state changes
    procedure TForm1.CircuitStateChange(Sender: TObject; const aKey: string;
      aOldState, aNewState: TsgcCircuitState);
    begin
      Log(Format('Circuit %s: %d -> %d', [aKey, Ord(aOldState), Ord(aNewState)]));
    end;

    // Handle fallback responses
    procedure TForm1.CircuitFallback(Sender: TObject; const aKey: string;
      var aResponse: string);
    begin
      Log('Fallback used for ' + aKey);
    end;
    

 

Thread Safety

 

The circuit breaker is fully thread-safe. All public methods use internal critical sections and thread-safe lists to protect concurrent access to per-key state, the rolling window and the metrics counters. A single component can be shared by multiple HTTP API clients and accessed from any thread (event handlers, timers, background workers) without external synchronization.

 

Reference