本ドキュメントは、『beautyspot』の実装記録(ジャーナル)を保持する。
ソースコードの実装構造、設計判断の根拠、および技術的負債について記録する。
Spot.mark() メソッドが本仕様の中核。二段階デコレータファクトリとして、
オプションを受け取る外側の mark() と、関数をラップする内側の decorator() で構成される。
sync/async の判定は inspect.iscoroutinefunction() でデコレーション時に行い、
同期関数は _execute_sync()、非同期関数は _execute_async() に委譲する。
functools.wraps(fn) により元関数のメタデータ(__name__, __doc__, __module__, __qualname__)を保持する。
@mark() (括弧あり)と @mark(括弧なし)の両方をサポートするため、
引数の型で分岐するパターンを採用した。第一引数が callable なら括弧なし、
そうでなければ括弧ありと判定する。
デコレーション時に判定する方式を採用。呼び出し時に毎回 inspect する方式と比較し、
ランタイムオーバーヘッドがゼロになる利点がある。ただしデコレーション後に
関数の性質が動的に変わるケースには対応できない(実用上問題なし)。
inspect.isgeneratorfunction() と inspect.isasyncgenfunction() の
両方をチェックし、ConfigurationError を送出する。
ジェネレータの戻り値はイテレータであり、キャッシュの意味論と矛盾するため。
inspect.signature の保持は functools.wraps が設定する __wrapped__ 属性に依存する_resolve_settings() で Spot レベルのデフォルト → 関数レベルのオーバーライドの順で行われるkeygen パラメータが指定された場合、KeyGenPolicy.bind(fn) でシグネチャにバインドされたキー生成関数に変換される@mark を2回付ける)時の検出・警告が未実装Spot.cached_run() はコンテキストマネージャとして実装。
渡された関数群を内部で mark() と同等のラッピングを行い、
コンテキスト内で呼び出すとキャッシュが効く一時的なラッパーを返す。
単一関数の場合は結果を直接返し、複数関数の場合はタプルで返す。
これにより wrapped_fn = spot.cached_run(fn) のように自然に書ける。
0個の場合は ValidationError で早期失敗させる。
@contextmanager デコレータを使用。__enter__ でラップ済み関数を返し、
__exit__ で後処理(drain 等)は with spot: に委譲する設計。
@overload を使用しているfunctools.wraps が元関数に適用されるSpot._execute_sync() と Spot._execute_async() が実行エンジンの中核。
両メソッドは同一のフロー(キー生成→キャッシュ検索→Herd待機→関数実行→保存)を
それぞれ同期/非同期のセマンティクスで実装する。
mark() がデコレーション時に inspect.iscoroutinefunction() で判定し、
適切な方を選択する。
_execute_sync と _execute_async は処理フローが同一だが、
await の有無やロック機構(threading.Event vs asyncio.Future)が異なるため、
共通化せず並行して実装している。DRY 原則よりも明瞭性・デバッグ容易性を優先した。
Herd protection の結果ボックスへの結果格納は、DB/Blob 保存の前に行う。 これにより、保存が失敗しても待機中のスレッド/タスクは結果を受け取れる。 「保存の失敗で実行結果が失われる」ことを防ぐ意図的な設計。
save_sync=False 時の保存エラーは on_background_error コールバックに
通知し、ERROR ログを出力するが、例外を再送出しない。
関数の実行自体は成功しているため、ユーザーに例外を見せるのは不適切と判断した。
pre_execute, on_cache_hit, on_cache_miss)は
try/except で囲み、フック内の例外が実行結果に影響しないようにしている_eviction_guard_lock で非ブロッキングにガードし、
並行エビクションの重複実行を防止している_execute_async 内の保存処理は _BackgroundLoop.submit() でコルーチンとして投入されるKeyGen クラスの静的メソッド群がキャッシュキー生成を担う。
_default(args, kwargs) が主要なエントリポイントで、引数を
canonicalize() で正規化 → msgpack でシリアライズ → SHA-256 でハッシュする。
最終キーは func_identifier:input_id[:version] 形式の文字列を SHA-256 した値。
MD5 より衝突耐性が高く、Python 標準ライブラリの hashlib で利用可能。
キャッシュキーとして64文字の hex 文字列は十分にコンパクトで、
DB のインデックスとしても効率的。
正規化結果を直接 str() でハッシュする方式と比較し、
msgpack はバイナリ表現が安定しており、Python バージョン間での
repr() の差異に影響されない。
from_file_content() はファイルを 65KB チャンクで読み取り、
拡張子をハッシュに含める。大きなファイルでもメモリ効率が良い。
from_path_stat() は mtime + size のみでハッシュし、
ファイル内容の読み取りを避ける高速版。ファイル不在時は
"MISSING_{filepath}" を返し、不在自体をキーに反映する。
hash_items() は汎用リストハッシュ。内部でも _default でも使われるcanonicalize() は functools.singledispatch で実装された再帰的正規化関数。
型ごとにディスパッチハンドラを登録し、ネストされたデータ構造を再帰的に正規化する。
各ハンドラは型タグ付きのタプルを返し、異なるコレクション型が同じ内容でも
区別されるようにする。
if/elif チェーンと比較して、新しい型のサポート追加が局所的で、 既存コードへの影響がない。numpy や Pydantic のようなオプショナル依存の 型ハンドラも、条件付きで登録できる。
[1, 2](list)と (1, 2)(tuple)を区別するため、
正規化結果に ("__list__", ...), ("__tuple__", ...) のように
型タグを含める。これにより、構造的に同一でも型が異なるデータが
異なるキャッシュキーを生成する。
Python では True == 1 かつ isinstance(True, int) だが、
キャッシュキーとしては区別が必要。singledispatch は MRO 順でマッチするため、
bool を int より先にチェックするインライン処理を base handler に配置した。
__dict__ と __slots__ の両方を MRO 走査で収集する。
__dict__ スロット自体はスキップするOrderedDict は順序が意味を持つため、キーのソートを行わない(dict とは異なる)defaultdict は default_factory を無視し、内容のみで正規化するEnum は 型名.メンバー名 + value で正規化し、モジュール・qualname も含めるstr() にフォールバックし、警告を出力する(非決定的)str() にフォールバックするが、安定性は保証されないStrategy enum(DEFAULT, IGNORE, FILE_CONTENT, PATH_STAT の4種)と
KeyGenPolicy クラスで構成。KeyGenPolicy は引数名→戦略のマッピングを保持し、
bind(func) で inspect.signature() を使って関数シグネチャにバインドされた
キー生成関数を返す。ファクトリメソッド KeyGen.ignore(), KeyGen.map(),
KeyGen.file_content(), KeyGen.path_stat() で宣言的にポリシーを構築できる。
デコレーション時に inspect.signature(func) を呼び、
位置引数・キーワード引数・デフォルト値を解決する。
これにより、呼び出し時にはシグネチャ解析のオーバーヘッドがなくなる。
引数ごとの戦略を enum で宣言する方式を採用。
関数を渡す方式(keygen=lambda ...)と比較して、
シリアライズ可能で、デバッグ時の可読性が高い。
bind() 内で RecursionError を catch し、警告を出力してフォールバックするFILE_CONTENT 戦略は KeyGen.from_file_content() に委譲し、65KB チャンク読み取りを行うPATH_STAT 戦略は KeyGen.from_path_stat() に委譲し、mtime + size でハッシュするIGNORE 戦略の引数はキー計算から完全に除外される(値に関係なくキャッシュヒット)SQLiteTaskDB クラスが中核。Writer Thread + Reader Threads パターンで実装。
単一の _writer_thread(デーモン)が _write_queue から書き込みタスクを取り出して
直列処理し、読み取りは threading.local() のスレッドローカル接続で並行実行する。
WAL モードにより読み取りと書き込みが並行可能。
SQLite は単一書き込み接続しかサポートしないため、全書き込みを1つのスレッドに 集約する Producer-Consumer パターンを採用。キュー経由でタスクを受け渡し、 ライタースレッドがコミット/ロールバックを管理する。 マルチスレッドからの並行書き込みによるロック競合を根本的に回避する。
threading.local() に _ReadConnWrapper を格納し、スレッドごとに
読み取り専用接続(PRAGMA query_only = ON)を保持する。
接続エラー時は自動的に再作成し、リーク防止のため WeakSet で全接続を追跡する。
書き込みタスクを PENDING → RUNNING → DONE(または CANCELLED)の
状態マシンで管理。タイムアウト時の try_cancel() により、
キュー内で待機中のタスクを安全にキャンセルできる。
既に RUNNING 状態のタスクはキャンセルせず完了を待つ。
PRAGMA table_info で既存カラムを確認し、
不足カラム(content_type, version, result_data, func_identifier, expires_at)を追加するget() 時に expires_at < NOW を確認し、期限切れなら None を返す。
物理削除は beautyspot gc コマンドに委譲するflush() は空の no-op タスクをキューに投入し、その完了を待つことで
先行する全書き込みの完了を保証するshutdown() は全読み取り接続のクローズ → _STOP センチネルの投入 →
ライタースレッドの join で安全に停止する_read_connect() のダブルチェックパターンで TOCTOU 競合を回避しているDB 層のプロトコル階層を定義。TaskDBCore(ランタイム必須の CRUD)、
Flushable(書き込み同期)、Shutdownable(安全な停止)、
Maintenable(GC・統計等の管理操作)の4つのプロトコルと、
それらを統合した TaskDBMaintenable を提供する。
TaskDBBase は ABC として Maintenable のデフォルト実装(安全な no-op)を提供する。
単一の巨大インターフェースではなく、責務ごとにプロトコルを分割した。
これにより、カスタム DB 実装は TaskDBCore のみ実装すればランタイムで動作し、
メンテナンス機能は段階的にオプトインできる。
TaskDBBase のメンテナンスメソッド(delete_expired(), prune() 等)は
デフォルトで空リスト返却や no-op を行い、サブクラスが未実装でも安全に動作する。
CLI の gc コマンドが未対応の DB バックエンドでもエラーにならない設計。
Flushable.flush(timeout) のデフォルトは no-op。SQLiteTaskDB のみがキュー同期を実装Shutdownable.shutdown(wait) のデフォルトは no-op。リソースを持たない軽量実装に配慮isinstance(db, Maintenable) で機能の有無を判定できる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 自体は保持する
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 判定用)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 を使用。
ユーザーコードの警告フィルタに影響されないためMsgpackSerializer クラスが SerializerProtocol と TypeRegistryProtocol を実装。
dumps(obj) -> bytes / loads(data) -> Any が主要 API。
カスタム型は msgpack の ExtType(code, payload) で拡張する。
_default_packer() がエンコード時のカスタム型ディスパッチ、
_ext_hook() がデコード時の型復元を行う。
_encoders / _decoders 辞書は register() 時に新しい辞書を作成して
アトミックに差し替える(CoW)。読み取り側はスナップショットを参照するため、
ロックなしで安全に読める。GIL に依存せず、PEP 703(free-threading)にも
対応できる設計。世代カウンタ _registry_generation をレジストリ差し替えの前に
インクリメントすることで、読み取り側が古いキャッシュを使い続けることを防ぐ。
MRO スキャンの結果(サブクラス→登録済み親クラスの解決)を threading.local() の
OrderedDict にキャッシュする。_cache_generation と _registry_generation を比較し、
世代が変わったらキャッシュを破棄する。LRU のサイズ上限は max_cache_size で制御。
Pydantic モデルのサブクラスのように、親クラスが登録済みなら
サブクラスも自動的に同じエンコーダで処理できる。type(obj).__mro__ を走査し、
最初にマッチした登録型のエンコーダを使用する。
register() の code は 0-127(msgpack ExtType の制約)ConfigurationError で拒否_ext_hook 内のデシリアライズは再帰的に msgpack.unpackb(ext_hook=self._ext_hook) を
呼ぶことで、ネストされたカスタム型も復元できるSerializationError のメッセージには未登録型のヒント(spot.register() の使い方)を含める2つのレイヤーで構成:
- MsgpackSerializer.register(type, code, encoder, decoder): 低レベルの型登録 API
- Spot.register() / Spot.register_type(): ユーザー向けの高レベル API
Spot.register() はデコレータ形式、Spot.register_type() は命令形式で、
いずれも内部で serializer.register() に委譲する(TypeRegistryProtocol 準拠時のみ)。
デコレータ形式 @spot.register(code=1, ...) はクラス定義と同時に登録でき、
宣言的で読みやすい。命令形式 spot.register_type(MyClass, code=1, ...) は
外部ライブラリのクラスなどデコレートできない場合に使用する。
シリアライザが TypeRegistryProtocol を実装していない場合(カスタムシリアライザ)、
register() / register_type() は ConfigurationError を送出する。
これにより、型登録非対応のシリアライザを使用する場合のエラーメッセージが明確になる。
register() の内部で _write_lock を取得してから CoW で辞書を差し替えるmodel_validate(data) を使用し、
バリデーション付きの復元が可能_BackgroundLoop クラスが非同期タスクのバックグラウンド実行を管理する。
初期化時に asyncio.new_event_loop() で新しいイベントループを作成し、
_thread(デーモンスレッド)上で loop.run_forever() を実行する。
外部からは submit() を通じてコルーチンを投入でき、run_coroutine_threadsafe で
スレッドセーフにループへ渡される。
スレッドは daemon=True とし、メインスレッド終了時にプロセスがブロックされないようにしている。
しかし、安全なリソース解放のために drain() メソッドを提供し、
投入された全タスクの完了を drain_timeout の範囲で待機する。
メインスレッドのイベントループと競合しないよう、専用のイベントループを
作成してバックグラウンドスレッドで駆動する。これにより save_sync=False 時の
保存処理などが、呼び出し元の asyncio 環境から完全に隔離される。
submit() は投入されたタスクを _tasks 集合に登録し、add_done_callback で
完了時に自身を削除することで、未完了タスクの追跡を可能にしている_lock による排他制御を行っているatexit ハンドラとしてもシャットダウンが登録されるSpot クラスにおいて、バックグラウンド書き込みの同期と完了待機を制御する。
save_sync=False の場合、キャッシュへの保存処理は _bg_loop.submit() で
バックグラウンドに投入される。これら未完了のタスクやDBキューを同期するため、
flush() およびコンテキストマネージャによる drain が実装されている。
flush(): DBライタースレッドのキュー (self.db.flush()) を空になるまで待機する__exit__: コンテキストマネージャ終了時に flush() を呼び出した上で、
さらに _bg_loop.drain() を呼び出し、非同期タスクの完了も待つ
これにより、スクリプト終了時やバッチ処理の区切りで、データの完全な永続化を保証する。バックグラウンド保存時のエラーは呼び出し元のメインフローを妨げないよう、
on_background_error コールバックに通知され、ログ出力のみ行う。
例外の再送出は行わない。
flush() にはタイムアウトを設定でき、ハングアップを防止しているon_background_error は SaveErrorContext を引数に取り、失敗時の
キーや関数の詳細情報を提供するLifecyclePolicy クラスは Rule オブジェクトのリストを保持し、
関数名に基づいてキャッシュの保持期間を決定する。
resolve() メソッドが fnmatch.fnmatch() を使って関数名をパターンと照合し、
最初にマッチしたルールの保持期間を返す。
ルールのリストは順序が意味を持ち、最初にマッチしたものが採用される。
これにより、特定プレフィックスの関数には短い保持期間を設定し、
最後に *(ワイルドカード)でデフォルトポリシーを設定するような
フォールバック構造が簡単に記述できる。
後方互換性と柔軟性のため、まず func_identifier (モジュール名付きの完全修飾名) で
マッチングを試み、マッチしなかった場合は func_name (短い関数名) で再度マッチングを
試みる resolve_with_fallback() を提供している。
LifecyclePolicy コンストラクタに default_retention パラメータを追加。
どのルールにもマッチしない場合に返す保持期間を指定できる。
LifecyclePolicy.default() は default_retention="30d" で生成し、
デフォルトで30日の保持期間を設定する。
LifecyclePolicy.default() は空のルールリスト + default_retention="30d" を持つdefault_retention=None を指定すれば従来通り無期限保持となるRetention クラスを名前空間として使用し、保持期間のパースや
特殊な定数(INDEFINITE, FOREVER)を管理する。
parse_retention() 関数は文字列("7d", "12h"等)、timedelta、
または秒数(int/float)を受け取り、標準化された timedelta オブジェクトに変換する。
Retention.FOREVER はポリシーを強制的にバイパスするための特殊値。
PEP 703 (free-threading) 環境での安全性を考慮し、
_ForeverSentinel は threading.Lock を用いたダブルチェックロッキングで
厳密なシングルトンとして実装されている。
"7d", "12h", "30m", "10s" のような文字列フォーマットを
正規表現 _TIME_PATTERN でパースすることで、設定ファイルや
ハードコード時の可読性を高めている。
parse_retention() は無効なフォーマットや負の値に対して ValidationError を送出する_ForeverSentinel は __bool__() で True を返すようオーバーライドされているLimiterProtocol を実装する TokenBucket クラス。
GCRA (Generic Cell Rate Algorithm) に基づき、スレッドセーフおよび
非同期対応のスムーズなレートリミッタを提供する。
内部状態として Theoretical Arrival Time (TAT) を保持し、
consume() で同期的スリープ、consume_async() で非同期的スリープを行う。
伝統的なトークンバケットとは異なり、長時間アイドル後に 一気にバーストを許容しない「Strict Pacing」を実現できる。 TATの更新と現在時刻の比較だけで待機時間を計算できるため、 メモリ効率と計算効率に優れる。
時刻の取得に time.monotonic() を使用し、システム時刻の変更(NTP同期など)の
影響を受けない堅牢な設計としている。
_consume_reservation() メソッドが待機時間の計算と TAT 更新を
threading.Lock でアトミックに行うcost > max_cost(バケット容量超過)の場合は、どれだけ待っても
処理できないため即座に ValueError を送出するtime.sleep()、非同期パスは asyncio.sleep() で待機するタスク実行ライフサイクルに介入するインターフェース HookBase と、
そのスレッドセーフ版 ThreadSafeHookBase の実装。
pre_execute, on_cache_hit, on_cache_miss の各コールバックが提供される。
ThreadSafeHookBase は __init_subclass__ を利用し、サブクラスで定義された
フックメソッドを自動的にロックでラップする。
ユーザーが手動でロックを書く手間を省くため、メタクラス的なアプローチを採用。
クラス定義時にフックメソッドを抽出し、_wrap_with_lock デコレータで包むことで、
ユーザーコードを汚さずに完全な排他制御を実現している。
当初の Lock から RLock に変更された。
サブクラスが super().pre_execute(...) のように親のメソッドを呼び出した際、
同一スレッドが再度ロックを取得しようとしてデッドロックする問題を防ぐため。
_wrap_with_lock は functools.wraps を使い、元のメタデータを保持するThreadSafeHookBase は __getattr__ をオーバーライドし、
super().__init__() の呼び出し忘れに対して親切なエラーメッセージを返すMaintenanceService クラスがDBとBlobストレージ間の整合性チェック、
期限切れキャッシュの削除、孤立ファイルの検出といったガベージコレクション(GC)を担当。
主にCLIの beautyspot gc コマンドから呼び出される。
対象となる DB (TaskDBMaintenable) とストレージ (BlobStorageMaintenable) を受け取り、
複数のフェーズに分けてクリーンアップを実行する。
clean_garbage() は以下の順序で安全にGCを実行する:
1. 期限切れDBレコードの削除
2. Blobストレージの一時ファイル(.spot_tmp)のクリーンアップ
3. 孤立したBlobファイル(DBに存在しないファイル)の特定
4. 孤立ファイルの削除
5. 空ディレクトリの剪定
実行中のタスクがBlobを書き込んだ直後で、まだDBにメタデータが保存されていない
タイミングでGCが走ると、必要なファイルが孤立と誤認されるリスクがある。
これを防ぐため、orphan_grace_seconds(デフォルト60秒)より新しいファイルは
孤立判定から除外する設計とした。
scan_garbage() は、ストレージの全キーを列挙し、
DBの get_blob_keys() との差分を取ることで行うscan_orphan_projects() も提供されるTyper を利用したコマンドラインインターフェースの実装。
list, show, stats, clear, clean, gc, prune, version 等の
コマンドを提供し、rich ライブラリを用いてコンソール出力(テーブル、パネル、
プログレスバー等)をリッチにフォーマットしている。
Typer は型ヒントベースでコマンドやオプションを定義でき、コードの記述量と メンテナンスコストを大幅に削減できる。Rich による出力は、単なるテキストではなく 構造化された情報(JSONやMarkdown)の視認性を劇的に向上させ、DXを高める。
clear や clean など、データを大規模に削除するコマンドについては、
--force オプションが指定されない限り、rich.prompt.Confirm を用いて
ユーザーに明示的な確認を求めることで、誤操作によるデータ喪失を防いでいる。
MaintenanceService などの内部 API を利用して処理を行う--project / -p) に対して操作を行うが、
一部のコマンド(list 等)はワークスペース全体の走査もサポートするbeautyspot パッケージのメインエントリポイントとなる Spot ファクトリ関数の実装。
引数として渡された各コンポーネント(DB、Serializer、Storage等)を依存性注入(DI)で
解決し、デフォルトのコンポーネント(SQLiteTaskDB, MsgpackSerializer, LocalStorage等)を
インスタンス化して CacheManager と _Spot コアエンジンを組み立てる。
_Spot クラス自体のコンストラクタは複雑な依存関係を要求するが、
ユーザー向けに Spot() 関数を提供することで、通常は name を渡すだけで
「ゼロ設定」で動作するようにカプセル化している。
同時に、高度なユーザーは各コンポーネントを自由に差し替え可能な DI アーキテクチャを維持している。
Spot() 関数内でデフォルトのDB(SQLiteTaskDB)を自動生成した場合、
spot._owns_db = True フラグを立て、Spotエンジンのシャットダウン時に
DBも自動でクローズされるようにする。一方、ユーザーが明示的に db= を
渡した場合は、DBのライフサイクル管理は呼び出し元に委ねる(勝手に閉じない)。
save_blob フラグや引数によって WarningOnlyPolicy,
AlwaysBlobPolicy 等に解決される__all__ に各種プロトコルやデフォルト実装、例外クラスをエクスポートし、
ライブラリとしての公開API境界を明確に定義しているCacheManager クラス内で、Thundering Herd(キャッシュミス時に同一キーへの
大量アクセスが同時に発生する問題)を防止する直列化機構を実装。
_inflight 辞書で実行中のキーを管理し、最初の1スレッド/タスクだけが関数を実行し、
後続の呼び出しは wait_herd_sync / wait_herd_async でその完了を待機する。
_inflight 辞書の値として (threading.Event, list[asyncio.Future], list[result]) の
タプルを保持し、スレッドベースの待機 (Event.wait) と asyncio ベースの待機 (Future) の
両方を単一の管理下で混在できるようにしている。
関数の実行がハングアップした場合に待機側が永遠にブロックされるのを防ぐため、
HERD_TIMEOUT(300秒)と HERD_MAX_RETRIES(3回)を設定。
タイムアウト時には警告ログを出しつつ再試行し、上限を超えれば TimeoutError で
フェイルファストする堅牢な設計とした。
notify_and_cleanup_inflight() で待機タスクへの結果伝播と _inflight からの削除を
_inflight_lock によりアトミックに行うresult_box(要素数1のリスト)をリファレンス渡しで共有することで、
Event が set された際に安全に結果を取得できるようにしている_notify_future() は call_soon_threadsafe を使い、非同期Futureに対して
別スレッドから安全に結果(または例外)をセットする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 すべきことを規定