|
| 1 | +import type { |
| 2 | + DisasterRecoveryAssessment, |
| 3 | + DisasterRecoveryPolicy, |
| 4 | + RegionAssessment, |
| 5 | + RegionHealth, |
| 6 | +} from './types'; |
| 7 | + |
| 8 | +export const DEFAULT_DR_POLICY: DisasterRecoveryPolicy = { |
| 9 | + criticalPathP99Ms: 100, |
| 10 | + maxReplicationLagMs: 5_000, |
| 11 | + maxErrorRate: 0.01, |
| 12 | + heartbeatTimeoutMs: 30_000, |
| 13 | + availabilityTarget: 0.9999, |
| 14 | + minHealthySecondaries: 1, |
| 15 | +}; |
| 16 | + |
| 17 | +export function assessRegion( |
| 18 | + region: RegionHealth, |
| 19 | + now: number, |
| 20 | + policy: DisasterRecoveryPolicy = DEFAULT_DR_POLICY, |
| 21 | +): RegionAssessment { |
| 22 | + const reasons: string[] = []; |
| 23 | + |
| 24 | + if (region.latencyMsP99 > policy.criticalPathP99Ms) { |
| 25 | + reasons.push(`p99 latency ${region.latencyMsP99}ms exceeds ${policy.criticalPathP99Ms}ms target`); |
| 26 | + } |
| 27 | + |
| 28 | + if (region.replicationLagMs > policy.maxReplicationLagMs) { |
| 29 | + reasons.push(`replication lag ${region.replicationLagMs}ms exceeds ${policy.maxReplicationLagMs}ms target`); |
| 30 | + } |
| 31 | + |
| 32 | + if (region.errorRate > policy.maxErrorRate) { |
| 33 | + reasons.push(`error rate ${region.errorRate} exceeds ${policy.maxErrorRate} target`); |
| 34 | + } |
| 35 | + |
| 36 | + if (now - region.lastHeartbeatAt > policy.heartbeatTimeoutMs) { |
| 37 | + reasons.push(`heartbeat is stale by ${now - region.lastHeartbeatAt}ms`); |
| 38 | + } |
| 39 | + |
| 40 | + return { |
| 41 | + ...region, |
| 42 | + healthy: reasons.length === 0, |
| 43 | + reasons, |
| 44 | + }; |
| 45 | +} |
| 46 | + |
| 47 | +export function selectFailoverRegion(assessments: RegionAssessment[]): RegionAssessment | undefined { |
| 48 | + return assessments |
| 49 | + .filter((region) => region.role === 'secondary' && region.healthy) |
| 50 | + .sort((left, right) => { |
| 51 | + if (left.replicationLagMs !== right.replicationLagMs) { |
| 52 | + return left.replicationLagMs - right.replicationLagMs; |
| 53 | + } |
| 54 | + |
| 55 | + if (left.latencyMsP99 !== right.latencyMsP99) { |
| 56 | + return left.latencyMsP99 - right.latencyMsP99; |
| 57 | + } |
| 58 | + |
| 59 | + return left.region.localeCompare(right.region); |
| 60 | + })[0]; |
| 61 | +} |
| 62 | + |
| 63 | +export function buildDisasterRecoveryAssessment( |
| 64 | + regions: RegionHealth[], |
| 65 | + now: number = Date.now(), |
| 66 | + policy: DisasterRecoveryPolicy = DEFAULT_DR_POLICY, |
| 67 | +): DisasterRecoveryAssessment { |
| 68 | + const assessments = regions.map((region) => assessRegion(region, now, policy)); |
| 69 | + const primary = assessments.find((region) => region.role === 'primary'); |
| 70 | + const secondaries = assessments.filter((region) => region.role === 'secondary'); |
| 71 | + const observers = assessments.filter((region) => region.role === 'observer'); |
| 72 | + const healthySecondaries = secondaries.filter((region) => region.healthy); |
| 73 | + const recommendedFailoverRegion = primary?.healthy ? undefined : selectFailoverRegion(assessments)?.region; |
| 74 | + const rpoMs = Math.max(0, ...assessments.map((region) => region.replicationLagMs)); |
| 75 | + const rtoMs = recommendedFailoverRegion ? Math.max(1_000, rpoMs) : 0; |
| 76 | + const alerts: string[] = []; |
| 77 | + |
| 78 | + if (!primary) { |
| 79 | + alerts.push('no primary region is configured'); |
| 80 | + } else if (!primary.healthy) { |
| 81 | + alerts.push(`primary region ${primary.region} is unhealthy`); |
| 82 | + } |
| 83 | + |
| 84 | + if (healthySecondaries.length < policy.minHealthySecondaries) { |
| 85 | + alerts.push(`healthy secondary count ${healthySecondaries.length} is below required ${policy.minHealthySecondaries}`); |
| 86 | + } |
| 87 | + |
| 88 | + if (recommendedFailoverRegion) { |
| 89 | + alerts.push(`fail over to ${recommendedFailoverRegion} using blue-green promotion`); |
| 90 | + } |
| 91 | + |
| 92 | + return { |
| 93 | + generatedAt: now, |
| 94 | + meetsAvailabilityTarget: Boolean(primary?.healthy) && healthySecondaries.length >= policy.minHealthySecondaries, |
| 95 | + primary, |
| 96 | + secondaries, |
| 97 | + observers, |
| 98 | + recommendedFailoverRegion, |
| 99 | + rpoMs, |
| 100 | + rtoMs, |
| 101 | + alerts, |
| 102 | + }; |
| 103 | +} |
0 commit comments