# 機能設計書 95-分散ロック

## 概要

本ドキュメントは、Medusaコマースプラットフォームにおける分散ロック機能の設計仕様を記述する。分散ロック機能は、分散環境でのリソースの排他制御を提供し、複数インスタンス間での競合を防止する。

### 本機能の処理概要

分散ロック機能は、複数のサーバーインスタンスが同一リソースに同時アクセスする際の競合を防ぐための排他制御機能を提供する。ロッキングプロバイダー（Redisベースなど）を通じてロックの取得（acquire）、解放（release）、および排他的実行（execute）を行う。

**業務上の目的・背景**：ECプラットフォームでは、在庫更新、注文処理、支払い確認など、複数のインスタンスが同時に同じリソースを操作する可能性がある。例えば、同一商品の在庫を複数の注文が同時に確保しようとした場合、適切なロック機構がないとオーバーセル（在庫超過販売）が発生する。分散ロック機能により、このような競合状態を防止し、データの整合性を保証する。

**機能の利用シーン**：
- 在庫数量の更新時の排他制御
- 注文番号の採番処理
- 支払いセッションの処理
- 同一顧客の並行注文制御
- キャッシュ更新の排他制御

**主要な処理内容**：
1. ロックの取得（acquire）- 指定キーのロックを取得
2. ロックの解放（release）- 取得済みロックを解放
3. 全ロックの解放（releaseAll）- 特定オーナーの全ロックを解放
4. 排他的実行（execute）- ロック取得→処理実行→ロック解放を一括で行う

**関連システム・外部連携**：
- Redis：分散ロックのバックエンドストレージ
- ロッキングプロバイダー：抽象化されたロック操作インターフェース

**権限による制御**：分散ロック機能自体には権限制御は設けられていない。呼び出し元のサービス層で適切な認可を実装する。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| - | - | - | 本機能は直接UIを持たないバックエンド機能 |

## 機能種別

排他制御 / インフラストラクチャ

## 入力仕様

### 入力パラメータ（acquire）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| keys | string \| string[] | Yes | ロック対象のキー（単一または複数） | - |
| args | object | No | ロック取得オプション | - |
| args.ownerId | string \| null | No | ロック所有者ID | - |
| args.expire | number | No | ロック有効期限（秒） | 正の整数 |
| args.provider | string | No | 使用するプロバイダーID | 登録済みプロバイダー |
| sharedContext | Context | No | 共有コンテキスト | - |

### 入力パラメータ（release）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| keys | string \| string[] | Yes | 解放対象のキー | - |
| args | object | No | ロック解放オプション | - |
| args.ownerId | string \| null | No | ロック所有者ID | - |
| args.provider | string | No | 使用するプロバイダーID | - |
| sharedContext | Context | No | 共有コンテキスト | - |

### 入力パラメータ（execute）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| keys | string \| string[] | Yes | ロック対象のキー | - |
| job | () => Promise<T> | Yes | ロック内で実行する非同期関数 | - |
| args | object | No | 実行オプション | - |
| args.timeout | number | No | ロック取得タイムアウト（ミリ秒） | 正の整数 |
| args.provider | string | No | 使用するプロバイダーID | - |
| sharedContext | Context | No | 共有コンテキスト | - |

### 入力パラメータ（releaseAll）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| args | object | No | オプション | - |
| args.ownerId | string \| null | No | 解放対象のオーナーID | - |
| args.provider | string | No | 使用するプロバイダーID | - |
| sharedContext | Context | No | 共有コンテキスト | - |

### 入力データソース

- サービス層からの直接呼び出し
- ワークフローステップ内からの呼び出し

## 出力仕様

### 出力データ（acquire）

| 項目名 | 型 | 説明 |
|--------|-----|------|
| (void) | void | ロック取得成功時は何も返さない |

### 出力データ（release）

| 項目名 | 型 | 説明 |
|--------|-----|------|
| success | boolean | ロック解放が成功したか |

### 出力データ（execute）

| 項目名 | 型 | 説明 |
|--------|-----|------|
| result | T | job関数の実行結果 |

### 出力先

