Recovery Runbooks
This guide provides step-by-step procedures for recovering from common operational failures in Excalibur applications.
Before You Start
- .NET 10.0
- Access to your production or staging SQL Server instance
- Familiarity with performance tuning and health checks
SQL Server Recovery Scenarios
Session Killed During CDC Processing (Error 596)
Symptoms:
- CDC processor stops processing events
- Log shows error 596: "Cannot continue the execution because the session is in the kill state"
- Projection updates stop
Diagnosis:
-- Check for active CDC sessions
SELECT session_id, status, start_time
FROM sys.dm_cdc_sessions
WHERE end_time IS NULL;
-- Check CDC capture position
SELECT * FROM cdc.lsn_time_mapping
ORDER BY tran_begin_time DESC;
Recovery Steps:
- The retry policy automatically handles error 596
- If processor doesn't recover within retry attempts:
# Restart the applicationsystemctl restart myapp
- If position is invalid after restart:
// Configure the recovery strategy on the CDC builder.// FallbackToEarliest is already the default; set it explicitly to make the choice visible.services.AddCdcProcessor(cdc => cdc.UseSqlServer(sql => sql.ConnectionString(connectionString)).WithRecovery(recovery => recovery.Strategy(StalePositionRecoveryStrategy.FallbackToEarliest)));
Prevention:
- Configure CDC recovery options in application startup
- Monitor CDC processor health via metrics
- Alert on repeated retry attempts
Database Backup/Restore Invalidates LSN Position
Symptoms:
- CDC processor fails with "Invalid LSN" error
- Event store cannot find expected position
- Projection processor stuck
Diagnosis:
-- The oldest change still available for each capture instance
SELECT capture_instance, sys.fn_cdc_get_min_lsn(capture_instance) AS min_lsn
FROM cdc.change_tables;
-- The position the processor saved for each capture instance.
-- [Cdc].[CdcProcessingState] is the default; use your configured schema and table name if you changed them.
SELECT DatabaseName, TableName AS CaptureInstance, LastProcessedLsn, ProcessedAt
FROM [Cdc].[CdcProcessingState];
The saved position is stale when a capture instance's LastProcessedLsn is lower than its min_lsn: the
changes in between have already been removed by SQL Server's CDC cleanup.
Recovery Steps:
-
Automatic (Recommended): use the
FallbackToEarlieststrategy, which is the default:services.AddCdcProcessor(cdc => cdc.UseSqlServer(sql => sql.ConnectionString(connectionString)).WithRecovery(recovery => recovery.Strategy(StalePositionRecoveryStrategy.FallbackToEarliest))); -
Manual Reset: If automatic recovery fails. Stop every processor instance first: a running processor can overwrite the row, and this update bypasses the leader-election fence the processor's own writes respect.
-- Move one capture instance's saved position to the oldest change still available.UPDATE [Cdc].[CdcProcessingState]SET LastProcessedLsn = sys.fn_cdc_get_min_lsn(TableName),LastProcessedSequenceValue = NULLWHERE TableName = 'dbo_YourTable'; -- the capture instance nameChanges that CDC cleanup had already removed are not recovered by this; it moves the processor past the gap. Treat whatever those changes carried as lost, and reconcile it from the source tables if it matters.
-
Restart the processor and verify events are processing
Prevention:
- Use
FallbackToEarlieststrategy for data consistency - Monitor position age vs CDC retention
- Schedule backups during low-activity periods
Connection Pool Corruption
Symptoms:
- Intermittent connection failures
- "Connection is broken" errors
- Some operations succeed, others fail
Diagnosis:
-- Check for orphaned connections
SELECT session_id, login_name, status, last_request_end_time
FROM sys.dm_exec_sessions
WHERE program_name LIKE '%YourApp%'
ORDER BY last_request_end_time;
Recovery Steps:
-
Clear all connection pools:
SqlConnection.ClearAllPools(); -
If in Kubernetes, rolling restart:
kubectl rollout restart deployment/myapp -
Monitor for recurring issues
Prevention:
- Configure connection lifetime limits
- Implement health checks that test connections
- Use Azure SQL maintenance windows
PostgreSQL Recovery Scenarios
Broken Pipe / Connection Lost (08xxx Errors)
Symptoms:
- "Connection broken" or "57P01 admin_shutdown" errors
- Operations fail mid-transaction
- CDC processor stops
Diagnosis:
-- Check active connections
SELECT pid, usename, application_name, state, query_start
FROM pg_stat_activity
WHERE application_name LIKE '%YourApp%';
-- Check for terminated backends
SELECT * FROM pg_stat_activity WHERE state = 'idle in transaction';
Recovery Steps:
-
Retry policy handles transient errors automatically
-
If persistent, clear connection pool:
NpgsqlConnection.ClearAllPools(); -
Check PostgreSQL logs for root cause:
tail -100 /var/log/postgresql/postgresql-15-main.log
Prevention:
- Configure
tcp_keepalives_idlein connection string - Monitor
pg_stat_activityfor long-running transactions - Use connection pool health checks
Deadlock Detection (40P01)
Symptoms:
- Operations fail with "deadlock detected" error
- Concurrent writes to same aggregates
Diagnosis:
-- Find blocking queries
SELECT blocked_locks.pid AS blocked_pid,
blocking_locks.pid AS blocking_pid,
blocked_activity.query AS blocked_statement
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
JOIN pg_catalog.pg_locks blocking_locks ON blocking_locks.locktype = blocked_locks.locktype;
Recovery Steps:
- Automatic retry handles deadlocks
- If frequent, review aggregate design:
- Reduce aggregate scope
- Implement optimistic concurrency
- Use advisory locks for coordination
Prevention:
- Design aggregates to minimize contention
- Use consistent lock ordering
- Monitor deadlock frequency via metrics
Cloud Provider Recovery Scenarios
CosmosDB Rate Limiting (429)
Symptoms:
- Operations fail with 429 "Request rate too large"
- Throughput drops dramatically
- SDK retries exhausted
Recovery Steps:
-
SDK handles 429 automatically with backoff
-
If persistent, increase RU/s:
az cosmosdb sql container throughput update \--account-name myaccount \--database-name mydb \--name mycontainer \--throughput 10000 -
Enable autoscale:
az cosmosdb sql container throughput migrate \--account-name myaccount \--database-name mydb \--name mycontainer \--resource-group mygroup \--throughput-type autoscale
Prevention:
- Enable autoscale for variable workloads
- Monitor RU consumption via Azure Monitor
- Implement bulk operations for high-throughput scenarios
DynamoDB Throttling
Symptoms:
- Operations fail with ProvisionedThroughputExceededException
- Latency spikes
Recovery Steps:
- SDK handles throttling automatically
- If persistent, increase capacity:
aws dynamodb update-table \--table-name MyTable \--provisioned-throughput ReadCapacityUnits=100,WriteCapacityUnits=100
Prevention:
- Enable on-demand capacity for unpredictable workloads
- Monitor consumed capacity via CloudWatch
- Implement exponential backoff in application code
General Recovery Procedures
Event Store Recovery
When to use: Event store corruption or position drift
-
Verify event store integrity:
-- SQL ServerSELECT StreamId, COUNT(*) as EventCount, MAX(Version) as MaxVersionFROM [dbo].[Events]GROUP BY StreamIdHAVING COUNT(*) != MAX(Version) + 1; -
Rebuild projections if needed:
await projectionRebuilder.RebuildAsync<MyProjection>(cancellationToken); -
Verify projection state:
SELECT * FROM [dbo].[ProjectionCheckpoints]WHERE ProjectionName = 'MyProjection';
Outbox Recovery
When to use: Messages stuck in outbox, duplicate delivery suspected
-
Check outbox status:
SELECT Status, COUNT(*) as CountFROM [dbo].[OutboxMessages]GROUP BY Status; -
Reprocess stuck messages:
Statusis a number (0Staged,1Sending,2Sent,3Failed,4PartiallyFailed,5DeadLettered). The processor picks upFailedmessages again by itself until theirRetryCountreaches the configured maximum; a message is only stuck once it has used up its retries. To give those another round once the cause is fixed:UPDATE [dbo].[OutboxMessages]SET Status = 0, RetryCount = 0, NextAttemptAt = NULLWHERE Status = 3 AND CreatedAt > DATEADD(hour, -24, SYSDATETIMEOFFSET());Messages already moved to the dead-letter queue (
5) are recovered by replaying them from the dead-letter queue, not by editing this table. -
Monitor for successful delivery
Monitoring and Alerting
Key Metrics to Monitor
| Metric | Warning Threshold | Critical Threshold |
|---|---|---|
| Retry rate | > 5% of operations | > 20% of operations |
| CDC lag | > 1 minute | > 5 minutes |
| Connection errors | > 1/minute | > 10/minute |
| Deadlock rate | > 1/hour | > 10/hour |
Recommended Alerts
# Example Prometheus alerting rules
groups:
- name: excalibur-resilience
rules:
- alert: HighRetryRate
expr: rate(dispatch_write_store_retry_count_total[5m]) > 0.05
for: 5m
labels:
severity: warning
- alert: CdcLagHigh
expr: dispatch_cdc_lag_seconds > 300
for: 5m
labels:
severity: critical
Related Documentation
- Operational Resilience - Retry policies and configuration
- Observability - Monitoring setup
- Health Checks - Application health monitoring
See Also
- Disaster Recovery — Restoring state, and what a restore does to fences, checkpoints, and deduplication windows
- Fault and Degraded-Mode Catalog — The log event id and metric for each fault, and what the framework does on its own
- CDC Troubleshooting — Diagnose and recover from Change Data Capture issues
- Performance Tuning — Optimize event store, outbox, and projection throughput
- Health Checks — Application health monitoring and diagnostics
- Dead Letter Pattern — Handling failed messages with dead letter queues