retry-fallback について
TypeScript と Python のアプリケーションで一時的な障害に対処するスキルです。上限付きのバックオフとジッターによる再試行、代替データソース、依存先障害の影響を抑える縮退運転を扱います。
主な機能
- 01指数バックオフによる再試行
- 02ジッター付き再試行間隔
- 03再試行対象エラーの選択
- 04代替値またはデータソース
- 05縮退運転
活用シーン
- 01一時的なネットワーク障害の再試行
- 02API 停止時のキャッシュ利用
- 03プライマリ、レプリカ、キャッシュの順次試行
- 04依存先障害中の主要機能の維持
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ dadbodgeoff/ drift --skill retry-fallbackインストール後、Claude Code で/retry-fallbackを入力して読み込まれたか確認します。
詳細
再試行とフォールバックのパターン
一時的な障害を適切に処理します。
このスキルを使う場面
- ネットワークリクエストが時折失敗する場合
- 外部サービスが一時的に停止する場合
- 依存関係に障害が発生した際に、機能を適切に縮退させる必要がある場合
- 障害の連鎖を防ぎたい場合
基本概念
- 指数バックオフ - 再試行の間隔を徐々に延ばします
- ジッター - ランダムな変動を加えて、リクエストの集中を防ぎます
- フォールバック - 優先データソースが失敗した場合に、代替データソースを使います
- 適切な機能縮退 - 完全に失敗するよりも、機能を制限して動作させるほうが有効です
TypeScript の実装
指数バックオフによる再試行
// retry.ts
export interface RetryConfig {
maxRetries: number;
baseDelayMs: number;
maxDelayMs: number;
backoffMultiplier: number;
jitter: boolean;
retryableErrors?: (error: Error) => boolean;
}
const DEFAULT_CONFIG: RetryConfig = {
maxRetries: 3,
baseDelayMs: 1000,
maxDelayMs: 30000,
backoffMultiplier: 2,
jitter: true,
};
function calculateDelay(attempt: number, config: RetryConfig): number {
let delay = config.baseDelayMs * Math.pow(config.backoffMultiplier, attempt);
delay = Math.min(delay, config.maxDelayMs);
if (config.jitter) {
const jitterRange = delay * 0.25;
delay = delay + (Math.random() * jitterRange * 2 - jitterRange);
}
return Math.floor(delay);
}
function isRetryable(error: Error): boolean {
const message = error.message.toLowerCase();
return (
message.includes('network') ||
message.includes('timeout') ||
message.includes('rate limit') ||
message.includes('429') ||
message.includes('503') ||
message.includes('502')
);
}
export async function retry<T>(
fn: () => Promise<T>,
config: Partial<RetryConfig> = {}
): Promise<T> {
const cfg = { ...DEFAULT_CONFIG, ...config };
const shouldRetry = cfg.retryableErrors || isRetryable;
let lastError: Error;
for (let attempt = 0; attempt <= cfg.maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
if (attempt === cfg.maxRetries || !shouldRetry(lastError)) {
throw lastError;
}
const delay = calculateDelay(attempt, cfg);
console.log(`[Retry] Attempt ${attempt + 1} failed, retrying in ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError!;
}
フォールバックパターン
// fallback.ts
export interface FallbackConfig<T> {
timeout?: number;
fallbackValue?: T;
fallbackFn?: () => T | Promise<T>;
onFallback?: (error: Error) => void;
}
export async function withFallback<T>(
fn: () => Promise<T>,
config: FallbackConfig<T>
): Promise<T> {
const { timeout, fallbackValue, fallbackFn, onFallback } = config;
try {
if (timeout) {
return await Promise.race([
fn(),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeout)
),
]);
}
return await fn();
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
if (onFallback) onFallback(err);
if (fallbackFn) return await fallbackFn();
if (fallbackValue !== undefined) return fallbackValue;
throw err;
}
}
export async function tryMultiple<T>(
sources: Array<() => Promise<T>>,
options: { timeout?: number } = {}
): Promise<T> {
const errors: Error[] = [];
for (const source of sources) {
try {
if (options.timeout) {
return await Promise.race([
source(),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), options.timeout)
),
]);
}
return await source();
} catch (error) {
errors.push(error instanceof Error ? error : new Error(String(error)));
}
}
throw new AggregateError(errors, 'All sources failed');
}
組み合わせ:再試行とフォールバック
// resilient-fetch.ts
export async function resilientFetch<T>(
fn: () => Promise<T>,
config: {
retry?: Partial<RetryConfig>;
fallback?: FallbackConfig<T>;
} = {}
): Promise<T> {
const withRetryFn = config.retry
? () => retry(fn, config.retry)
: fn;
if (config.fallback) {
return withFallback(withRetryFn, config.fallback);
}
return withRetryFn();
}
Python の実装
# retry.py
import asyncio
import random
from typing import Callable, TypeVar, Optional
from functools import wraps
T = TypeVar('T')
class RetryConfig:
def __init__(
self,
max_retries: int = 3,
base_delay: float = 1.0,
max_delay: float = 30.0,
backoff_multiplier: float = 2.0,
jitter: bool = True,
):
self.max_retries = max_retries
self.base_delay = base_delay
self.max_delay = max_delay
self.backoff_multiplier = backoff_multiplier
self.jitter = jitter
def calculate_delay(attempt: int, config: RetryConfig) -> float:
delay = config.base_delay * (config.backoff_multiplier ** attempt)
delay = min(delay, config.max_delay)
if config.jitter:
jitter_range = delay * 0.25
delay = delay + random.uniform(-jitter_range, jitter_range)
return delay
async def retry(
fn: Callable[[], T],
config: Optional[RetryConfig] = None,
retryable: Optional[Callable[[Exception], bool]] = None,
) -> T:
config = config or RetryConfig()
def is_retryable(error: Exception) -> bool:
if retryable:
return retryable(error)
msg = str(error).lower()
return any(x in msg for x in ['network', 'timeout', '429', '503'])
last_error: Exception = Exception("No attempts made")
for attempt in range(config.max_retries + 1):
try:
return await fn()
except Exception as e:
last_error = e
if attempt == config.max_retries or not is_retryable(e):
raise
delay = calculate_delay(attempt, config)
print(f"[Retry] Attempt {attempt + 1} failed, retrying in {delay:.1f}s")
await asyncio.sleep(delay)
raise last_error
def with_retry(config: Optional[RetryConfig] = None):
"""Decorator for retry."""
def decorator(fn):
@wraps(fn)
async def wrapper(*args, **kwargs):
return await retry(lambda: fn(*args, **kwargs), config)
return wrapper
return decorator
# Fallback
async def with_fallback(
fn: Callable[[], T],
fallback: T | Callable[[], T],
timeout: Optional[float] = None,
) -> T:
try:
if timeout:
return await asyncio.wait_for(fn(), timeout=timeout)
return await fn()
except Exception as e:
print(f"[Fallback] Primary failed: {e}")
if callable(fallback):
return await fallback() if asyncio.iscoroutinefunction(fallback) else fallback()
return fallback
使用例
基本的な再試行
const data = await retry(
() => fetch('https://api.example.com/data').then(r => r.json()),
{ maxRetries: 3 }
);
カスタムロジックによる再試行
const result = await retry(
() => processPayment(order),
{
maxRetries: 5,
baseDelayMs: 2000,
retryableErrors: (error) => error.message.includes('temporary'),
}
);
キャッシュへのフォールバック
const data = await withFallback(
() => fetchFromAPI(),
{
timeout: 5000,
fallbackFn: () => getFromCache(),
onFallback: (error) => {
console.warn('Using cached data:', error.message);
},
}
);
複数のソースを試す
const user = await tryMultiple([
() => fetchFromPrimaryDB(userId),
() => fetchFromReplicaDB(userId),
() => fetchFromCache(userId),
], { timeout: 3000 });
組み合わせたパターン
const dashboard = await resilientFetch(
() => fetchFromMLPipeline(),
{
retry: { maxRetries: 2, baseDelayMs: 500 },
fallback: {
timeout: 5000,
fallbackFn: async () => {
const snapshot = await fetchLatestSnapshot();
return snapshot || { status: 'degraded', data: getCachedData() };
},
},
}
);
適切な機能縮退
interface DegradedResponse<T> {
data: T;
degraded: boolean;
message?: string;
}
async function withDegradation<T>(
fullFn: () => Promise<T>,
degradedFn: () => Promise<T>,
minimalFn: () => T
): Promise<DegradedResponse<T>> {
try {
return { data: await fullFn(), degraded: false };
} catch {
try {
return { data: await degradedFn(), degraded: true, message: 'Some features unavailable' };
} catch {
return { data: minimalFn(), degraded: true, message: 'Limited functionality' };
}
}
}
// Usage
const response = await withDegradation(
() => fetchRealtimeAnalytics(),
() => fetchCachedAnalytics(),
() => ({ message: 'Analytics unavailable', data: [] })
);
ベストプラクティス
- 一時的なエラーのみを再試行する - 400 Bad Request は再試行しないでください
- ジッターを使う - リクエストの集中を防ぎます
- 最大待機時間を設定する - いつまでも待たないようにします
- 再試行をログに記録する - 再試行の頻度を追跡します
- フォールバックを用意する - 常に代替策を用意してください
よくある間違い
- 一時的でないエラー(400、401、404)を再試行する
- ジッターを使わない(すべてのインスタンスが同時に再試行する)
- 無限に再試行する(上限がない)
- 重要な処理経路にフォールバックがない
- 再試行の試みをログに記録しない
関連スキル
コメント
Markdown 対応