Транзакции¶
Оберните блок записей, чтобы они закоммитились вместе или откатились вместе.
from endocore.orm import atomic
with atomic():
account.balance -= 100
account.save()
ledger = Ledger.objects.create(amount=-100)
# коммит при чистом выходе; откат, если исключение выходит наружу
Если блок бросает исключение, всё внутри откатывается, а исключение распространяется дальше.
Вложенные блоки используют savepoints¶
Внутренний atomic() становится SAVEPOINT, поэтому можно поймать внутренний
сбой и сохранить более раннюю работу внешней транзакции:
with atomic():
do_a() # часть внешней транзакции
try:
with atomic(): # SAVEPOINT
do_b() # падает
except SomeError:
pass # откат до savepoint; do_a() уцелела
do_c()
# внешний коммит: do_a() и do_c() сохранены, do_b() откачена
По алиасам¶
Async-код: aatomic()¶
В async-обработчике используйте async-близнеца — он берёт соединение из пула в worker-потоке, поэтому ожидание исчерпанного пула никогда не блокирует event loop:
from endocore.orm import aatomic
async def handler(request):
async with aatomic():
acc = await Account.objects.aget(pk=1)
acc.balance -= 100
await acc.asave()
await Ledger.objects.acreate(amount=-100)
# коммит при чистом выходе; откат, если исключение выходит наружу
a*-вызовы внутри блока присоединяются к транзакции: владение транзакцией
отслеживается токеном в contextvars, а asyncio.to_thread переносит его в
тредпул. Вложенные aatomic() становятся SAVEPOINT'ами, как и у atomic().
(a*-вызовы одной транзакции выполняйте последовательно — без gather.)
Вызов обычного with atomic(): в потоке event loop'а даёт RuntimeWarning:
если пул исчерпан, ожидание свободного соединения заблокировало бы весь
loop, а не только текущий запрос.
Как коммитятся записи вне atomic()¶
Вне блока atomic() каждая запись (save, create, update/delete на
queryset'е, bulk_*) коммитится немедленно. Внутри блока коммит откладывается
до выхода из самого внешнего atomic().
Замечания о конкурентности¶
- Транзакция закрепляет за собой одно соединение из пула на весь блок, поэтому конкурентные запросы не могут вклиниться внутрь неё своими стейтментами.
- У каждого алиаса — небольшой пул соединений:
configure(..., pool_size=N)(по умолчанию: SQLite 1, PostgreSQL 5). С пулом 1 у SQLite транзакции сериализуются, а автокоммит-записи ждут открытую транзакцию; с PostgreSQL доpool_sizeтранзакций идут по-настоящему параллельно. - Ожидание свободного соединения из пула прекращается через
pool_timeoutсекунд (по умолчанию 30) и бросаетPoolTimeoutError— по-настоящему исчерпанный или зависший пул падает быстро и громко, а не подвешивает всех вызывающих навсегда. - SQLite по природе однописательный; PostgreSQL хорошо справляется с конкурентными записями.