TGViewer
Channel Public Channel
Сергей Озеранский

Сергей Озеранский

@sergeiozeranskii

О разработке, DevOps, DevSecOps, архитектуре, безопасности. Без воды и скучных теорий - только реальный мой опыт, инсайты из практики и немного жизни.
Subscribers
2.35K
Photos
233
Videos
27
Links
183
Recent Posts 20 shown
Post #616 74
Еще немного про ускорения. На этот раз про тесты, а точнее про coverage (Python).

Библиотека coverage.py используется почти во всех проектах, где я работал, и де-факто стала стандартом для отчетов о покрытии тестами. Но я ни разу не видел, чтобы ее работу оптимизировали.

У coverage.py три ядра измерения: ctrace (C-реализация sys.settrace), pytrace (то же на чистом Python) и sysmon (на базе sys.monitoring, PEP 669, Python 3.12+). Ядро выбирается переменной COVERAGE_CORE или, начиная с coverage 7.9, настройкой [run] core.

Нас интересует sysmon. Разберем, почему он быстрее и когда его включать.

settrace вызывает trace-функцию на каждое выполнение каждой строки, хотя для покрытия достаточно знать, выполнялась ли строка хоть раз. sys.monitoring позволяет отключить событие после первого срабатывания, и дальше этот код работает без накладных расходов. По оценке автора coverage.py, для line coverage оверхед часто ниже 5%.

Пример из практики: Trail of Bits на тестах PyPI (Python 3.12, поверх `pytest-xdist`) ускорили прогон с 58 до 27 секунд. Замер сделан в 2024 году. На актуальных версиях coverage в конфигурации "3.12 + branch coverage" такого эффекта уже не будет, почему - ниже.

Но есть нюансы:

• На 3.12/3.13 sysmon не работает с branch coverage: при branch = true coverage выдает предупреждение и измеряет через ctrace, ускорения нет. На 3.14+ sysmon умеет измерять и ветки, так что branch coverage тоже ускоряется.
• concurrency = gevent / eventlet / greenlet и dynamic_context в конфиге: coverage переключается на ctrace.
• --cov-context=test в pytest-cov: переключения на ctrace нет, и контексты под sysmon теряются. До 7.15.3 это происходило без сообщений, сейчас с предупреждением.
• Плагины (Cython, django_coverage_plugin и другие) под sysmon не работают.
• Реализация молодая: исправления для sysmon выходили в 2025-2026 годах (ложные missing branches, KeyError на сложных условиях и except*, падения на Jinja-шаблонах). Держите coverage.py свежим.

Как проверить, что заработало
На 3.14+ sysmon выбран по умолчанию, при конфликтах coverage переключается на ctrace. coverage run --debug=core покажет, какое ядро реально используется и почему.

Как итог:

• 3.14+: ничего не менять, проверить через --debug=core.
• 3.12/3.13, только line coverage: включать.
• 3.12/3.13 с branch coverage: включать бессмысленно.
• Нужны контексты по тестам или плагины: sysmon не подходит.

Документация: coverage.readthedocs.io/en/latest/config.html#run-core
  • 👍 1
  • 🔥 1
  • 👏 1
Post #615 423
Обнаружил, что агенты Claude Code в разных сессиях умеют общаться между собой.

У меня сейчас три сессии, каждая работает со своим репозиторием. Я как правило просил агента описать задачу для другого агента в друго репо, а вчера агент сходил в другую сессию, уточнил детали и применил полученную информацию. Я ему говорю, что без моего ведома так не делай, информируй, когда ходишь куда-то. Но это очень удобно, например, агенту в одном репо нужно изменение на стороне другого сервиса, и он не ждет меня и не гадает, а сам формулирует задачу соседней сессии, та задает встречные вопросы, и они договариваются.

Не то чтобы это полная автономия, я пока не очень готов к такому, но это точно удобней копипасты между сессиями.
  • 🤔 6
  • 👨‍💻 3
  • ❤ 2
  • 👾 2
Post #614 558
Возьмем Python-сервис в Kubernetes. Как правило на старте контейнера может проявляться спайк потребления ресурсов (CPU в основном) и даже может быть тротлинг (если у пода задан CPU limit), а поды начинают отвечать на health-проверки только через 6-7 секунд после старта, а может и того больше или уйти в crashloop.

Если измерить (например, через профайлер), на что уходит это время, окажется, что часть времени занимает импорт модулей (я про проблему импортов в Python даже делал доклад и писал посты), а не инициализация приложения.

Причина в том, что в образе нет байткода после сборки. Официальный образ python удаляет файлы .pyc при сборке, чтобы уменьшить размер, поэтому стандартная библиотека (stdlib) лежит в нем без байткода.

