Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

API Python i Jupyter

Ten rozdział opisuje API osadzonego silnika na master po scaleniu epiku #445 w PR #510 i portu Result z #511 w PR #512, wraz z kontraktem katalogu z #450. Instalację wheel i Jupyter, budowanie biblioteki oraz odbiór notebooków opisują osobne strony. Historia C2/C3 nie zastępuje tego kontraktu. Istniejący wheel może pochodzić ze starszej rewizji; jego wersja pakietu sama nie potwierdza zawartości tych zmian.

Role i zakres

InterfejsZastosowaniePotrzebuje działającego serwera
retractordb.EngineKompiluje RQL i liczy plan wewnątrz procesu PythonaNie
retractordb.Storage, load_descriptorCzyta istniejące magazyny offlineNie; aktywny serwer może wykluczać dostęp
retractordb.client.ClientObserwuje strumienie działającej instancji przez xqryTak

Engine liczy na osi czasu logicznej, bez zegara czasu rzeczywistego, wątku IPC ani terminala. Nie dołącza do magistrali, nie rezerwuje przestrzeni nazw i nie publikuje swoich strumieni dla xqry. Czas rzeczywisty zapewnia xretractor; strumienie jego serwisu czyta API monitorowania. Biblioteka osadzona nie deklaruje gwarancji czasu rzeczywistego.

Pakiet importuje _core dopiero przy żądaniu API osadzonego. Czysty klient może działać bez rozszerzenia. NumPy jest zależnością biblioteki osadzonej; PyTorch importuje wyłącznie jawnie zaimportowany retractordb.torch. Instalacja klienta pip install ./api/python zastępuje dystrybucję wheel o tej samej nazwie, włącznie z _core; używaj osobnych środowisk.

Kompletny przykład

Przykład tworzy wejście poza katalogiem należącym do silnika, wykonuje plan, sprawdza wartości oraz współdzielenie tablicy przez DLPack. Część PyTorch jest opcjonalna.

from pathlib import Path
from tempfile import TemporaryDirectory
import numpy as np
import retractordb as rdb

with TemporaryDirectory() as directory:
    source = Path(directory) / "input.txt"
    source.write_text("\n".join(str(value) for value in range(10, 81, 10)) + "\n")
    with rdb.Engine() as engine:
        engine.compile(f"""
            DECLARE a INTEGER STREAM src, 1/2 TEXTFILE '{source}'
            SELECT a*2 STREAM dst FROM src
        """)
        assert engine.run() == 8
        assert engine.slots_done == 8 and engine.time == 4
        assert [row[0] for row in engine.rows("dst")] == list(range(20, 161, 20))
        descriptor = engine.schema("dst")
        field = descriptor[0].name
        block = engine.to_numpy("dst", fields=[field], dtype="float64")
        np.testing.assert_array_equal(block[:, 0], np.arange(20, 161, 20))
        window = engine.window("dst", fields=[field], size=4, stride=2)
        assert window.shape == (3, 4, 1)
        try:
            import torch
        except ImportError:
            torch = None
        if torch is not None:
            tensor = torch.from_dlpack(window)
            assert tensor.data_ptr() == window.to_numpy().__array_interface__["data"][0]
    np.testing.assert_array_equal(block[:, 0], np.arange(20, 161, 20))
    assert window.shape == (3, 4, 1)

Osiem rekordów daje osiem slotów i czas logiczny 4 przy interwale 1/2. Record, tablica i okno są kopiami wyników; pozostają czytelne po close() silnika.

Engine: cykl życia i wykonanie

