Skip to content

Instantly share code, notes, and snippets.

@roseaar42
Last active July 26, 2025 07:42
Show Gist options
  • Select an option

  • Save roseaar42/3a70361cfb19a79cda6bfdfcb276c039 to your computer and use it in GitHub Desktop.

Select an option

Save roseaar42/3a70361cfb19a79cda6bfdfcb276c039 to your computer and use it in GitHub Desktop.
aurora-failover-toolkit - Production Node.js patterns for bulletproof multi-region Aurora with circuit breakers and intelligent failover

Aurora Resilience Patterns

Production-ready patterns for building bulletproof multi-region Aurora applications with intelligent failover, recovery, and graceful degradation.

Overview

This repository contains a complete set of Node.js patterns for handling regional failures in multi-region Aurora deployments. Rather than relying solely on AWS Aurora Global Database, these patterns give you application-level control over failover behavior, data consistency, and recovery procedures.

What This Solves

  • Regional Outages: Automatic failover when Aurora regions become unavailable
  • Cascading Failures: Circuit breaker patterns prevent one failure from bringing down your entire system
  • Data Loss: Write queuing ensures critical operations aren't lost during failover
  • Poor User Experience: Graceful degradation maintains core functionality during outages
  • Recovery Complexity: Safe procedures for bringing regions back online

Architecture

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   US-EAST-1     │    │   EU-WEST-1     │    │  AP-SOUTHEAST-1 │
│   (Primary)     │    │   (Fallback)    │    │   (Fallback)    │
└─────────────────┘    └─────────────────┘    └─────────────────┘
         │                       │                       │
         └───────────────────────┼───────────────────────┘
                                 │
                    ┌─────────────────┐
                    │  Node.js App    │
                    │  + Resilience   │
                    │    Patterns     │
                    └─────────────────┘

Quick Start

1. Install Dependencies

npm install express pg

2. Set Up Your Database Clients

const dbClients = {
  'us-east-1': new Client({ connectionString: process.env.DB_US_EAST_1 }),
  'eu-west-1': new Client({ connectionString: process.env.DB_EU_WEST_1 }),
  'ap-southeast-1': new Client({ connectionString: process.env.DB_AP_SOUTHEAST_1 })
};

3. Initialize the Resilience Layer

const { RegionManager } = require('./region-manager');
const { WriteQueue } = require('./write-queue');
const { FeatureManager } = require('./feature-manager');

const regionManager = new RegionManager(dbClients);
const writeQueue = new WriteQueue();
const featureManager = new FeatureManager();

4. Add the Middleware

const { failoverMiddleware } = require('./middleware');

app.use(failoverMiddleware(regionManager));
app.use(featureMiddleware(featureManager));

File Structure

File Purpose
circuit-breaker.js Circuit breaker pattern to prevent cascading failures
region-manager.js Intelligent routing with multi-tier fallbacks
write-queue.js Queuing system for critical writes during failover
feature-manager.js Feature flagging for graceful degradation
region-recovery.js Safe procedures for bringing regions back online
failover-metrics.js Observability and metrics collection
middleware.js Express middleware integration examples
health-checks.js Enhanced health monitoring functions

Core Patterns

Circuit Breaker

Prevents cascading failures by "opening" when error rates exceed thresholds:

const breaker = new RegionCircuitBreaker(5, 30000); // 5 failures, 30s timeout
await breaker.execute(() => db.query('SELECT 1'));

Intelligent Failover

Automatically routes to healthy regions with fallback chains:

const { client, region, isFallback } = await regionManager.getHealthyClient('us-east-1');

Write Queuing

Preserves critical operations during regional outages:

await safeWrite(() => db.query('INSERT INTO users...'), { userId: 123 }, req);

Graceful Degradation

Disables non-critical features during degraded operation:

if (req.features.analytics) {
  // Only run analytics if not in fallback mode
  await trackUserAction(req.user.id, 'page_view');
}

Production Considerations

Environment Variables

# Database connections
DB_US_EAST_1=postgresql://user:pass@aurora-us-east-1.cluster-xyz.us-east-1.rds.amazonaws.com:5432/mydb
DB_EU_WEST_1=postgresql://user:pass@aurora-eu-west-1.cluster-abc.eu-west-1.rds.amazonaws.com:5432/mydb
DB_AP_SOUTHEAST_1=postgresql://user:pass@aurora-ap-southeast-1.cluster-def.ap-southeast-1.rds.amazonaws.com:5432/mydb

# Circuit breaker settings
CIRCUIT_BREAKER_THRESHOLD=5
CIRCUIT_BREAKER_TIMEOUT=30000

# Health check settings
HEALTH_CHECK_TIMEOUT=5000
HEALTH_CHECK_INTERVAL=10000

Monitoring & Alerting

Set up alerts for:

  • Circuit breaker state changes
  • Failover events
  • Write queue depth
  • Regional health check failures
  • High latency thresholds

Connection Pool Management

Ensure your connection pools are configured for failover scenarios:

const dbClient = new Pool({
  connectionString: process.env.DB_CONNECTION,
  max: 20,
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 5000,
  statement_timeout: 10000,
  query_timeout: 10000
});

Testing Failover

Simulate Regional Failure

# Block connections to specific region
sudo iptables -A OUTPUT -d aurora-us-east-1.cluster-xyz.us-east-1.rds.amazonaws.com -j DROP

Test Circuit Breaker

// Force circuit breaker to open
for (let i = 0; i < 6; i++) {
  try {
    await regionManager.checkHealth('us-east-1');
  } catch (error) {
    console.log(`Failure ${i + 1}/6`);
  }
}

Validate Write Queuing

// Ensure writes are queued during fallback
const result = await safeWrite(() => db.query('INSERT INTO test_table VALUES (1)'), {}, req);
console.log(result.queued); // Should be true during fallback

Performance Impact

  • Circuit Breaker: ~0.1ms overhead per operation
  • Health Checks: Run every 10 seconds, ~5ms per region
  • Write Queuing: ~1-2ms overhead when queuing enabled
  • Feature Flags: ~0.05ms overhead per check

Best Practices

  1. Start with Conservative Thresholds: Begin with higher failure thresholds and shorter timeouts, then tune based on your traffic patterns

  2. Monitor Queue Depth: Set alerts when write queues exceed reasonable limits (e.g., 100 operations)

  3. Test Recovery Procedures: Regularly test bringing regions back online to ensure your recovery logic works

  4. Use Feature Flags Strategically: Only disable truly non-critical features during degraded operation

  5. Log Everything: Comprehensive logging is crucial for post-incident analysis

Common Gotchas

  • Connection Pool Exhaustion: Ensure pools can handle connection failures gracefully
  • DNS Caching: Regional failures often involve DNS issues that affect more than just database connectivity
  • Write Amplification: Be careful not to duplicate writes across regions unintentionally
  • Monitoring Blind Spots: Circuit breakers can mask underlying issues if not properly monitored

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for new patterns
  4. Submit a pull request

License

MIT License - feel free to adapt these patterns for your production systems.


Need help? These patterns are based on real production experience. For questions about implementation or customization for your specific use case, open an issue or reach out to the maintainers.