Poetry и uv по умолчанию не компилируют зависимости при установке. Часто в Dockerfile еще задан PYTHONDONTWRITEBYTECODE=1 и приложение работает не от root. Тогда Python не может сохранить скомпилированные модули, и каждый процесс при каждом старте заново компилирует их из исходников. В сервисе среднего размера это несколько тысяч модулей.

Сохранять байткод во время работы бесполезно: каждый новый контейнер стартует с файловой системы образа. Компилировать нужно при сборке, для этого достаточно двух команд в Dockerfile.

В базовом этапе, от root, компилируем стандартную библиотеку:

RUN python -m compileall -q -j0 "$(python -c 'import sysconfig; print(sysconfig.get_path("stdlib"))')"


В финальном этапе компилируем приложение и зависимости:

RUN python -m compileall -q -j0 app/ .venv/


В моих замерах время импорта приложения сократилось вдвое, под в кластере стал готов вдвое быстрее. За первые 90 секунд после старта под израсходовал на 45% меньше CPU, память после старта снизилась на 6%. В установившемся режиме потребление не меняется: байткод влияет только на импорт. Образ вырастает примерно на 4%.

Изменение безопасно. Python сравнивает .pyc с исходным файлом и не использует устаревший байткод, поэтому ошибка в этом шаге не приведёт к выполнению старого кода. Документация uv рекомендует компилировать байткод в продовых образах. У Poetry для этого есть флаг poetry install --compile. pip компилирует байткод по умолчанию.

Одно ограничение: compileall возвращает ошибку, если в каком-нибудь пакете окажется файл с синтаксисом, который текущая версия Python не разбирает. Сборка тогда упадет. pip и poetry install --compile такие файлы пропускают.

Проверить свой образ можно так:


docker run --rm --entrypoint sh <image> -c \
'find / -name "*.py" -path "*python3*" | wc -l; find / -name "*.pyc" -path "*python3*" | wc -l'


Первое число - количество исходных файлов, второе - файлов байткода. Если второе намного меньше первого, образ компилирует модули при каждом старте.

Изменение ускоряет запуск контейнера и уменьшает всплеск CPU при старте. Поэтому при настройке requests и limits в Kubernetes можно закладывать меньше запаса на старт приложения.
  • 🔥 12
  • 👍 3
  • ❤ 2
  • ⚡ 1
Post #613 573
Сегодня случилось страшное.

То, чего я никак не ожидал. За все годы на MacBook со мной такого ни разу не было.

Сегодня случился kernel panic.

Судя по логам, ядро уронил watchdogd, потому что диск минуту не отвечал после пробуждения.
  • 🤔 8
  • 👾 5
  • 👨‍💻 3
Post #612 999
Я вот не понимаю, зачем люди идут в OSS и контрибьютят на отвали. Ценности в этом ноль для всех: ни мейнтейнеру, ни самому контрибьютору.

По-моему, OSS как раз то место, где инженер должен выложиться на максимум. И контрибьютить в OSS пиздец как круто. Тут никто не бьет тебя по рукам со словами "нет времени делать хорошо". Нет дедлайна от продакта, нет "давай на скорую руку, потом поправим". Есть только ты, твоя работа и твое творчество.

Поэтому для себя я делаю простой вывод: то, что человек делает в OSS, это потолок того, что он вообще умеет. Лучше уже не будет.

Вы скажете, что я слишком строг. Но как можно нести PR на ревью, когда у тебя падает линтер? Серьезно. Такое несут, и это дичь. Даже сейчас, когда есть LLM. И даже вопрос не в линтере, речь в том числе про степерь погруженности в задачу, про изучение документации и прочего, что помогает сделать ожидаемое действительностью.

Я не строгий, я объективный. OSS это тоже работа, и на нее уходит время. Причем не только время контрибьютора, но и мейнтейнеров, которые по ту сторону так же впахивают, часто бесплатно и по вечерам, чтобы выдать продукт, которым пользуются люди. Да, OSS это продукт. И к нему применимо все то же, что и к продуктовой разработке.

Я очень уважаю мейнтейнеров, ведь я бесплатно пользуюсь результатами их труда. Поэтому, если я что-то контрибьючу, то сначала сам все проверяю: тесты, линтер, описание, чтобы было понятно, что и зачем. Сверяюсь со стандартами репозитория и так далее. Для меня это и есть благодарность. И это в первую очередь не звездочка на гитхабе, а PR, который не нужно гонять по кругу из десяти правок. Чтобы мейнтейнерам было приятно его открыть, а не тошно.

