# 機能設計書 90-キャッシュプロバイダー管理

## 概要

本ドキュメントは、Medusaコマースプラットフォームにおけるキャッシュプロバイダー管理機能の設計について記述する。Redis等の外部キャッシュストアや組み込みメモリキャッシュなど、複数のキャッシュプロバイダーの登録・解決・管理を行う。

### 本機能の処理概要

**業務上の目的・背景**：大規模ECサイトでは、環境や要件に応じて異なるキャッシュストアを選択する必要がある。開発環境ではメモリキャッシュ、本番環境ではRedisなど、柔軟なプロバイダー切り替えを実現する。また、複数のプロバイダーを併用するマルチティアキャッシュ戦略もサポートする。

**機能の利用シーン**：アプリケーション起動時のプロバイダー初期化、キャッシュ操作時のプロバイダー解決、設定変更によるプロバイダー切り替え、シャットダウン時のプロバイダークリーンアップなど。

**主要な処理内容**：
1. プロバイダー登録（registerProvider）：キャッシュプロバイダーをDIコンテナに登録
2. プロバイダー解決（retrieveProvider）：IDによるプロバイダーインスタンス取得
3. デフォルトプロバイダー管理：設定に基づくデフォルトプロバイダー決定
4. プロバイダーライフサイクル管理：起動時初期化、シャットダウン時クリーンアップ

**関連システム・外部連携**：Redis（ioredis）、メモリキャッシュ（node-cache）、Awilixコンテナと連携する。

**権限による制御**：キャッシュプロバイダーはシステムレベルの設定であり、管理者のみが設定変更可能。

## 関連画面

キャッシュプロバイダー管理は内部インフラ機能であり、直接操作する画面は存在しない。システム設定ファイル（medusa-config.js）で設定を行う。

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| - | システム設定 | 設定 | プロバイダー設定変更 |

## 機能種別

システム管理 / プロバイダー管理

## 入力仕様

### 入力パラメータ

#### CachingModuleOptions（モジュール設定）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| ttl | number | No | デフォルトTTL（秒） | 正の整数 |
| in_memory | object | No | メモリキャッシュ設定 | - |
| in_memory.enable | boolean | No | メモリキャッシュ有効化 | - |
| in_memory.ttl | number | No | TTL（秒） | 正の整数 |
| in_memory.maxKeys | number | No | 最大キー数 | 正の整数 |
| in_memory.maxSize | number | No | 最大サイズ（バイト） | 正の整数 |
| providers | array | No | 外部プロバイダー設定 | - |
| providers[].resolve | string | Yes | プロバイダーモジュールパス | - |
| providers[].id | string | Yes | プロバイダーID | 一意 |
| providers[].is_default | boolean | No | デフォルトフラグ | - |
| providers[].options | object | No | プロバイダー固有設定 | - |

#### RedisCacheModuleOptions（Redisプロバイダー設定）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| ttl | number | No | デフォルトTTL（秒） | 正の整数（デフォルト30） |
| redisUrl | string | No | Redis接続文字列 | 有効なURL |
| redisOptions | object | No | ioredis接続オプション | - |
| namespace | string | No | キープレフィックス | デフォルト"medusa" |

#### MemoryCacheModuleOptions（メモリプロバイダー設定）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| ttl | number | No | デフォルトTTL（秒） | デフォルト3600 |
| maxKeys | number | No | 最大キー数 | デフォルト25000 |
| checkPeriod | number | No | 期限切れチェック間隔（秒） | デフォルト60 |
| useClones | boolean | No | クローン使用 | デフォルトfalse |
| maxSize | number | No | 最大サイズ（バイト） | デフォルト300MB |

### 入力データソース

設定ファイル（medusa-config.js）、環境変数（REDIS_URL等）

## 出力仕様

### 出力データ

#### ICachingProviderService（プロバイダーインターフェース）

| メソッド名 | 戻り値 | 説明 |
|-----------|--------|------|
| get | Promise<any> | キャッシュ値取得 |
| set | Promise<void> | キャッシュ値設定 |
| clear | Promise<void> | キャッシュクリア |

