Select Git revision
-
Martin Mareš authored
Closes #314: Pokud organizátor odešle POST na přidání nového účastníka soutěže dvakrát rychle po sobě (třeba kvůli nějakému automatickému retry po rozpadu spojení), dvě různé DB transakce se snaží založit uživatele se stejným loginem. Jedna z nich selže na unikátnosti sloupce email. V defaultní úrovni izolace transakcí (READ COMMITTED) to nemá žádné hezké řešení. Nepomůže SELECT ... FOR UPDATE, jelikož ten zamyká pouze nalezené řádky, nikoliv neexistenci dalších řádků vyhovujících podmínce. Co by se dalo dělat: (1) Zvýšit úroveň izolace aspoň na READ REPEATABLE. To vyřeší problém, ale současně může začít víceméně jakákoliv zapisující transakce failovat. Vyžadovalo by dopsat retry do prakticky všech míst v OSMO, kde je nějaký commit. (2) Retryovat specificky transakce na zakládání užívatelů (a účastí apod.). Tohle nejde snadno, jelikoz jsou i součástí dlouho běžících transakcí v importech (zatím jsme se snažíli, aby byl celý import atomický a v případě selhání se celý rollbackoval). To by možná mohly vyřešit subtransakce. (3) Zamykat celou tabulku s uživateli, než na ní provedeme první SELECT. To by asi vyřešilo problém, ale byl by potřeba zápisový zámek, takže by paralelně nemohla běžet žádná čtení. A také by se to potenciálně mohlo deadlockovat (potřebujeme v jedné transakci postupně lock na uživatele, účastníky a účasti a locky platí až do konce transakce). (4) Používat INSERT ... ON CONFLICT <něco>. To vypadá bezpečně, jen to není moc pohodlné, zejména proto, že s tím nepočítá ORM, takže je potřeba dělat všechno ručně. Zatím jsem zvolil (4), protože mi přijde, že to změny udržuje lokální a funguje i s dlouhými transakcemi při importu. Výhledově bych se ale chtěl zamyslet nad tím, jak takové věci řešit co nejuniverzálněji a nejpohodlněji.
Martin Mareš authoredCloses #314: Pokud organizátor odešle POST na přidání nového účastníka soutěže dvakrát rychle po sobě (třeba kvůli nějakému automatickému retry po rozpadu spojení), dvě různé DB transakce se snaží založit uživatele se stejným loginem. Jedna z nich selže na unikátnosti sloupce email. V defaultní úrovni izolace transakcí (READ COMMITTED) to nemá žádné hezké řešení. Nepomůže SELECT ... FOR UPDATE, jelikož ten zamyká pouze nalezené řádky, nikoliv neexistenci dalších řádků vyhovujících podmínce. Co by se dalo dělat: (1) Zvýšit úroveň izolace aspoň na READ REPEATABLE. To vyřeší problém, ale současně může začít víceméně jakákoliv zapisující transakce failovat. Vyžadovalo by dopsat retry do prakticky všech míst v OSMO, kde je nějaký commit. (2) Retryovat specificky transakce na zakládání užívatelů (a účastí apod.). Tohle nejde snadno, jelikoz jsou i součástí dlouho běžících transakcí v importech (zatím jsme se snažíli, aby byl celý import atomický a v případě selhání se celý rollbackoval). To by možná mohly vyřešit subtransakce. (3) Zamykat celou tabulku s uživateli, než na ní provedeme první SELECT. To by asi vyřešilo problém, ale byl by potřeba zápisový zámek, takže by paralelně nemohla běžet žádná čtení. A také by se to potenciálně mohlo deadlockovat (potřebujeme v jedné transakci postupně lock na uživatele, účastníky a účasti a locky platí až do konce transakce). (4) Používat INSERT ... ON CONFLICT <něco>. To vypadá bezpečně, jen to není moc pohodlné, zejména proto, že s tím nepočítá ORM, takže je potřeba dělat všechno ručně. Zatím jsem zvolil (4), protože mi přijde, že to změny udržuje lokální a funguje i s dlouhými transakcemi při importu. Výhledově bych se ale chtěl zamyslet nad tím, jak takové věci řešit co nejuniverzálněji a nejpohodlněji.
jinja.py 2.29 KiB
# Konfigurace Jinjových šablon a pomocné funkce
from flask import url_for
from markupsafe import Markup
from typing import Any
import mo.config as config
import mo.db as db
import mo.util_format as util_format
from mo.web import app
from mo.web.org_contest import contest_breadcrumbs
from mo.web.org_place import place_breadcrumbs
# Konfigurace Jinjy
app.jinja_options['extensions'].append('jinja2.ext.do')
app.jinja_env.lstrip_blocks = True
app.jinja_env.trim_blocks = True
# Filtry definované v mo.util_format
app.jinja_env.filters.update(timeformat=util_format.timeformat)
app.jinja_env.filters.update(inflected=util_format.inflect_number)
app.jinja_env.filters.update(timedelta=util_format.timedelta)
app.jinja_env.filters.update(time_and_timedelta=util_format.time_and_timedelta)
app.jinja_env.filters.update(data_size=util_format.data_size)
# Exporty proměnných
app.jinja_env.globals.update(web_flavor=config.WEB_FLAVOR)
# Export enumů z mo.db:
app.jinja_env.globals.update(RoundState=db.RoundState)
app.jinja_env.globals.update(LogType=db.LogType)
app.jinja_env.globals.update(PartState=db.PartState)
app.jinja_env.globals.update(RoleType=db.RoleType)
app.jinja_env.globals.update(PaperType=db.PaperType)
app.jinja_env.globals.update(JobType=db.JobType)
app.jinja_env.globals.update(JobState=db.JobState)
# Další typy:
app.jinja_env.globals.update(Markup=Markup)
# Vlastní pomocné funkce
app.jinja_env.globals.update(contest_breadcrumbs=contest_breadcrumbs)
app.jinja_env.globals.update(place_breadcrumbs=place_breadcrumbs)
# Funkce asset_url se přidává v mo.ext.assets
@app.template_filter()
def user_link(u: db.User) -> Markup:
return Markup('<a href="{url}">{name}{test}</a>').format(url=user_url(u), name=u.full_name(), test=" (test)" if u.is_test else "")
def user_url(u: db.User) -> str:
if u.is_admin or u.is_org:
return url_for('org_org', id=u.user_id)
else:
return url_for('org_user', id=u.user_id)
@app.template_filter()
def pion_link(u: db.User, contest_id: int) -> Markup:
url = url_for('org_contest_user', contest_id=contest_id, user_id=u.user_id)
return Markup('<a href="{url}">{name}{test}</a>').format(url=url, name=u.full_name(), test=" (test)" if u.is_test else "")
@app.template_filter()
def or_dash(s: Any) -> str:
return str(s) if s else '–'