Уважайте чужое время. В OSS это особенно видно. И все что выше описано, это все релевантно и в рабочих задачах.
  • 👍 12
  • 🔥 6
  • 👏 3
  • ❤ 1
  • 🦄 1
Post #611 1.3K
Помните про https://github.com/ozeranskii/httptap?

Я писал о нем давно еще - > тут.

Наклепал много issue, для тех кто хочет вкатиться в OSS или попрактиковаться себя и свою LLM - welcome. Только, пожалуйста, без нейрослопа и не будьте meat-proxy. Не хочу тратить время на фиксы фиксов. Ибо вот даже простой фикс, я исправил (смотри историю коммитов в PR), так как почитал документацию, а автор видимо нет.
GitHub GitHub - ozeranskii/httptap: Rich-powered CLI that breaks each HTTP request into DNS, connect, TLS, wait, and transfer phases with… Rich-powered CLI that breaks each HTTP request into DNS, connect, TLS, wait, and transfer phases with waterfall timelines, compact summaries, or metrics-only output. - ozeranskii/httptap
Post #610 726
Поломали или у меня поломалось? Как у вас?
Post #608 792
Я понимаю, что полностью нейротекст читать, возможно, неинтересно. Но в части моих текстов я не припомню такого, чтобы мой текст был полностью написан с помощью LLM, потому что я хочу как раз сохранять свою естественность, своё настроение, свой стиль и так далее. Мой стиль, на самом деле, ещё и можно иногда перепутать с искусственным интеллектом, потому что я привык излагать свои мысли достаточно подробно, чётко и разжёвывать. У меня была проблема, и сейчас она иногда проявляется, что я слишком детально рассказываю что-либо людям — просто потому, что мне это интересно, я хочу, чтобы человек понял. Я, наверное, был бы хорошим преподавателем, но мне нужны хорошие ученики, которым это интересно. И я хороший ментор — это моя и субъективная, и объективная оценка, иначе люди не шли бы ко мне за менторской помощью.

Соответственно, у меня был даже случай, когда в одном из постов меня по сути обвинили в том, что это искусственный интеллект. Это был не последний пост, уже давно, и я, по-моему, даже особо ничего не ответил: ну сказано и сказано, а вопроса нет, то есть на что отвечать? На комментарии не всегда будешь отвечать, потому что комментарий может и не предполагать какого-то призыва к действию. Не то чтобы это обидно, но я вижу в этом в первую очередь вот это наше искажение восприятия реальности: мы стали меньше доверять, и искусственный интеллект это доверие нам подорвал. Раньше тоже было всякое разное, но это в большей степени создавали люди, а сейчас искусственный интеллект может создавать кучу фейков и прочего-прочего в интернет-пространстве, так что наша когнитивная нагрузка на фильтрацию контента стала только выше.
  • 👍 4
  • ❤ 3
Post #607 669
Искусственный интеллект нас очень сильно продвинул вперёд, но создал, как мне кажется, ещё одну проблему. Он подорвал доверие людей к людям.

Раньше, до искусственного интеллекта, мы хоть как-то верили тому, что люди пишут или создают какой-то цифровой контент в интернете. Да, был Photoshop, да, были и другие инструменты, которые ровно так же на самом деле помогали людям с созданием цифрового контента, как сейчас в том числе помогает искусственный интеллект. Раньше мы, как мне кажется, в принципе не сильно обращали внимание на то, что какая-то картинка отретуширована или в неё внесены какие-то корректировки. Мне, говорю за себя, в целом было на это особо без разницы: я знал, что за интересным мне контентом стоят реальные люди, и мне было интересно их слушать и воспринимать. А то, что мне было неинтересно, я и так не читал.

Сейчас искусственный интеллект — это тот же самый Photoshop, только в части текстов, так мне кажется. И лично я некоторые свои тексты перед публикацией пропускаю через искусственный интеллект, чтобы он исправил мои ошибки. Потому что пишу я с ошибками: я часто тороплюсь, пишу неправильные окончания слов — думаю, вы это частенько замечаете, и даже кто-то мне об этом пишет, я потом исправляю. Это очень классно: с одной стороны, я таким образом знаю, что мои тексты читают, что мне указывают на ошибки. Но я хочу давать качественный контент, который будет приятен для чтения и при этом, даже с условием того, что пост по сути обработал искусственный интеллект, всё ещё сохранять моё авторство, моё настроение, мой стиль изложения и так далее. Я не создаю тексты с нуля в искусственном интеллекте, я лишь ретуширую тексты, и то не все. Для меня как раз важно сохранить ту самую человечность.

