Атомарная выборка только одной записи
Представь простую очередь задач в SQL. У каждой есть уникальный ID, поле статуса (новая, в работе, закончена) и данные. Представь себе, что очередь у тебя разбирают несколько серверов.
Как сделать так, чтобы одну задачу забирал только один сервер?
На помощь приходит
Как только SQL сервер получает запрос на выборку
Кстати, организовывать очередь на SQL нужно аккуратно. Природа ее нагрузки такова, что очередь — небольшая, а SQL — уже задыхается. Не для очередей его взращивали.
https://deadcode.dev/257/
Представь простую очередь задач в SQL. У каждой есть уникальный ID, поле статуса (новая, в работе, закончена) и данные. Представь себе, что очередь у тебя разбирают несколько серверов.
Как сделать так, чтобы одну задачу забирал только один сервер?
На помощь приходит
SELECT ... FOR UPDATE.Как только SQL сервер получает запрос на выборку
FOR UPDATE, он никому другому больше не отдаст эту запись, пока ты не закроешь транзакцию.Кстати, организовывать очередь на SQL нужно аккуратно. Природа ее нагрузки такова, что очередь — небольшая, а SQL — уже задыхается. Не для очередей его взращивали.
https://deadcode.dev/257/
Как принимать платежные уведомления
Ты отправляешь пользователя на страничку платежки, там происходит магия, затем она постит тебе подтверждение платежа (IPN) на твой URL.
Как правильно принимать эти IPN:
Это должен быть отдельный http сервер (или .php), со своим конфигом, не использующий общий код проекта. Все, что должен делать этот скрипт — записать сырые данные платежного уведомления в базу и сказать HTTP 200 OK. И все, ничего больше!
Цель простая: как можно атомарнее и проще принять уведомление, надежно его записать и подтвердить платежке. Вокруг платежа может быть очень много разной логики, и она может сломать прием уведомлений. Не надо. Прими, затем разбирайся.
Не надо пытаться парсить эти данные. Ты можешь сломаться.
Нельзя использовать общий код проекта. Там может быть ошибка. И рестартить не надо будет.
А вот парсить их можно с помощью SELECT FOR UPDATE.
https://deadcode.dev/281/
Ты отправляешь пользователя на страничку платежки, там происходит магия, затем она постит тебе подтверждение платежа (IPN) на твой URL.
Как правильно принимать эти IPN:
Это должен быть отдельный http сервер (или .php), со своим конфигом, не использующий общий код проекта. Все, что должен делать этот скрипт — записать сырые данные платежного уведомления в базу и сказать HTTP 200 OK. И все, ничего больше!
Цель простая: как можно атомарнее и проще принять уведомление, надежно его записать и подтвердить платежке. Вокруг платежа может быть очень много разной логики, и она может сломать прием уведомлений. Не надо. Прими, затем разбирайся.
Не надо пытаться парсить эти данные. Ты можешь сломаться.
Нельзя использовать общий код проекта. Там может быть ошибка. И рестартить не надо будет.
А вот парсить их можно с помощью SELECT FOR UPDATE.
https://deadcode.dev/281/
/health/ check
Полезно пользоваться сервисами, которые мониторят аптайм твоего продукта и шлют уведомляшки в случае аварии. Но уверен ли ты, что правильно с ними разговариваешь?
А правильно вот так. Мониторилка должна приходить на URL, специально подготовленный для нее (
• в порядке ли коннект к базе (
• в порядке ли memcached/redis/etc (
• в порядке ли погода (
• и т.д.
Проверки выполняешь последовательно, и в случае ошибки выдаешь ее в теле со статусом HTTP 500. Так в случае аварии ты сразу узнаешь, кто сломался (конечно, если твой сервис само тело ответа показывает).
Важно: пользуйся существующими коннектами к базам. Что толку с того, что твой health check покажет, что к базе можно успешно открыть новое соединение, когда у тебя рабочие соединения мертвые и веб не работает?
И только если все хорошо, тогда говори HTTP 200. И не забудь отправить Content-Length (нулевой).
https://deadcode.dev/290/
Полезно пользоваться сервисами, которые мониторят аптайм твоего продукта и шлют уведомляшки в случае аварии. Но уверен ли ты, что правильно с ними разговариваешь?
А правильно вот так. Мониторилка должна приходить на URL, специально подготовленный для нее (
/health/). Там ты проверяешь:• в порядке ли коннект к базе (
SELECT 1)• в порядке ли memcached/redis/etc (
.set(), затем .get())• в порядке ли погода (
loadavg, достаточно ли свободной памяти и т.п.)• и т.д.
Проверки выполняешь последовательно, и в случае ошибки выдаешь ее в теле со статусом HTTP 500. Так в случае аварии ты сразу узнаешь, кто сломался (конечно, если твой сервис само тело ответа показывает).
Важно: пользуйся существующими коннектами к базам. Что толку с того, что твой health check покажет, что к базе можно успешно открыть новое соединение, когда у тебя рабочие соединения мертвые и веб не работает?
И только если все хорошо, тогда говори HTTP 200. И не забудь отправить Content-Length (нулевой).
https://deadcode.dev/290/
Есть два сервера
Практически любая задача должна выполняться минимум двумя серверами. Если один умрет - второй подхватит. Веб, кроны, база, memcached — всего этого должно быть по два или больше.
Разумеется, это предельно очевидно, даже непонятно, зачем такие советы писать.
Тем не менее, я регулярно вижу, как ребята поднимают memcached на одном сервере; затем, когда они его апгрейдят — у них каскадно падает вся инфраструктура. Причем каскадно и причем вся.
Поэтому: вместо одной виртуалки за $30 лучше подними две по $15.
https://deadcode.dev/293/
Практически любая задача должна выполняться минимум двумя серверами. Если один умрет - второй подхватит. Веб, кроны, база, memcached — всего этого должно быть по два или больше.
Разумеется, это предельно очевидно, даже непонятно, зачем такие советы писать.
Тем не менее, я регулярно вижу, как ребята поднимают memcached на одном сервере; затем, когда они его апгрейдят — у них каскадно падает вся инфраструктура. Причем каскадно и причем вся.
Поэтому: вместо одной виртуалки за $30 лучше подними две по $15.
%АНЕКДОТ_ПРО_ДВУХ_ЖЕН_ПО_20_ЛЕТ%https://deadcode.dev/293/
Есть два крона
Простой кейс: твой сервис работает на двух или трех серверах и у тебя есть задача, которую нужно по cron'у выполнять каждую минуту, например, разбирать очередь задач. Или чистить внешний сторадж. Как разделить таски так, чтобы сервера между собой не дрались?
Есть очень простой хак.
В одном кроне запускаешь свою задачу с ключем
Easy!
PS: Мнемоническое правило, как запомнить что even — это четное, а odd — нечетное: в слове even — четное количество букв.
https://deadcode.dev/295/
Простой кейс: твой сервис работает на двух или трех серверах и у тебя есть задача, которую нужно по cron'у выполнять каждую минуту, например, разбирать очередь задач. Или чистить внешний сторадж. Как разделить таски так, чтобы сервера между собой не дрались?
Есть очень простой хак.
В одном кроне запускаешь свою задачу с ключем
--odd, в другом с ключом --even. В самом же коде проверяешь номер задачи (или что угодно). Если он четный и тебе сказали --even — выполняешь, иначе — пропускаешь. И наоборот, скрипт с ключом --odd выполняет только нечетные задачи. Еще так можно по-быстрому распределить большие работы между серверами в одноразовых скриптах.Easy!
PS: Мнемоническое правило, как запомнить что even — это четное, а odd — нечетное: в слове even — четное количество букв.
https://deadcode.dev/295/
Код не для исполнения
Иногда можно писать код, который никогда не будет исполняться, но который очень важно чтобы никто другой не написал.
Например, у меня есть разветвистый
Если бы я не написал этой строчки
https://deadcode.dev/296/
Иногда можно писать код, который никогда не будет исполняться, но который очень важно чтобы никто другой не написал.
Например, у меня есть разветвистый
if .. else if .. else if ... В конце каждой ветки стоит почти одинаковый вызов. И вот в одной из веток его нет, а логика очень похожа. И чтобы мой коллега как-нибудь не заметил эту очевидную проблему и не скопировал туда строчку — это сделал я сам.Если бы я не написал этой строчки
this.$bus.$emit(), то рано или поздно она была бы кем-то сюда внесена, потому что везде же она есть.https://deadcode.dev/296/
Возврат неуспешного результата из функции в JS
Такой код прижился только в C. В скриптовых языках не следует возвращать -1 как признак (не)успешного выполнения. Канонически правильного способа вернуть ошибку толком и нет; сишный
В JavaScript нужно просто договориться с собой, что ты возвращаешь из функции структуру:
• избавься от проверки типа значения как признака. Никаких
• если метод возвращает success-структуру, то в результате всегда присутствует ключ
• если метод возвращает success-структуру, то он не имеет права вернуть
• если ты используешь сишный паттерн, модифицируя переданный в функцию хеш, то четко укажи это в названии:
Еще можно делать так, как на картинке.
https://deadcode.dev/302/ 💡
if (stat(filename) == -1) {Такой код прижился только в C. В скриптовых языках не следует возвращать -1 как признак (не)успешного выполнения. Канонически правильного способа вернуть ошибку толком и нет; сишный
errno — это костыль.В JavaScript нужно просто договориться с собой, что ты возвращаешь из функции структуру:
return {
success: true,
user: {
id: ...
}
}
• избавься от проверки типа значения как признака. Никаких
const count = returnCount() и затем if (count === null);• если метод возвращает success-структуру, то в результате всегда присутствует ключ
success. Не нужно его опускать, полагаясь на то, что в этом случае result.success == false;• если метод возвращает success-структуру, то он не имеет права вернуть
null;• если ты используешь сишный паттерн, модифицируя переданный в функцию хеш, то четко укажи это в названии:
parseUserAndAugmentWithIds(user = {});Еще можно делать так, как на картинке.
https://deadcode.dev/302/ 💡
Даты обновлений в таблицах
Хорошей привычкой при проектировании баз данных должна стать привычка добавлять поля
Подводный камень с этими полями:
Недавно мы кое-что обновляли во всех сущностях в базе компаниии. Легко, удобно, простой скрипт, спасибо вселенной за ORM. Надежно его протестировали, затем легким движением убили
Поэтому: внимательно следи за теми апдейтами, когда updatedAt должен остаться нетронутым. Таких кейсов очень мало, поэтому проще всего оставить одно поле
https://deadcode.dev/303/
Хорошей привычкой при проектировании баз данных должна стать привычка добавлять поля
createdAt и updatedAt к (почти) любой таблице и обновлять их соответственно при создании и при апдейте. Хорошие ORM это делают сами, но даже и без ORM важно чтобы эти данные у тебя были. Стоимость их хранения копеечная, а иной раз они крайне, крайне полезны. Не стесняйся показывать их пользователю.Подводный камень с этими полями:
updatedAt с точки зрения пользователя и с точки зрения системы — это разные даты. Системное время обновления — это когда и в самом деле запись поменялась в самой базе. Пользовательское время — это когда конкретно сам пользователь внес изменения. Человек часто по этим датам ориентируется.Недавно мы кое-что обновляли во всех сущностях в базе компаниии. Легко, удобно, простой скрипт, спасибо вселенной за ORM. Надежно его протестировали, затем легким движением убили
updatedAt для всех пользовательских данных. Написать и протестировать скрипт мииграции: три часа. Восстановить пользовательские даты из бекапов и накатить их на живую базу: два дня.Поэтому: внимательно следи за теми апдейтами, когда updatedAt должен остаться нетронутым. Таких кейсов очень мало, поэтому проще всего оставить одно поле
updatedAt, но быть внимательным. Да и вообще в целом проще всего быть внимательным.https://deadcode.dev/303/
Не показывай лоадер сразу
Юзер заполняет форму, кликает submit, форма отправляется на сервер, сервер думает. На время размышлений хочется показать юзеру "форма обрабатывается". В типичном случае показываем крутящийся лоадер.
И вот так делать не надо.
В подавляющем большинстве случаев форма обрабатывается мгновенно (<1s) и лоадер будет лишь только неприятным морганием, особенно если он полноэкранный и закрывает форму. Пользователь не успеет понять, что моргнуло и почему, но у него возникнет ощущение утраты контроля — самое неприятное ощущение, какое только может создать UI.
Лоадер надо показывать спустя некоторое время после сабмита, если данные не подгрузились.
Спустя какое? Это можно узнать вот так, как на картинке.
Или просто выставить его в две секунды.
https://deadcode.dev/305/
Юзер заполняет форму, кликает submit, форма отправляется на сервер, сервер думает. На время размышлений хочется показать юзеру "форма обрабатывается". В типичном случае показываем крутящийся лоадер.
И вот так делать не надо.
В подавляющем большинстве случаев форма обрабатывается мгновенно (<1s) и лоадер будет лишь только неприятным морганием, особенно если он полноэкранный и закрывает форму. Пользователь не успеет понять, что моргнуло и почему, но у него возникнет ощущение утраты контроля — самое неприятное ощущение, какое только может создать UI.
Лоадер надо показывать спустя некоторое время после сабмита, если данные не подгрузились.
Спустя какое? Это можно узнать вот так, как на картинке.
Или просто выставить его в две секунды.
https://deadcode.dev/305/
Стоит ли автоматизировать эту задачу?
Есть простой способ узнать ответ на этот вопрос.
Сколько по времени будет стоить выполнение этой задачи вручную в течении года и сколько по времени будет стоить разработка и сопровождение и использование кода автоматизации в течении года? Выбираешь более дешевое решение.
Пример. У нас в проекте есть партнерская программа для узкого круга элитных участников. Выплаты делаются каждые два-три месяца, участников меньше десяти. Выплачивать вручную одному участнику: минута. Накладные расходы на цикл выплаты: 10 минут (кофе, открыть excel, открыть paypal, etc). 10 участников × 1 минуту × 4 раза в год + 10 накладных минут × 4 раза в год = почти 2.5 часа. На поддержку докинули 1 час на год, потому что каждая выплата сделана человеком и он в контексте.
Разработать код автоматической выплаты посчитали минимально в 12 идеальных часов: код выплаты, код учета, код показывания чека, админка для суппорта, UI для юзера, очередь запросов на выплату и т.д. Поддержку пользователей по автоматической выплате прикинули на год в 3 часа, потому что по каждой нужно будет разбираться, почему было столько.
Итого: 3.5 часа против 15 часов.
Эту задачу автоматизировать точно не стоит.
https://deadcode.dev/306/
Есть простой способ узнать ответ на этот вопрос.
Сколько по времени будет стоить выполнение этой задачи вручную в течении года и сколько по времени будет стоить разработка и сопровождение и использование кода автоматизации в течении года? Выбираешь более дешевое решение.
Пример. У нас в проекте есть партнерская программа для узкого круга элитных участников. Выплаты делаются каждые два-три месяца, участников меньше десяти. Выплачивать вручную одному участнику: минута. Накладные расходы на цикл выплаты: 10 минут (кофе, открыть excel, открыть paypal, etc). 10 участников × 1 минуту × 4 раза в год + 10 накладных минут × 4 раза в год = почти 2.5 часа. На поддержку докинули 1 час на год, потому что каждая выплата сделана человеком и он в контексте.
Разработать код автоматической выплаты посчитали минимально в 12 идеальных часов: код выплаты, код учета, код показывания чека, админка для суппорта, UI для юзера, очередь запросов на выплату и т.д. Поддержку пользователей по автоматической выплате прикинули на год в 3 часа, потому что по каждой нужно будет разбираться, почему было столько.
Итого: 3.5 часа против 15 часов.
Эту задачу автоматизировать точно не стоит.
https://deadcode.dev/306/
А кстати,
вместо нулевого Content-Length можно отдавать HTTP статус 204.
Совет без номера потому что.
вместо нулевого Content-Length можно отдавать HTTP статус 204.
Совет без номера потому что.
Exceptions нужно избегать
Exceptions полностью ломают логику твоего кода. Когда кто-то его выбрасывает, управление переходит не к следующей строчке кода, а куда-то наверх, причем порой тебе неизвестно, куда именно. Exceptions проходят мимо всего твоего стека вызовов и делают return не из текущего блока, а черт знает где и куда.
Поэтому я считаю, что есть два подхода к exceptions.
Первый: ловить как можно ближе к источнику. Оборачивай свое обращение к файлу в
Второй: лови как можно выше и умирай. Считаем, что exception, который произошел выше нижайшего уровня (первый случай) — это катастрофа. Его нужно ловить лишь для того, чтобы умереть с достоинством (сообщить об ошибке и не зависнуть).
Не надо использовать exception как инструмент для возврата ошибки! Соблазн высок: вместо протаскивания какой-то информации об ошибке всю пачку вызовов на самый верх (сложно!) — просто кинули exception и типа где-то поймали (просто). Нельзя! Через некоторое время ты перестанешь понимать ветвление собственного кода, запутаешься в ресурсах, которые нужно освбождать, ну а вся борьба с memory leaks на этом и вовсе заканчивается.
https://deadcode.dev/309/
Exceptions полностью ломают логику твоего кода. Когда кто-то его выбрасывает, управление переходит не к следующей строчке кода, а куда-то наверх, причем порой тебе неизвестно, куда именно. Exceptions проходят мимо всего твоего стека вызовов и делают return не из текущего блока, а черт знает где и куда.
Поэтому я считаю, что есть два подхода к exceptions.
Первый: ловить как можно ближе к источнику. Оборачивай свое обращение к файлу в
try { } catch и считай, что в catch у тебя просто ветка ошибки чтения файла. Грубо говоря, это просто такой синтаксис: try { read(); } catch { } вместо if (!read()) { }. Это еще можно пережить. Например, в современном JavaScript они стали официальным методом получения ошибки из Promise (async/await).Второй: лови как можно выше и умирай. Считаем, что exception, который произошел выше нижайшего уровня (первый случай) — это катастрофа. Его нужно ловить лишь для того, чтобы умереть с достоинством (сообщить об ошибке и не зависнуть).
Не надо использовать exception как инструмент для возврата ошибки! Соблазн высок: вместо протаскивания какой-то информации об ошибке всю пачку вызовов на самый верх (сложно!) — просто кинули exception и типа где-то поймали (просто). Нельзя! Через некоторое время ты перестанешь понимать ветвление собственного кода, запутаешься в ресурсах, которые нужно освбождать, ну а вся борьба с memory leaks на этом и вовсе заканчивается.
https://deadcode.dev/309/
Fallback в коде: что, если не получилось?
Если в коде что-то не получилось (нет ответа от базы, API недоступен, платеж не прошел и т.д.) то важно предусмотреть правильное конструктивное поведение.
Пример из жизни (слегка упрощенный, конечно). Приложение при запуске делает запрос домой и спрашивает, оплатил ли пользователь подписку на ZZZ. Что делать приложению, если не удалось достучаться к серверам, или ответ некорректен? Скажем, прошло десять лет, компания закрылась, а домен ушел к порносайту.
Давать доступ к ZZZ.
Потому что не давать было бы деструктивным действием — у пользователя бы пропала важная для бизнеса функция и он бы страдал по нашей вине.
https://deadcode.dev/313/
Если в коде что-то не получилось (нет ответа от базы, API недоступен, платеж не прошел и т.д.) то важно предусмотреть правильное конструктивное поведение.
Пример из жизни (слегка упрощенный, конечно). Приложение при запуске делает запрос домой и спрашивает, оплатил ли пользователь подписку на ZZZ. Что делать приложению, если не удалось достучаться к серверам, или ответ некорректен? Скажем, прошло десять лет, компания закрылась, а домен ушел к порносайту.
Давать доступ к ZZZ.
Потому что не давать было бы деструктивным действием — у пользователя бы пропала важная для бизнеса функция и он бы страдал по нашей вине.
https://deadcode.dev/313/
Создавай хелперы для межмодельного кода
Вот есть пользователь (
Где разместить этот код?
В модели пользователя? Неправильно, ведь этот код дергает модель купона и он больше про скидку, чем про юзера.
В модели купона? Неправильно, ведь этот код изменяет только данные пользователя и ничьи больше.
Выход? Создай класс
А если метод только один, то назови класс
https://deadcode.dev/318/
Вот есть пользователь (
User), вот есть купоны на скидки (PromoCode). Тебе нужно применить одно к другому.Где разместить этот код?
В модели пользователя? Неправильно, ведь этот код дергает модель купона и он больше про скидку, чем про юзера.
В модели купона? Неправильно, ведь этот код изменяет только данные пользователя и ничьи больше.
Выход? Создай класс
PromoCodeToUserApplyController. Пусть даже там будет всего пара методов, зато точно понятно, зачем этот класс и к чему он относится. Поверь, он разрастется.А если метод только один, то назови класс
PromoCodeToUserApplyHelper. И, кстати, хорошо в объектно-ориентированном проекте избавиться он голых функций и все вынести в классы. Да, многие тебе скажут что так делать не надо, но они имеют в виду другое. Плохой тон не в том чтобы избегать классов там, где достаточно функции; плохой тон — плодить бессмысленные классы. То есть классы, название которых не отражает их суть. Я за то, чтобы было много всяких разных изолированных сущностей и много методов, и у каждого — свое понятное название. Тогда код легко читается.https://deadcode.dev/318/
Плавно вводи ресурсы в эксплуатацию
Допустим, у тебя система хранения с линейным масштабированием. Добавил новый сервер — расширилось место.
Это наивный подход к масштабированию. Если ты просто будешь добавлять в живую систему новые сервера, у тебя на них будет совершенно другой профиль нагрузки, т.к. там не будет старых лежащих данных, а будет лишь только происходить добавление новых. Этот сервер станет горячей точкой и узким горлышком.
В таких системах целесообразно сначала на новый сервер смигрировать часть старых лежащих данных и только потом вводить в эксплуатацию для добавления новых. Так ты сбалансируешь профиль нагрузки и он не будет сильно отличаться от других.
https://deadcode.dev/320/
Допустим, у тебя система хранения с линейным масштабированием. Добавил новый сервер — расширилось место.
Это наивный подход к масштабированию. Если ты просто будешь добавлять в живую систему новые сервера, у тебя на них будет совершенно другой профиль нагрузки, т.к. там не будет старых лежащих данных, а будет лишь только происходить добавление новых. Этот сервер станет горячей точкой и узким горлышком.
В таких системах целесообразно сначала на новый сервер смигрировать часть старых лежащих данных и только потом вводить в эксплуатацию для добавления новых. Так ты сбалансируешь профиль нагрузки и он не будет сильно отличаться от других.
https://deadcode.dev/320/
Монетарные значения должны быть целочисленными
Как известно, 0.2 + 0.1 = 0.30000000000000004. Казалось бы, можно округлить да и дело с концом. Но нет: НДС от суммы в счете и сумма НДСов счета запросто могут отличаться за счет ошибок округления.
Поэтому все денежные единицы стоит хранить и исчислять в целом количестве копеек и конвертировать в дробные доллары только на этапе показа пользователю. В базах это
С Satoshi посложнее, конечно.
https://deadcode.dev/323/
Как известно, 0.2 + 0.1 = 0.30000000000000004. Казалось бы, можно округлить да и дело с концом. Но нет: НДС от суммы в счете и сумма НДСов счета запросто могут отличаться за счет ошибок округления.
Поэтому все денежные единицы стоит хранить и исчислять в целом количестве копеек и конвертировать в дробные доллары только на этапе показа пользователю. В базах это
BIGINT. В js я делаю просто const amountHr = Math.floor(amount/100).toFixed(2).С Satoshi посложнее, конечно.
https://deadcode.dev/323/
Именованные параметры в JavaScript
Одна из самых лучших вещей, появившихся в JavaScript за последние годы — это возможность использовать destructuring прямо в определении функции. Выглядит это так.
Обрати внимание:
•
•
• При вызове метода можно использовать shorthand property names: см. как я передаю
Это настолько хорошо, что имеет смысл использовать практически во всех функциях и методах проекта.
https://deadcode.dev/330/
Одна из самых лучших вещей, появившихся в JavaScript за последние годы — это возможность использовать destructuring прямо в определении функции. Выглядит это так.
Обрати внимание:
•
shouldKeepData = false: можно указывать дефолтные значения для параметров.•
= {} в конце требуется, чтобы можно было вызвать функцию без аргументов вообще: deleteUser(). Без = {} будет exception.• При вызове метода можно использовать shorthand property names: см. как я передаю
userId.Это настолько хорошо, что имеет смысл использовать практически во всех функциях и методах проекта.
https://deadcode.dev/330/
👎1🤮1🖕1