Спящие баги: модуль здоров, система болеет

Любимый самообман в разработке звучит так: “У нас всё работает, проблема на вашей стороне”. Обычно после этой фразы и начинается самое интересное.

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

Один сервис считает пустую строку нормой. Другой ждёт null. Третий ретраит событие. Четвёртый кэширует старое значение. Пятый вообще понимает статус processed как “почти готово”, а не “точно завершено”. И вот тут баг просыпается.

По сути, спящий баг — это дефект, который не проявляется в изоляции, но вылезает на взаимодействии модулей. Не потому, что интеграция что-то сломала. А потому, что интеграция наконец показала, что в системе и так лежало мёртвым грузом.

Да, это очень похоже на герпес.

Пока всё спокойно, он сидит тихо. Потом приходит стресс: нагрузка, новый сервис, асинхронность, дубликаты событий, смена схемы, таймауты. Иммунитет архитектуры проседает, и наружу вылезает то, что раньше казалось “стабильной системой”.

В этом и подлость таких багов: каждый модуль по отдельности может быть даже не ужасным. Проблема начинается на стыке ожиданий.

Типовые симптомы простые:

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

Отсюда и главный вывод: фраза “в моём модуле всё ок” почти ничего не значит. В сложной системе качество живёт не внутри отдельных кусков кода, а между ними.

Лечатся спящие баги тоже не магией:

- контрактами с нормальной семантикой, а не только типами полей; - интеграционными тестами на плохие сценарии, а не только на happy path; - идемпотентностью там, где есть ретраи и события; - явной фиксацией побочных эффектов; - наблюдаемостью, которая показывает цепочку, а не просто “что-то упало”.

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