Skip to content

Commit 057dca2

Browse files
Merge PR #140 with conflict resolution
2 parents 869ef0c + c17fef4 commit 057dca2

7 files changed

Lines changed: 284 additions & 1 deletion

File tree

MULTI_REGION_DR_ARCHITECTURE.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Multi-Region Replication and Disaster Recovery Architecture
2+
3+
## Objectives
4+
5+
- Keep critical UI/API decision paths under **100 ms P99** by continuously scoring regional latency.
6+
- Preserve a **99.99% availability** posture with one healthy primary and at least one healthy secondary region.
7+
- Bound recovery with an operational RPO derived from replication lag and an RTO derived from the failover promotion window.
8+
- Require security review for replication credentials, cross-region routing, audit logs, and data residency controls before production rollout.
9+
10+
## Runtime design
11+
12+
1. **Active-passive regional topology**: one primary region serves writes while secondary regions continuously receive replicated state. Observer regions can collect telemetry without being eligible for write promotion.
13+
2. **Health assessment loop**: each region reports P99 latency, replication lag, error rate, and heartbeat freshness. The frontend disaster-recovery planner converts those signals into a deterministic assessment for dashboards and runbooks.
14+
3. **Failover selection**: if the primary is unhealthy, the planner chooses the healthy secondary with the lowest replication lag, then lowest latency, then lexicographic region name for deterministic tie-breaking.
15+
4. **Blue-green promotion**: operators promote the selected secondary into a green stack, run smoke and canary analysis, then shift traffic in controlled increments.
16+
5. **Monitoring and alerting**: emit alerts when the primary is degraded, the minimum healthy-secondary count is not met, or a failover region is recommended.
17+
18+
## Disaster recovery test workflow
19+
20+
1. Capture baseline regional metrics from production dashboards.
21+
2. Inject primary-region latency or heartbeat failure in a controlled game day.
22+
3. Verify that the assessment recommends a secondary region and records the expected alert messages.
23+
4. Promote the recommended region through blue-green deployment.
24+
5. Run canary analysis against critical paths and confirm P99 remains below 100 ms.
25+
6. Restore replication in the original primary and document RPO/RTO outcomes in the incident log.
26+
27+
## Runbook checklist
28+
29+
- Confirm at least one secondary is healthy before starting failover.
30+
- Freeze non-critical deployments during the disaster recovery exercise.
31+
- Rotate or validate cross-region replication credentials after the exercise.
32+
- Capture dashboard screenshots for latency, error rate, replication lag, and synthetic availability.
33+
- File a security-review artifact for routing, credential, and audit-log changes.
34+
35+
## Implementation notes
36+
37+
The `src/lib/disasterRecovery` module contains pure TypeScript helpers for assessing regions and selecting failover targets. Keeping the logic side-effect free makes it safe to reuse in dashboards, API routes, scheduled checks, and unit tests.

