В один из прекрасных рабочих дней мне прилетела задачка на то, что необходимо исправить очередной баг. Проблема заключалась в следующем: на сайте при заказе товара можно было выбрать временной интервал для доставки, и если клиент задумался или отошел на n-ое количество времени, то он не мог сделать заказ, так как временной интервал на сервере обновился, и стал недоступен, а на клиенте остались старые интервалы.
На первый взгляд, данную задачу решить максимально просто, сделать запрос, получить новые интервалы, обновить состояние (работаем на react) и отрисовать. Вот и мне показалось, что решение займет пару часов.
Итак, я приступил, начал с того, что позвонил backend разработчику, что бы узнать по какому принципу обновляются интервалы на сервере. В итоге выяснилось, что они обновляются каждые 15 минут, и по его словам, в скрипте много бизнес-логики и отрабатывает он довольно долго.
Было принято решение, чтобы не нагружать сервер делать запрос раз в 15 минут, получать актуальные данные и все прекрасно. Спустя несколько минут код был написан и осталось лишь дождаться обновления интервалов на сервере и запроса.
После проверки стало понятно, что необходимо учесть промежуток времени, до истечения 15 минутного интервала. Например, первый интервал, который будет доступен клиенту это с 14:00 до 14:15, интервалы должны обновиться в 13:45 и первым доступным интервалом должно стать время с 14:15 до 14:30. Таким образом, если клиент будет делать заказ в 13:40, то следующий запрос будет в 13:55, соответственно у нас 10 минут будут висеть неактуальные интервалы.
Что с этим делать? Нужно высчитать время до первого интервала от текущего времени, вычесть 15 минут и сделать первый запрос по истечению этого времени, а дальше, как и до этого, каждые 15 минут. В целом неплохая идея, как мне тогда показалось, я приступил к реализации:
const timingUpdate = () => {
if (checkout.intervals) {
let first = null;
for (let key in checkout.intervals) {
if (!first) {
first = checkout.intervals[key][0][0].split(":");
}
}
let currentTime = new Date();
currentTime = (currentTime.getHours() * 3600 + currentTime.getMinutes() * 60 + currentTime.getSeconds());
return (+first[0] * 3600 + +first[1] * 60 - currentTime - 14.9 * 60) * 1000;
}
};
useEffect(() => {
const currentTiming = timingUpdate();
let onInterval = setTimeout(() => {
if (checkout.currentRestaurant) {
dispatch(fetchRestaurantIntervals({ restaurant_id: currentRestaurant.id }));
}
}, currentTiming);
return () => {
clearTimeout(onInterval);
};
}, [intervals]);
Хотелось бы отметить, что пришлось повозиться, так как с сервера приходил объект а не массив и первый интервал вычленять было довольно неприятно. Так же, можно заметить, что запрос я делаю не через 15 минут ровно, а через 15,1 чтобы дать время отработать "тяжелому" скрипту на сервере. Мои скромные мануальные тесты были пройдены и я отправил задачу на тестирование.
Спустя неделю, тестировщик дал сценарий, при котором интервалы не обновлялись и заказ было невозможно сделать, потом последовало несколько созвонов с тестировщиком и backend разработчиком, и как выяснилось, интервалы обновляются на сервере не всегда через 15 минут, а в зависимости от загрузки курьеров. Время обновления присылать с сервера оказалось невозможным.
Я начал терзаться муками, делать вебсокет для такой простой задачи не хотелось, но и слать запросы каждую минуту казалось не логичным. Вообщем, лучшее враг хорошего, поэтому решил, что от запроса раз в минуту сервер не упадет.
const timingUpdate = () => {
let timeUpdate = new Date();
return (65 - timeUpdate.getSeconds()) * 1000;
};
useEffect(() => {
let currentTiming = timingUpdate();
let onInterval = null;
let timer = () => {
currentTiming = timingUpdate();
if (checkout.currentRestaurant) {
dispatch(fetchRestaurantIntervals({ restaurant_id: currentRestaurant.id }, intervals));
}
};
let onTimeout = setTimeout(() => {
timer();
clearInterval(onInterval);
onInterval = setInterval(timer, currentTiming);
}, currentTiming);
return () => {
clearInterval(onInterval);
clearTimeout(onTimeout);
};
}, [intervals]);
Пришлось немного повозиться с setTimeout и setInterval, так как теперь, если у нас не приходил ответ, то состояние не менялось, перерендера не было и соответственно setTimeout не запускался. Поэтому я использовал setTimeout для запуска первого запроса, который срабатывал через несколько секунд, когда наступала новая минута, и далее через setInterval каждую минуту. Так же можно заметить, что я сделал дополнительные 5 секунд на отработку "тяжелого" скрипта на и обновления интервалов на сервере(Как показали эксперименты, 5 секунд было всегда достаточно). Ну и конечно же нужно не забыть очистить setTimeout и setInterval, иначе при изменении состояний различных кнопочек на странице мы получим огромное количество перерендеров.
Резюмируем: Не всегда простая на вид задача, на самом деле простая! Ну, и конечно же, лучше 10 раз переспросить как работает backend, прежде чем пытаться быстрее реализовать frontend, чтобы не делать двойную работу.
P.S.: Первая статья, не судите строго!
Комментарии (6)
xxxphilinxxx
16.03.2022 11:27+1В итоге просто бомбардируете сервер тяжелыми запросами, да еще и подгоняете интервалы под нюансы реализации бэка: таких глубоких сведений у фронта быть не должно, это слишком жесткая связь за пределами интерфейса. Ну и если сейчас 5 секунд достаточно, то когда-нибудь под нагрузкой их перестанет хватать и решение будет работать со сбоями.
Какие я вижу варианты:
На бэке при получении формы заказа возвращаем ошибку "интервал не существует", на фронте при ее получении запрашиваем интервалы и просим пользователя вернуться и выбрать заново. Минимум запросов и правок, простая логика, не остается тупика, но чуть напрягаем пользователя.
Тяжелый обсчет интервалов по крону? Кладем результат в кеш и отдаем фронту из кеша. Запрашивай хоть каждую секунду. Просто, дешево и быстро. Хорошо дополняет предыдущий вариант, но и само по себе годится.
Неизвестно, когда интервалы обновились, а почему-то все равно надо дергать тяжелый запрос? Делаем еще один эндпоинт, возвращающий дату последнего обновления интервалов. Он дешевый, дергать можно будет часто, а интервалы запрашивать, только если полученная дата свежее, чем на клиенте.
Если на бэке работы не провести, то вот еще как можно выкрутиться на фронте:
Интервалы обновляем однократно перед отправкой формы заказа, а если изменились и выбранные пользователем больше не применимы - выдаем пользователю уведомление и просим еще раз их выставить, показав уже новые. Почти как первый вариант, но без бэка.
В конце концов, можно отслеживать активность пользователя на вкладке с сайтом: движения мыши, клики, нажатия клавиш. Если он надолго ушел, то перестать впустую нагружать сервер, а загрузить интервалы, когда он вернется после перерыва.
danilabot Автор
16.03.2022 11:48Большое спасибо за комментарий, действительно, работы на беке провести невозможно. Первый вариант был реализован до того, как я приступил к работе, вопрос в том, что бизнес хотел, что бы интервалы обновлялись автоматически.
Идея с отслеживанием движения мышки действительно хороша, попробую реализовать.
funca
18.03.2022 09:55который срабатывал через несколько секунд, когда наступала новая минута, и далее через setInterval каждую минуту
Интересно, не получится-ли так, что все зашедшие на страницу клиенты будут отправлять запрос на еле живой бекенд практически одновременно (по границе минут) и не добьёт-ли его такой характер нагрузки?
Хотя в коде у вас это сделано не так как в описании, что оставляет некоторые шансы.
onTimeout = setTimeout(() => {...}, currentTiming);
onInterval = setInterval(timer, currentTiming);
currentTiming это сколько секунд осталось до конца минуты. Использование такого значения в качестве задержки в первом случае похоже на правду. Но подставляя его в интервал, вы получаете запуск функции с некоторой плавающей периодичностью не раз в минуту, а где-то чаще, где-то - реже.
danilabot Автор
18.03.2022 12:27Первый раз у нас высчитывается время, до обновления минуты. Каждый последующий запрос будет проходить с интервалом в минуту. Так как данный функционал лежит на проде уже две недели. Думаю, что это показывает состоятельность решения. Всегда можно сделать лучше, однако, как показало время, данное решение имеет место быть.
sinneren
сегодня да - завтра новые эксперименты и гадания на кофейной гуще?
непонятно, а юзер-то узнает, что у него интервалы изменились? Задача выглядит как бизнесовая, а не фронтовая, в первую очередь.
danilabot Автор
Спасибо за комментарий. Согласен с Вами. К сожалению бек изменить нельзя, и подсказать точно время обновления интервалов мне не смогли, поэтому пришлось гадать на кофейной гуще.
Юзер узнает о смене интервалов, так как при их обновлении интервалы скинуться и ему придется заново выбирать интервалы.
Задача действительно больше бизнесовая, но к сожалению имеем то, что имеем. Задача есть в спринте, бизнес хочет получить результат, соответственно пришлось выполнять, надеюсь, что кому то мой опыт будет полезен.
Так же мне понравилась идея @xxxphilinxxx c отслеживанием активности пользователя, чтобы не грузить сервер тяжелыми запросами.