Skip to content

Timeoutパターン

タイムアウトパターンを詳しく解説します。

問題のある実装:

// ❌ 悪い例: タイムアウトがない
class PaymentService {
async chargePayment(orderId: string, amount: number): Promise<PaymentResult> {
// タイムアウトがない場合、無限に待機する可能性がある
const response = await fetch('https://payment-api.example.com/charge', {
method: 'POST',
body: JSON.stringify({ orderId, amount }),
});
return await response.json();
}
}

問題点:

  1. リソースの浪費: 長時間待機するリクエストがリソースを占有
  2. 可用性の低下: スレッドプールが枯渇する可能性
  3. ユーザー体験の悪化: ユーザーが長時間待機する

影響:

  • リソースの浪費
  • 可用性の低下
  • ユーザー体験の悪化

改善された実装:

// ✅ 良い例: Timeoutパターンを使用
class TimeoutPolicy {
async execute<T>(fn: () => Promise<T>, timeoutMs: number): Promise<T> {
return Promise.race([
fn(),
new Promise<T>((_, reject) => {
setTimeout(() => {
reject(new TimeoutError(`Operation timed out after ${timeoutMs}ms`));
}, timeoutMs);
}),
]);
}
}
class PaymentService {
private timeoutPolicy: TimeoutPolicy;
constructor() {
this.timeoutPolicy = new TimeoutPolicy();
}
async chargePayment(orderId: string, amount: number): Promise<PaymentResult> {
return await this.timeoutPolicy.execute(async () => {
const response = await fetch('https://payment-api.example.com/charge', {
method: 'POST',
body: JSON.stringify({ orderId, amount }),
});
if (!response.ok) {
throw new Error('Payment failed');
}
return await response.json();
}, 5000); // 5秒でタイムアウト
}
}

メリット:

  • リソースの節約: 長時間待機するリクエストを中断
  • 可用性の向上: リソースを解放して他のリクエストを処理
  • ユーザー体験の向上: 適切なタイムアウトでユーザーにフィードバック

実装例:

class HierarchicalTimeout {
async execute<T>(
fn: () => Promise<T>,
timeouts: { operation: number; total: number }
): Promise<T> {
const startTime = Date.now();
return Promise.race([
this.executeWithOperationTimeout(fn, timeouts.operation),
new Promise<T>((_, reject) => {
setTimeout(() => {
reject(new TimeoutError(`Total timeout after ${timeouts.total}ms`));
}, timeouts.total);
}),
]);
}
private async executeWithOperationTimeout<T>(
fn: () => Promise<T>,
timeoutMs: number
): Promise<T> {
return Promise.race([
fn(),
new Promise<T>((_, reject) => {
setTimeout(() => {
reject(new TimeoutError(`Operation timeout after ${timeoutMs}ms`));
}, timeoutMs);
}),
]);
}
}
// 使用例
const timeout = new HierarchicalTimeout();
await timeout.execute(
async () => {
// 複数の操作を実行
await paymentService.chargePayment(orderId, amount);
await inventoryService.reserveInventory(orderId, items);
await notificationService.sendConfirmation(userId);
},
{
operation: 3000, // 各操作は3秒でタイムアウト
total: 10000, // 全体は10秒でタイムアウト
}
);

サービスごとのタイムアウト設定

Section titled “サービスごとのタイムアウト設定”

設定例:

interface ServiceTimeouts {
payment: number; // 5秒
inventory: number; // 3秒
notification: number; // 2秒
search: number; // 1秒
}
const timeouts: ServiceTimeouts = {
payment: 5000, // 決済は重要なので長め
inventory: 3000, // 在庫は中程度
notification: 2000, // 通知は短めでOK
search: 1000, // 検索は短め
};
class ServiceClient {
async callService<T>(
serviceName: keyof ServiceTimeouts,
fn: () => Promise<T>
): Promise<T> {
const timeout = timeouts[serviceName];
return Promise.race([
fn(),
new Promise<T>((_, reject) => {
setTimeout(() => {
reject(new TimeoutError(`${serviceName} timeout after ${timeout}ms`));
}, timeout);
}),
]);
}
}
class TimeoutPolicy {
async execute<T>(fn: () => Promise<T>, timeoutMs: number): Promise<T> {
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
}, timeoutMs);
try {
const result = await fn();
clearTimeout(timeoutId);
return result;
} catch (error) {
clearTimeout(timeoutId);
if (controller.signal.aborted) {
throw new TimeoutError(`Operation timed out after ${timeoutMs}ms`);
}
throw error;
}
}
}
class PaymentService {
async chargePayment(orderId: string, amount: number): Promise<PaymentResult> {
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
}, 5000);
try {
const response = await fetch('https://payment-api.example.com/charge', {
method: 'POST',
body: JSON.stringify({ orderId, amount }),
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error('Payment failed');
}
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new TimeoutError('Payment request timed out');
}
throw error;
}
}
}

Timeoutパターンのポイント:

  • なぜ必要か: リソースの浪費を防ぎ、可用性を向上
  • 階層的タイムアウト: 操作レベルと全体レベルのタイムアウト
  • サービスごとの設定: サービスごとに適切なタイムアウト値を設定
  • 実装: AbortControllerを使用してリクエストを中断

適切なTimeoutパターンの実装により、リソースを効率的に使用できるシステムを構築できます。