Skip to content

Transiency Resolvers

Transiency classifies exceptions as transient (retryable) or terminal (non-retryable), enabling intelligent retry policies throughout Fusion.

Key Types

TypeDescriptionSource
TransiencyException classification resultTransiency.cs
TransiencyResolverDetermines if exception is transientTransiencyResolver.cs
TransiencyResolversBuilt-in resolver implementationsTransiencyResolvers.cs
RetryPolicyConfigurable retry with backoffRetryPolicy.cs

Why Transiency?

Not all errors should trigger retries:

Error TypeShould Retry?Example
Network timeoutYesTimeoutException
Connection resetYesSocketException
Server overloadedYesHTTP 503
Invalid argumentNoArgumentException
Not foundNoHTTP 404
Authentication failedNoHTTP 401

TransiencyResolver makes this classification automatic and consistent.

Transiency Enum

cs
public enum Transiency
{
    Unknown = 0,      // Not determined
    Terminal = 1,     // Don't retry
    Transient = 2,    // Safe to retry
    SuperTransient = 3 // Retry immediately (no delay)
}
ValueMeaningTypical Action
UnknownClassification failedTreat as terminal
TerminalPermanent errorDon't retry, report error
TransientTemporary errorRetry with backoff
SuperTransientVery temporaryRetry immediately

TransiencyResolver

Basic Usage

cs
var resolver = TransiencyResolver.New(services);

try {
    await DoOperation();
}
catch (Exception ex) {
    var transiency = resolver.Invoke(ex);

    if (transiency.IsTransient()) {
        // Retry the operation
        await Task.Delay(backoff);
        await DoOperation();
    }
    else {
        // Report permanent failure
        throw;
    }
}

Default Resolver

The default resolver handles common transient exceptions:

cs
// Built-in transient exceptions:
// - OperationCanceledException (SuperTransient)
// - TimeoutException
// - SocketException
// - HttpRequestException (certain status codes)
// - DbException (certain error codes)

var resolver = TransiencyResolver.New(services);

Custom Resolver

cs
// Add custom logic
var customResolver = TransiencyResolver.New(
    TransiencyResolvers.PreferTransient,  // Base behavior
    ex => ex switch {
        MyCustomException e => e.IsRetryable
            ? Transiency.Transient
            : Transiency.Terminal,
        _ => Transiency.Unknown  // Let other resolvers decide
    }
);

Chaining Resolvers

Resolvers can be chained — first non-Unknown result wins:

cs
var resolver = TransiencyResolver.New(
    // Check custom exceptions first
    ex => ex is MyException me ? me.Transiency : Transiency.Unknown,
    // Then fall back to defaults
    TransiencyResolvers.PreferTransient
);

Built-in Resolvers

TransiencyResolvers.PreferTransient

Assumes unknown exceptions are transient:

cs
var resolver = TransiencyResolvers.PreferTransient;
// Unknown → Transient

TransiencyResolvers.PreferTerminal

Assumes unknown exceptions are terminal:

cs
var resolver = TransiencyResolvers.PreferTerminal;
// Unknown → Terminal

RetryPolicy

Combines TransiencyResolver with retry logic:

cs
var policy = new RetryPolicy(
    maxRetryCount: 5,
    retryDelays: RetryDelaySeq.Exp(1, 60),  // 1s → 60s exponential
    transiencyResolver: TransiencyResolver.New(services)
);

// Execute with retry
await policy.Run(async ct => {
    await DoOperationAsync(ct);
}, cancellationToken);

Retry Delay Sequences

cs
// Exponential backoff: 1s, 2s, 4s, 8s, ... up to 60s
var exp = RetryDelaySeq.Exp(
    TimeSpan.FromSeconds(1),
    TimeSpan.FromSeconds(60)
);

// Fixed delay: always 5s
var fixed = RetryDelaySeq.Fixed(TimeSpan.FromSeconds(5));

// Linear: 1s, 2s, 3s, 4s, ... up to 30s
var linear = RetryDelaySeq.Linear(
    TimeSpan.FromSeconds(1),
    TimeSpan.FromSeconds(30)
);

Usage in Fusion

Operations Framework Reprocessing

The Operations Framework uses transiency to determine if failed operations should be reprocessed:

cs
// In operation reprocessing
catch (Exception ex) {
    var transiency = TransiencyResolver.Invoke(ex);
    if (transiency.IsTransient()) {
        // Mark for reprocessing
        await ScheduleReprocess(operation);
    }
    else {
        // Log permanent failure
        Log.Error(ex, "Operation failed permanently");
    }
}

See Operations Framework Reprocessing for details.

RPC Connection Handling

RPC uses transiency for reconnection decisions:

cs
// Connection error handling
catch (Exception ex) {
    var transiency = TransiencyResolver.Invoke(ex);
    if (transiency.IsTransient()) {
        // Attempt reconnection with backoff
        await ReconnectWithBackoff();
    }
    else {
        // Connection permanently failed
        await NotifyConnectionLost();
    }
}

DbEntityResolver

Database entity resolvers use transiency for query retry:

cs
// Retry transient database errors
var policy = new RetryPolicy(3, RetryDelaySeq.Exp(0.1, 5));
return await policy.Run(async ct => {
    await using var db = await DbHub.CreateDbContext(ct);
    return await db.Entities.FindAsync(key, ct);
}, cancellationToken);

Common Patterns

Service Method with Retry

cs
public async Task<Data> FetchDataAsync(CancellationToken ct)
{
    var policy = new RetryPolicy(
        maxRetryCount: 3,
        retryDelays: RetryDelaySeq.Exp(0.5, 10),
        transiencyResolver: _transiencyResolver
    );

    return await policy.Run(async innerCt => {
        return await _httpClient.GetFromJsonAsync<Data>(url, innerCt);
    }, ct);
}

Custom Exception Classification

cs
// Mark your exceptions as transient/terminal
public class RateLimitedException : Exception
{
    public TimeSpan RetryAfter { get; }
}

// Register resolver
services.AddSingleton<TransiencyResolver>(sp =>
    TransiencyResolver.New(
        ex => ex switch {
            RateLimitedException => Transiency.Transient,
            AuthenticationException => Transiency.Terminal,
            _ => Transiency.Unknown
        },
        TransiencyResolvers.PreferTransient
    )
);

Logging Transient vs Terminal Errors

cs
catch (Exception ex) {
    var transiency = _transiencyResolver.Invoke(ex);

    if (transiency.IsTransient()) {
        _log.LogWarning(ex, "Transient error, will retry");
    }
    else {
        _log.LogError(ex, "Permanent error");
        throw;
    }
}

Best Practices

Default to Terminal for Unknown

cs
// Good: Safer default
var resolver = TransiencyResolvers.PreferTerminal;

// Risky: May retry non-retryable operations
var resolver = TransiencyResolvers.PreferTransient;

Classify Your Exceptions

cs
// Good: Explicit classification
public class MyServiceException : Exception
{
    public bool IsTransient { get; init; }
}

// Register resolver
TransiencyResolver.New(ex =>
    ex is MyServiceException mse
        ? mse.IsTransient ? Transiency.Transient : Transiency.Terminal
        : Transiency.Unknown
);

Use Appropriate Backoff

cs
// Good: Exponential backoff with reasonable bounds
RetryDelaySeq.Exp(
    TimeSpan.FromMilliseconds(100),  // Start small
    TimeSpan.FromSeconds(30)          // Don't wait too long
)

// Bad: Immediate retry (may overwhelm failing service)
RetryDelaySeq.Fixed(TimeSpan.Zero)