Element APIKontrakt
Engine(storage_dir="", lock_dir="", config="")Własna instancja i Memory Store; domyślnie prywatny katalog tymczasowy
compile(rql, until_eof=True)Zamyka poprzedni plan, parsuje, kompiluje i tworzy nowy model
step()Zwraca indeks wykonanego slotu od 0 albo None po końcu wejścia
run(slots=None)Zwraca liczbę slotów wykonanych w tym wywołaniu; bez budżetu liczy do EOF
has_plan, failed, end_of_inputStan planu, zatrzaśnięta awaria slotu oraz koniec wejścia
slots_done, timeŁączna liczba wykonanych slotów i dokładny czas logiczny Fraction
storage_dirEfektywny katalog planu; bez planu katalog domyślny
streams(), schema(stream), is_declared(stream)Nazwy strumieni, deskryptor i informacja o deklarowanym źródle
record_count(stream), retained_from(stream)Łączny licznik rekordów oraz pierwszy nadal dostępny indeks
record(stream, index)Kopia jednego rekordu; ujemny indeks liczy od końca
rows(stream, first=None, count=None)Iterator kopii rekordów; domyślnie od najstarszego zachowanego
close(), with Engine() as engineZamyka plan i zwalnia jego zasoby; zwykłe zamknięcie jest idempotentne

Domyślne until_eof=True zatrzymuje plan po slocie, w którym pierwsze deklarowane źródło się wyczerpało. until_eof=False pozwala źródłom zawijać się po EOF. Plan bez kończącego się wejścia wymaga budżetu, np. run(slots=8), albo przerwania. Nie utożsamiaj slotu z liczbą rekordów każdego strumienia: plan może mieć różne interwały i opóźnione początki.

Nieudana rekompilacja również zamyka poprzedni plan; odmowa nie przywraca go. Po K2/K3 można poprawić żądanie i kompilować ponownie. Awaria podczas slotu ustawia failed; kolejne step() powtarza klasę awarii do rekompilacji. Dotychczasowe wyniki mogą pozostać czytelne po K4. K1 dodatkowo skazuje moduł do restartu interpretera.

compile, step, run i odczyty rekordów zwalniają GIL. run() sprawdza sygnały Pythona co około 50 ms pomiędzy slotami i umożliwia KeyboardInterrupt; to nie jest gwarantowany maksymalny czas przerwania długiego slotu. Diagnostyka silnika trafia do logging.getLogger("retractordb").

Wyjątek z własnego handlera logging jest zgłaszany przez sys.unraisablehook; nie zastępuje pierwotnego błędu kompilacji i sam nie skazuje modułu do restartu. Test test_logging_handler_failure_preserves_the_plan_error_and_next_compile sprawdza, że po takim zdarzeniu można poprawić plan i uruchomić go ponownie.

Katalog magazynu i dostęp offline

Engine() tworzy prywatny katalog, który pozostaje po close() i znika przy zniszczeniu instancji. Jawny storage_dir oraz nadpisująca go dyrektywa RQL STORAGE muszą wskazywać katalog pusty przy pierwszym użyciu albo należący do silnika przez poprawny znacznik .retractordb-embedded. Obcy katalog jest odrzucany. Znacznik nie jest dowodem, że proces właściciela nadal działa.

Jeśli zapis nowo tworzonego znacznika zawiedzie, StorageGuard próbuje usunąć niekompletny znacznik i zwraca IOError z przyczyną. Nie traktuje częściowo zapisanego znacznika jako udanej inicjalizacji katalogu.

Blokada katalogu trwa do zamknięcia magazynów planu. Wyklucza drugi Engine oraz Storage w oznaczonym katalogu, także przez alias ścieżki i z innego procesu. Śmierć procesu zwalnia blokadę. Jawny katalog pozostaje na dysku; rekompilacja może zastąpić jego deskryptory i wyniki. Odmowa preflight zachowuje poprzednie artefakty, lecz zamyka plan; późniejsze błędy I/O nie są transakcyjne.

