Django, testy, fixtůry a posraté rekurze
Psát testy je pruda. O to větší, když po dvouhodinové přípravě ku*va-fix tur spustíte test a výstup v konzoli je plný hlášek typu:
Exception RuntimeError: 'maximum recursion depth exceeded in *' in ignored
(za * si dosaďte cokoli vás napadne)
Kura co to je? Google neporadí (teda ne přímo), ve výstupu absolutně nedopátratelné z kama vítr vane. Tak až se vám podobná situace namane, vězte, že chyba není ve vás. To je útěcha co?
Django (či Python?) nemá rádo velké, navzájem do sebe propletené fixtury. V mém případě měl JSON něco okolo 2000 řádek s celkem 70 objekty z nejrůznějších modelů. V naprosté zoufalosti jsem tuto jednu “obřímí” fixturu rozbil na menší a hle, po recursion mrdce ni vidu, ni slechu.
No ale přeci nepodlehnu takové blbosti. Nechci mít jednu fixturu rozbitou na deset menších!
Google tentokrát pomohl. Přidejte si do testu totok a pošlete mi pusu na dobrou noc:
import sys
sys.setrecursionlimit(2000)
PS: určo záleží na systému, pod kterým pracujete. Na mém OSX sněžném kočičákovi je defaultně recursionlimit nastaven na hodnotu 1500.
Tonight we’ve released Django 1.3 beta 1, as well as Django 1.2.4 and Django 1.1.3 to correct a pair of security issues.
- Beta announcement: http://www.djangoproject.com/weblog/2010/dec/22/13-beta-1/
- Security announcement: http://www.djangoproject.com/weblog/2010/dec/22/security/
All affected users are urged to upgrade immediately.
Žangovinky #32
Komentovaný překlad Django Dose Community Catchup, Episode #32.
Během prosince se na svém blogu Seek Nuance pěkně rozepsal John DeRosa. V
článku “A performance lesson on Django QuerySets” dokazuje, že
bezmyšlenkovité nasazení QuerySet funkce iterator() přináší spoustu
problémů. Menší paměťové nároky byly v jeho případě draze vykoupeny četností
dotazů na databázi (co iterace to dotaz, navíc umocněné dvaceti simultánně
spuštěnými Celery tasky).
V druhé polovině článku se zmiňuje o špatném pochopení vlastnosti QuerySetu,
tj. že jsou lazy (k vyhodnocení dojde až je to nezbytně nutné) a kešované. Na
praktické ukázce demonstruje, že každý vytvořený QuerySet generuje databázový
dotaz, což v kontextu jeho kódu znamenalo neúměrné zatížení PostgreSQL.
David Cramer je masakrální drsoň. Pokaždé když něco publikuje, tak je to
perla. A pokaždé mi trvá sakra dlouho, než pochopím o co jde.
Nejinak tomu je i v článku “Tracking changes to fields in Django”.
Pokud jsem se někde šeredně nesek, tak nám dává k dispozici dekorátor, s
jehož pomocí můžeme sledovat historické změny atributů u konkrétního modelu.
Fachá to tak, že pokud např. obj.name == 'Petr' a přenastavíme jej na
obj.name = 'Pavel', pak metoda obj.has_changed('name') vrátí True
(atribut name se změnil), metoda obj.old_value('name') vrátí ‘Petr’ a metoda
obj.whats_changed() nás podaruje slovníkem změněných atributů. Tato ultimátní
“změnokeška” se resetuje při vytvoření instance a během uložení objektu.
Daniel Greenfeld rozvířil debatu o Template Languages. Ve svém postu hájí
směr, který tvrdí že šablonovací jazyk musí být stupidní. Čím méně toho umí,
tím lépe.
Reakce na sebe nenechaly dlouho čekat. Autoři alternativních šablonovací jazyků
(Armin Ronacher — Jinja2, Mike Bayer — Mako) vysvětlují, proč toho
jejich jazyky umí víc + jeden pohled zvenčí.
Špetka sci-fi z velkého světa (pro mě rozhodně). Pokud stejně jako chlapci
z The Washington Times spravujete vícero serverů a začínáte mít binec v
Python balíčcích (na serveru A verzi 1, na serveru B verzi 1.2 a na serveru C
nic), může se vám šiknout jejich fabfile.py.
Poslední zpráva hodná zmínky: nedávno implementovaná unittest metoda
assertNumQueries dráždila Luke Planta natolik, že pro ni napsal speciální
pomocnou třídu FuzzyInt. S její pomocí je možné testovat počet SQL dotazů v
zadaném intervalu. Jak Luke trefně komentuje, vlivem kešování se lehce stane,
že počet SQL dotazů v aplikaci kolísá, a testovat je na exaktní počet přináší
spíš problémy než užitek.
A teď něco úplně jiného. Novinky z chobotu:
- Byly rozšířeny funkce mail_admins() a mail_managers(). Odteďka je možné k nim připínat HTML přílohy, čehož se hned využilo pro odesílání přítulnějších error emailů. 3× hurá!
- Odskočme si do views, přesněji na jejich konce. Pokud nejste moc indie, budete
tam mít buďto nějakou
HttpResponse,render_to_responsenebo generické view. Odteďka ale budeme potkávat nového kamaráda TemplateResponse. Ten dostal do vínku kopec lenosti, a obsah generuje až to je skutečně nutné. Díky této povaze bude např. možné změnit na poslední chvíli jméno šablony, nebo do ní poslat upravený či rozšířený kontext. Už se těším na prčovní middlewary, které z této vlastnosti vytěží maximum.
Poznámka: Kopa práce, těžká témata a blížící se vánoce. Výsledek? Žangovinky #31 nejsou a nebudou. Komu chybí, nechť opráší AČ slovník, a postne sem link.
Pěkné vánoce!
The first alpha preview package for Django 1.3 is now available.
- Release notes: http://docs.djangoproject.com/en/dev/releases/1.3-alpha-1/
- Download instructions: http://www.djangoproject.com/download/
Konzultačka u RevSys
5. listopadu 2010 se na IRC otevřely virtuální dvéřka do společnosti Revolution Systems, ve které působí i Jacob Kaplan-Moss, core vývojář Djanga. Během dvou hodin padlo téměř čtyřicet zajímavých dotazů, a ještě zajímavějších odpovědí.
Povinná četba!
Co jsem nasál:
- MySQL je divné a zatlouká si další a další hřebíčky do rakve. Modří už stejně dávno přešli na PostgreSQL.
- MediaTemplate je něco mezi WebFaction (sdílený hosting) a vlastním VPS
- Tak to vypadá, že za rok, za dva, budeme po internetu rajtovat se stádem jednorožců. Prý toho moc nepožerou (cca o 10-20% méně RAM v porovnání s mod_wsgi) a povozí víc lidí.
- Na doméně https://www.djangy.com/ se peče něco zajímavého
- Odolný a prověřený podvozek pro Django aplikace? nginx, Apache, mod_wsgi, PostgreSQL
- Dobrodruhové ale krosí s nginx, gunicorn, celery, rabbitmq, redis, PostgreSQL
- Django závidí Railsům migrace
- Začátečníci se na Jacobových kurzech ztrácí v balíčcích a cestách, pokročilí považují Django za nedotknutelnou černou skříňku. V prvním případě pomůže pip/virtualenv, v druhém četba zdrojáku Djanga.
- Když už je třeba štupovat, tak nejdřív s opicema a pak když tak s mercurial queues. Nebo forknout projekt k sobě a pozašívat to v něm.
Co zaujalo vás?
Žangovinky #30
Komentovaný překlad Django Dose Community Catchup, Episode #30.
18.října měla být představena alpha 1 verze Djanga 1.3. Bohužel se ale stále objevují drobné chyby a datum se odsouvá. Na termínu oficiálního představení nové verze se naštěstí nic nemění, 17.ledna 2011 nás objede Ježoch a doveze překrásný dáreček. Co bude uvnitř?
- generická view převlečená do class-based kabátku
- jednodušší správa média souborů napříč aplikacemi
- podpora spouštění databázových transakcí uvnitř bloků
with - a spoustu dalších vylepšení
Jednou z nejvýraznějších přednášek na letošním portlandském DjangoConu
byla Why Django Sucks. Eric Florenzano trefně poukazuje na bolesti Djanga
— klesající výkon s každou novou verzí, monolitický settings.py, obtížné
přizpůsobování aplikací třetích stran. Zatímco některá z kritizovaných témat se
podařilo poměrně rychle vyřešit (např. rozšíření týmu core vývojářů), u jiných
to bude běh na dlouhou trať. Andy McKay ve svém článku Django Apps rock
kontruje na Ericovu poznámku ohledně znovupoužitelnosti: nezapomínejme, že
většinu našich webů pohání také “non-reusable” aplikace. A v této rovině Django
exceluje, rychlostí i efektivitou.
U Andyho blogu ještě zůstaneme. Uvažovali jste o tom, že svůj Django projekt nasadíte na Googlím App Enginu? Ceny jsou přijatelné (pro většinu z nás 0,- USD), Django je dlouhodobě podporováno, škáluje to! Možná si napřed přečtete článek When App Engine went wrong a pouvažujte, jestli svůj projekt nerozjet přeci jen jinde.
Jak sledujete chyby na produkci? Necháváte si posílat traceback reporty
přes email nebo používáte sofistikovanější řešení jako třeba Arecibo
nebo Sentry? Pokud patříte do druhé skupiny, nechte si Davidem
Cramerem poradit jak správně používat knihovnu logging, aby se
hlášky v Sentry zobrazovaly jak mají.
A teď něco úplně jiného. Novinky v chobotu:
HttpRequestnyní můžeme číst stejně jako soubor- nový tag
localizenám umožní vypínat/zapínat podporu lokalizace v šablonách (nezávisle na USE_L10N) - testy budou fičet zase o něco rychleji
Poslední téma není můj šálek kávy, ale určitě za zmínku stojí: Postgres 9 Streaming Replication and Django-Balancer.
Žangovinky #29
Komentovaný překlad Django Dose Community Catchup, Episode #29.
Začněme blogy. Peter Bengtsson ve svém článku Local Django development with Nginx radí, jak na lokální mašině nakonfigurovat Nginx a využít jej k servírování statického obsahu (CSS, JS, obrázky). Ukazuje jednoduchý postup, kterým dosáhl zrychlení z původních 446.34 req/s na 15709.54 req/s, což se pozitivně projevilo během ladění Javascriptových kódu v prohlížeči.
A teď něco úplně jiného. Troubení z chobotu:
- Zásadní zpráva. Seznam autorů byl doplněn o Honzu Krále, jediného našince který se na vývoji Djanga aktivně podílí. Gratulace! (a velké díky)
- Od revize 14139 Django podporuje knihovnu unittest2, která mimo jiné přináší nové assert metody pro pohodlnější testování seznamů, slovníků nebo datetime objektů.
- Třída
TestCasebyla doplněna o metodu assertNumQueries (revize 14183), s pomocí které je možné ověřit počet databázových dotazů generovaných zadaným kódem. - U testování ještě zůstaneme. Díky nové třídě RequestFactory (revize 14191) je
možné vytvořit fake request objekt a ten v testu podsunout konkrétnímu view
(čímž elegantně obejdeme pravidla v
urls.py)
Poslední novinka se týká “class based views”, oficiálního začlenění views v podobě tříd (konečně!). Russell Keith-Magee sepsal letový plán, který by nám měl do Djanga 1.3 dopravit tuto vysněnou vlastnost. Držme palce.
Ladění výkonu Django aplikací
V září 2010 proběhl v Portlandu DjangoCon. Seznam probíraných témat je docela velký. Pokud to čas dovolí, pokusím se některé zajímavé přednášky zpracovat a pak se s nimi podělit zde na blogu.
První přednáška, která mě padla do oka byla “First steps in performace tuning” od Russella Keith-Mageeho řešící výkon Django aplikací.
Nejdříve si uvědomme základní fakta:
- vývoj probíhá na lokále, který je zatěžován sporadicky
- systém má k dispozici velké množství paměti
- k aplikaci přistupuje jediný uživatel
Situace na serveru ale býva přesně opačná.
Nejčastější prohřešky
Před publikováním aplikace na server je vhodné proklepnout několik bodů:
- Každé view by mělo svůj úkol vyřešit co nejrychleji. Odezva do 0.1s je dobrá, do 1s obstojná, nad 2s špatná. Pokud se view na nečem dlouho zdržuje, je lepší úkol poslat do fronty (message queue) a zpracovat jej offline. Posloužit může i AJAX, cron či “persistent services” (jestli jsem to dobře pochopil, pak měl Russell na mysli démona, který jede někde bokem, udržuje v paměti persistentí data a ta na žádost velice rychle vrací).
- Minimalizuj počet SQL dotazů. S pomocí Django Debug Toolbaru odhal neefektivní místa aplikace. Sniž počet SQL dotazů na minimum. Pro relační dotazy se vyplatí použít select_related. Pozor! Hromada malých “levných” dotazů je stejně špatná, jako jeden “drahý” dotaz (SQL příkaz EXPLAIN napoví).
- Na velikosti dat záleží. Pokud view zpracovává pouze zlomek atributů, které sebou model nese, nasaď only, defer, values nebo values_list. S jejich pomocí Django z DB tabulek vyzobne jen to co je třeba a objem přenášených dat mezi databází a aplikací výrazně klesne.
- Používej databázové indexy. U atributů, podle kterých aplikace třídí nebo vyhledává užší množinu dat (get, filter, exclude, order_by) je vhodné zvážit nasazení databázových indexů, viz parametr db_index.
- Denormalizuj. Když je to třeba. Dokud to jde, drž se čistého návrhu. Až to začne drhnou (výkonostně), neostýchej se denormalizace. Pamatuj, existují i jiné možnosti (databázová view, “umělé” tabulky s předpočítanými hodnotami, apod).
- Šablonový systém je pomalý. No a co (Russellova reakce). Pokud tvůj projekt obsahuje hodně aplikací, nebo máš v šablonách hodně {% include %} či {% extend %} tagů, nebo hodně TEMPLETE_DIRS adresářů, nasaď django.template.loaders.cached.Loader. Nebo použij jiný šablonový systém, třeba Jinja2 (říkám já).
Kešování
Invalidace
Russell se ptá: máš stránku, která se mění sporadicky? Pak ji zakešuj. Pak pokračuje: máš stránku která se mění často? A bude vadit, když po určitou dobu uvidí návštěvníci starý obsah?
Asi jak kde. S kešováním totiž přichází nepříjemný efekt, a to invalidace hodnot. Pokud například změním titulek u článku, většinou nestačí zničit keš s detailem článku, ale také přehledy článku v kategoriích, možná také úvodní stránku a bůh ví co ještě. Na invalidaci žádný recept nemá (naťukává sice signály, ale nijak zvlášť je nerozebírá). Sází spíše na jednoduchost, a k tomu mu bohatě stačí adekvátně nastavená doba expirace nakešovaných hodnot a smíření se s faktem, že návštěvníci jeho webu po určitou dobu neuvidí čerstvý obsah.
Cold start, thundering
Během této partie mě ale zaujalo jiné téma: “cold start” a “dogpiling/thundering herd”. Hrozné názvy.
Za optimální situace bude prvním návštěvníkem webu obsah stránky zakešován a každý další uživatel už uvidí obsah z keše. Pokud ale skokově, během malého okamžiku, dojde na stránku více uživatelů, bude se pro každého z nich stránka renderovat znovu a znovu, bez využití keše (protože v ní ještě nic uloženo není). Podobný efekt může nastat i při vyexpirování hodnoty z keše.
Russell radí, aby se před startem serveru spustil skript, který jednorázově vyrenderuje nejnavštěvovanější stránky (ale třeba i querysety, či jiná často používaná data) a uloží je do kešky. Doporučuje prostudovat aplikaci django-newcache od Erica Florenzana, která “thundering” efekt elegenatně řeší.
ETagy
Posledním kašotématem byly ETagy. Zjednodušeně: jde o mechanismus, který vyrenderovanou stránku “otaguje” na úrovni protokolu HTTP (libovolným řetězcem). Když uživatel navštíví stránku podruhé, pošle prohlížeč spolu s GETem i původně získaný ETag. Pokud aplikace zjistí, že se požadovaná stránka nijak nezměnila (vygeneruje stejný tag jako ten co přichází od prohlížeče), pak odpoví status kódem “304 Not Modified” a server je ušetřen generováním a přenosem nezměněné HTML stránky.
Jak ale efektivně zjistit, jestli se stránka změnila nebo ne? Pokud například máme view, které generuje seznam článků, pak musíme nejdřív provést dotaz na DB, vytáhnout naposledy přidaný či změněný článek a porovnat jej s ETagem. To ale není ono, lepší by bylo úplně DB obejít, situaci rychle vyhodnotit a kód ve view vůbec neprovádět.
Chytrý trik na svém blogu nabízí Will McGugan. View odekorujeme, a
informaci s posledním ETagem necháme v cache (Will má ve svých modelech atribut
version, kterou s každým uložením inkrementuje). Porovnání je pak mimořádně
rychlé, nicméně opět se dostáváme k invalidaci — musíme zajistit mazání keše
(ideálně pověšené opět na save).
Load testing
Jakmile aplikace jede na serveru, je třeba měřit. Všecko možné (response time z logů, využití paměti, zbývající místo na disku, zatížení I/O). Prostě vše.
Russell upozorňuje na memory leaks Pythoních C extensions (což jsou údajně všechna rozhraní populárních DB), několikrát naráží na neobvyklosti MySQL (podivné zamykání tabulek, chování planneru, apod).
K prověřování zátěže doporučuje curl, ab a siege.
Dotazy z publika
Na konci přednášky zaznělo z pléna i několik dotazů:
- Kterou optimalizací začít? Těžko říct, záleží na aplikaci. Nejdříve je nutné najít bolavé místo a pak ho řešit.
- Jak je to s thredy uvnitř view? Vytvořit thread uvnitř view je blbost a spíš to ukazuje na špatný návrh.
- Nějaký tip na ladící nástroje? Django Debug Toolbar + plugin pro profilování.
Video
No a konečně i záznam Russellovy přednášky:
Přidáváme objekt alá User
Všimli jste si někdy, že přidání nového uživatele v administračním rozhraní Djanga probíhá trochu jinak, než přidání jakéhokoliv jiného objektu?
Pro připomenutí — nový uživatel se zadává prostřednictvím dvou po sobě navazujících formulářů, kdy v první fázi vyplníte pouze username a heslo, a teprve při druhém kroku je možné doplnit zbytek informací.
Potřeboval jsem vytvořit podobné workflow a po nakouknutí do zdrojáku Djanga mě opět překvapilo, jak jednoduše je možné podobného efektu dosáhnout. Proměte si prsty, jdeme na to.
Do administrační třídy si přidejte 3 nové atributy:
class MenuAdmin(admin.ModelAdmin):
add_form = MenuAdminAddForm
add_fieldsets = (
(None, {
'fields': ('system_name', 'name')}
),
)
add_form_template = 'admin/menu/menu/add_form.html'
Atribut add_form definuje formulářovou třídu, která bude použita pouze při vytváření nového objektu (do atributu form můžete klíďo-píďo uvést jiný formík, který bude použitý při editaci existujícího záznamu). Atribut add_fieldsets definuje seznam položek z add_form, které se na admin stránce zobrazí. A konečně add_form_template říká, že pro vykreslení stránky bude použita uvedená (mírně upravená) šablona.
Modří už tuší, že tohle stačit nebude. Django o žádných add_* atributech nemá ani páru. Musíme mu proto lehulince pomoct. Do admin třídy doplňte metodu:
def get_form(self, request, obj=None, **kwargs):
defaults = {}
if obj is None:
defaults.update({
'form': self.add_form,
'fields': admin.util.flatten_fieldsets(self.add_fieldsets),
})
defaults.update(kwargs)
return super(MenuAdmin, self).get_form(request, obj, **defaults)
Metoda get_form dokáže rozpoznat jestli se přidává nový objekt, a v tomto připadku upraví argumenty pro rodičovský get_form tak, aby namísto obvyklého form a fieldsets použila add_form a add_fieldsets. Pokud by došlo k editaci existujícího objektu, Django se bude chovat standardně, tj. použije atribut form a fieldsets (více viz oficiální dokumentace).
A k čemu ten add_form_template? Ten pouze přidá do formuláře informativní hlášku o tom, že po vytvoření objektu bude uživateli nabídnut bohatší formulář s doplňujícími informacemi:
{% extends "admin/change_form.html" %}
{% load i18n %}
{% block form_top %}
<p>{% trans "First, enter a username and password.
Then, you'll be able to edit more user options." %}</p>
<input type="hidden" name="_continue" value="1" />
{% endblock %}
Vtipný kvíz na konec: víte jakou roli má hidden input _continue?
(není tam náhodou)
Taky se vám NĚKDY odpálí signál dvakrát?
Možná se Vám už někdy stalo, že při použití signálu v Djangu se vaše obslužná funkce zavolala vícekrát. Mě se to naposledy stalo dnes.
Best practice
Na umístění kódu pro signály si dávám pozor. Dle dobrých Django mravů mám v adresáři s aplikací soubor signals.py, v něm kód obslužné funkce i její pověšení na konkrétní signál. Na konci models.py pak vkládám import signals a všechno funguje jako víno.
Až do dneška.
Prapodivný test
Dělal jsem drobné změny v projektu a po jejich dokončení spustil testy. Hle, chybička! A jak už to tak bývá, šlo o něco divného — když jsem spustil všechny testy aplikací najednou, došlo k chybě v jednom konkrétním testu. Když jsem ale spustil problémový test samostatně, tak prošel.
Po delší analýze jsem přišel na to, že během inicializace testů dojde k dvojímu zaregistrování funkce na signál (a kvůli tomu má obslužná funkce odvedla svou práci rovněž dvakrát).
Co s tím?
Krátká odpověď: dispatch_uid
Delší odpověď: funkci connect můžete zavolat s parametrem dispatch_uid, který vaší obsluze přiřadí jedinečný identifikátor. Pokud by někdy v budoucnu náhodou mělo dojít k vícenásobnému zaregistrování funkce na signál, s pomocí dispatch_uid funkce connect pozná, že obsuha již zaregistrována byla a další registrace neprovede.
Zavěšování funkcí na signály proto doporučuji provádět následovně:
from django.db.models.signals import pre_save
from myapp.models import MyModel
def my_handler(sender, **kwargs):
...
pre_save.connect(my_handler, sender=MyModel, dispatch_uid="jedinecne id")
Poučení? Registrujte své funkce s využitím parametru dispatch_uid a pište testy.