/**
* Circuit Breaker Pattern for Aurora Regional Failover
*
* Prevents cascading failures by "opening" when error rates exceed thresholds.
* States: CLOSED (normal), OPEN (failing), HALF_OPEN (testing recovery)
*/
class RegionCircuitBreaker {
constructor(threshold = 5, timeout = 30000) {
this.failureCount = 0;
this.threshold = threshold; // Number of failures before opening
this.timeout = timeout; // Time to wait before attempting recovery (ms)
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
this.nextAttempt = Date.now();
this.lastFailureTime = null;
this.successCount = 0;
}
/**
* Execute an operation through the circuit breaker
* @param {Function} operation - Async function to execute
* @returns {Promise} - Result of the operation
* @throws {Error} - If circuit is open or operation fails
*/
async execute(operation) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) {
throw new Error(`Circuit breaker is OPEN. Next attempt in ${this.nextAttempt - Date.now()}ms`);
}
// Transition to half-open to test recovery
this.state = 'HALF_OPEN';
this.successCount = 0;
}
try {
const result = await operation();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
/**
* Handle successful operation
*/
onSuccess() {
this.failureCount = 0;
this.lastFailureTime = null;
if (this.state === 'HALF_OPEN') {
this.successCount++;
// Require multiple successes to fully close
if (this.successCount >= 2) {
this.state = 'CLOSED';
}
} else {
this.state = 'CLOSED';
}
}
/**
* Handle failed operation
*/
onFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.threshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.timeout;
}
}
/**
* Get current circuit breaker status
* @returns {Object} - Status information
*/
getStatus() {
return {
state: this.state,
failureCount: this.failureCount,
threshold: this.threshold,
nextAttempt: this.state === 'OPEN' ? this.nextAttempt : null,
lastFailureTime: this.lastFailureTime,
timeUntilRetry: this.state === 'OPEN' ? Math.max(0, this.nextAttempt - Date.now()) : 0
};
}
/**
* Reset the circuit breaker to closed state
*/
reset() {
this.state = 'CLOSED';
this.failureCount = 0;
this.lastFailureTime = null;
this.successCount = 0;
this.nextAttempt = Date.now();
}
/**
* Check if the circuit breaker is allowing requests
* @returns {boolean} - True if requests are allowed
*/
isRequestAllowed() {
if (this.state === 'CLOSED' || this.state === 'HALF_OPEN') {
return true;
}
if (this.state === 'OPEN' && Date.now() >= this.nextAttempt) {
return true;
}
return false;
}
}
module.exports = { RegionCircuitBreaker };
/**
* Failover Metrics and Observability
*
* Comprehensive metrics collection and monitoring for Aurora failover events,
* system health, and performance tracking across all regions.
*/
class FailoverMetrics {
constructor(options = {}) {
this.options = {
retentionPeriodMs: options.retentionPeriodMs || 24 * 60 * 60 * 1000, // 24 hours
aggregationIntervalMs: options.aggregationIntervalMs || 60 * 1000, // 1 minute
maxEvents: options.maxEvents || 10000,
enableAutoAggregation: options.enableAutoAggregation !== false
};
// Core metrics
this.metrics = {
failoverEvents: 0,
fallbackRequests: 0,
queuedOperations: 0,
circuitBreakerTrips: 0,
recoveryAttempts: 0,
successfulRecoveries: 0,
failedRecoveries: 0
};
// Per-region metrics
this.regionMetrics = {};
// Event tracking
this.events = [];
this.aggregatedMetrics = [];
// Performance tracking
this.performanceMetrics = {
latencyHistogram: {},
errorRates: {},
throughput: {}
};
// Health check tracking
this.healthMetrics = {};
if (this.options.enableAutoAggregation) {
this.startAutoAggregation();
}
}
/**
* Record a failover event
* @param {string} fromRegion - Region failed over from
* @param {string} toRegion - Region failed over to
* @param {Object} metadata - Additional event metadata
*/
recordFailover(fromRegion, toRegion, metadata = {}) {
this.metrics.failoverEvents++;
this.incrementRegionMetric(fromRegion, 'failoversFrom');
this.incrementRegionMetric(toRegion, 'failoversTo');
const event = {
type: 'failover',
timestamp: Date.now(),
fromRegion,
toRegion,
metadata,
id: this.generateEventId()
};
this.addEvent(event);
console.log(`FAILOVER METRIC: ${fromRegion} → ${toRegion}`, {
eventId: event.id,
timestamp: new Date(event.timestamp).toISOString(),
...metadata
});
}
/**
* Record a fallback request
* @param {string} region - Region handling the fallback request
* @param {Object} requestMetadata - Request metadata
*/
recordFallbackRequest(region, requestMetadata = {}) {
this.metrics.fallbackRequests++;
this.incrementRegionMetric(region, 'fallbackRequests');
const event = {
type: 'fallback_request',
timestamp: Date.now(),
region,
metadata: requestMetadata,
id: this.generateEventId()
};
this.addEvent(event);
}
/**
* Record a queued write operation
* @param {Object} operationMetadata - Operation metadata
*/
recordQueuedOperation(operationMetadata = {}) {
this.metrics.queuedOperations++;
const event = {
type: 'queued_operation',
timestamp: Date.now(),
metadata: operationMetadata,
id: this.generateEventId()
};
this.addEvent(event);
}
/**
* Record a circuit breaker trip
* @param {string} region - Region where circuit breaker tripped
* @param {Object} circuitBreakerState - Circuit breaker state
*/
recordCircuitBreakerTrip(region, circuitBreakerState = {}) {
this.metrics.circuitBreakerTrips++;
this.incrementRegionMetric(region, 'circuitBreakerTrips');
const event = {
type: 'circuit_breaker_trip',
timestamp: Date.now(),
region,
metadata: circuitBreakerState,
id: this.generateEventId()
};
this.addEvent(event);
}
/**
* Record a recovery attempt
* @param {string} region - Region being recovered
* @param {boolean} successful - Whether recovery was successful
* @param {Object} recoveryMetadata - Recovery metadata
*/
recordRecoveryAttempt(region, successful, recoveryMetadata = {}) {
this.metrics.recoveryAttempts++;
if (successful) {
this.metrics.successfulRecoveries++;
this.incrementRegionMetric(region, 'successfulRecoveries');
} else {
this.metrics.failedRecoveries++;
this.incrementRegionMetric(region, 'failedRecoveries');
}
const event = {
type: 'recovery_attempt',
timestamp: Date.now(),
region,
successful,
metadata: recoveryMetadata,
id: this.generateEventId()
};
this.addEvent(event);
}
/**
* Record latency for a region
* @param {string} region - Region
* @param {number} latencyMs - Latency in milliseconds
* @param {string} operation - Operation type (optional)
*/
recordLatency(region, latencyMs, operation = 'general') {
if (!this.performanceMetrics.latencyHistogram[region]) {
this.performanceMetrics.latencyHistogram[region] = {};
}
if (!this.performanceMetrics.latencyHistogram[region][operation]) {
this.performanceMetrics.latencyHistogram[region][operation] = [];
}
this.performanceMetrics.latencyHistogram[region][operation].push({
latency: latencyMs,
timestamp: Date.now()
});
// Keep only recent latency data
const cutoff = Date.now() - this.options.retentionPeriodMs;
this.performanceMetrics.latencyHistogram[region][operation] =
this.performanceMetrics.latencyHistogram[region][operation]
.filter(entry => entry.timestamp > cutoff);
}
/**
* Record an error for a region
* @param {string} region - Region where error occurred
* @param {string} errorType - Type of error
* @param {Object} errorMetadata - Error metadata
*/
recordError(region, errorType, errorMetadata = {}) {
if (!this.performanceMetrics.errorRates[region]) {
this.performanceMetrics.errorRates[region] = {};
}
if (!this.performanceMetrics.errorRates[region][errorType]) {
this.performanceMetrics.errorRates[region][errorType] = [];
}
this.performanceMetrics.errorRates[region][errorType].push({
timestamp: Date.now(),
metadata: errorMetadata
});
// Keep only recent error data
const cutoff = Date.now() - this.options.retentionPeriodMs;
this.performanceMetrics.errorRates[region][errorType] =
this.performanceMetrics.errorRates[region][errorType]
.filter(entry => entry.timestamp > cutoff);
const event = {
type: 'error',
timestamp: Date.now(),
region,
errorType,
metadata: errorMetadata,
id: this.generateEventId()
};
this.addEvent(event);
}
/**
* Record health check result
* @param {string} region - Region
* @param {boolean} healthy - Health status
* @param {Object} healthData - Health check data
*/
recordHealthCheck(region, healthy, healthData = {}) {
if (!this.healthMetrics[region]) {
this.healthMetrics[region] = {
checks: [],
lastHealthy: null,
lastUnhealthy: null,
consecutiveFailures: 0,
consecutiveSuccesses: 0
};
}
const regionHealth = this.healthMetrics[region];
const now = Date.now();
regionHealth.checks.push({
timestamp: now,
healthy,
data: healthData
});
if (healthy) {
regionHealth.lastHealthy = now;
regionHealth.consecutiveSuccesses++;
regionHealth.consecutiveFailures = 0;
} else {
regionHealth.lastUnhealthy = now;
regionHealth.consecutiveFailures++;
regionHealth.consecutiveSuccesses = 0;
}
// Keep only recent health checks
const cutoff = now - this.options.retentionPeriodMs;
regionHealth.checks = regionHealth.checks.filter(check => check.timestamp > cutoff);
}
/**
* Get current metrics summary
* @returns {Object} - Metrics summary
*/
getMetricsSummary() {
return {
core: { ...this.metrics },
regions: { ...this.regionMetrics },
eventCount: this.events.length,
aggregationCount: this.aggregatedMetrics.length,
dataRetentionPeriodMs: this.options.retentionPeriodMs,
lastUpdated: Date.now()
};
}
/**
* Get performance metrics for a region
* @param {string} region - Region to get metrics for
* @returns {Object} - Performance metrics
*/
getRegionPerformanceMetrics(region) {
const latencyData = this.performanceMetrics.latencyHistogram[region] || {};
const errorData = this.performanceMetrics.errorRates[region] || {};
const healthData = this.healthMetrics[region] || {};
// Calculate latency statistics
const latencyStats = {};
Object.keys(latencyData).forEach(operation => {
const latencies = latencyData[operation].map(entry => entry.latency);
if (latencies.length > 0) {
latencyStats[operation] = {
count: latencies.length,
min: Math.min(...latencies),
max: Math.max(...latencies),
avg: latencies.reduce((sum, l) => sum + l, 0) / latencies.length,
p95: this.calculatePercentile(latencies, 95),
p99: this.calculatePercentile(latencies, 99)
};
}
});
// Calculate error rates
const errorStats = {};
Object.keys(errorData).forEach(errorType => {
const errors = errorData[errorType];
const recentErrors = errors.filter(e => e.timestamp > Date.now() - 60 * 60 * 1000); // Last hour
errorStats[errorType] = {
total: errors.length,
lastHour: recentErrors.length,
rate: recentErrors.length / 60 // errors per minute
};
});
// Calculate health statistics
const healthStats = {
totalChecks: healthData.checks?.length || 0,
consecutiveFailures: healthData.consecutiveFailures || 0,
consecutiveSuccesses: healthData.consecutiveSuccesses || 0,
lastHealthy: healthData.lastHealthy,
lastUnhealthy: healthData.lastUnhealthy
};
if (healthData.checks && healthData.checks.length > 0) {
const recentChecks = healthData.checks.filter(c => c.timestamp > Date.now() - 60 * 60 * 1000);
const healthyChecks = recentChecks.filter(c => c.healthy).length;
healthStats.healthRate = recentChecks.length > 0 ? healthyChecks / recentChecks.length : 0;
}
return {
region,
latency: latencyStats,
errors: errorStats,
health: healthStats,
timestamp: Date.now()
};
}
/**
* Get events within a time range
* @param {number} startTime - Start timestamp
* @param {number} endTime - End timestamp
* @param {string[]} eventTypes - Filter by event types (optional)
* @returns {Array} - Filtered events
*/
getEvents(startTime, endTime = Date.now(), eventTypes = []) {
let filteredEvents = this.events.filter(event =>
event.timestamp >= startTime && event.timestamp <= endTime
);
if (eventTypes.length > 0) {
filteredEvents = filteredEvents.filter(event =>
eventTypes.includes(event.type)
);
}
return filteredEvents.sort((a, b) => b.timestamp - a.timestamp);
}
/**
* Get aggregated metrics for a time period
* @param {number} periodMs - Time period in milliseconds
* @returns {Object} - Aggregated metrics
*/
getAggregatedMetrics(periodMs = 60 * 60 * 1000) { // Default: 1 hour
const endTime = Date.now();
const startTime = endTime - periodMs;
const periodEvents = this.getEvents(startTime, endTime);
const aggregated = {
period: { startTime, endTime, durationMs: periodMs },
events: {
total: periodEvents.length,
byType: {}
},
regions: {}
};
// Aggregate events by type
periodEvents.forEach(event => {
if (!aggregated.events.byType[event.type]) {
aggregated.events.byType[event.type] = 0;
}
aggregated.events.byType[event.type]++;
// Aggregate by region
const region = event.region || event.fromRegion || event.toRegion;
if (region) {
if (!aggregated.regions[region]) {
aggregated.regions[region] = { events: 0, types: {} };
}
aggregated.regions[region].events++;
if (!aggregated.regions[region].types[event.type]) {
aggregated.regions[region].types[event.type] = 0;
}
aggregated.regions[region].types[event.type]++;
}
});
return aggregated;
}
/**
* Start automatic metric aggregation
*/
startAutoAggregation() {
if (this.aggregationInterval) {
return;
}
this.aggregationInterval = setInterval(() => {
this.performAggregation();
this.cleanup();
}, this.options.aggregationIntervalMs);
console.log('Auto-aggregation started');
}
/**
* Stop automatic metric aggregation
*/
stopAutoAggregation() {
if (this.aggregationInterval) {
clearInterval(this.aggregationInterval);
this.aggregationInterval = null;
console.log('Auto-aggregation stopped');
}
}
/**
* Perform metric aggregation
*/
performAggregation() {
const aggregated = this.getAggregatedMetrics(this.options.aggregationIntervalMs);
aggregated.timestamp = Date.now();
this.aggregatedMetrics.push(aggregated);
// Keep only recent aggregations
const cutoff = Date.now() - this.options.retentionPeriodMs;
this.aggregatedMetrics = this.aggregatedMetrics.filter(agg => agg.timestamp > cutoff);
}
/**
* Clean up old data
*/
cleanup() {
const cutoff = Date.now() - this.options.retentionPeriodMs;
// Clean up events
this.events = this.events.filter(event => event.timestamp > cutoff);
// Clean up performance metrics
Object.keys(this.performanceMetrics.latencyHistogram).forEach(region => {
Object.keys(this.performanceMetrics.latencyHistogram[region]).forEach(operation => {
this.performanceMetrics.latencyHistogram[region][operation] =
this.performanceMetrics.latencyHistogram[region][operation]
.filter(entry => entry.timestamp > cutoff);
});
});
Object.keys(this.performanceMetrics.errorRates).forEach(region => {
Object.keys(this.performanceMetrics.errorRates[region]).forEach(errorType => {
this.performanceMetrics.errorRates[region][errorType] =
this.performanceMetrics.errorRates[region][errorType]
.filter(entry => entry.timestamp > cutoff);
});
});
// Clean up health metrics
Object.keys(this.healthMetrics).forEach(region => {
if (this.healthMetrics[region].checks) {
this.healthMetrics[region].checks =
this.healthMetrics[region].checks.filter(check => check.timestamp > cutoff);
}
});
}
/**
* Helper methods
*/
incrementRegionMetric(region, metric) {
if (!this.regionMetrics[region]) {
this.regionMetrics[region] = {};
}
if (!this.regionMetrics[region][metric]) {
this.regionMetrics[region][metric] = 0;
}
this.regionMetrics[region][metric]++;
}
addEvent(event) {
this.events.push(event);
// Prevent memory issues by limiting event count
if (this.events.length > this.options.maxEvents) {
this.events = this.events.slice(-this.options.maxEvents);
}
}
generateEventId() {
return `evt_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
calculatePercentile(values, percentile) {
const sorted = [...values].sort((a, b) => a - b);
const index = Math.ceil(sorted.length * percentile / 100) - 1;
return sorted[index] || 0;
}
/**
* Export metrics for external monitoring systems
* @returns {Object} - Exportable metrics
*/
exportMetrics() {
return {
timestamp: Date.now(),
metrics: this.getMetricsSummary(),
regions: Object.keys(this.regionMetrics).map(region =>
this.getRegionPerformanceMetrics(region)
),
recentEvents: this.getEvents(Date.now() - 60 * 60 * 1000), // Last hour
aggregated: this.aggregatedMetrics.slice(-24) // Last 24 aggregations
};
}
/**
* Reset all metrics (useful for testing)
*/
reset() {
this.metrics = {
failoverEvents: 0,
fallbackRequests: 0,
queuedOperations: 0,
circuitBreakerTrips: 0,
recoveryAttempts: 0,
successfulRecoveries: 0,
failedRecoveries: 0
};
this.regionMetrics = {};
this.events = [];
this.aggregatedMetrics = [];
this.performanceMetrics = {
latencyHistogram: {},
errorRates: {},
throughput: {}
};
this.healthMetrics = {};
}
}
module.exports = { FailoverMetrics };
/**
* Feature Manager for Graceful Degradation
*
* Manages feature flags to gracefully disable non-critical functionality
* during regional outages or degraded performance conditions.
*/
class FeatureManager {
constructor(options = {}) {
// Default feature configuration
this.features = options.features || {
// Critical features (always enabled)
userAuth: { critical: true, enabled: true, description: 'User authentication and authorization' },
coreApi: { critical: true, enabled: true, description: 'Core API functionality' },
payments: { critical: true, enabled: true, description: 'Payment processing' },
// Non-critical features (can be disabled during degraded operation)
analytics: { critical: false, enabled: true, description: 'User behavior analytics' },
notifications: { critical: false, enabled: true, description: 'Email and push notifications' },
reporting: { critical: false, enabled: true, description: 'Dashboard reports and metrics' },
recommendations: { critical: false, enabled: true, description: 'AI-powered recommendations' },
search: { critical: false, enabled: true, description: 'Advanced search functionality' },
socialFeatures: { critical: false, enabled: true, description: 'Social sharing and comments' },
integrations: { critical: false, enabled: true, description: 'Third-party integrations' },
backgroundJobs: { critical: false, enabled: true, description: 'Non-urgent background processing' }
};
// Feature override rules during degraded operation
this.degradationRules = options.degradationRules || {
fallback: {
disableNonCritical: true,
allowedLatency: 5000, // 5 seconds
maxQueueDepth: 100
},
highLatency: {
threshold: 3000, // 3 seconds
disableFeatures: ['analytics', 'recommendations', 'socialFeatures']
},
highErrorRate: {
threshold: 0.1, // 10% error rate
disableFeatures: ['integrations', 'backgroundJobs']
}
};
// Current system state
this.systemState = {
isFallback: false,
averageLatency: 0,
errorRate: 0,
queueDepth: 0,
lastUpdate: Date.now()
};
// Feature usage statistics
this.stats = {
featureChecks: {},
disabledFeatureAttempts: {},
degradationEvents: []
};
}
/**
* Check if a feature should be enabled based on current system state
* @param {string} featureName - Name of the feature to check
* @param {Object} req - Express request object (optional)
* @returns {boolean} - True if feature should be enabled
*/
shouldEnableFeature(featureName, req = null) {
const feature = this.features[featureName];
if (!feature) {
console.warn(`Unknown feature requested: ${featureName}`);
return false;
}
// Track feature check
this.stats.featureChecks[featureName] = (this.stats.featureChecks[featureName] || 0) + 1;
// Always allow critical features
if (feature.critical) {
return feature.enabled;
}
// Check if feature is globally disabled
if (!feature.enabled) {
this.trackDisabledAttempt(featureName, 'globally_disabled');
return false;
}
// Apply degradation rules
const degradationReasons = this.checkDegradationRules(featureName, req);
if (degradationReasons.length > 0) {
this.trackDisabledAttempt(featureName, degradationReasons.join(', '));
return false;
}
return true;
}
/**
* Check degradation rules against current system state
* @param {string} featureName - Feature to check
* @param {Object} req - Express request object
* @returns {string[]} - Array of degradation reasons
*/
checkDegradationRules(featureName, req) {
const reasons = [];
// Check fallback mode
if (this.systemState.isFallback && this.degradationRules.fallback.disableNonCritical) {
reasons.push('fallback_mode');
}
// Check if using fallback from request
if (req && req.isFallback && this.degradationRules.fallback.disableNonCritical) {
reasons.push('request_fallback');
}
// Check high latency
if (this.systemState.averageLatency > this.degradationRules.highLatency.threshold) {
if (this.degradationRules.highLatency.disableFeatures.includes(featureName)) {
reasons.push('high_latency');
}
}
// Check high error rate
if (this.systemState.errorRate > this.degradationRules.highErrorRate.threshold) {
if (this.degradationRules.highErrorRate.disableFeatures.includes(featureName)) {
reasons.push('high_error_rate');
}
}
// Check queue depth
if (this.systemState.queueDepth > this.degradationRules.fallback.maxQueueDepth) {
reasons.push('high_queue_depth');
}
return reasons;
}
/**
* Update system state for degradation rule evaluation
* @param {Object} state - Current system state
*/
updateSystemState(state) {
const previousState = { ...this.systemState };
this.systemState = {
...this.systemState,
...state,
lastUpdate: Date.now()
};
// Check for degradation events
this.checkDegradationEvents(previousState, this.systemState);
}
/**
* Check for degradation events and log them
* @param {Object} previousState - Previous system state
* @param {Object} currentState - Current system state
*/
checkDegradationEvents(previousState, currentState) {
const events = [];
// Fallback mode change
if (previousState.isFallback !== currentState.isFallback) {
events.push({
type: 'fallback_mode_change',
from: previousState.isFallback,
to: currentState.isFallback
});
}
// Latency threshold crossing
const latencyThreshold = this.degradationRules.highLatency.threshold;
if (previousState.averageLatency <= latencyThreshold && currentState.averageLatency > latencyThreshold) {
events.push({
type: 'high_latency_start',
latency: currentState.averageLatency,
threshold: latencyThreshold
});
} else if (previousState.averageLatency > latencyThreshold && currentState.averageLatency <= latencyThreshold) {
events.push({
type: 'high_latency_end',
latency: currentState.averageLatency,
threshold: latencyThreshold
});
}
// Error rate threshold crossing
const errorThreshold = this.degradationRules.highErrorRate.threshold;
if (previousState.errorRate <= errorThreshold && currentState.errorRate > errorThreshold) {
events.push({
type: 'high_error_rate_start',
errorRate: currentState.errorRate,
threshold: errorThreshold
});
} else if (previousState.errorRate > errorThreshold && currentState.errorRate <= errorThreshold) {
events.push({
type: 'high_error_rate_end',
errorRate: currentState.errorRate,
threshold: errorThreshold
});
}
// Log and store events
events.forEach(event => {
console.log('DEGRADATION_EVENT', {
...event,
timestamp: new Date().toISOString()
});
this.stats.degradationEvents.push({
...event,
timestamp: Date.now()
});
});
}
/**
* Track attempts to use disabled features
* @param {string} featureName - Name of the disabled feature
* @param {string} reason - Reason for disabling
*/
trackDisabledAttempt(featureName, reason) {
if (!this.stats.disabledFeatureAttempts[featureName]) {
this.stats.disabledFeatureAttempts[featureName] = {};
}
if (!this.stats.disabledFeatureAttempts[featureName][reason]) {
this.stats.disabledFeatureAttempts[featureName][reason] = 0;
}
this.stats.disabledFeatureAttempts[featureName][reason]++;
}
/**
* Get current feature status for all features
* @param {Object} req - Express request object (optional)
* @returns {Object} - Feature status for all features
*/
getAllFeatureStatus(req = null) {
const status = {};
Object.keys(this.features).forEach(featureName => {
const enabled = this.shouldEnableFeature(featureName, req);
const feature = this.features[featureName];
const degradationReasons = enabled ? [] : this.checkDegradationRules(featureName, req);
status[featureName] = {
enabled,
critical: feature.critical,
description: feature.description,
degradationReasons
};
});
return status;
}
/**
* Enable or disable a feature globally
* @param {string} featureName - Name of the feature
* @param {boolean} enabled - Whether to enable the feature
* @param {string} reason - Reason for the change
*/
setFeatureEnabled(featureName, enabled, reason = 'manual_override') {
if (!this.features[featureName]) {
throw new Error(`Unknown feature: ${featureName}`);
}
const previousState = this.features[featureName].enabled;
this.features[featureName].enabled = enabled;
console.log(`Feature ${featureName} ${enabled ? 'enabled' : 'disabled'}: ${reason}`);
// Log the change
this.stats.degradationEvents.push({
type: 'feature_toggle',
feature: featureName,
from: previousState,
to: enabled,
reason,
timestamp: Date.now()
});
}
/**
* Get feature usage statistics
* @returns {Object} - Usage statistics
*/
getStats() {
return {
...this.stats,
systemState: this.systemState,
totalFeatureChecks: Object.values(this.stats.featureChecks).reduce((sum, count) => sum + count, 0),
totalDisabledAttempts: Object.values(this.stats.disabledFeatureAttempts)
.reduce((sum, reasons) => sum + Object.values(reasons).reduce((s, c) => s + c, 0), 0),
degradationEventsCount: this.stats.degradationEvents.length
};
}
/**
* Clear statistics (useful for testing or periodic cleanup)
*/
clearStats() {
this.stats = {
featureChecks: {},
disabledFeatureAttempts: {},
degradationEvents: []
};
}
/**
* Create middleware that adds feature flags to request object
* @returns {Function} - Express middleware function
*/
createMiddleware() {
return (req, res, next) => {
req.features = {};
Object.keys(this.features).forEach(featureName => {
req.features[featureName] = this.shouldEnableFeature(featureName, req);
});
// Update system state based on request context
if (req.isFallback !== undefined) {
this.updateSystemState({ isFallback: req.isFallback });
}
next();
};
}
}
module.exports = { FeatureManager };
/**
* Enhanced Health Check Functions for Aurora Regions
*
* Provides comprehensive health monitoring beyond simple connectivity checks.
* Includes latency monitoring, connection pool status, and query performance.
*/
const { RegionCircuitBreaker } = require('./circuit-breaker');
/**
* Enhanced health check for a region with latency and performance monitoring
* @param {Object} regionClient - Database client for the region
* @param {RegionCircuitBreaker} circuitBreaker - Circuit breaker instance
* @param {Object} options - Health check configuration
* @returns {Promise<Object>} - Health status with metrics
*/
async function checkRegionHealth(regionClient, circuitBreaker, options = {}) {
const {
timeoutMs = 5000,
latencyThresholdMs = 3000,
includePoolStats = true,
customQuery = 'SELECT 1 as health_check'
} = options;
return circuitBreaker.execute(async () => {
const start = Date.now();
let poolStats = null;
try {
// Check connection pool stats if available
if (includePoolStats && regionClient.pool) {
poolStats = {
totalConnections: regionClient.pool.totalCount || 0,
idleConnections: regionClient.pool.idleCount || 0,
waitingClients: regionClient.pool.waitingCount || 0
};
// Fail if no connections available
if (poolStats.totalConnections === 0) {
throw new Error('No database connections available');
}
// Warn if connection pool is under stress
if (poolStats.waitingClients > 5) {
console.warn(`High connection pool pressure: ${poolStats.waitingClients} waiting clients`);
}
}
// Execute health check query with timeout
const queryPromise = regionClient.query(customQuery);
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Health check timeout')), timeoutMs);
});
await Promise.race([queryPromise, timeoutPromise]);
const latency = Date.now() - start;
// Check latency threshold
if (latency > latencyThresholdMs) {
throw new Error(`High latency detected: ${latency}ms (threshold: ${latencyThresholdMs}ms)`);
}
return {
healthy: true,
latency,
timestamp: new Date().toISOString(),
poolStats
};
} catch (error) {
const latency = Date.now() - start;
throw new Error(`Health check failed after ${latency}ms: ${error.message}`);
}
});
}
/**
* Comprehensive health check that tests multiple aspects of database connectivity
* @param {Object} regionClient - Database client for the region
* @param {RegionCircuitBreaker} circuitBreaker - Circuit breaker instance
* @returns {Promise<Object>} - Detailed health status
*/
async function comprehensiveHealthCheck(regionClient, circuitBreaker) {
const checks = [];
try {
// Basic connectivity check
const basicCheck = await checkRegionHealth(regionClient, circuitBreaker, {
timeoutMs: 2000,
customQuery: 'SELECT 1 as basic_check'
});
checks.push({ name: 'basic_connectivity', ...basicCheck });
// Write capability check (if not in read-only mode)
try {
const writeStart = Date.now();
await regionClient.query('SELECT pg_is_in_recovery()');
const writeLatency = Date.now() - writeStart;
checks.push({
name: 'write_capability',
healthy: true,
latency: writeLatency,
timestamp: new Date().toISOString()
});
} catch (writeError) {
checks.push({
name: 'write_capability',
healthy: false,
error: writeError.message,
timestamp: new Date().toISOString()
});
}
// Replication lag check (if applicable)
try {
const lagResult = await regionClient.query(`
SELECT EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp())) as lag_seconds
`);
const lagSeconds = lagResult.rows[0]?.lag_seconds || 0;
checks.push({
name: 'replication_lag',
healthy: lagSeconds < 60, // Consider unhealthy if > 60 seconds behind
lagSeconds,
timestamp: new Date().toISOString()
});
} catch (lagError) {
// Might not be a replica, ignore this check
checks.push({
name: 'replication_lag',
healthy: true,
note: 'Not a replica or lag check not supported',
timestamp: new Date().toISOString()
});
}
const overallHealthy = checks.every(check => check.healthy);
const avgLatency = checks
.filter(check => check.latency)
.reduce((sum, check) => sum + check.latency, 0) / checks.filter(check => check.latency).length;
return {
healthy: overallHealthy,
averageLatency: avgLatency || 0,
checks,
timestamp: new Date().toISOString()
};
} catch (error) {
return {
healthy: false,
error: error.message,
checks,
timestamp: new Date().toISOString()
};
}
}
/**
* Lightweight health check for high-frequency monitoring
* @param {Object} regionClient - Database client for the region
* @returns {Promise<boolean>} - Simple healthy/unhealthy status
*/
async function quickHealthCheck(regionClient) {
try {
const start = Date.now();
await regionClient.query('SELECT 1');
const latency = Date.now() - start;
// Quick fail if latency is too high
return latency < 5000;
} catch (error) {
return false;
}
}
/**
* Create a health checker with automatic retry logic
* @param {Object} regionClient - Database client for the region
* @param {RegionCircuitBreaker} circuitBreaker - Circuit breaker instance
* @param {Object} options - Configuration options
* @returns {Object} - Health checker with methods
*/
function createHealthChecker(regionClient, circuitBreaker, options = {}) {
const {
retryAttempts = 2,
retryDelayMs = 1000,
healthCheckIntervalMs = 10000
} = options;
let lastHealthStatus = null;
let healthCheckInterval = null;
const checker = {
/**
* Start automatic health monitoring
*/
startMonitoring() {
if (healthCheckInterval) {
clearInterval(healthCheckInterval);
}
healthCheckInterval = setInterval(async () => {
try {
lastHealthStatus = await this.checkHealth();
} catch (error) {
console.warn('Automatic health check failed:', error.message);
lastHealthStatus = { healthy: false, error: error.message };
}
}, healthCheckIntervalMs);
},
/**
* Stop automatic health monitoring
*/
stopMonitoring() {
if (healthCheckInterval) {
clearInterval(healthCheckInterval);
healthCheckInterval = null;
}
},
/**
* Get the last known health status
*/
getLastStatus() {
return lastHealthStatus;
},
/**
* Perform health check with retry logic
*/
async checkHealth() {
let lastError;
for (let attempt = 1; attempt <= retryAttempts; attempt++) {
try {
return await checkRegionHealth(regionClient, circuitBreaker);
} catch (error) {
lastError = error;
if (attempt < retryAttempts) {
await new Promise(resolve => setTimeout(resolve, retryDelayMs * attempt));
}
}
}
throw lastError;
}
};
return checker;
}
module.exports = {
checkRegionHealth,
comprehensiveHealthCheck,
quickHealthCheck,
createHealthChecker
};
/**
* Express Middleware for Aurora Resilience
*
* Integration middleware that combines all resilience patterns into
* a cohesive Express.js middleware stack for easy implementation.
*/
const { RegionManager } = require('./region-manager');
const { WriteQueue, safeWrite } = require('./write-queue');
const { FeatureManager } = require('./feature-manager');
const { FailoverMetrics } = require('./failover-metrics');
const { RegionRecovery } = require('./region-recovery');
/**
* Create the main failover middleware
* @param {RegionManager} regionManager - Region manager instance
* @param {Object} options - Middleware options
* @returns {Function} - Express middleware function
*/
function createFailoverMiddleware(regionManager, options = {}) {
const {
defaultRegion = 'us-east-1',
requireWriteCapability = false,
timeoutMs = 10000,
retryAttempts = 1,
metrics = null
} = options;
return async (req, res, next) => {
const startTime = Date.now();
try {
// Determine preferred region for this request
const preferredRegion = determineRegion(req) || defaultRegion;
// Get healthy database client with failover
const {
client,
region,
isFallback,
preferredRegion: requestedRegion,
healthResult,
failoverEvents
} = await regionManager.getHealthyClient(preferredRegion, {
requireWriteCapability,
excludeRegions: req.excludeRegions || []
});
// Attach database client and metadata to request
req.db = client;
req.region = region;
req.isFallback = isFallback;
req.originalRegion = requestedRegion;
req.healthResult = healthResult;
req.failoverEvents = failoverEvents;
// Record metrics if available
if (metrics && isFallback) {
metrics.recordFailover(requestedRegion, region, {
requestId: req.headers['x-request-id'] || req.id,
userAgent: req.headers['user-agent'],
ip: req.ip,
path: req.path,
method: req.method
});
metrics.recordFallbackRequest(region, {
originalRegion: requestedRegion,
latency: healthResult?.latency
});
}
// Add latency tracking
req.dbLatency = healthResult?.latency || 0;
req.requestStartTime = startTime;
// Log failover if it occurred
if (isFallback) {
console.warn(`Request failover: ${requestedRegion} → ${region}`, {
requestId: req.headers['x-request-id'],
path: req.path,
method: req.method,
failoverEvents: failoverEvents.length
});
}
next();
} catch (error) {
// Record error metrics
if (metrics) {
metrics.recordError('middleware', 'failover_failed', {
error: error.message,
preferredRegion,
requestId: req.headers['x-request-id']
});
}
console.error('Failover middleware error:', error);
// Return service unavailable
res.status(503).json({
error: 'Database services temporarily unavailable',
requestId: req.headers['x-request-id'],
timestamp: new Date().toISOString()
});
}
};
}
/**
* Create feature flag middleware
* @param {FeatureManager} featureManager - Feature manager instance
* @returns {Function} - Express middleware function
*/
function createFeatureMiddleware(featureManager) {
return (req, res, next) => {
// Add feature flags to request
req.features = {};
// Get all feature statuses for this request
const allFeatures = featureManager.getAllFeatureStatus(req);
Object.keys(allFeatures).forEach(featureName => {
req.features[featureName] = allFeatures[featureName].enabled;
});
// Update system state based on request context
featureManager.updateSystemState({
isFallback: req.isFallback || false,
averageLatency: req.dbLatency || 0
});
// Add helper function to check features
req.hasFeature = (featureName) => {
return req.features[featureName] === true;
};
next();
};
}
/**
* Create write safety middleware
* @param {WriteQueue} writeQueue - Write queue instance
* @param {FailoverMetrics} metrics - Metrics instance (optional)
* @returns {Function} - Express middleware function
*/
function createWriteMiddleware(writeQueue, metrics = null) {
return (req, res, next) => {
// Add safe write function to request
req.safeWrite = async (queryFn, metadata = {}) => {
const writeMetadata = {
...metadata,
requestId: req.headers['x-request-id'] || req.id,
userId: req.user?.id,
path: req.path,
method: req.method,
timestamp: Date.now()
};
try {
const result = await safeWrite(queryFn, writeMetadata, req, writeQueue);
// Record metrics
if (metrics && result.queued) {
metrics.recordQueuedOperation(writeMetadata);
}
return result;
} catch (error) {
if (metrics) {
metrics.recordError(req.region || 'unknown', 'write_failed', {
...writeMetadata,
error: error.message
});
}
throw error;
}
};
next();
};
}
/**
* Create request timing middleware
* @param {FailoverMetrics} metrics - Metrics instance
* @returns {Function} - Express middleware function
*/
function createTimingMiddleware(metrics) {
return (req, res, next) => {
const startTime = Date.now();
req.startTime = startTime;
// Override res.end to capture timing
const originalEnd = res.end;
res.end = function(...args) {
const duration = Date.now() - startTime;
// Record latency metrics
if (req.region) {
metrics.recordLatency(req.region, duration, 'request');
}
// Record error if status indicates failure
if (res.statusCode >= 400) {
metrics.recordError(req.region || 'unknown', 'http_error', {
statusCode: res.statusCode,
path: req.path,
method: req.method,
duration
});
}
originalEnd.apply(this, args);
};
next();
};
}
/**
* Create health monitoring middleware
* @param {RegionManager} regionManager - Region manager instance
* @param {FailoverMetrics} metrics - Metrics instance
* @returns {Function} - Express middleware function
*/
function createHealthMonitoringMiddleware(regionManager, metrics) {
return (req, res, next) => {
// Record health metrics for the current region
if (req.region && req.healthResult) {
metrics.recordHealthCheck(req.region, req.healthResult.healthy, req.healthResult);
}
// Add health status to request
req.regionHealth = regionManager.getAllRegionHealth();
next();
};
}
/**
* Create error handling middleware
* @param {FailoverMetrics} metrics - Metrics instance (optional)
* @returns {Function} - Express error middleware function
*/
function createErrorMiddleware(metrics = null) {
return (error, req, res, next) => {
// Record error metrics
if (metrics) {
metrics.recordError(req.region || 'unknown', 'application_error', {
error: error.message,
stack: error.stack,
path: req.path,
method: req.method,
requestId: req.headers['x-request-id']
});
}
console.error('Application error:', {
error: error.message,
path: req.path,
method: req.method,
region: req.region,
isFallback: req.isFallback,
requestId: req.headers['x-request-id']
});
// Don't expose internal errors in production
const isDevelopment = process.env.NODE_ENV === 'development';
res.status(500).json({
error: 'Internal server error',
requestId: req.headers['x-request-id'],
timestamp: new Date().toISOString(),
...(isDevelopment && { details: error.message, stack: error.stack })
});
};
}
/**
* Determine the preferred region for a request
* @param {Object} req - Express request object
* @returns {string} - Preferred region
*/
function determineRegion(req) {
// Check for explicit region header
if (req.headers['x-preferred-region']) {
return req.headers['x-preferred-region'];
}
// Check for user preference
if (req.user && req.user.preferredRegion) {
return req.user.preferredRegion;
}
// Determine based on geographic headers (CloudFlare, AWS ALB, etc.)
const cfCountry = req.headers['cf-ipcountry'];
const awsRegion = req.headers['x-amzn-trace-id'];
if (cfCountry) {
return mapCountryToRegion(cfCountry);
}
// Check client IP for geographic routing (simplified example)
if (req.ip) {
return mapIpToRegion(req.ip);
}
// Default to null (will use default region)
return null;
}
/**
* Map country code to AWS region
* @param {string} countryCode - ISO country code
* @returns {string} - AWS region
*/
function mapCountryToRegion(countryCode) {
const regionMap = {
// North America
'US': 'us-east-1',
'CA': 'us-east-1',
'MX': 'us-east-1',
// Europe
'GB': 'eu-west-1',
'DE': 'eu-west-1',
'FR': 'eu-west-1',
'IT': 'eu-west-1',
'ES': 'eu-west-1',
'NL': 'eu-west-1',
// Asia Pacific
'JP': 'ap-northeast-1',
'KR': 'ap-northeast-1',
'SG': 'ap-southeast-1',
'AU': 'ap-southeast-2',
'IN': 'ap-south-1',
'CN': 'ap-southeast-1' // Fallback for China
};
return regionMap[countryCode] || 'us-east-1';
}
/**
* Map IP address to region (simplified example)
* @param {string} ip - Client IP address
* @returns {string} - AWS region
*/
function mapIpToRegion(ip) {
// This is a simplified example. In production, you'd use a proper
// IP geolocation service like MaxMind GeoIP or AWS's geolocation routing
// For demo purposes, just return default
return 'us-east-1';
}
/**
* Create a complete middleware stack for Aurora resilience
* @param {Object} components - All resilience components
* @param {Object} options - Configuration options
* @returns {Array} - Array of middleware functions
*/
function createResilienceStack(components, options = {}) {
const {
regionManager,
writeQueue,
featureManager,
metrics,
recovery
} = components;
const middlewareStack = [];
// 1. Request timing (must be first)
if (metrics) {
middlewareStack.push(createTimingMiddleware(metrics));
}
// 2. Failover middleware (core functionality)
middlewareStack.push(createFailoverMiddleware(regionManager, options.failover));
// 3. Health monitoring
if (metrics) {
middlewareStack.push(createHealthMonitoringMiddleware(regionManager, metrics));
}
// 4. Feature flags
if (featureManager) {
middlewareStack.push(createFeatureMiddleware(featureManager));
}
// 5. Write safety
if (writeQueue) {
middlewareStack.push(createWriteMiddleware(writeQueue, metrics));
}
return middlewareStack;
}
/**
* Create health check endpoint
* @param {Object} components - All resilience components
* @returns {Function} - Express route handler
*/
function createHealthEndpoint(components) {
const { regionManager, writeQueue, featureManager, metrics } = components;
return (req, res) => {
const health = {
status: 'healthy',
timestamp: new Date().toISOString(),
regions: regionManager.getAllRegionHealth(),
writeQueue: writeQueue ? writeQueue.getStats() : null,
features: featureManager ? featureManager.getStats() : null,
metrics: metrics ? metrics.getMetricsSummary() : null
};
// Determine overall health
const healthyRegions = regionManager.getHealthyRegions();
if (healthyRegions.length === 0) {
health.status = 'unhealthy';
res.status(503);
} else if (healthyRegions.length < Object.keys(regionManager.dbClients).length) {
health.status = 'degraded';
}
res.json(health);
};
}
/**
* Create metrics endpoint
* @param {FailoverMetrics} metrics - Metrics instance
* @returns {Function} - Express route handler
*/
function createMetricsEndpoint(metrics) {
return (req, res) => {
const period = req.query.period ? parseInt(req.query.period) : 60 * 60 * 1000; // 1 hour default
const format = req.query.format || 'json';
if (format === 'prometheus') {
// Export in Prometheus format
res.type('text/plain');
res.send(exportPrometheusMetrics(metrics));
} else {
// Export in JSON format
const data = {
summary: metrics.getMetricsSummary(),
aggregated: metrics.getAggregatedMetrics(period),
export: metrics.exportMetrics()
};
res.json(data);
}
};
}
/**
* Export metrics in Prometheus format (simplified)
* @param {FailoverMetrics} metrics - Metrics instance
* @returns {string} - Prometheus formatted metrics
*/
function exportPrometheusMetrics(metrics) {
const summary = metrics.getMetricsSummary();
const timestamp = Date.now();
let output = '';
// Core metrics
output += `# HELP aurora_failover_events_total Total number of failover events\n`;
output += `# TYPE aurora_failover_events_total counter\n`;
output += `aurora_failover_events_total ${summary.core.failoverEvents} ${timestamp}\n\n`;
output += `# HELP aurora_fallback_requests_total Total number of fallback requests\n`;
output += `# TYPE aurora_fallback_requests_total counter\n`;
output += `aurora_fallback_requests_total ${summary.core.fallbackRequests} ${timestamp}\n\n`;
output += `# HELP aurora_queued_operations_total Total number of queued operations\n`;
output += `# TYPE aurora_queued_operations_total counter\n`;
output += `aurora_queued_operations_total ${summary.core.queuedOperations} ${timestamp}\n\n`;
// Per-region metrics
Object.keys(summary.regions).forEach(region => {
const regionData = summary.regions[region];
Object.keys(regionData).forEach(metric => {
output += `aurora_region_${metric}{region="${region}"} ${regionData[metric]} ${timestamp}\n`;
});
});
return output;
}
module.exports = {
createFailoverMiddleware,
createFeatureMiddleware,
createWriteMiddleware,
createTimingMiddleware,
createHealthMonitoringMiddleware,
createErrorMiddleware,
createResilienceStack,
createHealthEndpoint,
createMetricsEndpoint,
determineRegion,
mapCountryToRegion
};
/**
* Region Manager for Intelligent Aurora Failover
*
* Manages multiple Aurora regions with intelligent routing, health monitoring,
* and automatic failover to healthy regions with multi-tier fallback chains.
*/
const { RegionCircuitBreaker } = require('./circuit-breaker');
const { checkRegionHealth, createHealthChecker } = require('./health-checks');
class RegionManager {
constructor(dbClients, options = {}) {
this.dbClients = dbClients;
this.circuitBreakers = {};
this.healthCheckers = {};
this.regionHealth = {};
// Configuration
this.fallbackChain = options.fallbackChain || ['us-east-1', 'eu-west-1', 'ap-southeast-1'];
this.healthCheckInterval = options.healthCheckInterval || 10000;
this.circuitBreakerConfig = options.circuitBreakerConfig || {
threshold: 5,
timeout: 30000
};
this.init();
}
/**
* Initialize circuit breakers and health checkers for all regions
*/
init() {
Object.keys(this.dbClients).forEach(region => {
// Initialize circuit breaker
this.circuitBreakers[region] = new RegionCircuitBreaker(
this.circuitBreakerConfig.threshold,
this.circuitBreakerConfig.timeout
);
// Initialize health checker
this.healthCheckers[region] = createHealthChecker(
this.dbClients[region],
this.circuitBreakers[region],
{ healthCheckIntervalMs: this.healthCheckInterval }
);
// Start automatic health monitoring
this.healthCheckers[region].startMonitoring();
// Initialize health status
this.regionHealth[region] = { healthy: true, lastCheck: Date.now() };
});
console.log(`RegionManager initialized with regions: ${Object.keys(this.dbClients).join(', ')}`);
}
/**
* Get a healthy database client with intelligent failover
* @param {string} preferredRegion - The preferred region to use
* @param {Object} options - Additional options
* @returns {Promise<Object>} - Object containing client, region, and failover info
*/
async getHealthyClient(preferredRegion, options = {}) {
const { requireWriteCapability = false, excludeRegions = [] } = options;
// Build fallback chain starting with preferred region
const regions = [
preferredRegion,
...this.fallbackChain.filter(r => r !== preferredRegion && !excludeRegions.includes(r))
].filter(region => this.dbClients[region]);
const failoverEvents = [];
for (const region of regions) {
if (!this.dbClients[region]) {
continue;
}
try {
// Check circuit breaker state first
const circuitBreaker = this.circuitBreakers[region];
if (!circuitBreaker.isRequestAllowed()) {
failoverEvents.push({
region,
reason: 'circuit_breaker_open',
state: circuitBreaker.getStatus()
});
continue;
}
// Perform health check
const healthResult = await checkRegionHealth(
this.dbClients[region],
circuitBreaker,
{
timeoutMs: 3000,
includePoolStats: true
}
);
// Additional check for write capability if required
if (requireWriteCapability) {
await this.verifyWriteCapability(this.dbClients[region]);
}
// Update health status
this.regionHealth[region] = {
healthy: true,
lastCheck: Date.now(),
latency: healthResult.latency,
poolStats: healthResult.poolStats
};
const isFallback = region !== preferredRegion;
if (isFallback) {
console.warn(`Using fallback region ${region} for preferred region ${preferredRegion}`);
}
return {
client: this.dbClients[region],
region,
isFallback,
preferredRegion,
healthResult,
failoverEvents
};
} catch (error) {
// Mark region as unhealthy
this.regionHealth[region] = {
healthy: false,
lastCheck: Date.now(),
error: error.message
};
failoverEvents.push({
region,
reason: 'health_check_failed',
error: error.message
});
console.warn(`Region ${region} health check failed:`, error.message);
continue;
}
}
// No healthy regions found
const error = new Error('No healthy regions available');
error.failoverEvents = failoverEvents;
error.regions = regions;
throw error;
}
/**
* Verify write capability for a database client
* @param {Object} client - Database client to test
* @returns {Promise<void>}
*/
async verifyWriteCapability(client) {
try {
// Check if database is in recovery mode (read-only)
const result = await client.query('SELECT pg_is_in_recovery() as in_recovery');
const inRecovery = result.rows[0]?.in_recovery;
if (inRecovery) {
throw new Error('Database is in recovery mode (read-only)');
}
// Test write capability with a simple transaction
await client.query('BEGIN');
await client.query('SELECT 1');
await client.query('ROLLBACK');
} catch (error) {
throw new Error(`Write capability check failed: ${error.message}`);
}
}
/**
* Get health status for all regions
* @returns {Object} - Health status for all regions
*/
getAllRegionHealth() {
const status = {};
Object.keys(this.dbClients).forEach(region => {
const circuitBreakerStatus = this.circuitBreakers[region].getStatus();
const lastHealthCheck = this.healthCheckers[region].getLastStatus();
status[region] = {
...this.regionHealth[region],
circuitBreaker: circuitBreakerStatus,
lastHealthCheck
};
});
return status;
}
/**
* Get health status for a specific region
* @param {string} region - Region to check
* @returns {Object} - Health status for the region
*/
getRegionHealth(region) {
if (!this.dbClients[region]) {
throw new Error(`Region ${region} not configured`);
}
const circuitBreakerStatus = this.circuitBreakers[region].getStatus();
const lastHealthCheck = this.healthCheckers[region].getLastStatus();
return {
...this.regionHealth[region],
circuitBreaker: circuitBreakerStatus,
lastHealthCheck
};
}
/**
* Manually mark a region as healthy or unhealthy
* @param {string} region - Region to update
* @param {boolean} healthy - Health status
* @param {string} reason - Reason for the status change
*/
setRegionHealth(region, healthy, reason = 'manual_override') {
if (!this.dbClients[region]) {
throw new Error(`Region ${region} not configured`);
}
this.regionHealth[region] = {
healthy,
lastCheck: Date.now(),
reason
};
if (healthy) {
// Reset circuit breaker if marking as healthy
this.circuitBreakers[region].reset();
}
console.log(`Region ${region} manually set to ${healthy ? 'healthy' : 'unhealthy'}: ${reason}`);
}
/**
* Get list of currently healthy regions
* @returns {string[]} - Array of healthy region names
*/
getHealthyRegions() {
return Object.keys(this.regionHealth).filter(region => {
const health = this.regionHealth[region];
const circuitBreaker = this.circuitBreakers[region];
return health.healthy && circuitBreaker.isRequestAllowed();
});
}
/**
* Update fallback chain configuration
* @param {string[]} newFallbackChain - New fallback chain
*/
updateFallbackChain(newFallbackChain) {
this.fallbackChain = newFallbackChain;
console.log(`Fallback chain updated: ${newFallbackChain.join(' → ')}`);
}
/**
* Shutdown the region manager and stop all monitoring
*/
shutdown() {
Object.values(this.healthCheckers).forEach(checker => {
checker.stopMonitoring();
});
console.log('RegionManager shutdown complete');
}
/**
* Get statistics about failover events and region usage
* @returns {Object} - Usage statistics
*/
getUsageStats() {
const stats = {
totalRegions: Object.keys(this.dbClients).length,
healthyRegions: this.getHealthyRegions().length,
circuitBreakers: {}
};
Object.keys(this.circuitBreakers).forEach(region => {
stats.circuitBreakers[region] = this.circuitBreakers[region].getStatus();
});
return stats;
}
}
module.exports = { RegionManager };
/**
* Region Recovery Manager for Aurora
*
* Handles safe procedures for bringing failed regions back online,
* including health validation, replication lag checks, and gradual traffic restoration.
*/
const { checkRegionHealth, comprehensiveHealthCheck } = require('./health-checks');
class RegionRecovery {
constructor(regionManager, options = {}) {
this.regionManager = regionManager;
// Configuration
this.maxReplicationLagSeconds = options.maxReplicationLagSeconds || 300; // 5 minutes
this.recoveryTimeoutMs = options.recoveryTimeoutMs || 300000; // 5 minutes
this.healthCheckRetries = options.healthCheckRetries || 3;
this.healthCheckIntervalMs = options.healthCheckIntervalMs || 5000;
this.gradualTrafficEnabled = options.gradualTrafficEnabled || true;
// Recovery state tracking
this.recoveryStates = {};
this.recoveryAttempts = {};
}
/**
* Attempt to bring a region back online
* @param {string} region - Region to recover
* @param {Object} options - Recovery options
* @returns {Promise<Object>} - Recovery result
*/
async recoverRegion(region, options = {}) {
const {
skipReplicationCheck = false,
forceRecovery = false,
gradualTraffic = this.gradualTrafficEnabled
} = options;
console.log(`Starting recovery process for region: ${region}`);
// Initialize recovery state
this.recoveryStates[region] = {
status: 'starting',
startTime: Date.now(),
steps: [],
errors: []
};
try {
// Step 1: Basic connectivity validation
await this.validateConnectivity(region);
this.addRecoveryStep(region, 'connectivity_validated');
// Step 2: Comprehensive health check
await this.validateRegionHealth(region);
this.addRecoveryStep(region, 'health_validated');
// Step 3: Replication lag check (if applicable)
if (!skipReplicationCheck) {
await this.validateReplicationLag(region);
this.addRecoveryStep(region, 'replication_validated');
}
// Step 4: Write capability test
await this.validateWriteCapability(region);
this.addRecoveryStep(region, 'write_capability_validated');
// Step 5: Data consistency check
await this.validateDataConsistency(region);
this.addRecoveryStep(region, 'data_consistency_validated');
// Step 6: Bring region online
await this.bringRegionOnline(region, gradualTraffic);
this.addRecoveryStep(region, 'region_online');
// Update recovery state
this.recoveryStates[region].status = 'completed';
this.recoveryStates[region].completedAt = Date.now();
this.recoveryStates[region].duration = Date.now() - this.recoveryStates[region].startTime;
console.log(`Region ${region} successfully recovered in ${this.recoveryStates[region].duration}ms`);
return {
success: true,
region,
duration: this.recoveryStates[region].duration,
steps: this.recoveryStates[region].steps
};
} catch (error) {
// Update recovery state with failure
this.recoveryStates[region].status = 'failed';
this.recoveryStates[region].failedAt = Date.now();
this.recoveryStates[region].finalError = error.message;
this.recoveryStates[region].errors.push({
message: error.message,
timestamp: Date.now()
});
console.error(`Failed to recover region ${region}:`, error.message);
return {
success: false,
region,
error: error.message,
steps: this.recoveryStates[region].steps,
errors: this.recoveryStates[region].errors
};
}
}
/**
* Validate basic connectivity to the region
* @param {string} region - Region to validate
*/
async validateConnectivity(region) {
const client = this.regionManager.dbClients[region];
if (!client) {
throw new Error(`No database client configured for region: ${region}`);
}
console.log(`Validating connectivity for region: ${region}`);
try {
// Simple connectivity test with timeout
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Connectivity test timeout')), 10000);
});
const connectivityPromise = client.query('SELECT 1 as connectivity_test');
await Promise.race([connectivityPromise, timeoutPromise]);
} catch (error) {
throw new Error(`Connectivity validation failed: ${error.message}`);
}
}
/**
* Validate comprehensive health of the region
* @param {string} region - Region to validate
*/
async validateRegionHealth(region) {
console.log(`Performing comprehensive health check for region: ${region}`);
const client = this.regionManager.dbClients[region];
const circuitBreaker = this.regionManager.circuitBreakers[region];
try {
const healthResult = await comprehensiveHealthCheck(client, circuitBreaker);
if (!healthResult.healthy) {
const failedChecks = healthResult.checks
.filter(check => !check.healthy)
.map(check => check.name);
throw new Error(`Health validation failed. Failed checks: ${failedChecks.join(', ')}`);
}
console.log(`Health validation passed for region: ${region}`, {
averageLatency: healthResult.averageLatency,
checksCompleted: healthResult.checks.length
});
} catch (error) {
throw new Error(`Health validation failed: ${error.message}`);
}
}
/**
* Validate replication lag is within acceptable limits
* @param {string} region - Region to validate
*/
async validateReplicationLag(region) {
console.log(`Checking replication lag for region: ${region}`);
const client = this.regionManager.dbClients[region];
try {
const lagResult = await client.query(`
SELECT
CASE
WHEN pg_is_in_recovery() THEN
EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp()))
ELSE 0
END as lag_seconds
`);
const lagSeconds = lagResult.rows[0]?.lag_seconds || 0;
if (lagSeconds > this.maxReplicationLagSeconds) {
throw new Error(`Replication lag too high: ${lagSeconds}s (max: ${this.maxReplicationLagSeconds}s)`);
}
console.log(`Replication lag validation passed for region: ${region} (lag: ${lagSeconds}s)`);
} catch (error) {
throw new Error(`Replication lag validation failed: ${error.message}`);
}
}
/**
* Validate write capability
* @param {string} region - Region to validate
*/
async validateWriteCapability(region) {
console.log(`Validating write capability for region: ${region}`);
const client = this.regionManager.dbClients[region];
try {
// Check if database is in recovery mode
const recoveryResult = await client.query('SELECT pg_is_in_recovery() as in_recovery');
const inRecovery = recoveryResult.rows[0]?.in_recovery;
if (inRecovery) {
throw new Error('Database is in recovery mode (read-only)');
}
// Test actual write capability with a transaction
await client.query('BEGIN');
await client.query(`
CREATE TEMP TABLE recovery_test_${Date.now()} (
id SERIAL PRIMARY KEY,
test_data TEXT,
created_at TIMESTAMP DEFAULT NOW()
)
`);
await client.query('ROLLBACK');
console.log(`Write capability validation passed for region: ${region}`);
} catch (error) {
// Ensure we rollback if something went wrong
try {
await client.query('ROLLBACK');
} catch (rollbackError) {
// Ignore rollback errors
}
throw new Error(`Write capability validation failed: ${error.message}`);
}
}
/**
* Validate data consistency with a known data point
* @param {string} region - Region to validate
*/
async validateDataConsistency(region) {
console.log(`Validating data consistency for region: ${region}`);
const client = this.regionManager.dbClients[region];
try {
// Check that basic system tables exist and are accessible
const systemCheck = await client.query(`
SELECT count(*) as table_count
FROM information_schema.tables
WHERE table_schema = 'public'
`);
const tableCount = systemCheck.rows[0]?.table_count;
if (tableCount === undefined || tableCount < 0) {
throw new Error('System tables not accessible');
}
// Additional consistency checks could be added here
// For example, checking key business data points
console.log(`Data consistency validation passed for region: ${region} (${tableCount} tables)`);
} catch (error) {
throw new Error(`Data consistency validation failed: ${error.message}`);
}
}
/**
* Bring the region online in the region manager
* @param {string} region - Region to bring online
* @param {boolean} gradualTraffic - Whether to enable gradual traffic restoration
*/
async bringRegionOnline(region, gradualTraffic = false) {
console.log(`Bringing region online: ${region} (gradual: ${gradualTraffic})`);
try {
// Reset circuit breaker to allow traffic
this.regionManager.circuitBreakers[region].reset();
// Mark region as healthy
this.regionManager.setRegionHealth(region, true, 'recovery_completed');
if (gradualTraffic) {
// Implement gradual traffic restoration
await this.implementGradualTrafficRestoration(region);
}
console.log(`Region ${region} is now online and accepting traffic`);
} catch (error) {
throw new Error(`Failed to bring region online: ${error.message}`);
}
}
/**
* Implement gradual traffic restoration (placeholder for advanced implementation)
* @param {string} region - Region for gradual restoration
*/
async implementGradualTrafficRestoration(region) {
// This is a placeholder for more sophisticated traffic management
// In a real implementation, you might:
// - Start with 10% traffic, then 25%, 50%, 75%, 100%
// - Monitor error rates and latency during each phase
// - Roll back if issues are detected
console.log(`Implementing gradual traffic restoration for region: ${region}`);
// For now, just add a delay to simulate gradual restoration
const phases = [10, 25, 50, 75, 100];
for (const percentage of phases) {
console.log(`Restoring ${percentage}% traffic to region: ${region}`);
await new Promise(resolve => setTimeout(resolve, 5000)); // 5 second delay
// In a real implementation
/**
* Write Queue for Aurora Regional Failover
*
* Queues critical write operations during regional outages to prevent data loss.
* Includes retry logic, exponential backoff, and dead letter queue handling.
*/
class WriteQueue {
constructor(options = {}) {
this.queue = [];
this.processing = false;
this.deadLetterQueue = [];
// Configuration
this.maxRetries = options.maxRetries || 3;
this.baseDelayMs = options.baseDelayMs || 1000;
this.maxDelayMs = options.maxDelayMs || 30000;
this.maxQueueSize = options.maxQueueSize || 1000;
this.processingIntervalMs = options.processingIntervalMs || 5000;
// Statistics
this.stats = {
queued: 0,
processed: 0,
failed: 0,
deadLettered: 0
};
// Start automatic processing
this.startProcessing();
}
/**
* Add a write operation to the queue
* @param {Function} operation - Async function that performs the write
* @param {Object} metadata - Metadata about the operation
* @param {Object} options - Operation-specific options
* @returns {Promise<Object>} - Result of queuing operation
*/
async enqueue(operation, metadata = {}, options = {}) {
if (this.queue.length >= this.maxQueueSize) {
throw new Error(`Write queue is full (${this.maxQueueSize} items). Consider scaling or investigating failures.`);
}
const queueItem = {
id: this.generateId(),
operation,
metadata,
options,
timestamp: Date.now(),
retries: 0,
priority: options.priority || 0,
maxRetries: options.maxRetries || this.maxRetries
};
// Insert based on priority (higher priority first)
const insertIndex = this.queue.findIndex(item => item.priority < queueItem.priority);
if (insertIndex === -1) {
this.queue.push(queueItem);
} else {
this.queue.splice(insertIndex, 0, queueItem);
}
this.stats.queued++;
console.log(`Write operation queued: ${queueItem.id}`, {
metadata: queueItem.metadata,
queueSize: this.queue.length,
priority: queueItem.priority
});
return {
queued: true,
id: queueItem.id,
position: this.queue.length,
estimatedProcessingTime: this.estimateProcessingTime()
};
}
/**
* Start automatic queue processing
*/
startProcessing() {
if (this.processing) {
return;
}
this.processing = true;
this.processQueue();
}
/**
* Stop automatic queue processing
*/
stopProcessing() {
this.processing = false;
}
/**
* Process the write queue with retry logic
*/
async processQueue() {
while (this.processing && this.queue.length > 0) {
const item = this.queue.shift();
try {
console.log(`Processing write operation: ${item.id}`, {
metadata: item.metadata,
attempt: item.retries + 1,
queueSize: this.queue.length
});
const startTime = Date.now();
const result = await item.operation();
const duration = Date.now() - startTime;
this.stats.processed++;
console.log(`Write operation completed: ${item.id}`, {
metadata: item.metadata,
duration,
result: typeof result === 'object' ? 'object' : result
});
} catch (error) {
await this.handleFailedOperation(item, error);
}
}
// Schedule next processing cycle if still processing
if (this.processing) {
setTimeout(() => this.processQueue(), this.processingIntervalMs);
}
}
/**
* Handle a failed write operation with retry logic
* @param {Object} item - The failed queue item
* @param {Error} error - The error that occurred
*/
async handleFailedOperation(item, error) {
item.retries++;
item.lastError = error.message;
item.lastErrorTime = Date.now();
console.warn(`Write operation failed: ${item.id}`, {
metadata: item.metadata,
attempt: item.retries,
maxRetries: item.maxRetries,
error: error.message
});
if (item.retries < item.maxRetries) {
// Calculate exponential backoff delay
const delay = Math.min(
this.baseDelayMs * Math.pow(2, item.retries - 1),
this.maxDelayMs
);
console.log(`Retrying write operation: ${item.id} in ${delay}ms`);
// Re-queue with delay
setTimeout(() => {
const insertIndex = this.queue.findIndex(queueItem => queueItem.priority < item.priority);
if (insertIndex === -1) {
this.queue.push(item);
} else {
this.queue.splice(insertIndex, 0, item);
}
}, delay);
} else {
// Move to dead letter queue
this.deadLetterQueue.push({
...item,
deadLetteredAt: Date.now(),
finalError: error.message
});
this.stats.failed++;
this.stats.deadLettered++;
console.error(`Write operation dead lettered: ${item.id}`, {
metadata: item.metadata,
retries: item.retries,
finalError: error.message
});
// Emit event for monitoring
this.emitDeadLetterEvent(item, error);
}
}
/**
* Emit event when an operation is dead lettered
* @param {Object} item - The dead lettered item
* @param {Error} error - The final error
*/
emitDeadLetterEvent(item, error) {
// In a real application, you might emit to an event bus,
// send to monitoring systems, or trigger alerts
console.error('DEAD_LETTER_EVENT', {
id: item.id,
metadata: item.metadata,
retries: item.retries,
error: error.message,
queuedAt: new Date(item.timestamp).toISOString(),
deadLetteredAt: new Date().toISOString()
});
}
/**
* Get current queue statistics
* @returns {Object} - Queue statistics
*/
getStats() {
return {
...this.stats,
queueSize: this.queue.length,
deadLetterSize: this.deadLetterQueue.length,
processing: this.processing,
estimatedProcessingTime: this.estimateProcessingTime()
};
}
/**
* Get items currently in the queue
* @returns {Array} - Array of queue items (without operation functions)
*/
getQueueItems() {
return this.queue.map(item => ({
id: item.id,
metadata: item.metadata,
timestamp: item.timestamp,
retries: item.retries,
priority: item.priority,
age: Date.now() - item.timestamp
}));
}
/**
* Get items in the dead letter queue
* @returns {Array} - Array of dead lettered items
*/
getDeadLetterItems() {
return this.deadLetterQueue.map(item => ({
id: item.id,
metadata: item.metadata,
timestamp: item.timestamp,
retries: item.retries,
deadLetteredAt: item.deadLetteredAt,
finalError: item.finalError
}));
}
/**
* Retry items from the dead letter queue
* @param {string[]} itemIds - IDs of items to retry (empty array = retry all)
* @returns {number} - Number of items moved back to queue
*/
retryDeadLetterItems(itemIds = []) {
let retryCount = 0;
if (itemIds.length === 0) {
// Retry all dead letter items
while (this.deadLetterQueue.length > 0) {
const item = this.deadLetterQueue.shift();
this.requeueDeadLetterItem(item);
retryCount++;
}
} else {
// Retry specific items
itemIds.forEach(id => {
const itemIndex = this.deadLetterQueue.findIndex(item => item.id === id);
if (itemIndex !== -1) {
const item = this.deadLetterQueue.splice(itemIndex, 1)[0];
this.requeueDeadLetterItem(item);
retryCount++;
}
});
}
console.log(`Retried ${retryCount} items from dead letter queue`);
return retryCount;
}
/**
* Move a dead letter item back to the main queue
* @param {Object} item - Dead letter item to requeue
*/
requeueDeadLetterItem(item) {
// Reset retry count and remove dead letter metadata
const requeuedItem = {
...item,
retries: 0,
timestamp: Date.now(),
lastError: undefined,
lastErrorTime: undefined,
deadLetteredAt: undefined,
finalError: undefined
};
// Add back to queue with original priority
const insertIndex = this.queue.findIndex(queueItem => queueItem.priority < requeuedItem.priority);
if (insertIndex === -1) {
this.queue.push(requeuedItem);
} else {
this.queue.splice(insertIndex, 0, requeuedItem);
}
this.stats.queued++;
}
/**
* Clear the dead letter queue
* @returns {number} - Number of items cleared
*/
clearDeadLetterQueue() {
const count = this.deadLetterQueue.length;
this.deadLetterQueue = [];
console.log(`Cleared ${count} items from dead letter queue`);
return count;
}
/**
* Estimate processing time based on current queue size
* @returns {number} - Estimated processing time in milliseconds
*/
estimateProcessingTime() {
if (this.queue.length === 0) {
return 0;
}
// Rough estimate: 1 second per item + retry delays
const baseTime = this.queue.length * 1000;
const retryPenalty = this.queue.reduce((sum, item) => sum + (item.retries * this.baseDelayMs), 0);
return baseTime + retryPenalty;
}
/**
* Generate a unique ID for queue items
* @returns {string} - Unique identifier
*/
generateId() {
return `write_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Flush the queue (process all items immediately)
* @returns {Promise<Object>} - Results of flush operation
*/
async flush() {
const startTime = Date.now();
const initialQueueSize = this.queue.length;
console.log(`Flushing write queue (${initialQueueSize} items)...`);
// Process all items in the queue
while (this.queue.length > 0) {
const item = this.queue.shift();
try {
await item.operation();
this.stats.processed++;
} catch (error) {
await this.handleFailedOperation(item, error);
}
}
const duration = Date.now() - startTime;
console.log(`Queue flush completed in ${duration}ms`);
return {
processed: initialQueueSize,
duration,
remaining: this.queue.length,
deadLettered: this.deadLetterQueue.length
};
}
/**
* Gracefully shutdown the write queue
* @returns {Promise<void>}
*/
async shutdown() {
console.log('Shutting down write queue...');
this.stopProcessing();
// Process remaining items
if (this.queue.length > 0) {
console.log(`Processing ${this.queue.length} remaining items...`);
await this.flush();
}
console.log('Write queue shutdown complete');
}
}
/**
* Safe write function that uses the write queue during fallback
* @param {Function} queryFn - Function that performs the database write
* @param {Object} metadata - Metadata about the operation
* @param {Object} req - Express request object (to check if using fallback)
* @param {WriteQueue} writeQueue - Write queue instance
* @returns {Promise<Object>} - Result of the write operation
*/
async function safeWrite(queryFn, metadata, req, writeQueue) {
if (req.isFallback) {
// Queue writes when using fallback region to prevent data inconsistency
return await writeQueue.enqueue(queryFn, {
...metadata,
originalRegion: req.originalRegion || 'unknown',
fallbackRegion: req.region,
userId: req.user?.id,
requestId: req.id || req.headers['x-request-id']
});
}
try {
// Attempt direct write
const result = await queryFn();
return { success: true, result };
} catch (error) {
// Fallback to queueing if direct write fails
console.warn('Direct write failed, queueing operation:', error.message);
const queueResult = await writeQueue.enqueue(queryFn, {
...metadata,
fallbackReason: 'direct_write_failed',
error: error.message
});
return { queued: true, ...queueResult };
}
}
module.exports = { WriteQueue, safeWrite };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment