mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
feat(domain): add graceful degradation framework with multi-layer fallback
Add a standardized degradation pattern for services depending on external systems. withDegradation() tries primary → fallback → safe default, tracking status in a global registry for dashboard visibility. Features: - Async and sync variants - Global registry with per-feature status tracking - Degradation levels: full → reduced → minimal → default - Summary and report APIs for dashboard integration - Reason tracking for debugging Example: Rate limiting degrades from Redis → in-memory → permissive instead of crashing when Redis is unavailable. Closes #799
This commit is contained in:
253
src/domain/degradation.ts
Normal file
253
src/domain/degradation.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* Graceful Degradation Framework
|
||||
*
|
||||
* Provides a standardized pattern for services that depend on external
|
||||
* systems (Redis, vector databases, SSH, external APIs) to degrade
|
||||
* capability instead of failing completely.
|
||||
*
|
||||
* Hierarchy: Full → Reduced → Minimal → Safe Default
|
||||
*
|
||||
* Each service wraps its external calls with withDegradation(), which
|
||||
* tries the primary path, falls back to a secondary, and ultimately
|
||||
* returns a safe default if all backends are unavailable.
|
||||
*/
|
||||
|
||||
/** Degradation levels from best to worst */
|
||||
export type DegradationLevel = "full" | "reduced" | "minimal" | "default";
|
||||
|
||||
/** Status report for a degraded service */
|
||||
export interface DegradationStatus {
|
||||
/** Current operational level */
|
||||
level: DegradationLevel;
|
||||
/** Name of the feature/service */
|
||||
feature: string;
|
||||
/** Human-readable description of current capability */
|
||||
capability: string;
|
||||
/** Why the service is degraded (empty string if full) */
|
||||
reason: string;
|
||||
/** Timestamp of last status change */
|
||||
since: string;
|
||||
}
|
||||
|
||||
/** Result wrapper that includes degradation info */
|
||||
export interface DegradedResult<T> {
|
||||
/** The actual result (from primary, fallback, or default) */
|
||||
result: T;
|
||||
/** Degradation status */
|
||||
status: DegradationStatus;
|
||||
}
|
||||
|
||||
// ── Global degradation registry ─────────────────────────────────────────────
|
||||
|
||||
const registry = new Map<string, DegradationStatus>();
|
||||
|
||||
/**
|
||||
* Execute an operation with graceful degradation.
|
||||
*
|
||||
* Tries the primary function first. If it fails, tries the fallback.
|
||||
* If both fail, returns the safe default. All transitions are tracked
|
||||
* in the global registry for dashboard visibility.
|
||||
*
|
||||
* @param feature - Name of the feature (e.g., "rate-limiting", "semantic-search")
|
||||
* @param primary - Primary implementation (full capability)
|
||||
* @param fallback - Fallback implementation (reduced capability)
|
||||
* @param safeDefault - Safe default value (minimal/no capability)
|
||||
* @param options - Optional configuration
|
||||
* @returns Result with degradation status
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const { result, status } = await withDegradation(
|
||||
* 'rate-limiting',
|
||||
* () => redisRateLimit(key, limit), // Full: distributed
|
||||
* () => memoryRateLimit(key, limit), // Reduced: single-instance
|
||||
* { allowed: true, remaining: Infinity }, // Default: permissive
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export async function withDegradation<T>(
|
||||
feature: string,
|
||||
primary: () => T | Promise<T>,
|
||||
fallback: () => T | Promise<T>,
|
||||
safeDefault: T,
|
||||
options?: {
|
||||
/** Description of full capability */
|
||||
fullCapability?: string;
|
||||
/** Description of reduced capability */
|
||||
reducedCapability?: string;
|
||||
/** Description of default capability */
|
||||
defaultCapability?: string;
|
||||
/** Log function for degradation events */
|
||||
onDegrade?: (status: DegradationStatus) => void;
|
||||
}
|
||||
): Promise<DegradedResult<T>> {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
// Try primary
|
||||
try {
|
||||
const result = await primary();
|
||||
const status: DegradationStatus = {
|
||||
level: "full",
|
||||
feature,
|
||||
capability: options?.fullCapability ?? "Full capability",
|
||||
reason: "",
|
||||
since: now,
|
||||
};
|
||||
updateRegistry(feature, status);
|
||||
return { result, status };
|
||||
} catch (primaryError) {
|
||||
// Primary failed, try fallback
|
||||
try {
|
||||
const result = await fallback();
|
||||
const status: DegradationStatus = {
|
||||
level: "reduced",
|
||||
feature,
|
||||
capability: options?.reducedCapability ?? "Reduced capability (fallback active)",
|
||||
reason: primaryError instanceof Error ? primaryError.message : String(primaryError),
|
||||
since: now,
|
||||
};
|
||||
updateRegistry(feature, status);
|
||||
options?.onDegrade?.(status);
|
||||
return { result, status };
|
||||
} catch (fallbackError) {
|
||||
// Both failed, return safe default
|
||||
const reason = [
|
||||
primaryError instanceof Error ? primaryError.message : String(primaryError),
|
||||
fallbackError instanceof Error ? fallbackError.message : String(fallbackError),
|
||||
].join(" → ");
|
||||
|
||||
const status: DegradationStatus = {
|
||||
level: "default",
|
||||
feature,
|
||||
capability: options?.defaultCapability ?? "Safe default (all backends unavailable)",
|
||||
reason,
|
||||
since: now,
|
||||
};
|
||||
updateRegistry(feature, status);
|
||||
options?.onDegrade?.(status);
|
||||
return { result: safeDefault, status };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous version for non-async code paths.
|
||||
*/
|
||||
export function withDegradationSync<T>(
|
||||
feature: string,
|
||||
primary: () => T,
|
||||
fallback: () => T,
|
||||
safeDefault: T,
|
||||
options?: {
|
||||
fullCapability?: string;
|
||||
reducedCapability?: string;
|
||||
defaultCapability?: string;
|
||||
onDegrade?: (status: DegradationStatus) => void;
|
||||
}
|
||||
): DegradedResult<T> {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
try {
|
||||
const result = primary();
|
||||
const status: DegradationStatus = {
|
||||
level: "full",
|
||||
feature,
|
||||
capability: options?.fullCapability ?? "Full capability",
|
||||
reason: "",
|
||||
since: now,
|
||||
};
|
||||
updateRegistry(feature, status);
|
||||
return { result, status };
|
||||
} catch (primaryError) {
|
||||
try {
|
||||
const result = fallback();
|
||||
const status: DegradationStatus = {
|
||||
level: "reduced",
|
||||
feature,
|
||||
capability: options?.reducedCapability ?? "Reduced capability",
|
||||
reason: primaryError instanceof Error ? primaryError.message : String(primaryError),
|
||||
since: now,
|
||||
};
|
||||
updateRegistry(feature, status);
|
||||
options?.onDegrade?.(status);
|
||||
return { result, status };
|
||||
} catch (fallbackError) {
|
||||
const status: DegradationStatus = {
|
||||
level: "default",
|
||||
feature,
|
||||
capability: options?.defaultCapability ?? "Safe default",
|
||||
reason: `${primaryError} → ${fallbackError}`,
|
||||
since: now,
|
||||
};
|
||||
updateRegistry(feature, status);
|
||||
options?.onDegrade?.(status);
|
||||
return { result: safeDefault, status };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Registry management ─────────────────────────────────────────────────────
|
||||
|
||||
function updateRegistry(feature: string, status: DegradationStatus): void {
|
||||
const existing = registry.get(feature);
|
||||
// Only update 'since' if level actually changed
|
||||
if (existing && existing.level === status.level) {
|
||||
status.since = existing.since;
|
||||
}
|
||||
registry.set(feature, status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get degradation status for all tracked features.
|
||||
*/
|
||||
export function getDegradationReport(): DegradationStatus[] {
|
||||
return Array.from(registry.values()).sort((a, b) => {
|
||||
const order: Record<DegradationLevel, number> = {
|
||||
default: 0,
|
||||
minimal: 1,
|
||||
reduced: 2,
|
||||
full: 3,
|
||||
};
|
||||
return (order[a.level] ?? 4) - (order[b.level] ?? 4);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status for a specific feature.
|
||||
*/
|
||||
export function getFeatureStatus(feature: string): DegradationStatus | null {
|
||||
return registry.get(feature) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any feature is degraded.
|
||||
*/
|
||||
export function hasAnyDegradation(): boolean {
|
||||
for (const status of registry.values()) {
|
||||
if (status.level !== "full") return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of features at each degradation level.
|
||||
*/
|
||||
export function getDegradationSummary(): Record<DegradationLevel, number> {
|
||||
const summary: Record<DegradationLevel, number> = {
|
||||
full: 0,
|
||||
reduced: 0,
|
||||
minimal: 0,
|
||||
default: 0,
|
||||
};
|
||||
for (const status of registry.values()) {
|
||||
summary[status.level]++;
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the registry. Useful for testing.
|
||||
*/
|
||||
export function resetDegradationRegistry(): void {
|
||||
registry.clear();
|
||||
}
|
||||
Reference in New Issue
Block a user