### 出力先

DIコンテナ（Awilixコンテナに登録されたプロバイダーインスタンス）

## 処理フロー

### 処理シーケンス

```
1. モジュールロード
   └─ loadProviders()ローダー実行

2. メモリキャッシュ登録（in_memory.enable=true時）
   └─ MemoryCachingProviderをSINGLETONで登録
   └─ デフォルトプロバイダーとして設定

3. 外部プロバイダーロード
   └─ moduleProviderLoader()で各プロバイダーをロード
   └─ registrationFn()でコンテナに登録

4. デフォルトプロバイダー決定
   └─ is_default=trueのプロバイダーをデフォルトに設定
   └─ 単一プロバイダーの場合は自動的にデフォルト

5. プロバイダー解決（実行時）
   └─ retrieveProvider(providerId)でインスタンス取得

6. シャットダウン時クリーンアップ
   └─ __hooks.onApplicationShutdownで切断処理
```

### フローチャート

```mermaid
flowchart TD
    A[モジュールロード] --> B{in_memory.enable?}
    B -->|Yes| C[MemoryCachingProvider登録]
    C --> D[デフォルトプロバイダー設定]
    B -->|No| E[外部プロバイダーのみ]
    D --> F[moduleProviderLoader実行]
    E --> F
    F --> G{providers配列をループ}
    G --> H[registrationFn呼び出し]
    H --> I[コンテナに登録]
    I --> G
    G -->|完了| J{is_default指定あり?}
    J -->|Yes| K[デフォルトプロバイダー更新]
    J -->|No| L{プロバイダー数=1?}
    L -->|Yes| K
    L -->|No| M{in_memory有効?}
    M -->|Yes| N[メモリキャッシュをデフォルト]
    M -->|No| O[エラー: デフォルト未設定]
    K --> P[初期化完了]
    N --> P
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-90-1 | identifier必須 | プロバイダークラスはstatic identifierを持つ必要がある | プロバイダー登録時 |
| BR-90-2 | デフォルト必須 | 少なくとも1つのデフォルトプロバイダーが必要 | モジュール初期化時 |
| BR-90-3 | 単一プロバイダー自動デフォルト | プロバイダーが1つの場合は自動的にデフォルト | モジュール初期化時 |
| BR-90-4 | メモリキャッシュフォールバック | 外部プロバイダー未設定時はメモリキャッシュがデフォルト | in_memory有効時 |
| BR-90-5 | プロバイダーID必須 | 各プロバイダーには一意のIDが必要 | プロバイダー登録時 |

### 計算ロジック

プロバイダー登録キー生成：
```
registrationKey = CachingProviderRegistrationPrefix + providerId
// CachingProviderRegistrationPrefix = "lp_"
// 例: "lp_redis", "lp_cache-memory"
```

メモリサイズ計算（MemoryCachingProvider）：
```
entrySize = Buffer.byteLength(JSON.stringify(data))
          + Buffer.byteLength(key)
          + Buffer.byteLength(JSON.stringify(tags))
