Десять лет до одной двери
Журнал движений и кэш остатка расходились в ERPNext десять лет: задним числом, вперёд, через гонку, через вторую дверь. Последний коммит удаляет мёртвую функцию — только за то, что она второй путь записи.
miki/desyat-let-do-odnoy-dveri · v1
Журнал движений и кэш остатка расходились в ERPNext десять лет: задним числом, вперёд, через гонку, через вторую дверь. Последний коммит удаляет мёртвую функцию — только за то, что она второй путь записи.
ERPNext, 2016 — 2026.
В любой складской системе рано или поздно заводятся две вещи: журнал движений — что пришло, что ушло, когда и кто — и число «сколько сейчас лежит». Журнал надёжен, но по нему долго считать. Число быстрое, но это кэш.
У ERPNext они называются Stock Ledger Entry и Bin. Дальше — история про то, как эти двое расходились.
20 декабря 2021 — задним числом.
fix: correct bin qty on backdated transactions
Движение, проведённое задним числом, встаёт в середину журнала. Журнал это переживает — он про события. А число посчитано по тому, что было известно раньше, и после вставки в середину оно неверно.
4 июля 2022 — и в будущем тоже.
fix: LCV updates wrong future qty/Bin qty · PR #31515
Полтора года спустя — то же самое с другой стороны. Не прошлое портит число, а пересчёт стоимости портит будущие остатки.
Между этими двумя кадрами полтора года. Между вторым и следующим — четыре.
5 июля 2026 — оказывается, ещё и гонки.
fix(stock): close postgres lock-then-read races in pick list and stock reservation
Прочитали, потом заблокировали. Между «прочитали» и «заблокировали» успевает пройти чужая транзакция, и решение принимается по числу, которого уже нет.
16 июля 2026 — блокировка на пару.
fix(stock): serialize stock writes per (item, warehouse) with a txn advisory lock on postgres
Через одиннадцать дней. Заметьте формулировку: не «починили ещё один случай», а «сериализуем записи».
Через пять лет после первого кадра они перестали чинить проявления и начали чинить способ.
11 августа 2026 — одна дверь.
refactor: stock write-path cleanups (SLE/Bin chokepoint groundwork)
Chokepoint — узкое место, через которое обязана проходить любая запись. Внутри — три правки, и одна из них не про баг вовсе:
refactor: remove dead
update_entries_after.update_bin_dataNo callers anywhere in the codebase; it duplicates
update_bin()with subtly different semantics (no update_modified) and would only invite accidental resurrection as a second Bin write path.
Функция мёртвая. Её никто не вызывает. Её удаляют не потому, что она мешает, — а потому, что она вторая дверь к числу, и однажды кто-нибудь её откроет.
Там же bin.update_qty переименовывают в update_qty_from_sle.
Имя теперь само говорит, откуда берётся число: из журнала. Не «обнови остаток», а «пересчитай остаток по журналу».
25 августа 2026 — и ноль тоже число.
fix(stock): reset bin when no stock ledger entries remain · PR #58362
Удалили все движения — а число осталось. Последний кадр десятилетия: даже опустевший журнал должен доходить до кэша.
Что это значит для нас
Если у числа есть источник правды, оно обязано вычисляться из него и обновляться той же транзакцией, что пишет событие.
- Событие и число пишутся одной транзакцией
- Число вычисляется из события, а не приходит отдельным аргументом
Выписать все места, где кэшируемое число вообще меняется. Мёртвые пути — удалить, а не оставлять на всякий случай.
- Список мест записи составлен грепом, а не по памяти
- Каждое место либо идёт через общую точку, либо удалено
- Имя функции говорит, откуда берётся число
Запрос, сравнивающий кэш с суммой событий. Только читает, ненулевой код возврата при расхождении.
- Сверка запускается регулярно, а не один раз
- Проверено запуском: подложенное расхождение действительно ловится
- Сверка ничего не чинит сама — какое число верное, решает человек
И про сроки.
Между первым кадром и последним — десять лет, и это в одной из самых больших открытых ERP, с сотнями участников и тысячами установок. Не потому что там работают плохо.
Потому что кэш, у которого есть источник правды, расходится с ним не однажды, а постоянно и по-разному: задним числом, вперёд, через гонку, через вторую дверь, через опустевший журнал.
Такие места не чинят — их сводят к одной двери и потом сторожат.
Related lists
Пятеро разработчиков ERPNext по очереди чинили одну трещину, и никто из них не ошибался. Пока один не спросил: а почему это вообще работает? Оказалось — по случайному свойству чужой базы данных.
Как проект живёт и принимает чужие правки, и кто в нём встречался в разборах — по поступкам, датам и ссылкам
Семнадцать дней копий не было, и это выглядело точно так же, как если бы они были. Разбор чужой истории: флаг, который умел только портиться, мусор, забивший диск, и починка, приехавшая боком.
graphile-worker: очередь на Postgres, замеры в комментариях и настройка, не пережившая полутора лет
Найти в открытом проекте настоящую историю поломки, восстановить ход мысли по написанному и закончить проверкой своего кода
Как проект живёт и принимает чужие правки, и кто в нём встречался в разборах — по поступкам, датам и ссылкам

