MEASUREMENT
bench/idempotency/practice.py
The script that produced the numbers in the article, and the record of the run. The file is read from the repository at build time — this is the code that was run, not a copy of it.
- Cited in
- /en/interview/sre/idempotency
- How to run it
python3 bench/idempotency/keys.py > bench/idempotency/runs/keys.txt python3 bench/idempotency/practice.py > bench/idempotency/runs/practice.txt
The run below is recorded in Russian. It is a lab record, kept in the language it was written in; the numbers, the tables and the code read the same either way.
Record of the run
Замеры для урока «Идемпотентность»
| Файл | Что делает |
|---|---|
keys.py |
пять наблюдений: сколько раз применяется эффект при повторах без ключа и с ключом; что происходит в окне между эффектом и потерянным ответом; чем отличаются два порядка работы с ключом при одновременных повторах; и что даёт новый ключ на каждую попытку |
practice.py |
ответы к задачам урока: число эффектов без ключа, с ключом и при ключе, записанном после работы, а также кратность между одним ключом и новым на каждую попытку |
Запуск из корня репозитория:
python3 bench/idempotency/keys.py > bench/idempotency/runs/keys.txt
python3 bench/idempotency/practice.py > bench/idempotency/runs/practice.txt
Что здесь считается эффектом
Одно увеличение счётчика внутри сервиса — то, что нельзя сделать дважды: «деньги списаны», «письмо отправлено», «заказ создан». Предмет замера — именно этот счётчик, а не число полученных запросов и не то, что увидел клиент.
Обе стороны живут в одном процессе на loopback. Сети между машинами здесь нет, падений сервера нет, «база» — словарь в памяти под замком. Долговечность записи в предмет замера не входит.
Что воспроизводимо
Числа эффектов: четыре при четырёх повторах без ключа, один с ключом, один при двух одновременных повторах с ключом, занятым до работы, и два при ключе, записанном после. Три эффекта при трёх попытках с новыми ключами.
Не воспроизводятся тексты ответов вида ok #1: это внутренняя нумерация
счётчика, и она зависит от порядка блоков.
Требования к среде
Только CPython и loopback, прав root не требуется. Прогон занимает несколько секунд.
Числа сняты на CPython 3.11.15, Linux 6.18.44.
Script
92 lines"""Практика к уроку про идемпотентность: три ответа и одна цена.
ЗАЧЕМ ОТДЕЛЬНЫЙ ФАЙЛ. Задачи урока проверяются сборкой
(scripts/validate-practice.mjs): показанный читателю код обязан построчно быть
в скрипте, верный вариант — дословно встречаться в записи прогона, а
заявленное число — печататься самой программой.
ЧТО ЗДЕСЬ ПРОВЕРЯЕТСЯ. Два места, где ошибаются чаще всего. Первое: сколько
раз применяется эффект при четырёх повторах без ключа, с ключом и при
одновременных повторах, когда ключ записан после работы. Второе: во сколько
раз больше эффектов даёт новый ключ на каждую попытку.
Сервис и клиент берутся из `keys.py`, чтобы задача и разбор урока стояли на
одном и том же коде.
ЗАПУСК: python3 bench/idempotency/practice.py
Вывод: runs/practice.txt
"""
import os
import sys
import threading
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from keys import Service, call # noqa: E402
def retries(use_keys: bool, times: int = 4) -> int:
"""Сколько раз применился эффект при `times` одинаковых повторах."""
service = Service(use_keys=use_keys)
for _ in range(times):
call(service.port, "order-1")
applied = service.applied
service.close()
return applied
def simultaneous(claim_key: bool) -> int:
"""Два повтора в один момент: ключ занимают до работы или пишут после."""
service = Service(use_keys=True, slow_reply=0.05, claim_key=claim_key)
threads = [
threading.Thread(target=call, args=(service.port, "order-2")) for _ in range(2)
]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
applied = service.applied
service.close()
return applied
def fresh_key_each_time(times: int = 3) -> int:
"""Новый ключ на каждую попытку: сколько эффектов."""
service = Service(use_keys=True)
for i in range(times):
call(service.port, f"attempt-{i}")
applied = service.applied
service.close()
return applied
def main() -> None:
print(f"Python {sys.version.split()[0]} · Linux {os.uname().release} · loopback")
print("an effect is one increment of a counter: the thing that must not happen twice")
print()
# --- Часть 1: три ответа о поведении --------------------------------
without_key = retries(use_keys=False)
with_key = retries(use_keys=True)
late_key = simultaneous(claim_key=False)
print(without_key)
print(with_key)
print(late_key)
# --- Часть 2: цена нового ключа на каждую попытку --------------------
#
# ПОЧЕМУ КРАТНОСТЬ ПЕЧАТАЕТ ПРОГРАММА. Деление в уме на стороне редакции —
# ровно тот шаг, где вкрадывается ошибка, которую потом нечем поймать.
one_key = retries(use_keys=True, times=3)
many_keys = fresh_key_each_time(3)
print()
print(f"attempts in each case {3}")
print(f"effects with one key {one_key}")
print(f"effects with a new key each time {many_keys}")
print(f"times more effects {many_keys / one_key:.1f}")
if __name__ == "__main__":
main()