```

## データベース操作仕様

キャッシュプロバイダー管理はデータベース操作を行わない。プロバイダー自体がキャッシュストアとして機能する。

### 操作別データベース影響一覧

| 操作 | 対象ストア | 操作種別 | 概要 |
|-----|-----------|---------|------|
| set | Redis / Memory | SET | キャッシュ値設定 |
| get | Redis / Memory | GET | キャッシュ値取得 |
| clear | Redis / Memory | DELETE | キャッシュクリア |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| INVALID_ARGUMENT | 検証エラー | identifierなしでプロバイダー登録 | identifierを定義 |
| INVALID_ARGUMENT | 検証エラー | IDなしでプロバイダー登録 | id設定を追加 |
| - | 初期化エラー | デフォルトプロバイダー未設定 | is_defaultを設定 |
| AwilixResolutionError | 解決エラー | 存在しないプロバイダーID指定 | プロバイダー設定を確認 |
| ECONNREFUSED | 接続エラー | Redis接続失敗 | Redis接続設定を確認 |

### リトライ仕様

- Redis接続エラー時はioredisの組み込みリトライ機構が動作
- メモリキャッシュは接続不要のためリトライなし

## トランザクション仕様

- キャッシュ操作は非同期実行（fire-and-forget）
- トランザクション管理不要

## パフォーマンス要件

- プロバイダー解決：1ms以内（コンテナからの取得）
- Redis SET/GET：50ms以内（ネットワーク依存）
- メモリキャッシュ SET/GET：1ms以内
- メモリキャッシュ最大サイズ：デフォルト300MB
- メモリキャッシュ最大キー数：デフォルト25000

## セキュリティ考慮事項

- Redis接続情報は環境変数で管理（ハードコード禁止）
- ネームスペースによるキー分離（マルチテナント対応）
- 機密情報のキャッシュは暗号化を検討

## 備考

- デフォルトTTLはRedis 30秒、メモリ 3600秒と異なる
- Redisプロバイダーはシャットダウン時に自動切断
- メモリプロバイダーはuseClones=falseで高速化（参照渡し）

---

## コードリーディングガイド

本機能を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: プロバイダーインターフェースを理解する

ICachingProviderServiceインターフェースの定義を把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | types (framework) | `packages/core/types/src/cache/index.ts` | ICachingProviderService |

**読解のコツ**: プロバイダーはget/set/clearの3メソッドを実装する単純なインターフェース。

#### Step 2: プロバイダーローダーを理解する

モジュール初期化時のプロバイダー登録処理。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | providers.ts | `packages/modules/caching/src/loaders/providers.ts` | ローダー関数 |

**主要処理フロー**:
- **37-106行目**: ローダー関数
  - **47行目**: CachingIdentifiersRegistrationNameの初期化
  - **55-74行目**: in_memory有効時のMemoryCachingProvider登録
  - **77-81行目**: moduleProviderLoaderで外部プロバイダーロード
  - **83-90行目**: デフォルトプロバイダー決定
  - **93-104行目**: デフォルト未設定時のエラー処理

#### Step 3: プロバイダーサービスを理解する

プロバイダー解決を担当するCacheProviderService。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | cache-provider.ts | `packages/modules/caching/src/services/cache-provider.ts` | retrieveProvider |

**主要処理フロー**:
- **25-35行目**: `getRegistrationIdentifier`静的メソッド
  - **28-32行目**: identifier未定義時のエラー
- **37-59行目**: `retrieveProvider`メソッド
  - **39-41行目**: プレフィックス付きキーでコンテナから取得
  - **43-51行目**: AwilixResolutionErrorのハンドリング

#### Step 4: Redisプロバイダー実装を理解する

RedisCacheServiceの実装詳細。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | redis-cache.ts | `packages/modules/cache-redis/src/services/redis-cache.ts` | 全体 |

**主要処理フロー**:
- **5-7行目**: 定数定義
  - DEFAULT_NAMESPACE = "medusa"
  - DEFAULT_CACHE_TIME = 30秒
  - EXPIRY_MODE = "EX"
- **18-25行目**: コンストラクタ（Redis接続、TTL、namespace設定）
- **27-31行目**: `__hooks.onApplicationShutdown`でRedis切断
- **40-55行目**: `set`メソッド
  - **45-47行目**: TTL=0時はキャッシュスキップ
  - **49-54行目**: JSON.stringifyしてRedisにSET
- **61-72行目**: `get`メソッド
  - **63-67行目**: Redisから取得してJSON.parse
  - **68-70行目**: パースエラー時はキー削除
- **78-101行目**: `invalidate`メソッド
  - **81-100行目**: SCANでパターンマッチしてパイプラインで削除
- **107-109行目**: `getCacheKey`でネームスペース付きキー生成

#### Step 5: メモリプロバイダー実装を理解する

MemoryCachingProviderの実装詳細。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | memory-cache.ts | `packages/modules/caching/src/providers/memory-cache.ts` | 全体 |

**主要処理フロー**:
- **5行目**: THREE_HUNDRED_MB = 300 * 1024 * 1024
- **14行目**: static identifier = "cache-memory"
- **18-22行目**: タグ管理用Map（tagIndex, keyTags, entryOptions, keySizes）
- **27-62行目**: コンストラクタ
  - **45-50行目**: NodeCacheインスタンス化
  - **55-61行目**: expired/delイベントでタグ参照クリーンアップ
- **64-78行目**: `calculateEntrySize`でメモリ使用量計算
- **80-104行目**: `cleanupTagReferences`でタグ関連データ削除
- **141-205行目**: `set`メソッド
  - **157-164行目**: maxSize超過時のスキップ
  - **176-190行目**: タグインデックス管理
  - **200-204行目**: メモリ使用量追跡
- **207-281行目**: `clear`メソッド
  - **226-231行目**: ワイルドカード"*"で全クリア
  - **258-277行目**: autoInvalidateオプション処理

### プログラム呼び出し階層図

```
medusa-config.js
    │
    └─ Module(Modules.CACHING)
           │
           ├─▶ loadHash() ローダー
           │
           └─▶ loadProviders() ローダー
                  │
                  ├─▶ MemoryCachingProvider登録（in_memory有効時）
                  │
                  └─▶ moduleProviderLoader()
                         │
                         └─▶ registrationFn()
                                │
                                └─▶ CacheProviderService.getRegistrationIdentifier()

