Jesteś fanem wolnego oprogramowania (nie mylić z powolnym) i używasz GNOME? Na co dzień pracujesz z innym systemem, ale wykorzystujesz aplikacje oparte na GTK+? A może obudził się w tobie instynkt filantropa? Jeśli tak, to Fundacja GNOME z radością przyjmie twoje pieniądze (i jeśli kiedyś postanowiłeś mi kupić piwo, to nie czekaj i podaruj te pieniądze fundacji)!
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:
-
Nałożenie na Haystack mojej łatki, która zapewnia bezpieczeństwo wątków: haystack-whoosh.patch
- Upewnienie się, że Django działa w serwerze wątkowanym, a nie forkowanym. Dla używających
manage.pysprowadza się to do dopisania parametrumethod:
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.
Pewni spamerzy wysłali mi właśnie taki potworek:
Szanowni Panstwo !
Biuro Tlumaczen MEGALING oferujace niedrogie tlumaczenia zwraca sie do
Panstwa firmy z prosba o wyrazenie zgody na otrzymanie droga elektroniczna
informacji handlowych na temat produktów naszej firmy.Jezeli wyrazaja Panstwo zgode na przeslanie przez nas droga elektroniczna
informacji handlowych w rozumieniu ustawy z dnia 18 lipca 2002 roku o
swiadczeniu uslug droga elektroniczna (Dz. U. Nr 144, poz. 1204 ze zm.)
prosimy o wyslanie pustego maila zwrotnego z tematem TAK.
Oczywiście wierzę, że nie mówimy tu o masowym zbieraniu i przetwarzaniu adresów poczty elektronicznej. Wszak spamerzy tłumacze z całą pewnością moje dane znaleźli w katalogu przedsiębiorców, gdyż jak powszechnie wiadomo, mój blog jest zarejestrowaną czternastoosobową spółką akcyjną, na giełdzie jest notowany pod symbolem CYCKI i od lat w nagłówku wyświetla własny identyfikator KRS. Oczywiście.
Drogie dzieciaczki (ton celowo protekcjonalny, dzięki temu powstrzymuję się od użycia co dobitniejszych epitetów), jeśli wysyłacie zapytanie o zgodę na spamowanie, to macie obowiązek podać pełne dane podmiotu, który o taką zgodę prosi. Co ważniejsze jednak, wasze zapytanie nie może zawierać samej oferty. Dla zobrazowania, scenka rodzajowa.
— Czy pozwoli Pan, że napiszę mu, że oferujemy tanie tłumaczenia?
— Nie pozwolę.
— Nie jest Pan zatem zainteresowany informacją, że tłumaczymy za jedyne 25 PLN od strony?
— Obawiam się, że nie.
— Jeśli nie interesują pana tłumaczenia pod przysięgą, być może zezwoli pan na wysłanie oferty dotyczącej
sprzedam Opla, rocznik 1998, stan dobry, lekko tłuczony?
cieszmy się jednak, że taka logika
merketoidiotów ogranicza się do reklam.
— Czy pozwoli Pan, żebym kopnął, o tak?
— Aaaa, moje jaja!
— Rozumiem. Nie jest pan zainteresowany, zatem powstrzymam się od przemocy.
Pozwala serwować wiele domen za pomocą jednego tylko projektu Django. Inspiracją był ten snippet.
Zmiany w settings.py:
from threading import local SITE_THREAD_INFO = local() SITE_THREAD_INFO.SITE_ID = 1 class SiteIDHook(object): def __int__(self): return SITE_THREAD_INFO.SITE_ID def __hash__(self): return SITE_THREAD_INFO.SITE_ID SITE_ID = SiteIDHook()
Nowa klasa middleware:
from django.conf import settings from django.contrib.sites.models import Site from django.http import HttpResponseRedirect class MultiSiteMiddleware(object): def process_request(self, request): settings.SITE_THREAD_INFO.SITE_ID = 0 # Fail by default host = request.META.get('HTTP_HOST').split(':')[0] if host: try: site = Site.objects.get(domain = host) settings.SITE_THREAD_INFO.SITE_ID = site.id except Site.DoesNotExist: return HttpResponseRedirect('http://example.com/')
Bonus — automatyczne fitrowanie obiektów:
from django.contrib.sites.models import Site
from django.db import models
def create_custom_manager(*args, **kwargs):
class CustomManager(models.Manager):
def get_query_set(self):
return super(CustomManager, self).get_query_set().filter(*args, **kwargs)
return CustomManager()
class Product(models.Model):
site = models.ForeignKey(Site, default = Site.objects.get_current)
# ...
objects = create_custom_manager(site = Site.objects.get_current)
Przypuśćmy, że nasza aplikacja pozwala kupić koszulkę. Dostępne kolory zależą od rozmiaru i trzeba pozwolić je jakoś wybrać. Dwa selecty? Będzie się dało wybrać nieistniejącą kombinację. Łączony select? Nieczytelnie, chyba że…
Poniższe rozwiązanie używa klasy GroupedSelect pochodzącej z Django Snippets.
from django.forms import ModelChoiceField, ValidationError from django.forms.util import smart_unicode from django.utils.itercompat import groupby from operator import attrgetter class GroupedModelChoiceIterator(object): def __init__(self, field, grouper): self.field = field self.queryset = field.queryset self.grouper = grouper def __iter__(self): if self.field.empty_label is not None: yield (None, ((u'', self.field.empty_label), )) for grouper, items in groupby(self.queryset.all(), attrgetter(self.grouper)): yield (unicode(grouper), tuple((o.pk, unicode(o)) for o in items)) class GroupedModelChoiceField(ModelChoiceField): def __init__(self, field, widget = GroupedSelect, *args, **kwargs): self._field = field super(GroupedModelChoiceField, self).__init__(widget = widget, *args, **kwargs) def _get_choices(self): return GroupedModelChoiceIterator(self, self._field) choices = property(_get_choices, None) def clean(self, value): """ Validates that the input is in self.choices. """ if value in (None, ''): value = u'' value = smart_unicode(value) valid_values = [] for group_label, group in self.choices: valid_values += [str(k) for k, v in group] if value not in valid_values: raise ValidationError(u'Select a valid choice. That choice is not one of the available choices.') return super(GroupedModelChoiceField, self).clean(value)
Przykład (zakładając, że color jest referencją na obiekt, który posiada pole size):
class SizeAndColorForm(forms.ModelForm): class Meta: model = Product fields = ['color'] color = GroupedModelChoiceField(field = 'size', label = u'size/color')
Jeśli ktoś przegapił dyskusję na Blipie, to z pewnością powinien nadrobić i zapoznać się ze startupem Wieczne Odpoczywanie.
Uwaga: notka zawiera słownictwo nieodpowiednie dla osób, które nie sięgają brodą do klawiatury.
Tu właściwie mógłbym zakończyć.
Jaka mądrość płynie z powyższego fragmentu kodu? Nie ma żadnego powodu, żeby komentarze przeznaczone dla ekipy opuszczały serwer. Ładnie, ale to się nie sprawdza. Podejrzewam wręcz, że autorowi w ogóle do głowy nie przyszło, że da się to odczytać poza serwerem.
Bardziej ogólna wersja tego przypadku wygląda tak:
if impossible: print 'FUCK!!1!' # ← this will never happen
Prawdziwy wniosek jest taki: nigdy nie umieszczaj w programie kodu, którego efekt działania jest kompromitujący. I nie dotyczy to tylko komentarzy w HTML. Główni podejrzani to przede wszystkim różnego rodzaju niemożliwe
asercje (błędy typu wyjebało się połączenie do bazy
okazują się całkiem możliwe po telefonie od klienta).
Nie zapominajmy też o jednej z najpopularniejszych metod debugowania, żartobliwie ochrzczonej przeze mnie chujotestem (polega na wstawianiu w różne miejsca w kodzie właściwej dla danego języka instrukcji drukującej nazwę pewnej popularnej wśród pań części ciała; choć brzmi całkiem niepoważnie, jest bardzo skuteczną metodą śledzenia przepływu sterowania w programowaniu wielowątkowym).
Chcę też zastrzec, że o ile to właśnie religijna tematyka serwisu sprawia, że komentarze w kodzie stają się tak zabawne, to nie chcę, żeby ktoś odebrał to jako nagonkę na jakikolwiek kościół (w sensie organizacji, nie budynku). Miejsce na dyskusje religijne jest pod wczorajszą notką.
PS: Kod HTML wspomnianego wyżej serwisu nadaje się na całą serię notek, celowo na zrzucie ekranu pozostawiłem niezwykle interesującą metodę ukrywania elementów. Ktoś umie zrobić to bardziej absurdalnie? Aż prosi się o XML.
— W jakiego koloru butach lubisz jeździć na nartach?
— Nie jeżdżę na nartach.
— Głupi jesteś? Pytałam o kolor!
Bez sensu, prawda? A może tak:
— Jakiego jesteś wyznania?
— Jestem ateistą.
— To niemożliwe, każdy w coś wierzy!
Brzmi znajomo? Jeśli oczekujesz od innych, że będą tolerować przeróżne historie o kosmicznym zombie, który jednocześnie jest własnym ojcem i swoim niewidzialnym przyjacielem, dał się sfragować, zrespawnował i codziennie z tobą czatuje, zrozum, że kogoś innego może to interesować tyle, co wampiry i wilkołaki.
Religie, nawracania, dżihady — wielki konwent, na którym spotkali się najbardziej oddani fani Batmana i Supermana. Kto jest potężniejszy? Który by wygrał? Tak właśnie to wygląda z boku. A mimo to nie każę ci palić kościołów, synagog i meczetów. Niektórzy potrzebują w coś wierzyć, więc jeśli lubisz narty, to baw się dobrze.
Pamiętaj tylko o jednym: tak jak niejeżdżenie na nartach nie jest kolorem, tak ateizm nie jest religią. A skoro nie jest, to przestań mnie namawiać na jej zmianę.
Hello Patryk Zawadzki,
I use Boxbe to protect my email address. While I did receive your email about
Re: [Bluez-devel] [Bluez-users] Motorola S9 and BlueZ 4.14, you are not currently on my email Guest List. I’ll be more likely to see your email and future messages if you are on my priority Guest List.Click here to be put directly on my Guest List
Taki właśnie komunikat widzę ostatnimi czasy codziennie. Co ciekawe, śmieci od Boxbe dotyczą wiadomości wysłanych na listy dyskusyjne. Pół roku temu.
Ludzie, takie wynalazki możecie sobie podłączać do ochrony
korespondencji z ciocią, na listach kwalifikują was na wypisanie i dożywotni zakaz wstępu. Podobnie jak każdy inny rodzaj autorespondera. Tych czterysta osób jakoś obejdzie się bez informacji, że od zeszłego wtorku do przyszłej środy opalacie dupę na Majorce i zastępuje was pani Kazia z pokoju 102.
Ile razy ręcznie składałem formularz w szablonie tylko po to, żeby pola były we właściwej kolejności? Nie mam pojęcia, ale od jakiegoś czasu stosuję poniższe rozwiązanie. Tak, to jest jedna z tych notek ku własnej pamięci.
class OrderedForm(object): def __init__(self, *args, **kwargs): super(OrderedForm, self).__init__(*args, **kwargs) if hasattr(self.Meta, 'fields_order'): self.fields.keyOrder = self.Meta.fields_order
Po umieszczeniu powyższego w jakimś ustronnym wspólnym module można zabrać się za używanie:
from django.contrib.auth.models import User from django import forms from core.forms import OrderedForm class UserNewForm(OrderedForm, forms.ModelForm): class Meta: model = User fields = ('first_name', 'last_name', 'username', 'password', 'email') fields_order = ('username', 'password', 'first_name', 'last_name', 'email') password = forms.CharField(label = u'Password', required = True, widget = forms.PasswordInput(attrs = {'autocomplete': 'off'}))
Trzeba zauważyć, że OrderedForm celowo nie dziedziczy po żadnej innej klasie Django, dzięki czemu równie dobrze działa z forms.ModelForm, co z forms.Form, jak i z dowolną własną (zgodną) implementacją.




