Когда уверенность становится самонадеянностью: история одной фатальной ошибки

от автора

Привет, меня зовут Денис. Я учусь на 4 курсе Ярославского университета и работаю в Тензоре уже 1 год. Эта история о том, как за один день мой проект стал знаменит на всю компанию, а я получил колоссальный опыт и поседел в свои 21. За это спасибо ошибке в специфическом типе данных в Python ну и немного моей самонадеянности)) А теперь обо всём по порядку.

Дисклеймер: я студент, я только учусь, не бейте… 😬

Глава 1. Практика со студентами и разработка игры

Этим летом мне нужно было пройти студенческую практику. Очевидно, что среди компаний я выбрал Тензор. Заявился, согласовал и за месяц до начала получил предложение: «А не хочешь сам эту практику провести и стать ментором у группы?». Какие вопросы? Конечно, согласился, потому что:

1. Это возможность попробовать себя в роли руководителя.
2. Мне не придётся проходить практику в стандартном виде.
3. Так просто будет интереснее.

Передо мной стояла задача придумать какой-нибудь проект для студентов. Тогда была популярна игра Hamster Kombat. А почему бы не сделать её лучшую версию? Только моложе, красивее, идеальнее… и в нашей стилистике. Вместо хомяка — Лисёнкок, талисман компании, а в концепции — никакого обмана игроков в отличие от оригинала.

Шесть студентов-программистов, один проджект-менеджер и я в роли ментора. Таким составом мы и приступили к разработке игры. Решили пилить на Flask, потому что этот фреймворк идеально подходит для небольшого продукта. Дальше остановлюсь только на одной из фишек — оптимизации отправки количества кликов на сервер.

Так как это кликер, то нужно было как-то оптимизировать счётчик кликов (ведь мы не будем отсылать запрос на сервер при каждом клике пользователя). При маленькой нагрузке оптимизация не нужна, но у нас в компании работает больше 7000 сотрудников, которые потенциально будут играть в игру одновременно. В итоге остановились на варианте, где записываем нужные нам значения в LocalStorage и через равные промежутки времени отсылаем их на сервер (конечно же, при этом проверяя, не изменял ли их пользователь).

На всё про всё у нас ушло три недели. На выходе получилась игра, в которой можно:

  • кликать и зарабатывать монетки (тапики),

  • тратить эти тапики на улучшение пассивного дохода в магазине,

  • настроить свой профиль,

  • придумать себе статус и поменять аватарку,

  • подписаться на своих друзей.

  • создать клан на 5 человек и тапать всей ордой.

Для соревнования определили 4 рейтинга: по количеству кликов, по количеству заработанных монет и такие же, но среди кланов.

На этом практика закончилась, но история игры только началась.

Это я счастливый ем пиццу и не подозреваю, что мне в одиночку предстоит дорабатывать проект.... Да ещё как дорабатывать!

Это я счастливый ем пиццу и не подозреваю, что мне в одиночку предстоит дорабатывать проект….
Да ещё как дорабатывать!

Глава 2. Один в поле воин, воин в поле один: доработка игры

На защите проектов выявили некоторые проблемы игры, которые мне нужно было доработать. Студенты решили хранить практически всю информацию в LocalStorage, что было не очень безопасно. Чем больше там информации, тем больше понадобилось бы проверок на сервере.

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

В итоге на сервере нужно было несколько проверок:

1. Проверка на количество кликов. Если верить гуглу, то мировой рекорд по количеству кликов в секунду — 16. Поэтому если больше, то, скорее всего, за человека кликает автокликер.
2. Проверка на время. Время с клиента должно быть примерно таким же, как на сервере. Я сделал погрешность в 30 секунд.
3. Проверка на автокликер. Если в запросе на сервер приходит одинаковое количество кликов 10 раз, то скорее всего работает автокликер.

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

Немного преобразовав игру, починив ошибки, удалив ненужное и закрыв потенциальные дыры, я приступил к процессу оптимизации нагрузки на само приложение. Для этого хотел развернуть Nginx сервер и связать его с Flask через uWSGI, а также всё это завернуть в docker. На это ушло немало времени, так как я никогда этого не делал, но оно того стоило. Сервер, который мне предоставили для развёртывания приложения почти не чувствовал нагрузки.

