Привет, меня зовут Денис. Я учусь на 4 курсе Ярославского университета и работаю в Тензоре уже 1 год. Эта история о том, как за один день мой проект стал знаменит на всю компанию, а я получил колоссальный опыт и поседел в свои 21. За это спасибо ошибке в специфическом типе данных в Python ну и немного моей самонадеянности)) А теперь обо всём по порядку.
Дисклеймер: я студент, я только учусь, не бейте... ?
Глава 1. Практика со студентами и разработка игры
Этим летом мне нужно было пройти студенческую практику. Очевидно, что среди компаний я выбрал Тензор. Заявился, согласовал и за месяц до начала получил предложение: "А не хочешь сам эту практику провести и стать ментором у группы?". Какие вопросы? Конечно, согласился, потому что:
1. Это возможность попробовать себя в роли руководителя.
2. Мне не придётся проходить практику в стандартном виде.
3. Так просто будет интереснее.
Передо мной стояла задача придумать какой-нибудь проект для студентов. Тогда была популярна игра Hamster Kombat. А почему бы не сделать её лучшую версию? Только моложе, красивее, идеальнее... и в нашей стилистике. Вместо хомяка — Лисёнкок, талисман компании, а в концепции — никакого обмана игроков в отличие от оригинала.

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

На всё про всё у нас ушло три недели. На выходе получилась игра, в которой можно:
кликать и зарабатывать монетки (тапики),
тратить эти тапики на улучшение пассивного дохода в магазине,
настроить свой профиль,
придумать себе статус и поменять аватарку,
подписаться на своих друзей.
создать клан на 5 человек и тапать всей ордой.
Для соревнования определили 4 рейтинга: по количеству кликов, по количеству заработанных монет и такие же, но среди кланов.
На этом практика закончилась, но история игры только началась.

Да ещё как дорабатывать!
Глава 2. Один в поле воин, воин в поле один: доработка игры
На защите проектов выявили некоторые проблемы игры, которые мне нужно было доработать. Студенты решили хранить практически всю информацию в LocalStorage, что было не очень безопасно. Чем больше там информации, тем больше понадобилось бы проверок на сервере.
Я сократил количество полей и оставил только количество кликов. Остальные расчёты решил проводить на сервере. При отправке запроса на него передавал время и дату с клиента, чтобы синхронизировать данные.

В итоге на сервере нужно было несколько проверок:
Проверка на количество кликов. Если верить гуглу, то мировой рекорд по количеству кликов в секунду — 16. Поэтому если больше, то, скорее всего, за человека кликает автокликер.
Проверка на время. Время с клиента должно быть примерно таким же, как на сервере. Я сделал погрешность в 30 секунд.
Проверка на автокликер. Если в запросе на сервер приходит одинаковое количество кликов 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. Итоги
На протяжении той недели меня поддерживало много людей. Кто-то скидывал видео, как коллеги сидят и играют в нашу со студентами игру. Некоторые даже пытались помочь.