- ロック状態：プロバイダーのバックエンドストレージ（Redis等）

## 処理フロー

### 処理シーケンス（execute）

```
1. プロバイダー決定
   └─ args.provider または defaultProviderId を使用

2. プロバイダー取得
   └─ retrieveProviderRegistration(providerId)

3. ロック取得
   └─ provider.acquire(keys, args)

4. ジョブ実行
   └─ await job()

5. ロック解放
   └─ provider.release(keys, args)

6. 結果返却
   └─ ジョブの実行結果を返却
```

### フローチャート

```mermaid
flowchart TD
    A[execute呼び出し] --> B[プロバイダー決定]
    B --> C[プロバイダー取得]
    C --> D{プロバイダー存在?}
    D -->|No| E[Error: Unable to retrieve provider]
    D -->|Yes| F[ロック取得 acquire]
    F --> G{ロック取得成功?}
    G -->|No| H[ロック待機/タイムアウト]
    H --> G
    G -->|Yes| I[ジョブ実行 job]
    I --> J{ジョブ成功?}
    J -->|Yes| K[ロック解放 release]
    J -->|No| L[エラー捕捉]
    L --> K
    K --> M[結果返却/エラー再スロー]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-95-1 | デフォルトプロバイダー | プロバイダーが指定されない場合はdefaultProviderIdを使用 | 全メソッド呼び出し時 |
| BR-95-2 | 所有者によるロック管理 | ownerIdを指定することで、同一オーナーのロックのみ操作可能 | ownerId指定時 |
| BR-95-3 | 複数キーのロック | keys配列を指定することで複数リソースを一括ロック | 複数キー指定時 |
| BR-95-4 | 有効期限自動解放 | expire指定時は有効期限経過後に自動解放 | expire指定時 |
| BR-95-5 | タイムアウト制御 | executeのtimeout指定によりロック待機時間を制御 | timeout指定時 |

### 計算ロジック

プロバイダー登録識別子の生成：
```typescript
// プロバイダークラスからの識別子取得
static getRegistrationIdentifier(providerClass: Constructor<ILockingProvider>) {
  if (!(providerClass as any).identifier) {
    throw new MedusaError(
      MedusaError.Types.INVALID_ARGUMENT,
      `Trying to register a locking provider without an identifier.`
    )
  }
  return `${(providerClass as any).identifier}`
}