CachingModuleService
    │
    └─▶ CacheProviderService.retrieveProvider()
           │
           └─▶ コンテナ["lp_" + providerId]
                  │
                  ├─▶ RedisCacheService
                  │      ├─▶ redis.set()
                  │      ├─▶ redis.get()
                  │      └─▶ redis.scan() + redis.unlink()
                  │
                  └─▶ MemoryCachingProvider
                         ├─▶ NodeCache.set()
                         ├─▶ NodeCache.get()
                         └─▶ NodeCache.del()
```

### データフロー図

```
[設定]                           [処理]                                [出力]

medusa-config.js ───▶ loadProviders() ───▶ Awilixコンテナ
     │                      │                    │
     ├─ in_memory           └─▶ moduleProviderLoader()
     │    ├─ enable                  │
     │    ├─ ttl                     └─▶ registrationFn()
     │    └─ maxSize
     │
     └─ providers[]
          ├─ resolve
          ├─ id
          ├─ is_default
          └─ options

キャッシュ操作時：

CachingModuleService ───▶ CacheProviderService.retrieveProvider()
     │                            │
     ├─ providerId                └─▶ コンテナ["lp_" + id]
     │                                      │
     └─▶ provider.get/set/clear             ├─▶ RedisCacheService
                                            │      │
                                            │      └─▶ Redis Server
                                            │
                                            └─▶ MemoryCachingProvider
                                                   │
                                                   └─▶ NodeCache (メモリ)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| providers.ts | `packages/modules/caching/src/loaders/providers.ts` | ローダー | プロバイダー登録 |
| cache-provider.ts | `packages/modules/caching/src/services/cache-provider.ts` | サービス | プロバイダー解決 |
| cache-module.ts | `packages/modules/caching/src/services/cache-module.ts` | サービス | キャッシュ操作 |
| memory-cache.ts | `packages/modules/caching/src/providers/memory-cache.ts` | プロバイダー | メモリキャッシュ実装 |
| redis-cache.ts | `packages/modules/cache-redis/src/services/redis-cache.ts` | プロバイダー | Redisキャッシュ実装 |
| types/index.ts | `packages/modules/caching/src/types/index.ts` | 型定義 | モジュールオプション |
| types/index.ts | `packages/modules/cache-redis/src/types/index.ts` | 型定義 | Redisオプション |
| index.ts | `packages/modules/caching/src/index.ts` | エクスポート | モジュールエクスポート |
