this :: IO Diary
42 subscribers
19 photos
3 videos
10 links
Download Telegram
Вот он, первый пост из цикла о паттернах в функциональном программировании, и уже он не влез в одно сообщение🫣
Для следующих длинных постов буду искать площадку для хостинга, так как Telegraph мне всё таки не нравится.
Будет интересно почитать, что ты думаешь об этом подходе, и какие решения применял для централизованной обработки ошибок и выстраивания цепочек действий, поскольку с этой проблемой я сталкиваюсь очень часто
3👍1
К каждому паттерну я буду прикреплять примеры использования, чтобы не ограничиваться местами абстрактными примерами. Использование ROP можете посмотреть здесь
41
Пробуем-с
😈3
Пропал на 3 недели, но не просто так – активно преисполнялся в Elixir. В субботу планирую постримить, но не могу выбрать направление:

– Супервизор процессов на 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
😁41
Начинаю ориентировочно в 12:00. Буду рад всем желающим🙏🏾
https://www.twitch.tv/lsdrfrx
Начинаем-с🏃‍♂
Через 10 минут продолжаем
this :: IO Diary
Пропал на 3 недели, но не просто так – активно преисполнялся в Elixir. В субботу планирую постримить, но не могу выбрать направление: – Супервизор процессов на Elixir — написать лёгкий менеджер, который будет управлять процессами: запускать, останавливать…
Стрим длился рекордные 4 часа 40 минут🫣
За это время успел накидать пару базовых компонентов, построить скелет фронтенда и сделать регистрацию и вход в аккаунт на стороне бэкенда. Попробовал новые для себя фреймворки Hono и Drizzle. Пока всё тяп-ляп, далее буду приводить все в красивый вид – как и структуру, так и визуал
Смотри запись тут: https://www.youtube.com/watch?v=jADfBMvaW4c
🔥31🥰1
Щас должно работать, забыл опубликовать👉🏾👈🏾
2
Затроллен нейронкой😔
2
К слову об искусстве нейминга🙊
☠️ Starvation

Не страшно, когда приложение падает, а журнал логов превращается в километровый красный ковёр трейсбэка ошибки. Не страшно даже, когда приложение падает без ошибок. Действительно страшно то, когда приложение умерло, но все выглядит так, что оно продолжает работать. Такую ошибку я поймал сегодня, буквально 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
Правильный пример:
# Модуль Processor
def handle_cast({:push, item}, state) do
unless worker_running?(item.stream_id) do
start_worker(item.stream_id)
end

:ok
end

# Модуль Worker
def init(stream_id) do
queue = Source.fetch_stream(stream_id)
send(self(), :process_next)
{:ok, %{stream_id: stream_id, queue: queue}}
end

def handle_info(:process_next, %{queue: [item | rest]} = state) do
process_item(item)
Source.mark_processed(item.id)
send(self(), :process_next)
{:noreply, %{state | queue: rest}}
end

def handle_info(:process_next, %{queue: []} = state) do
notify_processor(:queue_done, state.stream_id)
{:stop, :normal, state}
end


Более того, так и выглядит поаккуратнее.
5
«Напишу-ка я маленький сервис»
Маленький сервис через месяц:
😁4🔥1