Обновить

Один оператор сравнения чуть не удалил тысячу пользователей 

На днях наводил порядок в логике автопродления подписок Telegram-бота.
Казалось, задача на полчаса.

Если кратко: у пользователя заканчивается подписка, и бот проверяет дату планируемой оплаты.
Если срок подписки истёк и продления нет, то пользователь попадает в очередь на удаление. Всё просто.


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

Сначала я подумал, что платёж не успел записаться в базу. Проверил – платёж записался.

Потом начал искать проблему в часовом поясе. Тоже нет.

Дальше полез смотреть SQL-запросы, логи, время выполнения задач. Потратил на это больше часа и уже начал подозревать, что где-то появилась гонка между обработчиком платежей и задачей проверки подписок. Стал разбираться…

Причина оказалась намного проще: в фундаменте не стоял тот оператор сравнения.

Вместо subscription_end < now
я написал subscription_end <= now.


Из-за этого подписка была просроченной не только после истечения срока, но и ровно в момент его окончания. Если время проверки и обновления совпадало, пользователь попадал в удаление по граничному условию.

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

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

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

И если совпали бы время запуска задачи и обновление подписок, ошибочно удаленных пользователей было бы несколько сотен или тысяч.



Что изменил:

1. Перенастроил удаление: теперь оно происходит не сразу.

Пользователь сначала получает статус "ожидает удаления", а сама операция выполняется отдельной задачей через некоторое время.

2. Настроил перепроверку актуального состояния подписки через x часов, чтобы отсечь ложные срабатывания.

Если пользователь оплатил доступ или подписка продлилась, удаление отменяется.

3. Добавил тест на этот сценарий, чтобы избежать повторения <=ошибки.

Раньше я проверял только обычные случаи.
Теперь отдельно проверяются пограничные значения времени.



Мораль:
даже очевидные условия нужно проверять на границах. 

Смотришь на условие и кажется, что ошибиться невозможно. Но однажды выясняется, что была допущена элементарная ошибка, которая убила тонну времени.

Всегда нужно помнить о границе условий. Именно там чаще всего и живут самые неприятные ошибки.

Есть ощущение, что подобные баги зачастую находятся не в сложной логике, а в самых очевидных местах?
Интересно, как вы предостерегаете себя от таких стыдных ошибок? 

Теги:
+3
Комментарии0

Публикации