✓=レビュー済 ○=未レビュー ⚠=Suspect(複数同時表示あり。IDクリックで詳細へ)
| グループ | REQ | ARCH | SPEC | TST | IMPL |
|---|---|---|---|---|---|
| STORE | REQ004 ✓ 大きなキャッシュデータを外部ストレージ(ローカルファイルシステムやS3等)に保存できること。 DB直接保存とBlob保存を自動判定できること。 | ARCH004 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|判定| B[StoragePolicyProtocol... | SPEC009 ✓ ## インターフェース ```python class LocalStorage(BlobStorageMaintenable): def __ini... | TST009 ✓ ## 目的 `LocalStorage` はBlobデータのファイルシステム永続化を担い、並行書き込み時のデータ破損やパストラバーサルによるセキュリティ脆弱性... | IMPL009 ✓ ## 実装概要 - `LocalStorage` クラスが `BlobStorageBase` を実装。 ファイルは `{base_dir}/{key}.... |
| STORE | REQ004 ✓ 大きなキャッシュデータを外部ストレージ(ローカルファイルシステムやS3等)に保存できること。 DB直接保存とBlob保存を自動判定できること。 | ARCH004 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|判定| B[StoragePolicyProtocol... | SPEC010 ✓ ## インターフェース ```python class S3Storage(BlobStorageMaintenable): def __init__... | TST010 ✓ ## 目的 `S3Storage` はクラウド環境での大規模Blob保存を担い、ネットワーク障害・認証エラー・バケット不在などオンプレミスにはない障害モードが... | IMPL010 ✓ ## 実装概要 `S3Storage` クラスが `BlobStorageBase` を実装。 `s3://bucket/prefix/` 形式の URI を... |
| STORE | REQ004 ✓ 大きなキャッシュデータを外部ストレージ(ローカルファイルシステムやS3等)に保存できること。 DB直接保存とBlob保存を自動判定できること。 | ARCH004 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|判定| B[StoragePolicyProtocol... | SPEC011 ✓ ## インターフェース ```python class StoragePolicyProtocol(Protocol): def should_sav... | TST011 ✓ ## 目的 ストレージポリシーは「データをDB直接保存するか、Blobストレージに分離するか」を決定する戦略レイヤーである。閾値判定の誤りはDBの肥大化(性能... | IMPL011 ✓ ## 実装概要 `StoragePolicyProtocol` が `should_save_as_blob(data: bytes) -> bool` を定... |
| STORE | REQ004 ✓ 大きなキャッシュデータを外部ストレージ(ローカルファイルシステムやS3等)に保存できること。 DB直接保存とBlob保存を自動判定できること。 | ARCH004 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|判定| B[StoragePolicyProtocol... | SPEC024 ✓ ## インターフェース ```python class BlobStorageBase(ABC): @abstractmethod def s... | TST024 ✓ ## 目的 `BlobStorageBase` は全ストレージバックエンド(LocalStorage, S3Storage, サードパーティ)が 準拠すべき抽... | IMPL024 ✓ ## 実装概要 `storage.py` 内の `BlobStorageBase` ABC が SPEC024 の中核実装。 5つの抽象メソッド(`save`... |
| リンク方向 | カバー数 | カバー率 | 未カバー |
|---|---|---|---|
| ARCH → REQ | 1 / 1 | 100.0% | — |
| SPEC → ARCH | 1 / 1 | 100.0% | — |
| TST → SPEC | 4 / 4 | 100.0% | — |
| IMPL → SPEC | 4 / 4 | 100.0% | — |
大きなキャッシュデータを外部ストレージ(ローカルファイルシステムやS3等)に保存できること。
DB直接保存とBlob保存を自動判定できること。
親: —
子: ARCH004
graph TD
A[core.Spot] -->|判定| B[StoragePolicyProtocol];
A -->|保存/読込| C[BlobStorageBase];
D[LocalStorage] --> C;
E[S3Storage] --> C;
A -->|メタデータ| F[TaskDBBase];
| コンポーネント | 責務 | インターフェース |
|---|---|---|
| StoragePolicyProtocol | データのサイズ等に基づき、DB保存かBlob保存かを判定する | should_save_as_blob() |
| BlobStorageBase | 大規模データの外部ストレージ保存を抽象化する | save(), load(), delete() |
| LocalStorage | ローカルファイルシステムへのアトミックな保存と検証 | base_dir, os.replace |
| S3Storage | AWS S3互換オブジェクトストレージへの保存 | s3:// URI, boto3 |
sequenceDiagram
participant Spot as core.Spot
participant Policy as StoragePolicy
participant DB as TaskDB
participant Blob as BlobStorage
Spot->>Policy: should_save_as_blob(data)
alt Policy: True (Blob保存)
Spot->>Blob: save(key, data)
Blob-->>Spot: location (path/URI)
Spot->>DB: insert(metadata, storage_type=FILE, blob_key=location)
else Policy: False (Inline保存)
Spot->>DB: insert(metadata, storage_type=DIRECT_BLOB, result_data=data)
end
| 技術領域 | 選定 | 理由 |
|---|---|---|
| 保存判定 | 閾値ベース (Threshold) | DBの肥大化を防ぎつつ、小容量データのI/Oを最適化 |
| アトミック性 | 一時ファイル + Rename | 保存中のクラッシュや並行書き込みによる破損を完全に防止 |
| クラウド対応 | Boto3 (S3) | 業界標準のプロトコルによる高い互換性とスケーラビリティ |
安全性: LocalStorage では is_relative_to によるパストラバーサル防止を徹底
信頼性: 保存時は fsync を実行し、OSレベルでのデータ到達を確認
効率性: S3 保存時は upload_fileobj を使用し、5GB超のマルチパートアップロードに自動対応
親: REQ004
子: SPEC009, SPEC010, SPEC011, SPEC024
class LocalStorage(BlobStorageMaintenable):
def __init__(self, base_dir: str | Path): ...
def save(self, key: str, data: bytes) -> str: ...
def load(self, location: str) -> bytes: ...
save)tempfile.mkstemp で一時ファイルを作成flush() および os.fsync() でディスク到達を保証os.replace で最終的なパスへリネーム(アトミックな置換)load / delete)base_dir の配下にあることを is_relative_to で厳密にチェックし、範囲外へのアクセスを遮断する。| パラメータ | 説明 | 制限 |
|---|---|---|
key |
保存ファイル名のベース | .. や / を含む場合は ValidationError |
location |
save が返した識別子 |
相対パス形式を推奨 |
load 時にファイルがない場合 CacheCorruptedError を送出PermissionError 発生時は、GCでの回収に委ねるため警告ログのみ出力OSError を送出/ と \ の両方を検証対象とする親: ARCH004
class S3Storage(BlobStorageMaintenable):
def __init__(self, s3_uri: str, s3_opts: dict | None = None): ...
s3://bucket/prefix/key.bin 形式のURIをロケーション識別子として使用する。save)boto3.upload_fileobj を使用。5GBを超えるデータに対しては、自動的にマルチパートアップロードに切り替えられ、PutObjectの制限を回避する。get_mtime)head_object を使用し、データ本体をダウンロードせずに最終更新時刻(LastModified)を取得する。| 設定 | 内容 |
|---|---|
s3_uri |
s3://my-bucket/my-prefix 形式 |
s3_opts |
boto3.client('s3', **s3_opts) に渡される設定 |
boto3 がインポートできない環境でインスタンス化を試みると ImportError を送出botocore.exceptions.ClientError をキャッチし、状況に応じて CacheCorruptedError に変換親: ARCH004
class StoragePolicyProtocol(Protocol):
def should_save_as_blob(self, data: bytes) -> bool: ...
len(data) が threshold を超えた場合に True を返す。False を返すが、閾値を超えた場合にログ出力のみ行う(互換モード)。True を返す。| ポリシー型 | パラメータ | デフォルト | 説明 |
|---|---|---|---|
Threshold |
threshold |
なし | バイト単位の閾値。10MB等を推奨 |
Warning |
warning_threshold |
なし | 警告を出すサイズ閾値 |
False (DB保存) とみなし、エラーをログに記録する。len(data) > threshold のため、閾値と等しい場合はDB保存となる。親: ARCH004
class BlobStorageBase(ABC):
@abstractmethod
def save(self, key: str, data: ReadableBuffer) -> str: ...
@abstractmethod
def load(self, location: str) -> bytes: ...
@abstractmethod
def delete(self, location: str) -> None: ...
@abstractmethod
def list_keys(self) -> Iterator[str]: ...
@abstractmethod
def get_mtime(self, location: str) -> float: ...
| パラメータ | 型 | 説明 |
|---|---|---|
key |
str |
キャッシュキー(通常はハッシュ値) |
data |
bytes / memoryview |
シリアライズ済みバイナリ |
location |
str |
実体へのポインタ(ファイルパス、S3 URI等) |
CacheCorruptedError を送出すべきである。ReadableBuffer を受け入れることで、memoryview 等を使用したゼロコピー書き込みを可能にする。親: ARCH004
LocalStorage はBlobデータのファイルシステム永続化を担い、並行書き込み時のデータ破損やパストラバーサルによるセキュリティ脆弱性が発生しうる。アトミック書き込みとセキュリティバリデーションの正確性を保証する。
base_dir が存在しない場合に自動作成され、.gitignore が配置されることtempfile.mkstemp → fsync → os.replace のフローで、書き込み中のクラッシュや並行アクセスでファイルが破損しないこと.. や / を含む場合に ValidationError が送出され、base_dir 外へのアクセスが不可能であること。load() でも is_relative_to 検証が機能することclean_temp_files() が猶予期間超過の .spot_tmp ファイルを削除し、猶予期間内のファイルは保持することdelete() が存在しないキーに対してもエラーを送出しないことreferences: tests/integration/storage/test_local.py
親: SPEC009
子: —
S3Storage はクラウド環境での大規模Blob保存を担い、ネットワーク障害・認証エラー・バケット不在などオンプレミスにはない障害モードが存在する。boto3 オプショナル依存のガード処理も含め、クラウドストレージ統合の信頼性を検証する。
save / load / delete / list_keys が正しく動作することimport 時にわかりやすいエラーメッセージが表示されることreferences: tests/integration/storage/test_s3.py
親: SPEC010
子: —
ストレージポリシーは「データをDB直接保存するか、Blobストレージに分離するか」を決定する戦略レイヤーである。閾値判定の誤りはDBの肥大化(性能劣化)や不要なBlob分離(オーバーヘッド増加)を招く。3種のポリシー実装がそれぞれの契約を正しく満たすことを検証する。
False、閾値以上のデータで True を返すこと。境界値(ちょうど閾値サイズ)での挙動が明確であることFalse を返しDB直接保存を選択すること。ただし閾値超過時に WARNING レベルのログが出力されることTrue を返すこと@spot.mark(save_blob=True/False) の明示指定がポリシー判定より優先されること。save_blob=None(デフォルト)時にポリシーの should_save_as_blob() が呼ばれることreferences: tests/unit/test_storage_policy.py
親: SPEC011
子: —
BlobStorageBase は全ストレージバックエンド(LocalStorage, S3Storage, サードパーティ)が
準拠すべき抽象契約を定義する。この契約が正しく機能しないと、キャッシュデータの
保存・復元・削除・メンテナンス(GC)の全てが破綻する。
LocalStorage を具象実装として使い、インターフェース契約を網羅的に検証する。
bytes, bytearray, memoryview の全てを受け入れること(ゼロコピー書き込み)b"") を正常に保存・復元できることCacheCorruptedError を送出することreferences: tests/integration/storage/test_blob_storage_base.py
親: SPEC024
子: —
LocalStorage クラスが BlobStorageBase を実装。 ファイルは {base_dir}/{key}.bin 形式で保存される。
save() は tempfile.mkstemp() → fsync() → os.replace() の アトミック書き込みパターンで実装。
一時ファイルに書き込み → fsync でディスクに確実に反映 → os.replace で アトミックにリネーム。この3段階により、書き込み中のクラッシュや 並行アクセスでファイルが半壊状態になることを防ぐ。 一時ファイルには .spot_tmp 接尾辞を使用し、残存時のクリーンアップを容易にした。
save() / _validate_key(): キーに .., /, \ を含む場合に ValidationError
load() / delete(): 解決済みパスが base_dir.is_relative_to() を満たすか検証 二重のチェックにより、キー経由とロケーション経由の両方のパストラバーサルを防ぐ。
初期化時に base_dir を絶対パスに正規化し、ディレクトリと .gitignore を自動作成
delete() は冪等。ファイル不在時もエラーを送出しない。パーミッションエラーは警告のみ
list_keys() は rglob("*.bin") でレガシーサブディレクトリ構造にも対応
clean_temp_files() は猶予期間(デフォルト24時間)超過の .spot_tmp ファイルのみ削除。 ファイルロック(アンチウイルス等)のエラーは安全に無視する
prune_empty_dirs() はボトムアップで走査し、.DS_Store 等のシステムファイルのみの ディレクトリも空とみなして削除する。base_dir 自体は保持する
references: src/beautyspot/storage.py
親: SPEC009
子: —
S3Storage クラスが BlobStorageBase を実装。
s3://bucket/prefix/ 形式の URI を解析し、boto3 の S3 クライアントを使用。
ファイルは {prefix}/{key}.bin のキーで S3 に保存される。
boto3 は import 時にガードし、未インストール時は
クラス定義は成功するがインスタンス化で明確なエラーメッセージを表示する。
beautyspot 全体が boto3 に依存しないよう、遅延インポートパターンを採用。
s3://bucket/prefix 形式の URI から bucket と prefix を自動解析する
_parse_s3_uri() ヘルパーにより、ファクトリ関数がスキームに基づいて
LocalStorage と S3Storage を透過的に切り替えられる。
save() は upload_fileobj() を使用し、5GB 超のデータにも
マルチパートアップロードで対応するs3://bucket/prefix/key.bin)を返すlist_keys() は paginator で全キーを列挙し、prefix を除いた相対キーを返すget_mtime() は head_object で LastModified を取得(GC の grace period 判定用)references: src/beautyspot/storage.py
親: SPEC010
子: —
StoragePolicyProtocol が should_save_as_blob(data: bytes) -> bool を定義。
3つの実装を提供:
- ThresholdStoragePolicy: len(data) > threshold で判定
- WarningOnlyPolicy: 常に False を返し、閾値超過時に WARNING ログを出力
- AlwaysBlobPolicy: 常に True を返す
保存先判定ロジックを core.Spot から分離し、差し替え可能なポリシーオブジェクトに
委譲する。@mark(save_blob=True/False) の明示指定はポリシーより優先されるが、
save_blob=None(デフォルト)時にポリシーが判定を行う。
v2.0 との後方互換性を維持するため。v2.0 では全データが DB 直接保存だったため、
デフォルトを ThresholdStoragePolicy にすると既存ユーザーの挙動が変わる。
WARNING ログにより、ユーザーに Blob 分離の恩恵を段階的に周知する。
threshold パラメータを属性として保持WarningOnlyPolicy のログは warnings.warn ではなく logging.warning を使用。
ユーザーコードの警告フィルタに影響されないためreferences: src/beautyspot/storage.py
親: SPEC011
子: —
storage.py 内の BlobStorageBase ABC が SPEC024 の中核実装。
5つの抽象メソッド(save, load, delete, list_keys, get_mtime)を定義し、
LocalStorage と S3Storage が具象実装として継承する。
加えて、ランタイム型チェックのために BlobStorageCore・Maintenable・
BlobStorageMaintenable の3つの Protocol クラスを定義し、
isinstance() での型判定を可能にしている(@runtime_checkable)。
BlobStorageBase は ABC として抽象メソッドを強制する一方、
BlobStorageCore / Maintenable は Protocol として構造的部分型を提供する。
これにより、BlobStorageBase を継承しないサードパーティ実装でも
isinstance(obj, BlobStorageCore) で利用可能になる柔軟性を確保した。
BlobStorageCore: 実行時に必要な最小限(save/load/delete)Maintenable: メンテナンス(GC)に必要な拡張(list_keys/get_mtime)BlobStorageMaintenable: 両方を兼備する完全版これにより、save/load/delete のみを実装した軽量バックエンドも受け入れ可能。
bytes | bytearray | memoryview を ReadableBuffer として定義。
memoryview を受け入れることでゼロコピー書き込みが可能になり、
大きなデータを保存する際のメモリ効率を改善している。
BlobStorageBase 自体はロジックを持たず、インターフェース定義のみ@abstractmethod にはdocstringで契約を明記(冪等性、エラー時の挙動など)delete は冪等であるべきことを docstring で規定list_keys は delete と同じフォーマットの識別子を yield すべきことを規定references: src/beautyspot/storage.py
親: SPEC024
子: —