// コンテナからの取得キー
`lp_${providerId}`
```

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| - | - | - | データベース操作なし（プロバイダーに委譲） |

### 外部ストレージ操作（プロバイダー実装による）

| 操作 | メソッド | 概要 |
|-----|---------|------|
| ロック取得 | provider.acquire() | キーに対するロックを取得 |
| ロック解放 | provider.release() | 取得済みロックを解放 |
| 全ロック解放 | provider.releaseAll() | オーナーの全ロックを解放 |
| 排他的実行 | provider.execute() | ロック内でジョブを実行 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| INVALID_ARGUMENT | MedusaError | プロバイダーにidentifierがない | プロバイダークラスにidentifierを定義 |
| - | Error | プロバイダーが見つからない | 正しいプロバイダーIDを指定、または設定を確認 |
| - | AwilixResolutionError | DIコンテナからの解決失敗 | プロバイダーの登録を確認 |
| - | LockAcquisitionError | ロック取得タイムアウト | タイムアウト値を調整、またはリトライ |

### リトライ仕様

- ロック取得のリトライはプロバイダー実装に依存
- executeメソッドは失敗時にロック解放を保証

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

分散ロックはトランザクションとは独立して動作する。ただし、以下の連携パターンが推奨される：

1. ロック取得 → トランザクション開始 → 処理 → トランザクションコミット → ロック解放
2. executeメソッドを使用して、ロック取得とトランザクション処理を一括実行

## パフォーマンス要件

- ロック取得：プロバイダー実装に依存（Redisの場合はミリ秒オーダー）
- ロック解放：プロバイダー実装に依存
- タイムアウト：設定可能（デフォルト値はプロバイダー依存）

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

- Redis通信はTLS暗号化を推奨（本番環境）
- ownerIdは推測困難な値（UUIDなど）を使用することを推奨
- ロックキーに機密情報を含めないこと

## 備考

- プロバイダーはDIコンテナに `lp_{identifier}` 形式で登録される
- LockingDefaultProvider定数でデフォルトプロバイダーIDを設定
- エラーログにはAwilixResolutionErrorの詳細が含まれる

---

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

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

### 推奨読解順序

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

ロッキングモジュールのインターフェースを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | ILockingModule | `packages/core/types/src/locking/index.ts` | ILockingModuleインターフェースの定義 |

**読解のコツ**: ILockingModuleは4つのメソッド（execute, acquire, release, releaseAll）を定義している。

#### Step 2: モジュールサービスを理解する

ロッキングモジュールサービスの実装を確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | locking-module.ts | `packages/modules/locking/src/services/locking-module.ts` | LockingModuleServiceの実装 |

**主要処理フロー**:
- **18-30行目**: コンストラクタでプロバイダーサービスとデフォルトプロバイダーIDを設定
- **32-46行目**: execute()でロック内でジョブを実行
- **48-62行目**: acquire()でロックを取得
- **64-77行目**: release()でロックを解放
- **79-91行目**: releaseAll()で全ロックを解放

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

プロバイダーの管理と取得ロジックを確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | locking-provider.ts | `packages/modules/locking/src/services/locking-provider.ts` | LockingProviderServiceの実装 |

**主要処理フロー**:
- **18-23行目**: コンストラクタでコンテナとロガーを設定
- **25-35行目**: getRegistrationIdentifier()でプロバイダー識別子を生成
- **37-58行目**: retrieveProviderRegistration()でプロバイダーを取得、エラーハンドリング

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

```
LockingModuleService implements ILockingModule
    │
    ├─ execute<T>(keys, job, args, sharedContext)
    │      ├─ providerId = args?.provider ?? defaultProviderId
    │      ├─ provider = providerService_.retrieveProviderRegistration(providerId)
    │      └─ return provider.execute(keys, job, args, sharedContext)
    │
    ├─ acquire(keys, args, sharedContext)
    │      ├─ providerId = args?.provider ?? defaultProviderId
    │      ├─ provider = providerService_.retrieveProviderRegistration(providerId)
    │      └─ await provider.acquire(keys, args, sharedContext)
    │
    ├─ release(keys, args, sharedContext)
    │      ├─ providerId = args?.provider ?? defaultProviderId
    │      ├─ provider = providerService_.retrieveProviderRegistration(providerId)
    │      └─ return await provider.release(keys, args, sharedContext)
    │
    └─ releaseAll(args, sharedContext)
           ├─ providerId = args?.provider ?? defaultProviderId
           ├─ provider = providerService_.retrieveProviderRegistration(providerId)
           └─ return await provider.releaseAll(args, sharedContext)

LockingProviderService
    │
    └─ retrieveProviderRegistration(providerId)
           ├─ container[`lp_${providerId}`]
           └─ [エラー時] AwilixResolutionError → カスタムエラー
```

### データフロー図

```
[入力]                    [処理]                         [出力]

execute(keys, job)
    │
    ▼
プロバイダー決定
    │
    ▼
retrieveProviderRegistration()
    │
    ▼
ILockingProvider
    │
    ├──▶ acquire(keys) ────▶ [ロック取得]
    │                              │
    │                              ▼
    │                         [バックエンド]
    │                         (Redis等)
    │                              │
    │                              ▼
    ├──▶ job() ────────────▶ [ジョブ実行]
    │                              │
    │                              ▼
    └──▶ release(keys) ───▶ [ロック解放]
                                   │
                                   ▼
                              結果返却（T）
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| locking-module.ts | `packages/modules/locking/src/services/locking-module.ts` | ソース | ロッキングモジュールサービス |
| locking-provider.ts | `packages/modules/locking/src/services/locking-provider.ts` | ソース | プロバイダー管理サービス |
| types.ts | `packages/modules/locking/src/types.ts` | 型定義 | モジュール固有の型定義 |
| index.ts | `packages/core/types/src/locking/index.ts` | 型定義 | ILockingModuleインターフェース |