Так случилось, что во время этих приключений я ещё и заболел. В офисе меня не было, но ребята поделились, что игру обсуждали почти все и везде: в столовой, в зонах отдыха, в очередях за кофе.
Приятно, что игра вызвала такой отклик и многим понравилась. Я получил колоссальный опыт по руководству группой, по разработке продукта для массового потребителя, попробовал разворачивать продукт на продакшене и, конечно, научился принимать критику и выносить из неё максимум пользы, ведь любая ошибка — лишь шаг к тому, чтобы стать лучше.
Комментарии (20)
Logintsev
10.12.2024 12:38Мне статья показалась просто какой то записью в дневнике, нежели статьей компании.
DarkSh4per Автор
10.12.2024 12:38Так и есть, это просто история об одной маленькой ошибке, которая привела к немаленьким последствиям. Формат статьи (кейс) как раз описывает то, что мы столкнулись с интересным случаем и решили об этом написать)
jack_lark
10.12.2024 12:38здравствуйте Денис.
как вы думаете, какая работодателям выгода от проектов по геймификации ?
ведь это отвлечение персонала, будем говорить прямо, на ерунду вместо обычных бизнес-процессов,
потеря оплаченнго рабочего времени, погружение персонала в детство (инфантидизация) etc etc.
если вы об этом вообще не думали - попробуйте начать, вдруг понравится.DarkSh4per Автор
10.12.2024 12:38Здравствуйте. Я считаю, что от работы иногда стоит отвлекаться, потому что без этого можно легко выгореть. У нас в компании часто проводятся подобные мероприятия и активности, ведь корпоративная жизнь - это неотъемлемая часть рабочего процесса)
jack_lark
10.12.2024 12:38Я считаю, что от работы иногда стоит отвлекаться
я согласен с вами. но для отвлечения от работы есть 16 часов в сутки +
суббота-воскресенье-праздники (если график 5дневка),
либо сравнимое число времени при графиках 2/2 и сутки через трое.
вам не кажется что что-то здесь не так ?ps: не останавивайтесь, продолжайте думать дальше. возможно,
совместными усилиями мы прийдем к нетривиальным и неочевидным сейчас выводам.
Foxonn
10.12.2024 12:38Игра получилась затягивающей) Я как один из сотрудников(бывший) одного из филиалов компании, наблюдал за процессом и сам участвовал в поиске лазеек в функционале. Да, основная работа ушла мягко говоря в сторону, зато как весь офис активно коммуницировал)
sergey_prokofiev
Вы забыли написать в названии:
"история одной фатальной ошибки НИКОМУ НЕ НУЖНОЙ СТУДЕНЧЕСКОЙ ПОДЕЛКИ: КОРПОРАТИВНОЙ ИГРУШКИ"
lrrr11
на самом деле вообще не шутка, руководство командой из 7 человек для студента - это очень хорошая практика. Потому что в командной работе даже на самых простых проектах может вылезти столько косяков, что мама не горюй. А он его еще и успешно завершил.
А вот то что сам сел дорабатывать в конце - это минус, руководство так не поступает.
sergey_prokofiev
То что довел до результата - молодец. Но неаврное не стоит в заголовок выносить фразу про "фатальные ошибки" - после прочтения текста это даже не смешно.
DarkSh4per Автор
Слово "фатальный" можно применять не только в связи с серьезными и сложными ошибками. В данном случае оно относится к последствиям для продукта. ;)
Geswires
Интересно, а вы в студенчестве делали корпоративную игрушку на 7000 пользователей или также писали язвительные комментарии?
sergey_prokofiev
В студенчестве я писал сложный математический софт, который после этого около 20 лет использовался на производстве, для расчета 3х мерных параметров потока газа в компрессоре двуконтурного турбореактивного двигателя. Его использовали аж целых 5-10 инженеров. Куда там до игрушки для 7к программистов, и ФАТАЛЬНЫЕ ОШИБКИ.
Logintsev
Аплодирую
Geswires
Вы так выделяете "Фатальные ошибки" — вас так триггернул заголовок, который на это рассчитан?
sergey_prokofiev
Ну я именно это прямым текстом написал в стартпосте.
DarkSh4per Автор
Благодарю за такой интерес к моей статье! Не соглашусь, что игрушка была никому не нужна, так как в статье написано, что в неё поиграло много сотрудников. Смысл статьи не в том, чтобы рассказать об игре и её разработке. Я просто хотел поделиться своей историей и тем, как одна маленькая ошибка может привести к немаленьким последствиям
sergey_prokofiev
Ну а чуть позже вы откроете, что таймзон больше одной. И клиенты с серверами могут находится разных таймзонах, причем клиенты - перемещаться между ними. А еще могут быть стационарные локации, которые привязаны физически к таймзонам - и там будут дргуие правмила обработки. А если сильно повоезет, то узнаете что такое линия смены дат и почему хранение времени в UTC не всегда помогает.
DarkSh4per Автор
Я вас удивлю, но сотрудники нашей компании работают по всей России в разных часовых поясах, и всё это мы учли при разработке игры)
sergey_prokofiev
Но при этом в статье написали:
DarkSh4per Автор
Это использовалось в контексте краткого описания проверки. Считаю, что этого достаточно для понимания её работы, если не вдаваться в технические детали