package-lock.json

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"test:bridge": "tsx src/lib/bridge/__tests__/bridgeStore.test.ts",
3232
"test:staking": "tsx src/lib/staking/__tests__/stakingCalculator.test.ts",
3333
"test:all": "tsx src/lib/__tests__/offlineQueue.test.ts && tsx src/lib/__tests__/OptimisticTransactionManager.test.ts && tsx src/services/__tests__/localCache.test.ts && tsx src/lib/__tests__/txQueue.test.ts && tsx src/lib/__tests__/slidingWindow.test.ts && tsx src/lib/__tests__/idbClient.test.ts && tsx src/services/__tests__/sharedStateSync.test.ts && tsx src/store/__tests__/workspaceStore.test.ts && tsx src/lib/offline/tests/networkCache.test.ts && tsx src/lib/__tests__/invalidationRegistry.test.ts && tsx src/lib/horizon/__tests__/useHorizonPagination.test.ts && tsx src/lib/cache/__tests__/redisCache.test.ts && tsx src/lib/backup/__tests__/backupRestore.test.ts && tsx src/lib/backup/__tests__/scheduler.test.ts && tsx src/lib/scheduler/__tests__/scheduler.test.ts && tsx src/lib/approvalManager/__tests__/approvalManager.test.ts && tsx src/lib/bridge/__tests__/bridgeStore.test.ts && tsx src/lib/staking/__tests__/stakingCalculator.test.ts",
34-
"test:incident": "tsx src/lib/incident/__tests__/runbookAutomation.test.ts"
34+
"test:dr": "tsx src/lib/disasterRecovery/__tests__/replicationPlanner.test.ts"
3535
},
3636
"dependencies": {
3737
"@monaco-editor/react": "^4.7.0",
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import assert from 'node:assert/strict';
2+
import {
3+
buildDisasterRecoveryAssessment,
4+
DEFAULT_DR_POLICY,
5+
selectFailoverRegion,
6+
type RegionHealth,
7+
} from '../index';
8+
9+
const now = Date.UTC(2026, 6, 25, 12, 0, 0);
10+
11+
const healthyRegions: RegionHealth[] = [
12+
{
13+
region: 'us-east-1',
14+
role: 'primary',
15+
latencyMsP99: 82,
16+
replicationLagMs: 900,
17+
errorRate: 0.001,
18+
lastHeartbeatAt: now - 2_000,
19+
},
20+
{
21+
region: 'us-west-2',
22+
role: 'secondary',
23+
latencyMsP99: 91,
24+
replicationLagMs: 1_200,
25+
errorRate: 0.002,
26+
lastHeartbeatAt: now - 2_000,
27+
},
28+
];
29+
30+
const healthyAssessment = buildDisasterRecoveryAssessment(healthyRegions, now);
31+
assert.equal(healthyAssessment.meetsAvailabilityTarget, true);
32+
assert.equal(healthyAssessment.recommendedFailoverRegion, undefined);
33+
assert.deepEqual(healthyAssessment.alerts, []);
34+
35+
const degradedRegions: RegionHealth[] = [
36+
{
37+
...healthyRegions[0],
38+
latencyMsP99: 140,
39+
errorRate: 0.03,
40+
},
41+
{
42+
...healthyRegions[1],
43+
replicationLagMs: 750,
44+
},
45+
{
46+
region: 'eu-central-1',
47+
role: 'secondary',
48+
latencyMsP99: 88,
49+
replicationLagMs: 650,
50+
errorRate: 0.001,
51+
lastHeartbeatAt: now - 1_000,
52+
},
53+
];
54+
55+
const degradedAssessment = buildDisasterRecoveryAssessment(degradedRegions, now);
56+
assert.equal(degradedAssessment.meetsAvailabilityTarget, false);
57+
assert.equal(degradedAssessment.recommendedFailoverRegion, 'eu-central-1');
58+
assert.equal(degradedAssessment.rpoMs, 900);
59+
assert.ok(degradedAssessment.alerts.includes('primary region us-east-1 is unhealthy'));
60+
assert.ok(degradedAssessment.alerts.includes('fail over to eu-central-1 using blue-green promotion'));
61+
62+
const staleRegion = buildDisasterRecoveryAssessment(
63+
[{ ...healthyRegions[0], lastHeartbeatAt: now - DEFAULT_DR_POLICY.heartbeatTimeoutMs - 1 }],
64+
now,
65+
);
66+
assert.equal(staleRegion.primary?.healthy, false);
67+
assert.match(staleRegion.primary?.reasons.join('\n') ?? '', /heartbeat is stale/);
68+
69+
assert.equal(
70+
selectFailoverRegion(degradedAssessment.secondaries)?.region,
71+
'eu-central-1',
72+
'lowest-lag healthy secondary should be selected',
73+
);
74+
75+
console.log('replicationPlanner tests passed');

src/lib/disasterRecovery/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './replicationPlanner';
2+
export * from './types';
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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+
}

src/lib/disasterRecovery/types.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
export type RegionRole = 'primary' | 'secondary' | 'observer';
2+
3+
export interface RegionHealth {
4+
region: string;
5+
role: RegionRole;
6+
latencyMsP99: number;
7+
replicationLagMs: number;
8+
errorRate: number;
9+
lastHeartbeatAt: number;
10+
}
11+
12+
export interface DisasterRecoveryPolicy {
13+
criticalPathP99Ms: number;
14+
maxReplicationLagMs: number;
15+
maxErrorRate: number;
16+
heartbeatTimeoutMs: number;
17+
availabilityTarget: number;
18+
minHealthySecondaries: number;
19+
}
20+
21+
export interface RegionAssessment extends RegionHealth {
22+
healthy: boolean;
23+
reasons: string[];
24+
}
25+
26+
export interface DisasterRecoveryAssessment {
27+
generatedAt: number;
28+
meetsAvailabilityTarget: boolean;
29+
primary?: RegionAssessment;
30+
secondaries: RegionAssessment[];
31+
observers: RegionAssessment[];
32+
recommendedFailoverRegion?: string;
33+
rpoMs: number;
34+
rtoMs: number;
35+
alerts: string[];
36+
}

0 commit comments

Comments
 (0)