エンジン
エンジンの選び方
3つのエンジンはいずれも、UUIDv7のIDとデータ配列を保存しますが、ディスク上の配置は異なります。この配置によって、それぞれのエンジンの得意分野が決まります。
| DocStore | SegmentedLog | Queue | |
|---|---|---|---|
| ディスク上 | レコードごとに1つのJSONCファイル、UUID末尾でシャーディング | 長さとチェックサムを先頭に付けたJSON行を含む追記専用セグメントとマニフェスト | 追記専用のイベントログ1つ |
| 書き込み | レコードごとにアトミックなファイル置換 | チェックサム付きで追記。上書きと削除も新しいエントリーとして追記し、領域はcompact()で回収 |
キューイベントごとにチェックサム付きで追記 |
| 読み取り | IDでget()、インデックス付きクエリ |
カーソルおよび時間範囲のスキャン、状態インデックス経由のget() |
エンキュー順のclaim() |
| 得意分野 | 任意のコレクションサイズでのポイント読み取りとインデックス検索 | 約1Mレコードまでの追記中心のストリーム | プロセス間の永続的な作業ディスパッチ |
| 苦手分野 | 約10kレコードを超える、インデックスなしのフィールドスキャン | 頻繁な単一レコードの書き換え(古いバージョンは圧縮まで領域を占有) | 汎用レコードストアとしての利用。ジョブ完了後にペイロードは削除 |
DocStoreとSegmentedLogは、同じ流れるようなクエリAPIを共有します。同じwhere()チェーンをどちらのエンジンでも使え、異なるのは実行方法だけです。DocStoreはセカンダリインデックスを使って結果を返せますが、SegmentedLogはセグメントのスキャン中にフィルタリングします。Queueはクエリ可能ではなく、要求ベースです。ワーカーはジョブをフィルタリングする代わりに、次のジョブをclaim()します。
選び方の目安:
- 個別に取得、更新、削除し、フィールドで検索するレコード:
DocStore。フィルタリングするフィールドにはインデックスを追加してください。 - 主に追記し、順番または時間範囲で読み返すイベントやバージョン:
SegmentedLog。 - プロデューサーからワーカープロセスに渡すジョブ:
Queue。要求と完了は複数プロセス間で安全に処理され、claimMany()/completeMany()によってロックとfsyncのコストを分散できます。 - コレクション全体の結合、検索、レポート: SQLミラーを追加してください。ファイルが正規データとして残り、ミラーはSQLiteまたはMySQLで再構築できるプロジェクションです。
エンジンは組み合わせて使えます。共有するのはルートディレクトリだけなので、同じストレージルートの下で、コンテンツをDocStoreに、監査証跡をSegmentedLogに、バックグラウンド処理をQueueに保存できます。
DocStore
DocStoreは、永続レコードのポイント読み取りと単純なスキャンに適しています。レコードごとにJSONCファイルを1つ書き込みます。
$store = new Storh\DocStore($root, 'articles');
$record = $store->put(['slug' => 'hello', 'published' => true]);
$same = $store->get($record->id());
レコードファイルは、UUIDv7の書き込みが集中した際に複数ディレクトリへ分散するよう、UUID末尾の文字でシャーディングされます。
articles/data/2d/018bcfe5-6800-7000-8000-2d5dba6274da.jsonc
SegmentedLog
SegmentedLogは、追記中心のレコード、カーソル読み取り、時間範囲読み取りに適しています。レコードはアクティブセグメントに1行ずつ<byte-length>\t<xxh32>\t<json>の形式で書き込まれます(アトミック性を参照)。セグメントファイルの拡張子は.ndjsonですが、長さとチェックサムが先頭に付くため、通常のNDJSONツールでは読み取れません。stream()またはクエリビルダーでレコードを読み戻してください。満杯になったセグメントは、スパースインデックス付きの封印済みセグメントに切り替わります。
$events = new Storh\SegmentedLog($root, 'events');
$events->put(['type' => 'user.created']);
$events->compact();
キュー
Queueは、高スループットで永続性のあるワークキューに適しています。enqueue、claim、complete、requeue、purgeはqueue.logに追記専用イベントを書き込み、キューを開く際に状態がメモリへ再生されます。一括メソッドでは、多数のジョブ間でロックとフラッシュのコストを分散できます。イベントごとにログが増加し、stats()で現在のバイト数を確認できます。
$queue = new Storh\Queue($root, 'mail');
$queue->enqueue(['to' => 'person@example.test']);
$job = $queue->claim();
耐久性と並行処理
storhは対象ディレクトリ内でアトミックなrenameを使ってファイル書き込みを公開します。再オープンとrepair()では、所有プロセスが終了した放棄済みの一時ファイルを削除し、稼働中のライターの一時ファイルは残します。ライターマーカーによって確認は一定時間で完了するため、クラッシュ後に限りストアを開くときにコレクションを走査します。
SegmentedLogとQueueは、長さとチェックサムで保護されたログ行を書き込みます。再オープンまたは修復時には、途中で切れた末尾を最後にコミットされたイベントまで切り詰め、永続ログの内容からメモリ内の状態を再構築します。ログとキューへの書き込みは、ファイルシステムロックで直列化されます。
DocStoreの変更はコレクション単位の書き込みロックで直列化されるため、レコードファイルとセカンダリーインデックスは、プロセス間で一貫性を保つ単位として更新されます。同じファイルシステムのアトミックな名前変更が保証される前提では、異なるレコードIDへの同時書き込みは安全です。同じIDへの同時書き込みでは、最後に名前変更されたものが優先されます。
storhは、名前変更または追記の完了前にファイルハンドルをフラッシュしてfsyncし、アトミックな名前変更後には親ディレクトリのfsyncも試みます。電源障害時の耐久性は、使用するファイルシステムとマウントオプションがこれらの同期処理を尊重するかどうかに左右されます。