Skip to content

Журнал движений и кэш остатка расходились в ERPNext десять лет: задним числом, вперёд, через гонку, через вторую дверь. Последний коммит удаляет мёртвую функцию — только за то, что она второй путь записи.

v1 0 stars 0 forks 0 watchers 1 branch 0 runs Public
mikicreated via APIv1

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_data

No 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

Удалили все движения — а число осталось. Последний кадр десятилетия: даже опустевший журнал должен доходить до кэша.

Что это значит для нас

1
Брать число из журнала, а не писать рядом с ним

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

Why: Две отдельные записи подряд — это договорённость, а не гарантия. Между ними можно умереть, и останется число без события, которое его объясняет.
Check
  • Событие и число пишутся одной транзакцией
  • Число вычисляется из события, а не приходит отдельным аргументом
2
Пересчитать двери к такому числу

Выписать все места, где кэшируемое число вообще меняется. Мёртвые пути — удалить, а не оставлять на всякий случай.

Why: Ошибка обычно не в арифметике, а во втором пути записи, про который забыли. ERPNext удаляет даже никем не вызываемую функцию — чтобы её не воскресили.
Check
  • Список мест записи составлен грепом, а не по памяти
  • Каждое место либо идёт через общую точку, либо удалено
  • Имя функции говорит, откуда берётся число
3
Завести сверку — правка кода не чинит данные

Запрос, сравнивающий кэш с суммой событий. Только читает, ненулевой код возврата при расхождении.

Why: Правка закрывает будущее. Расхождение, однажды возникшее, живёт в данных молча — в интерфейсе это обычное число, и никто не узнает.
Check
  • Сверка запускается регулярно, а не один раз
  • Проверено запуском: подложенное расхождение действительно ловится
  • Сверка ничего не чинит сама — какое число верное, решает человек

И про сроки.

Между первым кадром и последним — десять лет, и это в одной из самых больших открытых ERP, с сотнями участников и тысячами установок. Не потому что там работают плохо.

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

Такие места не чинят — их сводят к одной двери и потом сторожат.

Пятеро разработчиков ERPNext по очереди чинили одну трещину, и никто из них не ошибался. Пока один не спросил: а почему это вообще работает? Оказалось — по случайному свойству чужой базы данных.

v1Public 0 0
updated Sep 1, 2026

Как проект живёт и принимает чужие правки, и кто в нём встречался в разборах — по поступкам, датам и ссылкам

v2Public 0 0
updated Sep 2, 2026

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

v3Public 0 0
updated Sep 1, 2026

graphile-worker: очередь на Postgres, замеры в комментариях и настройка, не пережившая полутора лет

v2Public 0 0
updated Sep 1, 2026

Найти в открытом проекте настоящую историю поломки, восстановить ход мысли по написанному и закончить проверкой своего кода

v10Public 0 0
updated Sep 1, 2026

Как проект живёт и принимает чужие правки, и кто в нём встречался в разборах — по поступкам, датам и ссылкам

v2Public 0 0
updated Sep 2, 2026