Tytułem wstępu: Whoosh to bardzo przyjemny silnik indeksujący i wyszukujący dokumenty w trybie full-text. Niestety, masa ludzi ma problemy z jego wdrożeniem.

Objawy

Główne objawy to:

OSError: [Errno 17] File exists: '/path/to/index/cache/_MAIN_LOCK'

oraz:

IOError: [Errno 2] No such file or directory: '/path/to/index/cache/_MAIN_123.tiz'

Oba z nich są na ogół różnymi objawami faktu, że więcej niż jeden proces próbuje używać tego samego indeksu w tym samym czasie. Główną przyczyną jest nieświadomość tego, że Whoosh nie gwarantuje bezpieczeństwa wątków.

Rozwiązanie pierwsze: gdy używasz Haystack

Ponieważ zdecydowana większość użytkowników nie odwołuje się bezpośrednio do API Whoosh, a korzysta z niego za pośrednictwem modułu Haystack, najpierw przedstawię rozwiązanie dla nich.

Jak ustaliliśmy przedwczoraj z autorem Haystack, przykładowy backend dostarczany dla Whoosh błędnie zakłada bezpieczeństwo wątków tego ostatniego.

Dodatkowo, Haystack używa leniwych obiektów (obiektów z odwleczoną inicjalizacją) do ładowania indeksów, co powoduje, że nawet przy zachowaniu bezpieczeństwa wątków, aplikacja nie będzie działać prawidłowo w środowisku typu prefork.

Najprawdopodobniej w przyszłości Haystack będzie uruchamiał Whoosh w postaci usługi systemowej, co pozwoli na wielodostęp do tego samego indeksu i tym samym zlikwiduje problemy związane z rozgałęzianiem procesów i wątkowaniem. Póki co, rozwiązanie składa się z dwóch kroków:

  1. Nałożenie na Haystack mojej łatki, która zapewnia bezpieczeństwo wątków: haystack-whoosh.patch

  2. Upewnienie się, że Django działa w serwerze wątkowanym, a nie forkowanym. Dla używających manage.py sprowadza się to do dopisania parametru method:
    python manage.py runfcgi method=threaded ...

Rozwiązanie drugie: gdy bezpośrednio wołasz Whoosh

Tutaj gotowej łatki podać nie mogę, bo każdy pisze kod po swojemu. Wystarczy jednak — podobnie jak zrobiłem to w łatce dla Haystack — użyć mechanizmu threading.Lock i ogrodzić wszystkie wywołania API wspólną blokadą (czy może ryglem, jak czasem jest to słowo tłumaczone). Na przykład:

import threading

whoosh_api = threading.Lock()

def do_something():
	whoosh_api.acquire()
	try:
		# use API
	finally:
		whoosh_api.release()

Należy się również upewnić, że w danej chwili istnieje tylko jeden obiekt typu whoosh.index.Index na każdy katalog indeksu, a także maksymalnie jedna instancja whoosh.writing.IndexWriter. Pierwszy przypadek rozwiązuje implementacja singletona, drugi — ogrodzenie całego cyklu życia obiektu (aż do pomyślnego wywołania metody commit lub cancel) wewnątrz tej samej blokady, co reszta wywołań API.