Wejścia muszą leżeć poza katalogiem wyników; wyjścia pozostają wewnątrz niego. Brak deklarowanego pliku źródłowego jest odrzucany podczas compile() jako IOError z errno=ENOENT lub ENOTDIR, nazwą strumienia i ścieżką w komunikacie; brak prawa odczytu daje IOError z errno=EACCES. Oba przypadki są błędem środowiska K4 również przy wykonaniu przez xretractor: proces kończy się kodem EIO (5 na Linuksie), z --until-eof i bez niego. Usunięcie źródła po kompilacji to osobny przypadek. Jawne DISPOSABLE nadal upoważnia do usunięcia źródła po użyciu.

Otwieranie Storage może odbudować indeks .meta; jest to dostęp offline. Dla katalogu używanego przez serwis podaj jego lock_dir albo config wskazujący TOML z paths.lock_dir; argument ma pierwszeństwo. Konfiguracja jest czytana wyłącznie po jawnym wskazaniu pliku. Bez obu parametrów kontrola zgaduje katalog z TMPDIR i zgłasza UserWarning oraz wpis INFO w loggerze retractordb.

[embedded] lenient_lock_dirs określa katalogi, w których tylko trzymana blokada odmawia dostępu. Bez klucza domyślnym katalogiem łagodnym jest systemowy katalog tymczasowy. Klucz zastępuje tę listę; inne katalogi są sprawdzane ściśle. Kontrola przy otwieraniu nie wyklucza późniejszego startu demona ani serwisu używającego innego katalogu blokad. Prywatny katalog Engine() pomija kontrolę blokad serwisu.

Storage, Descriptor i Record

Element APIKontrakt
load_descriptor(path)Odczytuje .desc; brak pliku to NoSuchStream, uszkodzony deskryptor to CorruptDescriptor
DescriptorSekwencja Field; size_bytes pomija pola konfiguracyjne, flat_element_count liczy elementy danych
FieldWłaściwości name, length, array_count, type (FieldType)
Metody deskryptorahas_field, field_index, byte_offset, field_type_name, storage_policy
Storage(qry_id, file_name, storage_param="", storage_type="DEFAULT", lock_dir="", config="")Otwiera istniejący magazyn offline, bez kompilowania planu
Storage.descriptor, record_count, is_declared, closedSchemat, licznik, rodzaj źródła i stan uchwytu
Storage.read(i), storage[i], len(storage)Kopia rekordu lub liczba rekordów; ujemny indeks od końca
Storage.close() i context managerZamyka magazyn, indeks i blokadę katalogu; odczyt po zamknięciu jest odrzucany
Record.index, descriptor, record[i]Indeks, schemat i spłaszczone elementy własnej kopii payload

Wartości wychodzą jako int, float, str, Fraction dla RATIONAL, krotka dla par i None dla NULL. STRING[N] stanowi jeden element wartości, a tablica numeryczna N elementów. Pola konfiguracyjne deskryptora nie zajmują pozycji w rekordzie. Bezpośredni Storage nie odczytuje źródeł BINFILE, TEXTFILE ani DEVICE; wyniki źródła należącego do planu można czytać przez Engine w granicach jego historii.

Retencja może wygasić indeksy. record_count jest licznikiem logicznym, nie liczbą wszystkich nadal dostępnych rekordów; użyj retained_from. Domyślne rows() i to_numpy() czytają zachowany ogon. Wygasły indeks jest odrzucany zamiast wskazywać inną pozycję pierścienia.

Błędy i granica fatalna

RetractorDBError
+-- ConfigError (także ValueError)
|   +-- PlanError
|       +-- RQLSyntaxError
|       +-- CompileError
|       +-- CorruptDescriptor
+-- IOError (także OSError)
+-- InternalError
+-- NoSuchStream
+-- StorageError
KlasaWynik w PythonieStan po błędzie
K1 zwrócone jako Result lub nieoczekiwany wyjątek C++ wychodzący z rdzeniaInternalErrorModuł skazany; chronione wejścia odmawiają do restartu interpretera; close() pozostaje dostępne
K2, odmowa planuPlanError: składnia, kompilacja, uszkodzony deskryptorMożna poprawić dane i spróbować ponownie; nieudany compile() nie pozostawia starego planu
K3, odmowa żądaniaConfigError, również ValueErrorInstancja pozostaje używalna
K4, błąd środowiskaIOError, również OSErrorAwaria slotu zatrzymuje plan do rekompilacji; odmowa kompilacji nie tworzy planu
K5, niepoprawna wartość danychNone lub NaNPlan pracuje dalej; ostrzeżenie silnika jest kompilowane poza Release
K6, przestarzała składniaDeprecationWarning z compile()Plan pozostaje używalny
K7, abort lub błąd poza obsługiwaną granicąZakończenie procesuKernel trzeba uruchomić ponownie

