Deep Engineering

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()