Jak nie blokować pętli zdarzeń i bezpiecznie obsłużyć anulowanie operacji I/O w UnixLocal

Opublikowano: 16 września 2026

Operacje na przestrzeni roboczej łatwo uznać za zwykły szczegół implementacyjny. W aplikacji opartej na asyncio stają się jednak częścią kontraktu całej pętli zdarzeń: rekurencyjne usuwanie katalogów, tworzenie archiwów czy ich rozpakowywanie potrafi wykonać dużo pracy na systemie plików i nie powinno blokować kodu asynchronicznego.

UnixLocal rozwiązuje ten problem, przenosząc wybrane operacje I/O poza właściwą pętlę zdarzeń. Ważne jest przy tym nie tylko to, gdzie wykonywana jest praca, ale również co dzieje się z nią po anulowaniu zadania wywołującego.

Trzy operacje, które muszą pozostać poza pętlą zdarzeń

Pierwszym przypadkiem jest rekurencyjne usuwanie katalogu. Zamiast wywoływać shutil.rmtree bezpośrednio w metodzie asynchronicznej, implementacja kieruje tę pracę do ścieżki przeznaczonej dla blokujących operacji przestrzeni roboczej. Niezmiennik jest prosty: rekurencyjne usuwanie nie powinno wykonywać ciężkiej pracy bezpośrednio w pętli zdarzeń.

Drugi przypadek to archiwizacja. tarfile.open oraz tar.add są wykonywane wewnątrz synchronicznej funkcji przekazywanej do run_blocking_workspace_io.

Trzeci to rozpakowywanie. Tutaj do tej samej synchronicznej operacji trafiają trzy elementy: utworzenie katalogu głównego, tarfile.open oraz safe_extract_tarfile.

Te ścieżki warto zachować jawnie również w testach i podczas przeglądu kodu. Nie chodzi o ogólne stwierdzenie "I/O powinno działać w tle", lecz o trzy konkretne operacje, dla których zachowanie zostało ustalone.

Anulowanie nie powinno porzucać rozpoczętej pracy

Osobnym problemem jest asyncio.CancelledError. run_blocking_workspace_io uruchamia asyncio.to_thread jako osobne zadanie. Jeżeli w tym czasie kod wywołujący zostanie anulowany, pomocnik nie porzuca rozpoczętej operacji: nadal czeka na jej zakończenie, pobiera wynik i dopiero później ponownie zgłasza anulowanie.

Sekwencja wygląda tak:

Uruchamiane jest zadanie oparte na asyncio.to_thread.

Kod wywołujący otrzymuje asyncio.CancelledError.

run_blocking_workspace_io nadal czeka na zakończenie rozpoczętej pracy.

Wynik tej pracy zostaje pobrany.

Anulowanie jest zgłaszane ponownie.

Najważniejsza granica znajduje się między krokami 3 i 4. Rozpoczęta operacja nadal ma właściciela i nie zostaje pozostawiona bez nadzoru tylko dlatego, że zewnętrzne zadanie zostało anulowane.

To osobny niezmiennik od samego kierowania operacji I/O poza pętlę zdarzeń: przed ponownym zgłoszeniem anulowania trzeba poczekać na zakończenie rozpoczętej pracy.

Test powinien obserwować pracę także podczas anulowania

Test regresyjny dla tego przypadku rejestruje początek i koniec rozpakowywania wokół momentu anulowania. Następnie sprawdza asyncio.CancelledError oraz potwierdza stan otwartego strumienia archiwum.

Dzięki temu test nie ogranicza się do stwierdzenia, że wywołujący został anulowany. Sprawdza również, czy operacja rozpoczęta poza pętlą zdarzeń rzeczywiście doszła do kontrolowanego zakończenia.

W commicie 494ea5978778a4daa48b513419dc5786084802d4 test hydrate_workspace_cancellation przeszedł. W tym uruchomieniu pominięto 14 testów, więc jest to ograniczony wynik odnoszący się do tego konkretnego commitu.

Trzy rzeczy, które warto sprawdzić

Przy podobnych operacjach przestrzeni roboczej warto rozdzielić trzy pytania:

Czy potencjalnie blokująca operacja omija pętlę zdarzeń?

Czy rozpoczęta praca pozostaje poprawnie zarządzana również po anulowaniu kodu wywołującego?

Czy test regresyjny sprawdza nie tylko CancelledError, ale również faktyczne zakończenie pracy w tle?

Dopiero te trzy warunki razem pokrywają zarówno wydajność pętli zdarzeń, jak i poprawność zachowania podczas anulowania.

Komentarze (0)

Brak komentarzy.

Dodaj komentarz

Komentarze są publikowane po moderacji. Adres e-mail nie będzie widoczny publicznie.