IOError.errno zachowuje kod systemowy albo używa EIO bez kodu; filename jest ustawiane, gdy wywołanie przyjmowało ścieżkę jako argument. W innych przypadkach ścieżka może być w komunikacie. Błędy nazwy pola/strumienia, indeksu, konwersji tablic i sygnatury mogą dawać standardowe KeyError, IndexError, ValueError lub TypeError; nie każdy wyjątek dziedziczy po RetractorDBError.

Rdzeń zwraca obsługiwalne błędy jako rdb::Result<T>; binding przekłada kategorię Error na powyższe klasy wyjątków Pythona. Błąd I/O przy odczycie deskryptora daje IOError z pierwotnym errno, a nie CorruptDescriptor; ta druga klasa dotyczy błędnej treści. Nieoczekiwany wyjątek C++ z wywołania rdzenia, np. bad_alloc lub length_error, daje InternalError i skaża moduł. Nie przechwytuj InternalError z zamiarem dalszego liczenia w tym samym kernelu.

rdb::fatal() i RDB_ASSERT wywołują osobny, niewracający handler procesu. Domyślnie kończy on hosta przez abort (SIGABRT), także kernel Jupyter; binding nie zamienia tej ścieżki na wyjątek Pythona. xretractor instaluje własny handler kończący proces z kodem 1 i sprzątaniem IPC. Handler jest polityką całego procesu, a nie pojedynczego Engine. RDB_NOEXCEPT_CHECK=OFF nie zmienia tego zachowania. Błędy w funkcji noexcept albo podczas destrukcji również nie mają ogólnej gwarancji przeżycia interpretera.

Niepoprawne wejście nie powinno trafiać do ścieżki fatal: kompilator odrzuca m.in. rozplot & lub % z argumentem równym interwałowi źródła jako CompileError. Można poprawić plan i ponownie użyć Engine. Granicę Result, wyjątku C++ oraz SIGABRT sprawdzają testy podprocesów w test_error_classes.py i test_fatal_paths.py; odmowę rozplotu sprawdzają również regresje ut_compiler.

Własność pamięci, NumPy i DLPack

OperacjaKształt i własność
to_numpy(stream, fields=None, dtype="float64", first=None, count=None)(rekordy, wartości); nowa tablica, kopia wyników silnika
window(stream, fields=..., size=..., stride=1, dtype="float32")(liczba_okien, size, wartości); ciągła tablica należąca do Window
Window.to_numpy(), np.asarray(window)Udostępnia tablicę okna bez kolejnej kopii przy zgodnym dtype
torch.from_dlpack(window)Współdzieli tę tablicę CPU; nie pamięć silnika

Kolejność fields określa oś wartości; tablice numeryczne są spłaszczane. STRING, INTPAIR i IDXPAIR nie mają gęstej reprezentacji numerycznej. Typy eksportu to float32, float64, int32, int64. NULL staje się NaN dla float; dla int NULL, nieskończoność i wynik poza zakresem powodują ValueError. Skończone wartości ułamkowe są obcinane w kierunku zera. Projekcja przechodzi przez double, dlatego pełnej dokładności dużych liczb całkowitych szukaj w Record, nie w gęstej tablicy.