Но вот что я заметил: у инженеров есть профессиональная деформация, и она во многом проявляется и в части искусственного интеллекта, я её сейчас вижу очень отчётливо. Когда-то давно, когда искусственный интеллект только начал приходить к нам в разработку, мы стеснялись его, так как считали, что он принижает наше профессиональное участие, да и профессионализм тоже как бы занижает — тем, что мы по факту пользуемся подсказкой. А старые инженеры привыкли решать задачи сами, проходя весь путь через пот, слёзы и кровь, и искусственный интеллект немножко шатал в этом направлении психику, мне кажется. Дальше люди приняли, что это нормально, что это всего лишь инструмент, который позволяет писать код эффективнее. И разработка уже на самом деле никогда не станет прежней. Я сомневаюсь, что в будущем мы решим, что писать код руками лучше. Где-то лучше, конечно, наверное, ещё остаются такие секторы, но в массовых задачах это как будто теряет какой-то практический смысл, и объяснить неиспользование искусственного интеллекта в разработке уже намного сложнее, чем его использование.

И когда инженер постоянно общается с искусственным интеллектом, он так или иначе видит какие-то паттерны его поведения и начинает замечать такие же похожие паттерны — или сам их воспроизводить — в написании текстов. Например, вы читаете одно и то же каждый день, общаетесь с одной и той же системой искусственного интеллекта, и когда видите где-то что-то похожее на то, что уже встречали в своей ежедневной работе, это сразу воспринимается как «кто-то что-то сделал с помощью искусственного интеллекта». Я думаю, вы понимаете, о чём я. И я считаю, что это частично профдеформация. Человек, который нацелен на то, чтобы изучить что-то новое, даже если это, как мне кажется, отретушировано с помощью искусственного интеллекта, пропустит этот момент, потому что в тексте всё равно есть экспертиза человека, который его написал.
  • ❤ 5
  • 👍 1
Post #606 695
Принес вам полезность - infosec.mozilla.org

Это публичные security-гайдлайны Mozilla. Рекомендации по безопасности написаные инженерами для инженеров. Регулярно заглядываю, при настройке чего либо и так же LLM у меня имеет этот сайт в списке референсов по DevSecOps, на равне с OWASP и NIST.

Чем полезен сайт:
— Web Security - чеклист заголовков и настроек для веба (CSP, HSTS, cookies, CORS). У каждого пункта приоритет и объяснение, зачем.

— OpenSSH - какие алгоритмы и ciphers оставить, какие выкинуть, готовые конфиги сервера и клиента. Тот случай, когда не нужно изобретать, а достаточно следовать best practices.

— Rapid Risk Assessment - методика оценки рисков сервиса. Понятный алгоритм и формула.

— Key Management - алгоритмы, длины ключей, сроки ротации.

— ssl-config.mozilla.org (configurator.tlsref.org) - генератор TLS-конфигов под nginx/haproxy/postgres и еще десяток серверов.

Если делаете сервис, на который не наплевать, то это сэкономит вам (или вашей LLM) время/токены.

Сохраняй пост, чтобы потом его никогда не прочитать.
  • 🔥 10
  • ❤ 5
  • 👍 4
Post #605 701
Ситуация. У вас сборка образа стала падать на этапе проверки CVE. Вы проверили и видите, что базовый образ Debian, который вы используете, еще не имеет обновленных пакетов, но сами пакеты fixed.

