Engines
Eine Engine auswählen
Alle drei Engines speichern dieselben Daten: eine UUIDv7-ID und ein Daten-Array. Auf der Festplatte legen sie sie jedoch unterschiedlich ab. Von diesem Layout hängt ab, wofür sich die jeweilige Engine eignet:
| DocStore | SegmentedLog | Queue | |
|---|---|---|---|
| Auf der Festplatte | eine JSONC-Datei pro Datensatz, nach UUID-Endzeichen aufgeteilt | Append-only-Segmente mit Längen- und Prüfsummenpräfixen in JSON-Zeilen sowie ein Manifest | ein einzelnes Append-only-Ereignislog |
| Schreiben | atomarer Dateiaustausch pro Datensatz | Prüfsummen-geschützte Anhänge; Überschreibungen und Löschungen fügen neue Einträge hinzu, die durch compact() freigegeben werden |
Prüfsummen-geschütztes Anhängen für jedes Queue-Ereignis |
| Lesen | get() nach ID, indizierte Abfragen |
Cursor- und Zeitbereichsscans, get() über den Zustandsindex |
claim() in der Reihenfolge der Einreihung |
| Besonders geeignet für | Einzelabrufe und indizierte Suchen bei Sammlungen jeder Größe | Append-lastige Streams mit bis zu etwa 1 Mio. Datensätzen | dauerhafte Arbeitsverteilung zwischen Prozessen |
| Schwäche | Scans nicht indizierter Felder ab etwa 10.000 Datensätzen | häufige Umschreibungen einzelner Datensätze (alte Versionen belegen bis zur Komprimierung Speicherplatz) | kein allgemeiner Datensatz-Store; Payloads werden nach Abschluss eines Jobs verworfen |
DocStore und SegmentedLog verwenden dieselbe fließende Query-API: Dieselbe where()-Kette läuft auf beiden Engines; nur die Ausführung unterscheidet sich. DocStore kann Ergebnisse aus seinen sekundären Indizes liefern, während SegmentedLog beim Scannen der Segmente filtert. Queue basiert auf Claims und lässt sich nicht abfragen: Worker claim() den nächsten Job, statt Jobs zu filtern.
Faustregeln:
- Datensätze, die du einzeln abrufst, aktualisierst oder löschst und nach Feldern abfragst:
DocStore. Füge Indizes für die Felder hinzu, nach denen du filterst. - Ereignisse oder Versionen, die du meist anhängst und der Reihe nach oder nach Zeitfenster abrufst:
SegmentedLog. - Jobs, die von Erzeugern an Worker-Prozesse übergeben werden:
Queue. Claims und Abschlüsse sind prozessübergreifend sicher, undclaimMany()/completeMany()verteilen die Kosten für Sperren und fsync auf mehrere Vorgänge. - Joins, Suche oder Berichte über beliebige Sammlungen: Füge einen SQL-Mirror hinzu. Die Dateien bleiben maßgeblich; der Mirror ist eine wiederherstellbare Projektion in SQLite oder MySQL.
Die Engines lassen sich kombinieren: Sie teilen nur das Stammverzeichnis. Eine Anwendung kann daher Inhalte in einem DocStore, einen Prüfpfad in einem SegmentedLog und Hintergrundaufgaben in einer Queue unter demselben Speicherstammverzeichnis halten.
DocStore
DocStore eignet sich für Einzelabrufe und einfache Scans dauerhafter Datensätze. Es schreibt eine JSONC-Datei pro Datensatz:
$store = new Storh\DocStore($root, 'articles');
$record = $store->put(['slug' => 'hello', 'published' => true]);
$same = $store->get($record->id());
Die Datensatzdateien werden anhand der letzten UUID Zeichen in Unterverzeichnisse aufgeteilt, damit sich Schreibspitzen mit UUIDv7 über mehrere Verzeichnisse verteilen:
articles/data/2d/018bcfe5-6800-7000-8000-2d5dba6274da.jsonc
SegmentedLog
SegmentedLog eignet sich für Append-lastige Datensätze, Cursor-Lesevorgänge und Zeitbereichsabfragen. Datensätze werden in das aktive Segment geschrieben, eine Zeile pro Datensatz im Format <byte-length>\t<xxh32>\t<json> (siehe Atomarität). Die Segmentdateien verwenden die Erweiterung .ndjson, aber wegen des Längen- und Prüfsummenpräfixes können gewöhnliche NDJSON-Tools sie nicht lesen. Rufe Datensätze mit stream() oder dem Query-Builder ab. Volle Segmente werden zu versiegelten Segmenten mit Sparse-Indizes.
$events = new Storh\SegmentedLog($root, 'events');
$events->put(['type' => 'user.created']);
$events->compact();
Queue
Queue eignet sich für dauerhafte Work-Queues mit hohem Durchsatz. enqueue, claim, complete, requeue und purge schreiben Append-only-Ereignisse in queue.log. Beim Öffnen der Queue wird ihr Status durch Wiedergabe des Logs in den Arbeitsspeicher geladen. Batch-Methoden verteilen die Kosten für Sperren und Flush auf viele Jobs. Das Log wächst mit jedem Ereignis; stats() gibt seine aktuelle Größe in Byte aus.
$queue = new Storh\Queue($root, 'mail');
$queue->enqueue(['to' => 'person@example.test']);
$job = $queue->claim();
Haltbarkeit und Nebenläufigkeit
storh veröffentlicht Dateischreibvorgänge durch ein atomares Umbenennen im Zielverzeichnis. Beim erneuten Öffnen und bei repair() werden verwaiste temporäre Dateien entfernt, deren Besitzerprozess beendet wurde. Temporäre Dateien aktiver Schreibprozesse bleiben erhalten. Schreibmarkierungen ermöglichen diese Prüfung in konstanter Zeit, sodass eine Sammlung beim Öffnen nur nach einem Absturz durchsucht wird.
SegmentedLog und Queue schreiben längen- und checksummegesicherte Logzeilen. Beim erneuten Öffnen oder Reparieren werden unvollständige Enden bis zum letzten bestätigten Ereignis entfernt und der Speicherzustand aus den dauerhaft gespeicherten Logdaten neu aufgebaut. Schreibvorgänge in Logs und Warteschlangen werden durch Dateisystemsperren serialisiert.
DocStore-Änderungen werden durch eine Schreibsperre auf Sammlungsebene serialisiert, sodass Datensatzdateien und Sekundärindizes prozessübergreifend gemeinsam aktualisiert werden. Gleichzeitige Schreibvorgänge für unterschiedliche Datensatz-IDs sind unter denselben Annahmen für atomare Umbenennungen im Dateisystem sicher. Bei gleichzeitigen Schreibvorgängen für dieselbe ID gewinnt die letzte Umbenennung.
storh leert Dateipuffer und synchronisiert Dateihandles mit fsync, bevor Umbenennungen oder Anhängevorgänge abgeschlossen werden, und versucht, nach atomaren Umbenennungen auch übergeordnete Verzeichnisse mit fsync zu synchronisieren. Die Dauerhaftigkeit bei Stromausfällen hängt weiterhin davon ab, ob das zugrunde liegende Dateisystem und die Einhängeoptionen diese Synchronisierungen unterstützen.