Deep Engineering

MEASUREMENT

bench/deadlines/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/timeouts-deadlines
How to run it
python3 bench/deadlines/chain.py    > bench/deadlines/runs/chain.txt
python3 bench/deadlines/practice.py > bench/deadlines/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

Замеры для урока «Таймауты и дедлайны»

Файл Что делает
chain.py цепочка из трёх сервисов на loopback: что происходит с работой после ухода клиента без бюджета и с бюджетом, и почему таймауты по цепочке не складываются в таймаут клиента
practice.py ответы к задачам урока: ответ клиенту в обоих режимах и число сервисов, взявшихся за работу

Запуск из корня репозитория:

python3 bench/deadlines/chain.py    > bench/deadlines/runs/chain.txt
python3 bench/deadlines/practice.py > bench/deadlines/runs/practice.txt

Что воспроизводимо

Счётчики: без бюджета работу доводят до конца все три сервиса, с бюджетом — один, а два отказываются. И то, что суммарное время цепочки больше таймаута любого отдельного шага.

Не воспроизводимо: миллисекунды. Выдержку 400 мс задаёт сам скрипт, поэтому измерять её — измерять собственную константу; содержательны счётчики.

Требования к среде

Только loopback и право открывать порты. Сервисы поднимаются потоками внутри одного процесса и закрываются вместе с ним.

Числа сняты на CPython 3.11.15, Linux 6.18.44.

Script

86 lines
"""Практика к уроку про таймауты и дедлайны: три ответа и одна цена.

ЗАЧЕМ ОТДЕЛЬНЫЙ ФАЙЛ. Задачи урока проверяются сборкой
(scripts/validate-practice.mjs): показанный читателю код обязан построчно быть
в скрипте, верный вариант — дословно встречаться в записи прогона, а
заявленное число — печататься самой программой.

ЧТО ЗДЕСЬ ПРОВЕРЯЕТСЯ. Одна и та же цепочка из трёх сервисов вызывается
дважды: без бюджета и с бюджетом. Ответ клиенту в двух случаях разный, но
интереснее другое — сколько работы сделано после того, как клиент ушёл.

Сервисы берутся из `chain.py`, чтобы задача и разбор урока стояли на одном и
том же коде, а не на двух похожих.

ЗАПУСК: python3 bench/deadlines/practice.py
Вывод: runs/practice.txt
"""

import os
import sys
import time

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from chain import CLIENT_TIMEOUT, WORK_MS, Service, call  # noqa: E402


def chain_of_three() -> tuple[Service, Service, Service]:
    tail = Service(work_ms=WORK_MS)
    middle = Service(work_ms=WORK_MS, downstream_port=tail.port)
    front = Service(work_ms=WORK_MS, downstream_port=middle.port)
    for svc in (tail, middle, front):
        svc.start()
    time.sleep(0.1)
    return tail, middle, front


def run(budget_ms: int) -> tuple[str, int, int, int]:
    """Ответ клиенту, число выполнивших работу, число отказавших не начиная и
    число тех, кого вообще не позвали.

    ПОЧЕМУ ТРИ ЧИСЛА, А НЕ ДВА. Суммарный счётчик отказов считает и отказ
    браться за работу, и отказ, который вызывающий передал дальше, получив
    `deadline` от следующего. Для урока это разные события, и складывать их
    в одно число значит рассказывать про прогон то, чего в нём нет.
    """
    tail, middle, front = chain_of_three()
    answer = call(front.port, budget_ms=budget_ms, timeout=CLIENT_TIMEOUT)
    time.sleep(1.6)
    done = tail.completed + middle.completed + front.completed
    refused_start = tail.refused_start + middle.refused_start + front.refused_start
    never_called = sum(1 for svc in (tail, middle, front) if svc.called == 0)
    return answer, done, refused_start, never_called


def main() -> None:
    print(f"Python {sys.version.split()[0]} · Linux {os.uname().release} · loopback")
    print()

    # --- Часть 1: три ответа о поведении --------------------------------
    budget = int(CLIENT_TIMEOUT * 1000)
    without_deadline, worked_without, _, _ = run(budget_ms=-1)
    with_deadline, worked_with, refused, uncalled = run(budget_ms=budget)
    print(without_deadline)
    print(worked_without)
    print(with_deadline)

    # --- Часть 2: сколько работы сделано впустую -------------------------
    #
    # ПОЧЕМУ СЧЁТ СЕРВИСОВ, А НЕ ВРЕМЯ. Миллисекунды здесь задаёт сам скрипт
    # (каждый сервис «работает» 400 мс), поэтому измерять их — измерять
    # собственную константу. А вот сколько звеньев цепочки взялись за работу
    # после ухода клиента — свойство механизма: без бюджета не знает никто,
    # с бюджетом знают все.
    print()
    print(f"client timeout, ms                   {budget}")
    print(f"work each service does, ms           {WORK_MS}")
    print(f"services that worked without budget  {float(worked_without):.1f}")
    print(f"services that worked with budget     {float(worked_with):.1f}")
    print(f"services that refused before start   {refused}")
    print(f"services never called at all         {uncalled}")


if __name__ == "__main__":
    main()