🛤 ROP – обработка ошибок без исключений и null
Представь: ты пишешь код, а в нём — сплошные
Проблема: необходимость обработки ошибок с каждого звена цепочки
Допустим, у нас есть типичный процесс регистрации пользователя. Нам нужно:
- провалидировать данные,
- проверить, нет ли уже такого пользователя,
- создать запись,
- отправить письмо для подтверждения.
На
Каждый шаг — это потенциальное ветвление. Каждая ошибка обрабатывается вручную.
А теперь посмотрите, как ту же логику можно выразить в функциональном стиле с использованием ROP:
Никаких
Но как это работает? Как мы обрабатываем ошибки?
Всё дело в том, что каждая операция может быть либо на «рельсе успеха», либо на «рельсе ошибки».
Что за рельсы?
Честно признаться, чтобы добиться такого же результата, что и в примере на
Здесь в игру вступает pattern matching — одна из самых мощных возможностей функциональных языков. Это достойно отдельного поста, поскольку одна эта вещь заменяет
Обрати внимание: у
- Если пришло
- Если пришло
Таким образом, ошибка, возникшая на любом этапе, «проскакивает» все следующие функции и доходит до
Полный пример: регистрация пользователя
Теперь давай соберём всё вместе:
Что в сухом остатке?
Представь: ты пишешь код, а в нём — сплошные
if-else, проверки статусов, возвраты ошибок на каждом шагу. Знакомо? Как будто хочется что-то исправить, но редко на ум приходит что-то такое, что можно было бы использовать где угодно, где есть какие-то цепочки вызовов. Прошу любить и жаловать – Railway-Oriented Programming!Проблема: необходимость обработки ошибок с каждого звена цепочки
Допустим, у нас есть типичный процесс регистрации пользователя. Нам нужно:
- провалидировать данные,
- проверить, нет ли уже такого пользователя,
- создать запись,
- отправить письмо для подтверждения.
На
Javascript это может выглядеть так:if (!validateData(registrationRequest))
return {status: "error", reason: "Данные не валидны"}
if (checkExistance(registrationRequest))
return { status: "error", reason: "Пользователь с такими данными уже существует" }
const user = createUser(registrationRequest)
if (!user)
return { status: "error", reason: "Не удалось создать пользователя" }
if (!sendEmailVerification(user))
return { status: "error", reason: "Не удалось отправить письмо" }
Каждый шаг — это потенциальное ветвление. Каждая ошибка обрабатывается вручную.
А теперь посмотрите, как ту же логику можно выразить в функциональном стиле с использованием ROP:
registration_request
|> validate_data()
|> check_existence()
|> create_user()
|> send_email_verification()
|> handle_error()
Никаких
if! Никаких промежуточных переменных! Абсолютная читаемость и понятность процесса. Поток данных последовательно передается от начала к концу, и глаз просто скользит сверху вниз.Но как это работает? Как мы обрабатываем ошибки?
Всё дело в том, что каждая операция может быть либо на «рельсе успеха», либо на «рельсе ошибки».
Что за рельсы?
Честно признаться, чтобы добиться такого же результата, что и в примере на
Javascript, нужно немного «достроить» пайплайн. Добавим несколько вспомогательных функций:defp bind({:ok, value}, func), do: func.(value)
defp bind({:error, reason}, _), do: {:error, reason}
defp map({:ok, value}, func), do: {:ok, func.(value)}
defp map({:error, reason}, _), do: {:error, reason}
defp handle_error({:error, reason}), do: IO.puts(reason)
defp handle_error(_), do: :okЗдесь в игру вступает pattern matching — одна из самых мощных возможностей функциональных языков. Это достойно отдельного поста, поскольку одна эта вещь заменяет
if и позволяет выполнять распаковку объектов, перегружать функции и еще много всего интересного. Обрати внимание: у
bind и map есть по две перегруженные версии (клаузы). Elixir автоматически выбирает ту, которая соответствует переданным данным.- Если пришло
{:ok, data} – остаемся на «рельсе успеха», применяем функцию и идём дальше.- Если пришло
{:error, reason} – переходим на «рельс ошибки», просто прокидываем ошибку до самого конца.Таким образом, ошибка, возникшая на любом этапе, «проскакивает» все следующие функции и доходит до
handle_error.Полный пример: регистрация пользователя
Теперь давай соберём всё вместе:
defp validate_data(data) do
# Логика валидации
# Если данные валидны, то возвращаем {:ok, data}
# Если не валидны – {:error, :invalid_data}
end
defp check_existence(data) do
# Логика проверки существования пользователя
# Данные свободны – {:ok, data}
# Иначе – {:error, :already_exist}
end
defp create_user(data) do
# Логика создания нового пользователя
# Удачно – {:ok, %User{}}
# Неудачно – {:error, :creation_failed}
end
defp send_email_verification(user) do
# Логика отправки сообщения
# Удачно – {:ok, true}
# Неудачно – {:error, :email_not_sent}
end
def handle_request(request) do
request
|> map(&validate_data/1)
|> map(&check_existence/1)
|> map(&create_user/1)
|> bind(&send_email_verification/1)
|> handle_error()
end
Что в сухом остатке?
🔥4❤1👍1
- Читаемость: код читается как последовательность шагов, а не как дерево условий.
- Надёжность: ошибки обрабатываются централизованно и не «теряются» по пути.
- Композируемость: мы можем легко переиспользовать, добавлять или убирать шаги в пайплайне.
Railway-Oriented Programming можно описать одной фразой: «Успех идёт вперёд, ошибка — прямиком к выходу». Если однажды попали на «рельс ошибки», то нет ни шанса попасть обратно.
P.S.: Справедливости ради стоит сказать, что такой подход очень легко реализовать в
#functional #patterns
- Надёжность: ошибки обрабатываются централизованно и не «теряются» по пути.
- Композируемость: мы можем легко переиспользовать, добавлять или убирать шаги в пайплайне.
Railway-Oriented Programming можно описать одной фразой: «Успех идёт вперёд, ошибка — прямиком к выходу». Если однажды попали на «рельс ошибки», то нет ни шанса попасть обратно.
P.S.: Справедливости ради стоит сказать, что такой подход очень легко реализовать в
Javascript с помощью then – получается абсолютно то же самое, что и в примере на Elixir. Именно поэтому из всех популярных языков программирования мне нравится именно он – по своей природе он очень близок к функциональным.#functional #patterns
❤4🔥2
Вот он, первый пост из цикла о паттернах в функциональном программировании, и уже он не влез в одно сообщение🫣
Для следующих длинных постов буду искать площадку для хостинга, так как Telegraph мне всё таки не нравится.
Будет интересно почитать, что ты думаешь об этом подходе, и какие решения применял для централизованной обработки ошибок и выстраивания цепочек действий, поскольку с этой проблемой я сталкиваюсь очень часто
Для следующих длинных постов буду искать площадку для хостинга, так как Telegraph мне всё таки не нравится.
Будет интересно почитать, что ты думаешь об этом подходе, и какие решения применял для централизованной обработки ошибок и выстраивания цепочек действий, поскольку с этой проблемой я сталкиваюсь очень часто
❤3👍1
К каждому паттерну я буду прикреплять примеры использования, чтобы не ограничиваться местами абстрактными примерами. Использование ROP можете посмотреть здесь
GitHub
todo-elixir/lib/todo.ex at master · lsdrfrx/todo-elixir
Contribute to lsdrfrx/todo-elixir development by creating an account on GitHub.
❤4⚡1
Пропал на 3 недели, но не просто так – активно преисполнялся в Elixir. В субботу планирую постримить, но не могу выбрать направление:
– Супервизор процессов на Elixir — написать лёгкий менеджер, который будет управлять процессами: запускать, останавливать, перезапускать, мониторить, добавлять в автозапуск. systemd писать не собираюсь, но минималистичный аналог runit — почему бы и нет? Вместе разберемся, как под капотом работают системы инициализации в *nix и напишем свою маленькую, да не абы как, а в функциональном стиле, на Elixir
– Fullstack на Vue3 + Express.js — помогаю товарищу с дипломной работой. В планах: авторизация, чат, вебсокеты и медитативная вёрстка.
Какой путь выбрать — решать тебе. Буду очень рад делегировать выбор на тебя😃
Выбираем реакцией:
Супервизор – 👀
Диплом – 🌚
– Супервизор процессов на Elixir — написать лёгкий менеджер, который будет управлять процессами: запускать, останавливать, перезапускать, мониторить, добавлять в автозапуск. systemd писать не собираюсь, но минималистичный аналог runit — почему бы и нет? Вместе разберемся, как под капотом работают системы инициализации в *nix и напишем свою маленькую, да не абы как, а в функциональном стиле, на Elixir
– Fullstack на Vue3 + Express.js — помогаю товарищу с дипломной работой. В планах: авторизация, чат, вебсокеты и медитативная вёрстка.
Какой путь выбрать — решать тебе. Буду очень рад делегировать выбор на тебя😃
Выбираем реакцией:
Супервизор – 👀
Диплом – 🌚
🌚6👀3👏1
P.S.: нормально настроил OBS в плане баланса звука, запись со стрима будет🫡
🎉4
То чувство, когда большая задача пошла в релиз без правок ни с ревью, ни с тестирования:
This media is not supported in your browser
VIEW IN TELEGRAM
😁4❤1
Начинаю ориентировочно в 12:00. Буду рад всем желающим🙏🏾
https://www.twitch.tv/lsdrfrx
https://www.twitch.tv/lsdrfrx
Twitch
lsdrfrx - Twitch
versatile developer
this :: IO Diary
Пропал на 3 недели, но не просто так – активно преисполнялся в Elixir. В субботу планирую постримить, но не могу выбрать направление: – Супервизор процессов на Elixir — написать лёгкий менеджер, который будет управлять процессами: запускать, останавливать…
Стрим длился рекордные 4 часа 40 минут🫣
За это время успел накидать пару базовых компонентов, построить скелет фронтенда и сделать регистрацию и вход в аккаунт на стороне бэкенда. Попробовал новые для себя фреймворки Hono и Drizzle. Пока всё тяп-ляп, далее буду приводить все в красивый вид – как и структуру, так и визуал
Смотри запись тут: https://www.youtube.com/watch?v=jADfBMvaW4c
За это время успел накидать пару базовых компонентов, построить скелет фронтенда и сделать регистрацию и вход в аккаунт на стороне бэкенда. Попробовал новые для себя фреймворки Hono и Drizzle. Пока всё тяп-ляп, далее буду приводить все в красивый вид – как и структуру, так и визуал
Смотри запись тут: https://www.youtube.com/watch?v=jADfBMvaW4c
🔥3❤1🥰1
☠️ Starvation
Не страшно, когда приложение падает, а журнал логов превращается в километровый красный ковёр трейсбэка ошибки. Не страшно даже, когда приложение падает без ошибок. Действительно страшно то, когда приложение умерло, но все выглядит так, что оно продолжает работать. Такую ошибку я поймал сегодня, буквально 2 часа назад, прогоняя нагрузочный тест – сервис просто внезапно умолк. Супервизор живой, все процессы живые, а в логах царит гробовая тишина. Не сказать, что я спец в конкурентности – у меня есть небольшой опыт разработки на языке Go (полтора-два года, если память не изменяет) – но так вышло, что сейчас я работаю над небольшим высоконагруженным сервисом, полностью пропитанным мультипроцессингом и конкурентностью. Думаю, многие слышали о
Эта ошибка не была для меня сюрпризом – я знал, где она спряталась. Более того – я осознанно допустил её, чтобы быстрее добиться рабочего результата на низких нагрузках. Сейчас пришло время её исправлять, и я об этом расскажу. То самое «исправлю потом» наступило для меня сегодня😃
У меня есть пайплайн обработки событий:
- события приходят из источника;
- они группируются по
- для каждого
- внутри одного стрима порядок обработки строго последовательный, между стримами – параллель.
На первый взгляд всё выглядит правильно: много воркеров, каждый занимается своим потоком данных, никакой блокировки на уровне бизнес-логики.
Но есть один нюанс.
Центральный процесс (
- брали события на обработку;
- после каждого шага синхронно спрашивали у
То есть десятки, сотни, тысячи воркеров постоянно делали синхронный запрос к одному процессу. Под небольшой нагрузкой система работала идеально. Под средней – начинала думать. Под большой нагрузкой во время стресс-теста – гробовая тишина. Процессы живые, supervisor живой, нет эксепшенов, ошибок в логах.
Это не deadlock в классическом смысле – никто не держит ресурсы навсегда. Это не livelock – процессы не крутятся в бесполезном цикле. Это starvation.
Центральный
- асинхронные сообщения о завершённой обработке;
- синхронные сообщения от воркеров на получение новых событий;
- служебные сообщения.
Я нарушил одно из базовых правил конкурентного дизайна:
Ещё хуже – воркеры не владели своими очередями, постоянно синхронно читали чужое состояние и зависели от одного центрального процесса.
Выглядит это следующим образом. Наглядный пример, как делать не надо:
Правильная модель оказалась сильно проще:
- каждый воркер владеет своей очередью;
- центральный процесс ничего не знает про содержимое очередей, он лишь запускает воркеры, мониторит их и реагирует на завершение или падение.
А чтобы очередь не терялась при падении воркера, источник данных становится единственным source of truth. Воркер загружает очередь при старте. Если он упал – его состояние легко восстановить.
Не страшно, когда приложение падает, а журнал логов превращается в километровый красный ковёр трейсбэка ошибки. Не страшно даже, когда приложение падает без ошибок. Действительно страшно то, когда приложение умерло, но все выглядит так, что оно продолжает работать. Такую ошибку я поймал сегодня, буквально 2 часа назад, прогоняя нагрузочный тест – сервис просто внезапно умолк. Супервизор живой, все процессы живые, а в логах царит гробовая тишина. Не сказать, что я спец в конкурентности – у меня есть небольшой опыт разработки на языке Go (полтора-два года, если память не изменяет) – но так вышло, что сейчас я работаю над небольшим высоконагруженным сервисом, полностью пропитанным мультипроцессингом и конкурентностью. Думаю, многие слышали о
deadlock и livelock, но сегодня я буду рассказывать не об этомЭта ошибка не была для меня сюрпризом – я знал, где она спряталась. Более того – я осознанно допустил её, чтобы быстрее добиться рабочего результата на низких нагрузках. Сейчас пришло время её исправлять, и я об этом расскажу. То самое «исправлю потом» наступило для меня сегодня😃
У меня есть пайплайн обработки событий:
- события приходят из источника;
- они группируются по
stream_id;- для каждого
stream_id создаётся отдельный воркер;- внутри одного стрима порядок обработки строго последовательный, между стримами – параллель.
На первый взгляд всё выглядит правильно: много воркеров, каждый занимается своим потоком данных, никакой блокировки на уровне бизнес-логики.
Но есть один нюанс.
Центральный процесс (
Processor) хранил состояние всех очередей, а воркеры:- брали события на обработку;
- после каждого шага синхронно спрашивали у
Processor, что у них сейчас в очереди.То есть десятки, сотни, тысячи воркеров постоянно делали синхронный запрос к одному процессу. Под небольшой нагрузкой система работала идеально. Под средней – начинала думать. Под большой нагрузкой во время стресс-теста – гробовая тишина. Процессы живые, supervisor живой, нет эксепшенов, ошибок в логах.
Это не deadlock в классическом смысле – никто не держит ресурсы навсегда. Это не livelock – процессы не крутятся в бесполезном цикле. Это starvation.
Центральный
Processor захлёбывается от входящих сообщений:- асинхронные сообщения о завершённой обработке;
- синхронные сообщения от воркеров на получение новых событий;
- служебные сообщения.
Я нарушил одно из базовых правил конкурентного дизайна:
Процесс, который владеет состоянием, не должен быть узким горлышком для параллельных воркеров.
Ещё хуже – воркеры не владели своими очередями, постоянно синхронно читали чужое состояние и зависели от одного центрального процесса.
Выглядит это следующим образом. Наглядный пример, как делать не надо:
# Модуль Processor
# Структура состояния:
# state = %{stream_id => [item1, item2, ...]}
def handle_cast({:push, item}, state) do
queue = Map.get(state.items, item.stream_id, [])
new_queue = queue ++ [item]
# если очередь новая — стартуем воркер
start_worker_if_needed(item.stream_id)
# Добавляем новое событие в состояние Processor
{:noreply, push(state.items[item.stream_id], new_queue)}
end
# Модуль Worker
def process_queue(stream_id) do
case GenServer.call(Processor, {:get_queue, stream_id}) do
[item | _] ->
process_item(item)
GenServer.cast(Processor, {:item_processed, item})
process_queue(stream_id)
[] ->
:ok
end
end
Ремарка для любопытных: функция `process_queue` рекурсивная, поскольку в уже существующий стрим могут докладываться новые события.
Правильная модель оказалась сильно проще:
- каждый воркер владеет своей очередью;
- центральный процесс ничего не знает про содержимое очередей, он лишь запускает воркеры, мониторит их и реагирует на завершение или падение.
А чтобы очередь не терялась при падении воркера, источник данных становится единственным source of truth. Воркер загружает очередь при старте. Если он упал – его состояние легко восстановить.
❤2