Saltar al contenido principal

Resiliencia

Vali-Blob incluye integración con Polly para agregar reintentos automáticos, circuit breaker y timeouts a las operaciones de almacenamiento. Esto hace la aplicación más resistente ante fallos transitorios de red o del proveedor de nube.

Configuración

builder.Services
.AddVali-Blob(o => o.DefaultProvider = "aws")
.AddProvider<AWSS3Provider>("aws", opts => { /* ... */ })
.WithResilience(r =>
{
r.MaxRetries = 3;
r.RetryDelay = TimeSpan.FromSeconds(1);
r.UseExponentialBackoff = true;
r.CircuitBreakerThreshold = 5;
r.CircuitBreakerDuration = TimeSpan.FromSeconds(30);
r.OperationTimeout = TimeSpan.FromSeconds(60);
});

ResilienceOptions

public class ResilienceOptions
{
/// <summary>Número máximo de reintentos por operación. Por defecto: 3.</summary>
public int MaxRetries { get; set; } = 3;

/// <summary>Tiempo base de espera entre reintentos. Por defecto: 1 segundo.</summary>
public TimeSpan RetryDelay { get; set; } = TimeSpan.FromSeconds(1);

/// <summary>Si true, usa backoff exponencial (1s, 2s, 4s...). Si false, espera fija.</summary>
public bool UseExponentialBackoff { get; set; } = true;

/// <summary>Agregar jitter aleatorio para evitar el problema de thundering herd.</summary>
public bool UseJitter { get; set; } = true;

/// <summary>Número de fallos consecutivos para activar el circuit breaker.</summary>
public int CircuitBreakerThreshold { get; set; } = 5;

/// <summary>Tiempo que el circuit breaker permanece abierto antes de intentar recuperarse.</summary>
public TimeSpan CircuitBreakerDuration { get; set; } = TimeSpan.FromSeconds(30);

/// <summary>Timeout por operación individual. Por defecto: 5 minutos.</summary>
public TimeSpan OperationTimeout { get; set; } = TimeSpan.FromMinutes(5);

/// <summary>Operaciones excluidas del reintento automático.</summary>
public ISet<StorageOperation> ExcludedFromRetry { get; set; } = new HashSet<StorageOperation>();

/// <summary>Códigos de error que NO deben reintentarse (errores de negocio, no transitorios).</summary>
public ISet<StorageErrorCode> NonRetryableErrors { get; set; } = new HashSet<StorageErrorCode>
{
StorageErrorCode.FileTooLarge,
StorageErrorCode.InvalidFileType,
StorageErrorCode.QuotaExceeded,
StorageErrorCode.VirusDetected,
StorageErrorCode.PermissionDenied,
StorageErrorCode.DuplicateFile,
StorageErrorCode.Cancelled
};
}

Backoff exponencial con jitter

Intento 1: Falla → Esperar ~1.0s  (1s base + jitter 0–200ms)
Intento 2: Falla → Esperar ~2.1s (2s base + jitter 0–200ms)
Intento 3: Falla → Esperar ~4.2s (4s base + jitter 0–200ms)
Intento 4 (último): Si falla → StorageResult.Failure final

Con UseJitter = true, el tiempo de espera incluye un componente aleatorio para evitar que múltiples instancias del servidor reintentan exactamente al mismo tiempo (thundering herd problem).

Circuit Breaker

El circuit breaker monitorea los fallos consecutivos y cuando superan el umbral "abre el circuito", devolviendo errores inmediatamente sin intentar la operación hasta que transcurra el tiempo de recuperación.

loading...
.WithResilience(r =>
{
r.CircuitBreakerThreshold = 5; // Abrir con 5 fallos consecutivos
r.CircuitBreakerDuration = TimeSpan.FromSeconds(30); // Recuperar tras 30 segundos
})

Cuando el circuit breaker está abierto, las operaciones retornan inmediatamente con:

StorageResult.Failure(StorageErrorCode.ProviderError, "Circuit breaker abierto.")

Configuración por proveedor

builder.Services
.AddVali-Blob(o => o.DefaultProvider = "aws")
// AWS S3: estable, reintentos moderados
.AddProvider<AWSS3Provider>("aws", opts => { /* ... */ })
.WithResilience(r =>
{
r.MaxRetries = 3;
r.RetryDelay = TimeSpan.FromMilliseconds(500);
r.UseExponentialBackoff = true;
r.CircuitBreakerThreshold = 10;
})
// Supabase: puede tener más latencia variable
.AddProvider<SupabaseStorageProvider>("supabase", opts => { /* ... */ })
.WithResilience(r =>
{
r.MaxRetries = 5;
r.RetryDelay = TimeSpan.FromSeconds(1);
r.UseExponentialBackoff = true;
r.CircuitBreakerThreshold = 3;
r.CircuitBreakerDuration = TimeSpan.FromSeconds(60);
});

Excluir operaciones del reintento

.WithResilience(r =>
{
r.MaxRetries = 3;
r.RetryDelay = TimeSpan.FromMilliseconds(500);
r.UseExponentialBackoff = true;
r.UseJitter = true;

// No reintentar descargas (el stream puede estar parcialmente consumido)
r.ExcludedFromRetry.Add(StorageOperation.Download);
})

Timeout por operación

.WithResilience(r =>
{
r.OperationTimeout = TimeSpan.FromSeconds(30); // Timeout general
r.UploadTimeout = TimeSpan.FromMinutes(10); // Subidas de archivos grandes
r.DownloadTimeout = TimeSpan.FromMinutes(5); // Descargas
})

Logging automático de reintentos

Vali-Blob registra automáticamente cada reintento y el resultado final:

[DBG] Vali-Blob: Upload falló (intento 1/3). Reintentando en 1000ms. Error: NetworkError
[DBG] Vali-Blob: Upload falló (intento 2/3). Reintentando en 2000ms. Error: NetworkError
[INF] Vali-Blob: Upload exitoso después de 3 intentos.

Monitorear el estado del circuit breaker

public class MonitorCircuitBreaker(
IStorageResilienceMonitor monitor,
ILogger<MonitorCircuitBreaker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(10));

while (await timer.WaitForNextTickAsync(ct))
{
var estado = monitor.GetCircuitBreakerState("aws");

if (estado == CircuitBreakerState.Open)
logger.LogWarning(
"Circuit breaker del proveedor 'aws' está ABIERTO. " +
"Las operaciones de almacenamiento fallarán inmediatamente.");
}
}
}
Consejo

Los errores de negocio como FileTooLarge, InvalidFileType o VirusDetected nunca deben reintentarse: el resultado será siempre el mismo. Solo reintentar errores transitorios (NetworkError, Timeout, ProviderError). Asegúrate de que los errores de negocio estén en NonRetryableErrors.

Advertencia

Con MaxRetries = 3 y backoff exponencial, el tiempo total máximo de espera antes del fallo definitivo puede ser hasta 1s + 2s + 4s = 7s. Para endpoints de API interactivos con usuarios, ajusta los valores de reintento o aplica reintentos solo a operaciones en background. Un timeout de OperationTimeout = 5 min es apropiado para subidas grandes, pero demasiado para llamadas de lectura de metadatos.