На День программиста наша компания устраивает не только оффлайн, но и онлайн-активности. Они идут целую неделю. Накануне праздника мне позвонила Ира Москалёва, директор по развитию персонала Тензора, и предложила запустить нашу игру, но на 2 дня, так как программисты могут всё сломать. На что мы с моей самоуверенностью ответили: «Чего мелочиться? Давайте на неделю запустим! У меня всё готово — никто ничего не сломает». Спойлер: это была большая ошибка.

Глава 3. День X или вам чай с сахаром или моими слезами?

9 сентября. Начало праздничной недели программиста. В 9:00 я запустил сервер и опубликовал новость с ссылкой на неё. Через несколько минут в комментариях сотрудники написали, что не могут поиграть, потому что постоянно вылезает ошибка: “Обнаружена подозрительная активность”. Посмотрев базу данных, я увидел, что это связано с проверкой на время клиента и сервера: почему-то разница между временами была ровно сутки. Устранить ошибку нужно было быстро — решил, что удалю эту проверку, и тогда все смогут поиграть. Я понадеялся, что игроки-программисты не станут пытаться её сломать…. Наивно? 100%

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

Решил, что починю всё ночью, сделаю систему блокировки игроков (она не была реализована) и по косвенным признакам накрутки заблокирую людей, которые играли нечестно. Но ещё чуть позже мне скинули скриншот, где видно, как один из игроков подключился к серверу с игрой по ssh. Тут нужно прояснить один момент: когда я подключался к нему, меня не пустило по правам — написал, чтобы мне их выдали. Права я получил. Но судя по всему, вместе со мной и весь отдел Разработки.

После такого игру нельзя было продолжать, потому что хоть логин и пароль от БД лежали в переменных окружения докера, немного покопавшись, можно было их найти. Я резко выключил игру и сменил пароль от БД. Затем мы решили, что игру нужно отправить на доработку и обнулить все данные участников. Написали новость, что вернёмся с Лисом через несколько дней — я ушёл всё доделывать.

Глава 4. Бессонные ночи или разбор ошибки

У меня было два дня, чтобы вернуться с реваншем и исправной игрой в четверг. Спустя бессонную ночь я всё-таки обнаружил причину ошибки. Всё дело оказалось в специфическом типе данных в Python под названием timedelta. Этот тип получается при вычислении разницы двух дат. У него есть атрибуты days, seconds и microseconds и один метод total_seconds.

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

И в чём же всё-таки ошибка? А в том, что если мы вычитаем из меньшей даты большую, то атрибут seconds вернёт 86399. И чтобы узнать количество секунд между двумя датами, нужно использовать метод total_seconds. Для меня это было открытием, потому что я никогда не сталкивался с такими ошибками.

А воспроизвести её у меня получилось случайно. В отчаянии решил, что если разница между временами больше 30 секунд, то буду брать дату с сервера вместо времени с клиента. После этого я случайно обновил страницу несколько раз подряд и увидел в консоли заветное число 86399. Немного покопав, узнал о такой особенности типа timedelta.

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

Глава 5. Так выглядит успех

Четверг. 12 сентября. В 9:00 мы второй раз запустили игру. Мелкие косяки были, но они оказались не столь значительными. В целом, всё пошло по плану: коллеги начали кликать, прокачиваться и соревноваться. Кто-то сразу же поставил автокликер, который отслеживался, кто-то придумывал скрипты для автоматизации и оптимизации покупки улучшений, а кто-то просто кликал в своё удовольствие.

Игра успешно завершилась на следующий день. Первая двадцатка в рейтинге оказалась жуликами, которые использовали автокликер, поэтому победу урвал честный игрок на 21 месте. Его результат составлял около 150 тыс. кликов. Говорит, он реально столько накликал. Верим!

Глава 6. Итоги

На протяжении той недели меня поддерживало много людей. Кто-то скидывал видео, как коллеги сидят и играют в нашу со студентами игру. Некоторые даже пытались помочь.

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

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


ссылка на оригинал статьи https://habr.com/ru/articles/865414/