Skip to content
Snippets Groups Projects
Select Git revision
  • 6aff4e917e5f14ab5e465251de369bc0b50467c4
  • devel default
  • master
  • fo
  • jirka/typing
  • fo-base
  • mj/submit-images
  • jk/issue-96
  • jk/issue-196
  • honza/add-contestant
  • honza/mr7
  • honza/mrf
  • honza/mrd
  • honza/mra
  • honza/mr6
  • honza/submit-images
  • honza/kolo-vs-soutez
  • jh-stress-test-wip
  • shorten-schools
19 results

users.py

Blame
    • Martin Mareš's avatar
      6aff4e91
      Bezpečné transakce při zakládání uživatelů/účastníků/účastí · 6aff4e91
      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.
      6aff4e91
      History
      Bezpečné transakce při zakládání uživatelů/účastníků/účastí
      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.
    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 ''