/** * セッションリカバリユーティリティ * * Fly.io/Lambda再起動時に未完了セッションを検出・再開する */ import type { PersistedSessionState, PendingSessionsFilter } from "./types.js"; import { getPendingSessions, updateStatus, deleteSession } from "./manager.js"; import { SessionStatus } from "./types.js"; /** * リカバリオプション */ export interface RecoveryOptions { /** ユーザーIDでフィルタ */ userId?: string; /** ギルドIDでフィルタ */ guildId?: string; /** チャンネルIDでフィルタ */ channelId?: string; /** 最大復元数 */ maxSessions?: number; /** タイムアウト(ms) */ timeout?: number; } /** * リカバリ結果 */ export interface RecoveryResult { /** 復元されたセッション */ sessions: PersistedSessionState[]; /** スキップされたセッション数 */ skipped: number; /** 削除された期限切れセッション数 */ expired: number; } /** * 再起動時に未完了セッションを復元 * * Fly.io/Lambda起動時に呼び出し、中断していたθサイクルを再開する。 * * @param options - リカバリオプション * @param onRestore - セッション復元時のコールバック * @returns リカバリ結果 */ export async function recoverPendingSessions( options: RecoveryOptions = {}, onRestore?: (session: PersistedSessionState) => void | Promise, ): Promise { const { userId, guildId, channelId, maxSessions = 100, timeout = 30000 } = options; const filter: PendingSessionsFilter = { userId, guildId, channelId, status: SessionStatus.RUNNING, }; const startTime = Date.now(); const sessions = await getPendingSessions(filter); const result: RecoveryResult = { sessions: [], skipped: 0, expired: 0, }; // 期限切れチェック const validSessions = sessions.filter((s) => { if (Date.now() > s.metadata.expiresAt * 1000) { deleteSession(s.metadata.sessionId); result.expired++; return false; } return true; }); // 最大数制限 const toRestore = validSessions.slice(0, maxSessions); for (const session of toRestore) { // タイムアウトチェック if (Date.now() - startTime > timeout) { result.skipped += validSessions.length - toRestore.indexOf(session); break; } try { // ステータスを一時停止に変更(二重実行防止) await updateStatus(session.metadata.sessionId, SessionStatus.PAUSED); // コールバック実行 if (onRestore) { await onRestore(session); } result.sessions.push(session); } catch (error) { console.error(`[Recovery] Failed to restore session ${session.metadata.sessionId}:`, error); result.skipped++; } } result.skipped += validSessions.length - toRestore.length - result.skipped; return result; } /** * セッションのハートビート更新 * * 定期的に呼び出し、TTL延長と生存確認を行う。 * * @param sessionId - セッションID * @param ttl - 新しいTTL (秒), デフォルト: 1時間 */ export async function heartbeatSession(sessionId: string, ttl: number = 3600): Promise { const expiresAt = Math.floor(Date.now() / 1000) + ttl; // TODO: UpdateItemでexpiresAtを更新 // 現状のupdateStatus()はlastUpdateTimeのみ更新 // 必要に応じてmanager.tsにupdateExpiresAt()を追加 } /** * セッションを完了状態にする * * θサイクル完了時に呼び出す。 * * @param sessionId - セッションID * @param finalState - 最終状態(オプション) */ export async function completeSession( sessionId: string, finalState?: Partial, ): Promise { await updateStatus(sessionId, SessionStatus.COMPLETED); // 必要に応じて最終状態を保存 if (finalState) { // TODO: finalStateをマージして保存 } // TTLを短く設定して早期削除(オプション) // 完了セッションは24時間後に削除 // const expiresAt = Math.floor(Date.now() / 1000) + 86400; // await updateExpiresAt(sessionId, expiresAt); } /** * セッションをエラー状態にする * * θサイクルエラー時に呼び出す。 * * @param sessionId - セッションID * @param error - エラー情報 */ export async function failSession(sessionId: string, error: Error): Promise { await updateStatus(sessionId, SessionStatus.ERROR); // エラー情報を記録 // TODO: エラーをstate.errorに保存して更新 }