Ваши действия?
1. apt-get update && apt-get -y upgrade && rm -rf /var/lib/apt/lists/*
2. добавить CVE в игнор, с датой окончания игнорирования
3. собрать собственный базовый образ
3. свой вариант в комментариях
Post #604 1.32K
Вижу риск для инженерии в будущем.

Всё написанное дальше — результат моих наблюдений и умозаключений. Могу ошибаться, а могу и не ошибаться. Я же человек.

С приходом LLM увеличилась скорость разработки, точнее скорость написания кода, в первую очередь. Конечно, LLM помогает на разных стадиях, но именно написание кода — это, наверное, та функциональность, которая ушла в массы. Давайте честно: мы и раньше не всегда успевали делать код-ревью коллег, а тут появился новый коллега, а иногда и не один, и звать его LLM — с кучей субагентов, и каждый выдаёт какую-то информацию. Собственно, отсюда и нейрослоп. Но, как мне кажется, его перестают бояться, ибо «кому вообще нужен этот ваш код, главное — фича есть». Качество тут будет хромать: баги, стоимость поддержки, безопасность. Понятно, что решать это теперь тоже будет LLM (ага).

Второй момент. Вчерашний джун теперь может «выдать» то, в чём сам не разобрался и чего не понял. Вот тут страшновато: каким бы прекрасным ни был промпт и как бы LLM его ни обработала, результат надо проверять. А как это сделать инженеру, если ему LLM про Кафку, а он про неё не алё? Поэтому у него два выхода, точнее три:
— вникнуть, изучить и стать лучше;
— довериться LLM, она умная;
— оставить как есть, другие разберутся (я про сеньоров в команде, которые стали инженерами до LLM и пропускали всё это дерьмо через себя).

При этом для себя LLM оцениваю очень позитивно: я стал сильно производительнее, и результат нашего с ней труда меня устраивает. Работаю я так же, как и без неё, только код руками теперь почти не пишу. Но так же его читаю, так же проектирую, так же задаю кучу вопросов и перепроверяю всё, что мне сказано. И новое изучаю быстрее — тут вообще сильно прокачался. Но опять же: я много раз видел, как LLM решает, казалось бы, лёгкую задачу с третьего-четвёртого раза. Не потому что она глупая, а потому что не видела того, что видел я, и не думала о том, о чём думаю я. Вот это и делает меня не meat-proxy. И я очень этому рад )))

И виноват тут не LLM. Я считаю, что человек по природе ленив, а быстрый результат — это, как правило, дофаминовый допинг. LLM его выдаёт щедро: не нужно теперь читать документацию и вникать в детали (на самом деле нужно, и даже сильнее, чем до LLM). К чему это приведёт — пока не знаю.
  • 👍 8
  • ❤ 6
  • 🔥 2
Post #600 1.34K
Леджер: журнал, которому можно верить

Баланс говорит, сколько у клиента денег. Леджер объясняет, откуда взялась эта цифра. Сегодня про него: как устроена таблица, почему в неё нельзя записать неверную строку и что происходит, когда одну и ту же операцию присылают дважды.

Только добавление

Леджер — таблица, в которую можно только вставлять. У репозитория нет методов update и delete для неё. Ошибку в леджере не исправляют, её объясняют новой записью.

В каждой строке: тип операции, сумма, баланс до, баланс после, ключ идемпотентности, ссылка на источник (джоба, платёж, возврат) и кто это сделал.

Семь типов операций

Три уменьшают баланс:
debit — списание за джобу,
refund — возврат клиенту через платёжного провайдера,
reversal — зарезервирован для внутренних отмен.

Три увеличивают:
credit — пополнение,
bonus — начисление сверх оплаты,
free_grant — стартовый грант.

Седьмой, adjustment, единственный со знаковой суммой: его пишет сверка, о которой ниже.

Направление операции задаётся типом, а не знаком. Сумма всегда положительная. Так по одной колонке видно, что это за операция, и нельзя случайно записать "списание на минус пять долларов".

Postgres проверяет арифметику

На таблице стоит CHECK:

(type IN ('debit','refund','reversal')
AND balance_after = balance_before - amount)
OR (type IN ('credit','bonus','free_grant')
AND balance_after = balance_before + amount)
OR type = 'adjustment'


Строку, в которой числа не сходятся, база отклонит, даже если в коде баг. Это последняя линия обороны, и она не зависит от того, кто и откуда пишет.

Откуда берутся баланс до и после? В прошлом посте списание делалось одним UPDATE с RETURNING, и именно возвращённое значение попадает в леджер. Одна транзакция: изменили баланс, получили новый остаток, вставили строку с ним. Разойтись эти два числа не могут.

Одна операция — одна запись

Мир вокруг нашего кода ненадёжен. Платежный провайдер присылает webhook об оплате, не получает ответ вовремя и присылает ещё раз. Воркер списания падает после UPDATE, но до commit, и повторяет цикл. Ретрай после сетевой ошибки. Во всех случаях операция одна, а попыток несколько.

Каждая операция несёт уникальный ключ: payment:<id платежа>, job:<id>:period:<начало периода>, refund:<id возврата>, grant:user:<id>. На колонке UNIQUE-индекс. Защита от дублирования на уровне БД.

Схема записи:

SAVEPOINT → UPDATE баланса → INSERT в леджер с ON CONFLICT DO NOTHING.


Если INSERT вернул строку, savepoint фиксируется. Если не вернул, ключ уже есть: savepoint откатывается, UPDATE баланса отменяется вместе с ним, а наружу возвращается существующая запись с пометкой "уже применено".

Почему не проверить ключ до UPDATE? Потому что между проверкой и вставкой второй процесс успеет сделать то же самое. Арбитром должен быть уникальный индекс, а не код. Savepoint нужен, чтобы откатить только эту операцию, не трогая внешнюю транзакцию: воркер списания держит блокировку на строке джобы, и терять её из-за дубликата нельзя.

Леджер это не двойная запись

В бухгалтерии каждая операция затрагивает два счёта, и сумма дебетов всегда равна сумме кредитов. У нас single-entry: одна строка, один аккаунт, направление в типе. Платформа ведёт только один вид счёта — обязательства перед клиентами. Выручка, комиссии провайдера и деньги на расчётном счёте живут в бухгалтерии. Если появятся внутренние взаиморасчёты между несколькими видами счетов, double-entry окупится. Пока это усложнение без выгоды.

Сверка

Баланс — денормализованная сумма леджера. Это ускоряет горячий путь, но два числа могут разойтись, если где-то есть баг. Раз в десять минут воркер считает по леджеру сумму со знаками для каждого аккаунта и сравнивает с хранимым остатком.

При расхождении: ERROR в лог и одна строка adjustment, где баланс до — сумма леджера, баланс после — хранимый остаток. Сам остаток не меняется: он источник истины для того, сколько клиенту можно тратить, и сверка не должна молча менять эту сумму. Она фиксирует разницу, чтобы леджер снова объяснял остаток целиком.

В норме таких строк ноль. Каждая — повод искать баг.
  • ❤ 5
  • 👍 3
  • 🔥 3
Post #598 867
Баланс: как хранить деньги клиента и не ошибиться

В прошлом посте разделили биллинг и леджер: баланс разрешает операции, леджер их объясняет. Сегодня про первую половину — таблицу баланса.

Одна строка на клиента

Баланс — это отдельная таблица с одной строкой на аккаунт. В ней текущий остаток и два накопительных счётчика: сколько всего зачислено и сколько всего потрачено. Счётчики не участвуют в решениях, они для дашборда и аналитики. Решает только остаток.

В чём хранить

Не во float. Это база: 0.1 + 0.2 в двоичной арифметике не равно 0.3. Баланс меняется каждые несколько секунд работы каждой джобы, это тысячи мелких списаний, и ошибка округления в нём накапливается.

Не в центах. Ставка раннера — доли цента за минуту, а списываем мы за секунды. В центах такая операция либо округляется в ноль, либо требует накопителя дробных остатков рядом с балансом.

Мы храним целое число микро-долларов: 1 USD = 1 000 000 единиц, BIGINT в Postgres. Целочисленная арифметика точна, а шести знаков после запятой хватает, чтобы за всю джобу накопить погрешность меньше одной единицы. Decimal живёт только на границах: ввод суммы пополнения и вывод пользователю.

Побочный эффект: ошибка в значении это значит ошибиться не на проценты, а в миллион раз. Поэтому число 1 000 000 в коде встречается ровно один раз, в модуле с прайсингом, а конвертация в доллары и обратно идёт только через две функции рядом с ним. Всё остальное работает в единицах и о долларах не знает.

Одна точка изменения

Баланс меняется только в одном классе — репозитории биллинга. Ни один сервис, роутер или воркер не пишет UPDATE в эту таблицу напрямую. Это гарантирует, что каждое изменение остатка сопровождается записью в леджер, проверкой лимитов и корректным ключом идемпотентности.

Списание одним запросом

Классическая ошибка: прочитать баланс, проверить в коде, что хватает, и записать новое значение. Между чтением и записью успевает другая операция, и клиент уходит в минус, которого никто не разрешал.

У нас проверка и изменение — одно выражение:

UPDATE tenant_billing
SET credits_balance = credits_balance - :amount
WHERE tenant_id = :id
AND credits_balance - :amount >= -:buffer
RETURNING credits_balance


Postgres берёт блокировку на строку, проверяет условие уже под ней и возвращает новый остаток. Два параллельных списания выстраиваются в очередь, второе видит результат первого.

Если запрос затронул ноль строк, это одно из двух: аккаунта нет или денег не хватает. Различаем отдельным запросом.

Зачем буфер

Обратите внимание на -:buffer в условии. Баланс может уйти в минус, но не ниже $1.

Причина в природе продукта. Проверки денег на старте джобы нет: пока баланс в порядке, раннеры клиента доступны, и джоба стартует. Дальше она работает минуты, а списание идёт по ходу. Если ровно на нуле отклонить очередное списание, джоба упадёт на середине, клиент потеряет и время, и уже потраченные деньги. Буфер даёт ей доработать.

При уходе ниже нуля аккаунт получает отметку блокировки, и его раннеры ставятся на паузу: новые джобы не получают исполнителя, работающие платформа не прерывает. Отметка ставится один раз и не перетирается повторными списаниями, так что видно, когда именно клиент ушёл в минус. Пополнение, выводящее баланс в плюс, отметку снимает.

Кто решает блокировать, а кто выполняет — разные слои. Репозиторий умеет только списать и вернуть остаток. Решение "остаток отрицательный, значит блокируем" принимает сервис. Так политику можно менять, не трогая механику.

Три способа уменьшить баланс

Обычное списание с буфером — для джоб. Увеличивает счётчик потраченного.

Принудительное списание без буфера — для возвратов. Refund применяется целиком, даже если клиент уже потратил часть пополнения; баланс уходит в минус, аккаунт приостанавливается. Счётчик потраченного не трогается: возврат — это не расход.

Отдельно стоит знаковая корректировка, и она баланс не трогает. Её пишет только фоновая сверка с леджером при расхождении: одна строка, объясняющая разницу. Ручных правок остатка нет: бонус можно начислить, но он пройдёт тем же путём, что и пополнение.
  • 👍 6
  • 🔥 2
  • ❤ 1
  • 👏 1
Post #597 912
Как устроены биллинг и леджер в tempus.build

tempus.build продаёт минуты CI-раннеров по предоплате. Списание посекундное, ошибки в деньгах недопустимы, а каждая финансовая операция должна быть прозрачна для аудита. Начну с теории, а в следующем посте покажу, как это реализовано.

Немного теории

В заголовке два слова: биллинг и леджер. Это два связанных инструмента, но решающих разные задачи. И прежде чем разбирать их, важно развести ещё одну вещь: биллинг не проводит платежи.

Проведением платежей занимается платёжный провайдер: авторизует карту, списывает деньги, делает расчет. Для tempus.build это внешняя система. Биллинг узнаёт от неё только факт: "от клиента X пришло N денег" — и с этого момента начинается его работа.

Биллинг — это тарификация и учёт. Он знает, сколько стоит секунда раннера, зачисляет пополнения, списывает за выполненные джобы и отвечает на вопрос "сколько у клиента денег сейчас и хватит ли на эту операцию". Его рабочий инструмент — баланс: одно число, которое читают перед каждой операцией и меняют после. Деньги в реальном мире биллинг не двигает, он ведёт их учёт внутри продукта.

Леджер отвечает на вопрос "как мы к этому числу пришли". Это история: журнал всех движений, каждое с суммой, направлением и причиной. В бухгалтерии этой идее очень много лет: ни одна запись в книге не стирается, ошибка исправляется новой записью.

Зачем разделять баланс и леджер? Потому что у них разные, местами противоположные требования.

Состояние должно быть быстрым и атомарным. Перед стартом джобы нужно за один запрос узнать, хватает ли денег, и сразу их зарезервировать, пока конкурирующая операция не сделала то же самое.

История должна быть неизменяемой и полной. По ней разбирают споры с клиентом, сверяют платежи с платёжными системами и банками, ищут баги в расчётах, строят аналитику. Если её можно править задним числом, ей нельзя верить. Это единственный источник истины обо всех движениях средств по балансу аккаунта.

Если хранить только баланс, при любой аномалии нечем объяснить, откуда взялась цифра. Если хранить только историю, каждая проверка "хватает ли денег" превращается в SUM по всей истории под блокировкой в БД. На горячем пути это неприемлемо. Да и просто неудобно.

Поэтому обе сущности живут рядом, но с чёткими ролями: баланс разрешает операции, леджер их объясняет. Главная инженерная задача — чтобы они никогда не разошлись.
  • 👍 3
  • 🔥 3
  • ❤ 2
  • 👏 1
Post #596 998
Зато точно. Или не очень? 😅

Пишите, в чём храните например, деньги или баллы: float, decimal, integer в минимальных единицах или свой вариант.
  • 👍 5
Post #594 1.24K
Если вы все еще хотите разбираться в программировании, а не просто просто нажимать “Yes” в консоле, то нашел интересный репозиторий https://github.com/faif/python-patterns

Это коллекция паттернов проектирования и идиом на Python.

Глобально, ничего нового там нет. Это в основном классические паттерны «банды четырёх», просто собранные на примерах и в одном месте. Так что, если хочется развиваться, то такое, я считаю, нужно знать даже с LLM.
GitHub GitHub - faif/python-patterns: A collection of design patterns/idioms in Python A collection of design patterns/idioms in Python. Contribute to faif/python-patterns development by creating an account on GitHub.
  • 👍 11
  • ❤ 6
  • 🦄 4
Post #593 1.3K
Хочется обнять всех, кому сейчас тяжело из-за того, что происходит с интернетом.

Я не в РФ, но сталкиваюсь с этим регулярно. Чуть больше года назад был в Мск месяц и страдал. И даже близко не представляю, насколько это тяжко сейчас. Держитесь 🫂
  • ❤ 27
  • 👏 2
  • ❤‍🔥 1
Post #592 1.33K
Значете, что меня в последнее время все более и более раздражает. То, что приходится решать проблемы, которых раньше и быть не могло, при решении очень простых, базовых, понях задач.

Да, это снова пост про яндекс облако. При всем моем уважении к поддержке яндекс (они классно работают, нет претензий), я выполняя казалось бы обычные задачи, сталкиваюсь с необычнми проблеми. И да, не всегда виноват яндекс облако, но так как я работаю с ним, то и упоминаю его.

Ситуация такая:

Собрал лендинг: Astro (SSG, чистая статика), выкатил на объектное хранилище с website-hosting + CDN перед ним. Открывается мгновенно, Lighthouse зелёный, OG-теги на месте. Все ок.

Кидаю ссылку в Telegram - превью не появляется. Открываю Facebook Sharing Debugger - HTTP 403 и приписка "This response code could be due to a robots.txt block. Please allowlist facebookexternalhit on your sites robots.txt config to utilize Facebook scraping". Ну я то знаю что дело не во мне.

Ну ок, поехали разбираться (а вообще для такого простого сценария я не должен дебажить это дерьмо)

1. robots.txt? -> User-agent: * / Allow: /. Чисто. Мимо.

2. Может, режется по User-Agent?

curl -A "facebookexternalhit/1.1" https://…/ -> 200
curl -A "TelegramBot" https://…/ -> 200

Оба 200. Не UA.

3. TLS? openssl s_client -> полная цепочка (leaf + 2 промежуточных), verify return code: 0. Тоже не то, да и FB получил именно HTTP 403, а не ошибку рукопожатия, значит соединение успешно установлено и FB получил "нормльный" HTTP ответ.

4. Мигает ли источник под нагрузкой?
40 параллельных запросов напрямую к website-эндпоинту хранилища, в обход CDN -> 40/40 = 200. Все ок. Да и это же CDN для статики! Какая нахрен нагрузка.

5. Может, гео-блок / вообще недоступность извне?
Прогнал через check-host из 12 стран (в т.ч. США и Нидерланды, там ДЦ FB и Telegram) -> везде 200. Сторонний OG-скраппер microlink.io (headless-браузер) -> корректно прочитал и title, и картинку. Даже в ВК (прости Господи) OG загрузился.

Burst 15 параллельных, HEAD-запрос, отдельно og-image, запрос с Range - все 200.

Из моего окружения 403 не воспроизводится ВООБЩЕ ничем. 200 получают: браузеры, curl с любым UA, 12 стран, США, Нидерланды, сторонние скрапперы. А 403 только реальные краулеры Facebook и Telegram.

Единственная переменная, которую я не могу подделать это исходные IP-диапазоны (ASN).

Еще раз проверил настройки CDN-ресурса: ограничение по IP нет, по странам нет, защита токеном нет. Конфиг пустой. Значит 403 прилетает на уровне edge/сети и фильтруются конкретные ASN краулеров.

Ну я пошел дальше в поддержку облака. Ответ был очень ожидаемым: "Со стороны Yandex Cloud фильтраций и ограничений трафика на уровне настроек ваших CDN-ресурсов нет — правила по IP, ASN, User-Agent и Referer не включены. Наблюдаемое поведение похоже на фильтрацию трафика на транзитных сетях за пределами инфраструктуры Yandex Cloud, повлиять на которую с нашей стороны напрямую нельзя."

"Заебись у вас хип-хоп", как читали в своем ганста репе группа Триагрутрика.

И все и всех понять помно. И Яндекс в той же ситуации, что и мы с вами. Им остается искать как и нам варианты решений. Но блин! Ну сколько можно!? И это вопрос не к Яндексу.

А помните, были времена когда мы на такую херню не тратили время и решали реальные инженерные задачи? Вот этот отрывок фильма вспомнился https://www.youtube.com/watch?v=oVrCN8ptO_8
YouTube Ублюдки хреновы я пришел на перестрелку... | Легенда Отрывок из фильма "Легенда" 2015 года, с Томом Харди в главной роли. Поддержать: https://vk.cc/c700Gt О фильме: Фильм расскажет историю близнецов Реджи и Ронни Крэй, культовых фигур преступного мира Великобритании 1960-х. Братья возглавляли самую влиятельную…
  • 💯 10
Older posts →

About this channel

How can I read @sergeiozeranskii without a Telegram account?
TGViewer shows the public web preview Telegram publishes for Сергей Озеранский: recent posts, photos, videos and the subscriber count, with no app, login or account.
How many subscribers does Сергей Озеранский have?
Сергей Озеранский (@sergeiozeranskii) has 2.35K subscribers on Telegram, refreshed roughly every 30 minutes.
Does Сергей Озеранский know I viewed it here?
No. Public channel previews carry no viewer identity, and TGViewer has no accounts or tracking of what you look up.
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →