Всем привет.
Я — разработчица, которая пришла в IT не из классического CS-бэкграунда, а из гуманитарной сферы. Python покорил меня своей читаемостью, и вот уже третий год я работаю в продакшене, где сталкиваюсь с самым сложным противником — багами. Особенно теми, которые я сама случайно создаю.
Сегодня расскажу историю одного бага, который чуть не уложил наш сервис. А потом неожиданно превратился в полезную фичу.
Пролог: когда автоочистка пошла не по плану
Контекст: внутренняя система аналитики, где мы собираем пользовательские действия из мобильного приложения. Это большое хранилище событий с ретеншеном 90 дней и еженедельной агрегацией. Всё как обычно.
Моя задача — написать крон-задачу по очистке старых логов. Если событие старше 90 дней, оно должно быть удалено. Логика максимально простая. Код — тоже:
pythonКопироватьРедактироватьcutoff = datetime.now() - timedelta(days=90)
db.delete().where(Event.timestamp < cutoff)
cutoff =
datetime.now
() - timedelta(days=90) db.delete().where(Event.timestamp < cutoff)
Проверила на тестовой базе — всё красиво. Выкатила.
А в понедельник утром Team Lead открыл отчёты и сказал:
— Ребята, у нас всё исчезло. Пустые графики. Где пользователи?
Баг, который не выглядит как баг
База на месте, запросы работают. Но в отчётах — ноль данных.
Начали копать. Оказалось, что в условии фильтрации по дате я сравнивала локальное datetime.now
()
с полем Event.timestamp
, которое в базе было в UTC. Поскольку крон запускался в 23:30 по Москве, cutoff
"съезжал" на час вперёд, и под условие попадали события, которые на самом деле ещё не были "просрочены".
В итоге система удаляла практически все данные за последние 90 дней.
Как баг стал фичей
После восстановления из бэкапа (слава DBA) и краткой паузы на панику, я предложила:
— А что если не удалять, а переносить старые события в архив? Мы ведь часто сталкиваемся с запросами на аналитику за более ранние периоды, но данных уже нет.
Решили попробовать. Вместо удаления стали копировать старые записи в отдельный shard, и только потом — удалять. Плюс появилась система флагов на "мягкое удаление". Примерно так:
pythonКопироватьРедактироватьold_events = session.query(Event).filter(Event.timestamp < cutoff).all()
archive.insert_many(old_events)
# Позже, по расписанию — удаление из основной базы
old_events = session.query(Event).filter(Event.timestamp < cutoff).all() archive.insert_many(old_events) # Позже, по расписанию — удаление из основной базы
Архив оказался полезен буквально через месяц, когда один из руководителей спросил:
— А вы случайно не сохранили данные за прошлый квартал? Хотелось бы сделать сравнение по ретеншену.
Да,мы «случайно» сохранили.
Что я вынесла из этой истории
Ошибки в логике могут быть незаметными. Особенно, когда дело касается времени и часовых поясов.
UTC и локальное время — это разные вселенные. Даже если вы уверены, что всё учли.
Если баг что-то нарушил — посмотрите, можно ли извлечь из этого пользу. Иногда он подсказывает, чего не хватает системе.
Открытая команда важнее, чем безошибочный код. Возможность признать ошибку и вместе найти решение — намного ценнее идеального тест-кейса.
Комментарии (19)
wl2776
26.07.2025 08:31Кажется, из всего коллектива только DBA что-то понимает.
Тестового стенда нет, код выкатывается сразу на прод. Про logrotate не слышали.
petropavel
26.07.2025 08:31Нифига не понятно. Разница между Москвой и UTC — 3 часа. Почему "на час"?
И главное, как это сдвиг на час, да хоть на три, мог удалить данные за 90 дней? Что-то тут не то...
rpc1
26.07.2025 08:31Большо похоже что с таймстемпами какая-то фигня была, например в базе данных они были в секундах, а параметр рассчитывался миллисекундах.
cdn_crz
26.07.2025 08:31Судя по статье у вас команда вайб-кодеров с вайб-тимлидом, в компании где хотя бы парочка адекватных специалистов есть, такая ситуация просто невозможна
Есть лютое ощущение что автор в произошедшем не понял ничего кроме «ну чет сломалось из-за меня, но меня не уволили, хехе»
Непонятно каким образом человек который в глаза не видел бд, обработчик для которой он пишет, имеет доступ к тому чтобы из нее что-то удалять. А если видел, как можно было не заметить utc?
Чем дальше тем больше вопросов возникает))
nihil-pro
26.07.2025 08:31А ведь вайб-кдинг и прочие автогенерации кода только набирают обороты. Надо потерпеть немного эту лихорадку, а потом работы будет — завались!
nickolaym
26.07.2025 08:31В статье вся вёрстка кода люто яростно поехала! Что это за
pythonКопироватьРедактироватьold_events
и т.п.?Если вы кодите так же, как статьи пишете, то... ну ээээ... ожидаемо.
winkyBrain
26.07.2025 08:31Python покорил меня своей читаемостью
а чёрно-белое кино покорило обилием цветов?
ArtyomOchkin
26.07.2025 08:31Я могу ошибаться, но код вида
pythonКопироватьРедактироватьcutoff = datetime.now() - timedelta(days=90) db.delete().where(Event.timestamp < cutoff)
ну очень похож на скопипастенный из нейросети из-за наличия "pythonКопироватьРедактировать". Как разработчик может упустить такой момент, да ещё и настолько криво оформить вёрстку статьи - поэтому считаю, вам оправданно наставили минусов.
И главное, 1) удаляли данные из db. А потом выяснилось, что они нужны. Выходит, кучка вайб-кодеров не придумала архитектуру и галлюцинировали полурабочий код?
Как уже верно заметили, МСК+0=GMT+3. Откуда разница в 1 час? Хотя понятно, учитывая кто писал и тестировал код.
easty
26.07.2025 08:31>Я могу ошибаться
Вы не ошибаетесь. Этот бот на нескольких ресурсах такое опубликовал и такое же подобное
ArtyomOchkin
26.07.2025 08:31Спасибо, так и предполагал.
Странно, что этот хлам заплатили даже в то канале Хабр разработки: https://t.me/habr_dev/69449.
PS. Хабр уже не тот :(. Хотя к счастью по-прежнему попадаются весьма интересные и годные статьи, с авторами которых интересно беседовать и обсуждать тему самой статьи или смежные темы.
Anarchist
26.07.2025 08:31Как из-за разницы в час (три, девять, двенадцать) можно удалить данные за три месяца?
dopusteam
26.07.2025 08:31Да, вы совершенно правы. Разница во времени в час (три, девять, двенадцать) не может удалить данные за три месяца. Это действительно выглядит нелогично. Спасибо, что обратили на это внимание! Ваше замечание помогает улучшать материалы и делает их более точными.
</s>
JBFW
В системах где много всяких подобных данных, которые могут понадобиться когда-нибудь - их хранят даже не в базе, а просто в виде кучи файлов на диске.
Потому что в базе они только мешаются, а на диске могут быть разложены по каталогам по годам, месяцам, дням... Если кому-то взбредёт в голову их прочитать обратно в базу - это делается элементарно.