size i stride muszą być dodatnie. Dla mniej niż size rekordów powstaje (0, size, wartości). Dla N zachowanych rekordów liczba okien wynosi 1 + (N-size)//stride, gdy N >= size. Okno korzysta z sliding_window_view, po czym kopiuje wybrane okna do ciągłej tablicy. Tablice i tensory pozostają ważne po następnych slotach, zamknięciu i zniszczeniu silnika. Zmiana tablicy okna jest widoczna we współdzielącym tensorze, ale nie modyfikuje wyników silnika.

Urządzenia, PyTorch i wątki

retractordb.torch.as_tensor(window, device="cpu") współdzieli pamięć CPU; jawny inny device wykonuje transfer i kopię. StreamDataset(engine, stream, fields=..., window=..., stride=1, dtype=torch.float32, device="cpu") daje elementy (size, wartości). Każda iteracja odczytuje bieżące wyniki, więc nowe sloty mogą dodać okna do kolejnej epoki.

Używaj DataLoader(..., num_workers=0). Worker jest odrzucany przed dostępem do silnika; fork oraz tworzenie Engine w initializerze workera nie należą do kontraktu tego datasetu. Serializuj wszystkie wywołania jednej instancji, również odczyty, właściwości, compile() i close() podczas run(). GIL nie chroni stanu silnika; równoczesna kompilacja lub wykonanie osobnych instancji nie ma ogólnej gwarancji bezpieczeństwa wątkowego.

CPU/DLPack nie przypina buforów silnika i nie zapewnia przypiętej pamięci hosta dla CUDA. Transfer na CUDA jest osobną operacją. MPS odrzuca float64 przed transferem; wybierz float32. Test CPU tej odmowy nie jest odbiorem sprzętowym MPS. Demonstracja Colab T4 z #448 używa CUDA, fp16 i GradScaler; nie stanowi dowodu dla wszystkich urządzeń. DeviceWindowLoader, odbiór MPS i publikacja PyPI pozostają opcjonalnymi dalszymi zadaniami poza epikiem.

Ograniczenia planu osadzonego

compile() odrzuca DUMP, SYSTEM, :ROTATION oraz źródła DEVICE jako CompileError: wymagają stanu lub fazy wykonania serwisu, których Engine nie ma. Brak pliku wejścia daje opisany wyżej IOError. Push ingest z Pythona i cele mobilne nie są dostępne. Pełny serwis oraz jego konfiguracja pozostają domeną xretractor.

Źródła i testy kontraktu

ObszarŹródła w repozytorium silnikaTesty w api/python/tests
Plan, EOF, rekompilacja i katalogsrc/embed/engine.cc, storageGuard.cc, src/python/module.cpptest_engine.py, C++ ut_embedEngine
Deskryptory i odczyt offlinesrc/python/module.cpp, src/rdb/lib/descriptor.cctest_descriptor.py, test_storage.py
K1-K7 i powtórne użyciesrc/include/rdb/error.hpp, src/rdb/lib/error.cc, src/include/fatalError.hpp, src/python/module.cpptest_error_classes.py, test_fatal_paths.py, test_reentry.py
Kopie, okna i urządzeniaretractordb/engine.py, retractordb/torch.py, src/python/module.cpptest_window.py
Izolacja i zgodność logicznasrc/embed/engine.cc, src/retractor/lib/dataModel.cpptest_isolation.py, test_reference.py

Ścieżki retractordb/*.py są względem api/python/. Nazwy testów określają pokrycie, nie deklarują wykonania na wszystkich platformach. test_reference.py porównuje wspólny zakres z xretractor -u, włącznie ze schematem, NULL, metadanymi i logicznym harmonogramem; nie mierzy czasu rzeczywistego. Odmowę brakującego źródła sprawdzają osobno test_engine.py, ut_embedEngine i it_untileof_stop-missing-sources (#488). Funkcje tylko serwisowe pozostają poza tym porównaniem. Warunki i wyniki konkretnego odbioru należy zapisać osobno.