Всем привет.
Я — разработчица, которая пришла в 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 и локальное время — это разные вселенные. Даже если вы уверены, что всё учли.
Если баг что-то нарушил — посмотрите, можно ли извлечь из этого пользу. Иногда он подсказывает, чего не хватает системе.
Открытая команда важнее, чем безошибочный код. Возможность признать ошибку и вместе найти решение — намного ценнее идеального тест-кейса.