Silniki
Wybór silnika
Wszystkie trzy silniki przechowują to samo: identyfikator UUIDv7 i tablicę danych, ale zapisują je na dysku w różny sposób. Od tego układu zależy, do czego najlepiej nadaje się każdy silnik:
| DocStore | SegmentedLog | Queue | |
|---|---|---|---|
| Na dysku | jeden plik JSONC na rekord, partycjonowany według końcowych znaków UUID | segmenty tylko do dopisywania z wierszami JSON poprzedzonymi długością i sumą kontrolną oraz manifest | jeden dziennik zdarzeń tylko do dopisywania |
| Zapis | atomowa podmiana pliku dla każdego rekordu | dopisywanie z sumą kontrolną; nadpisania i usunięcia dopisują nowe wpisy, odzyskiwane przez compact() |
dopisywanie z sumą kontrolną dla każdego zdarzenia kolejki |
| Odczyt | get() po ID, zapytania indeksowane |
skanowanie kursorem i zakresu czasu, get() przez indeks stanu |
claim() w kolejności dodania do kolejki |
| Najlepsze zastosowanie | odczyty pojedynczych rekordów i wyszukiwanie indeksowane przy dowolnym rozmiarze kolekcji | strumienie z dużą liczbą dopisań, do około 1 mln rekordów | trwałe przekazywanie pracy między procesami |
| Słaba strona | skanowanie pól bez indeksu przy ponad około 10 tys. rekordów | częste przepisywanie pojedynczych rekordów (stare wersje zajmują miejsce do czasu kompaktowania) | nie jest ogólnym magazynem rekordów; ładunki są usuwane po zakończeniu zadania |
DocStore i SegmentedLog współdzielą ten sam płynny mechanizm zapytania API: ten sam łańcuch where() działa w obu silnikach, różni się tylko wykonanie. DocStore może korzystać z indeksów pomocniczych, a SegmentedLog filtruje podczas skanowania segmentów.
Queue opiera się na przejmowaniu zadań, a nie na zapytaniach: procesy robocze claim() następne zadanie zamiast filtrować zadania.
Praktyczne wskazówki:
- Rekordy pobierane, aktualizowane lub usuwane pojedynczo i wyszukiwane według pola:
DocStore. Dodaj indeksy dla filtrowanych pól. - Zdarzenia lub wersje, które głównie dopisujesz, a potem odczytujesz w kolejności albo według przedziału czasu:
SegmentedLog. - Zadania przekazywane przez producentów procesom roboczym:
Queue. Przejmowanie zadań i ich kończenie są bezpieczne w wielu procesach, aclaimMany()/completeMany()rozkładają koszt blokad i fsync na wiele operacji. - Łączenia, wyszukiwanie lub raportowanie na dowolnej kolekcji: dodaj lustrzaną kopię SQL. Pliki pozostają kanonicznym źródłem danych; kopia jest projekcją, którą można odbudować w SQLite lub MySQL.
Silniki można łączyć: współdzielą tylko katalog główny, więc jedna aplikacja może przechowywać treści w DocStore, ścieżkę audytu w SegmentedLog, a pracę w tle w Queue, korzystając z tego samego katalogu głównego magazynu.
DocStore
DocStore służy do odczytów pojedynczych rekordów i prostego skanowania trwałych danych. Zapisuje jeden plik JSONC na rekord:
$store = new Storh\DocStore($root, 'articles');
$record = $store->put(['slug' => 'hello', 'published' => true]);
$same = $store->get($record->id());
Pliki rekordów są partycjonowane według ostatnich znaków UUID, dzięki czemu serie zapisów UUIDv7 rozkładają się między katalogi:
articles/data/2d/018bcfe5-6800-7000-8000-2d5dba6274da.jsonc
SegmentedLog
SegmentedLog służy do rekordów intensywnie dopisywanych oraz odczytów kursorem i według zakresu czasu.
Rekordy są zapisywane w aktywnym segmencie, po jednym wierszu na rekord, w formacie <byte-length>\t<xxh32>\t<json> (zob. Atomowość). Pliki segmentów mają rozszerzenie .ndjson, ale prefiks z długością i sumą kontrolną sprawia, że zwykłe narzędzia NDJSON nie mogą ich odczytać; odczytuj rekordy za pomocą stream() lub kreatora zapytań. Pełne segmenty przechodzą w zamknięte segmenty z rzadkimi indeksami.
$events = new Storh\SegmentedLog($root, 'events');
$events->put(['type' => 'user.created']);
$events->compact();
Queue
Queue służy do trwałych kolejek zadań o wysokiej przepustowości. Operacje enqueue, claim, complete, requeue i purge zapisują zdarzenia tylko do dopisywania w queue.log; stan kolejki jest odtwarzany w pamięci przy jej otwarciu. Metody wsadowe rozkładają koszt blokad i opróżniania bufora na wiele zadań. Log rośnie wraz z każdym zdarzeniem; stats() podaje jego bieżący rozmiar w bajtach.
$queue = new Storh\Queue($root, 'mail');
$queue->enqueue(['to' => 'person@example.test']);
$job = $queue->claim();
Trwałość i współbieżność
storh publikuje zapisy plików przez atomową zmianę nazwy w katalogu docelowym. Ponowne otwarcie i repair() usuwają porzucone pliki tymczasowe, których proces właściciela już nie działa, pozostawiając pliki tymczasowe aktywnych zapisujących bez zmian. Znaczniki zapisującego sprawiają, że sprawdzenie ma stały koszt, więc przegląd kolekcji podczas otwierania magazynu odbywa się tylko po awarii.
SegmentedLog i Queue zapisują wiersze logu zabezpieczone długością i sumą kontrolną. Ponowne otwarcie lub naprawa obcina uszkodzoną końcówkę do ostatniego zatwierdzonego zdarzenia i odbudowuje stan w pamięci na podstawie trwałej zawartości logu. Zapisy logu i kolejki są serializowane za pomocą blokad systemu plików.
Zmiany w DocStore są serializowane za pomocą blokady zapisu na poziomie kolekcji, dzięki czemu pliki rekordów i indeksy pomocnicze aktualizują się jako jedna granica spójności między procesami. Równoczesne zapisy różnych identyfikatorów rekordów są bezpieczne przy tych samych założeniach dotyczących atomowej zmiany nazwy w systemie plików. Przy równoczesnych zapisach tego samego ID wygrywa ostatnia zmiana nazwy.
storh opróżnia bufory i wykonuje fsync na uchwytach plików przed zmianą nazwy lub zakończeniem dopisywania, a po atomowych zmianach nazw próbuje wykonać fsync na katalogach nadrzędnych. Trwałość przy utracie zasilania nadal zależy od tego, czy bazowy system plików i opcje montowania respektują te operacje synchronizacji.