Каждый разработчик хотя бы раз в жизни сталкивался с ситуацией, когда баг, который вроде бы уже исправлен, неожиданно возвращался в продакшен. В этой статье я расскажу о тех случаях, когда ошибки служили для меня не провалом, а очень настойчивым, но полезным учителем. Да, иногда именно они объясняют архитектуру, принцип работы систем или забытый corner case лучше самых толстых документаций. Эта статья не учит идеализму — наоборот, она про то, как ценить несовершенство.

Вступление: почему баги порой умнее нас

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

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


Когда невинный race condition превращает тебя в параноика

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

Я тогда полез смотреть код и увидел, что проблема пряталась в самой очевидной части. В элементарной структуре, которую два горутины обновляли одновременно. То, что я поначалу счёл «микрооптимизацией», в итоге стало уроком, который я запомнил навсегда.

Вот тот самый фрагмент кода на Go, который сейчас вызывает у меня нервный тик:

package main

import (
    "fmt"
    "sync"
)

var counters = map[string]int{}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            counters["hits"]++
            wg.Done()
        }()
    }
    wg.Wait()
    fmt.Println(counters["hits"])
}

Ты наверняка видишь проблему сразу. Тогда — я почему-то нет. И знаете, почему этот баг обучающий? Потому что я много раз читал про то, что map в Go не thread-safe, но прочитать — одно, почувствовать на своей боли — совсем другое. Теперь я смотрю на подобные участки кода гораздо подозрительнее.


Когда исправление бага раскрывает архитектуру, которую ты не замечал

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

Оказалось, что при очень высокой нагрузке сервер очередей вёл себя иначе, чем под тестовым окружением. Он доставлял сообщения в пачках, а не по одному. И мой потребитель, который был написан «по документации», в реальности не учитывал, что пакетная доставка может быть неполной или пересортированной.

И вот что интересно: если бы не этот баг, я бы никогда не стал изучать механизм batch-доставки так подробно. Для меня это был просто внутренний механизм очередей. А после — я стал понимать, как он влияет на задержки, на пропускную способность, на поведение под пиковыми нагрузками.

Минималистичный пример на Python, демонстрирующий ошибку в логике обработки:

def process_batch(messages):
    for m in messages:
        if validate(m):
            handle(m)
        else:
            # На тестах такого не было, но в проде —
            # сообщения иногда приходили частично испорченными
            log("Skipping message", m)

def validate(m):
    return "id" in m and "payload" in m

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


Когда баг оказывается зеркалом твоего же упрощённого предположения

Есть особый класс ошибок, которые я называю «наказанием за излишний оптимизм». Это те случаи, когда мозг говорит тебе: да ладно, тут не может быть edge-case. А реальность говорит: держи.

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

Смешно, что решение было тривиальным: пропустить данные через более строгий CSV-ридер и добавить санитайзер. Но главное — баг заставил меня отказаться от привычного допущения «да там нормальные данные, что с ними будет?».

Кстати, вот тот экспериментальный код на Java, который тогда всё ломал:

BufferedReader reader = new BufferedReader(new FileReader("input.csv"));
String line;
while ((line = reader.readLine()) != null) {
    String[] parts = line.split(",");
    process(parts);
}

Если ты сейчас тихо усмехнулся, узнав себя — мы с тобой точно работали на похожих проектах.


Почему баги — лучший способ учиться архитектуре и ответственности

Самое интересное во всех таких историях — это то, что ошибка в итоге обучает не технологии, а мышлению. Ты начинаешь внимательнее смотреть на непредсказуемость. Просчитывать варианты. Проверять предположения. Закладывать обработку необычных сценариев. Многие называют это инженерной зрелостью — и, возможно, это так и есть.

И самое главное: когда баг учит тебя, он делает это глубже, чем любой формальный процесс. Даже чем чек-листы. Чек-лист подсказывает, что делать. Баг объясняет, почему это важно.

И да, иногда ошибки прошлого — это наши самые терпеливые наставники. Особенно если мы не слишком торопимся забыть их существование.

Комментарии (0)