Архитектуры на акторах: вступление

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

                               Говорящий не знает, а знающий – не говорит
                                                                     (БГ)

Какое-то время пытался найти в литературе или интернете описания бекенд-архитектур, построенных из акторов. Спрашивал здесь на форуме. Безрезультатно. При этом в эмбеддед телекоме они распространены, но тоже нет литературы. Поиски работы показали, что за такие знания не платят, поэтому выкладываю опыт предыдущих лет в опенсорс. Анализ большей частью проведен для локальных систем (обмен сообщениями между потоками), но есть шанс, что многие утверждения подходят и для распределенных сервисов. Надеюсь, будет интересно. И надеюсь на фидбек и исправления — у меня нет опыта в бекенде. Если зайдет — можно попытаться оформить публикацию.

Статья открывает небольшой цикл, цель которого — исследование возможных применений акторов в различных типах систем и определение архитектур (статические диаграммы, композиция), в которых их можно использовать. Акторы известны давно (1973), и много лет используются в embedded, telecom, и других областях реального времени. Также, многие современные архитектуры обработки данных по сути сводятся к акторам (просто об этом вслух не говорят). Более того, ООП вначале было сформулировано как взаимодействие объектов через обмен сообщениями (по сути — система акторов), и только позже превратилось в более удобные для написания бизнес-логики синхронные вызовы методов, которые стали стандартом программирования лет 30 как. Итак,

Что такое Акторы?

Актор — это сущность с определенным поведением (код) и состоянием (данные), которая взаимодействует с другими подобными сущностями (акторами) только асинхронной посылкой сообщений. В любой конкретный момент актор или ожидает входящего сообщения (спит), или обрабатывает одно из сообщений. Обычно входящие сообщения складываются в очередь (мейлбокс), а поток-обработчик их оттуда по одному вынимает и процессит. Наверное, знакомо.

Что здесь интересного:

  1. К памяти (состоянию) актора никто кроме самого актора не имеет доступа.
  2. По сути, актор является стейт машиной (конечным автоматом), управляемой входящими сообщениями.
  3. Если внутри актора нет недетерминистических вызовов (работа с ОС), то каждый раз, посылая актору в данном стартовом состоянии одну и ту же последовательность сообщений, будем получать одинаковый результат (event sourcing [MP, DDIA] же). Таким образом, записав события при п(р)оявлении бага, мы можем его выдебажить в день сурка.
  4. Так как (по классике) актор обрабатывает в любой момент не больше одного сообщения (он однопоточный), и так как в его данные никто не лезет, и он не лезет в чужие — про мютексы и синхронизацию можно забыть всерьез и надолго (кроме защиты очереди входящих сообщений, которая пишется один раз на старте проекта, или предоставляется фреймворком).
  5. Так как мы забыли про мютексы и синхронизацию, а память между акторами не шарится, у нас де факто нулевые потери производительности на синхронизации потоков — все ядра используются на 100%, если в системе достаточно нагрузки, чтобы акторам было чем заниматься, и акторов создано больше, чем ядер проца, и кеши памяти хорошо работают (хотя в теории есть еще синхронизация на месседжинге и на менеджере памяти).
  6. Актор очень похож на объект. Отличается тем, что интерфейс у объекта — синхронный, а у актора — асинхронный. Но это отличие приводит к тому, что очень неудобно писать логику, затрагивающую несколько акторов. Сообщения работают по методу fire and forget, то есть, мы не можем заблокироваться и дождаться ответа на наш запрос о чем-то к соседнему актору. Если ответ когда-то и придет, то перед ним может прилететь куча левых запросов, или отмена того самого запроса, на который хотим ответ. В общем, код ужасный, дебажить нереально, и туда лучше не ходить, и ничего там не делать.
  7. Зато внутрь актора можно запихать что угодно, хоть всю бизнес-логику программы, если в ней каждый запрос получается быстро проработать. То есть, никаких проблем с заворачиванием объектов в акторы. Как, в принципе, и наоборот — акторы за интерфейсом объектов (Active Object [POSA2]).

В части языков есть поддержка отсылки сообщений по каналам (Go), в части — отсылка на адресат (Erlang/Elixir). Разницы никакой, и если поддержки из коробки нет — акторы легко пишутся поверх потоков.

Для передачи сообщений могут использоваться подписки (pub/sub [EIP]), шина сообщений (message bus / event bus [EIP]), или прямая отсылка в очередь/сокет. Принципиальной разницы нет, кроме того, что если акторы живут в одном процессе (шарят адресное пространство), можно не сериализовать сообщения (отсылать древовидные структуры в явном виде, не копировать большие куски памяти).

В системе могут использоваться пары сообщений request/confirm, нотификации событий, или оба варианта вперемешку.

Акторы против объектов


Объекты


Акторы


Интерфейс


Синхронный (вызов метода)


Асинхронный (отправка сообщения)


Работа в системе


Последовательная


Параллельная


Детерминизм системы


Да если однопоточная


Нет


Энкапсуляция


Частичная (публичные данные) или полная


Полная (доступен только интерфейс)


Полиморфизм


Да при наследовании/имплементации интерфейса (статическая типизация) или одинаковых именах методов


Да при одинаковом интерфейсе (и никто не знает, что внутри)


Наследование имплементации


Да, часто


Редко, но возможно


Клонирование (копирование)


Да, иногда


Да, иногда

Кроме объектов, синхронно вызывающих друг друга в одном потоке, и акторов, асинхронно кидающих друг другу нотификации, есть смесь — Remote Procedure Call, когда объект дергает актор (отсылает запрос) и ожидает, пока актор обработает вызов (пришлет ответ). Плюс — более простое программирование внутри вызывающего объекта (последовательное как для обычного вызова метода). Минус — тормоза системы из-за использования сообщений и возможная нестабильность или зависание, если вызываемый актор упал. То есть — серединка (компромисс).

На диаграмме можно заметить:

  1. Прямые вызовы (объекты) работают в одном потоке, и вся цепочка событий выглядит проще и быстрее других.
  2. У пересылки событий (акторы) клиентский поток быстрее всего освобождается — он ничего не ждет.
  3. При пересылке событий (акторы) сервисы выполняют задание одновременно.
  4. При удаленном вызове (RPC) нет ни одного из перечисленных преимуществ, и этот сценарий — самый длинный.

И объекты, и акторы являются модулями (которые в последнее время мо(д/ж)но называть сервисами. Вот, собственно, вопрос: когда мы бьем систему на части, что лучше использовать в качестве частей: объекты или акторы? Или смесь синхронных и асинхронных интерфейсов (как многие современные микросервисы)?

Объекты не зря долго были (и сейчас есть для простых систем) решением по умолчанию — их намного проще отлаживать (дебаггер нормально работает так как нет асинхронности; однопоточная система из объектов в общем случае детерминистична и баги воспроизводятся). Рассмотрим случаи, в которых акторы получают преимущество:

  1. Мы хотим делать несколько дел одновременно (для отзывчивости или использования всех ресурсов процессора/кластера). Многопоточность затирает многие преимущества объектов, либо критические секции (синхронизация) сильно мешает эффективной многопоточности.
  2. Нашу систему формируют противоположные наборы сил, например: нужно одновременно работать с файлами / длительными расчетами, и быстро обрабатывать прерывания / показывать юзеру мультики. Мы можем во время расчета переключаться на анимацию, рендерить следующий кадр, и продолжать расчет, но код будет некрасивым и у расчетов, и у видео. Лучше их разделить на независимо работающие подсистемы — акторы. Расчет будет бежать линейно / рекурсивно и непрерывно в бекграунде, а анимация — обрабатывать события (таймер, клики) в более приоритетном форграунде, прерывая (если проц одноядерный) поток с расчетами.
  3. Требуется очень шустро реагировать на события, что нереально сделать в огромной синхронизированной кодовой базе (монолите), или
  4. Требуется легко разрабатывать и деплоить относительно независимый кусок, не завязнув в кодовой базе монолита.
  5. Мы хотим гарантировать, что соседняя команда не вставит нам костыль куда не нужно, чтобы вытянуть себе какие-то данные. С акторами они просто не смогут взаимодействовать мимо интерфейса, даже если дедлайн и ПМ и горит.
  6. Система тупо не влазит на один компьютер (по процу, памяти или диску).
  7. Код настолько разросся, что никто ничего не понимает. Надо переписать по кускам, но так, чтобы история не повторилась (см. пункт 5).

Во всех перечисленных случаях имеет смысл подумать о разделении системы асинхронным интерфейсом с последующим разнесением частей на разные компьютеры, если будет необходимость. Как только мы создали асинхронный интерфейс, и не пытаемся его использовать как синхронный (RPC) — полпути к распределению по нескольким серверам сделано.

Типы систем

Рассмотрим сферический проект в вакууме. На нем пишут идеальный код, который идеально компонуется, и дает возможность компилятору идеально все соптимизировать. Данные идеально расположены в плоскую структуру, и все правильно работает. Это — мир идей, в простонародье известный как спагетти, функциональное или процедурное программирование.

Когда он сталкивается с требованиями бизнеса, проект становится менее сферическим, и более большим. И у архитекторов начинаются проблемы с осознанием всех взаимодействий в домене и выведением из этих взаимодействий единственно верной архитектуры. Как в бородатой истории о временах, когда трава была зеленая, и автор The Timeless Way of Building только начинал изучать даосизм. Как-то команде архитекторов выдали энтерпрайз. Они его и туда, и в другое место — а он не дается. И поняли архитекторы, что надобно расширить сознание. Запаслись доскою, мелками (маркеры в те дни еще не покинули домен коллективного бессознательного) и приняли на ночь немного ЛСД. И посетило их озарение, и изрисовали они доску от края до края, чтобы утром не забыть какая для этого энтерпрайза правильная архитектура. И когда пришло утро, доска все еще была полна информации, но никто из архитекторов не мог осознать целиком запечатленную на доске картинку.

Вот в такие моменты просветления люди задумываются о смысле модульности. И код, и данные делятся по минимально связанным субдоменам, которые уже проще усвоить по-отдельности. Это — модульность, также известная как ООП. Бухгалтер занимается бумажками, сисадмин — разгружает контейнера. Разделение труда.

Но потом наступает жестокая реальность с нефункциональными требованиями, и модульность закапывают туда же, куда недавно проводили осознание доменной модели целиком. Потому что оказывается, что от разных модулей хотят сильно разного, странного, и мы вот это вот все странное никак не можем увязать вместе синхронными связями. И мы делаем модули асинхронными и независимыми. Это — акторы, или (микро-)сервисы.

Если архитектор — энтузиаст, и не знает меры, а дао — далеко и недостижимо, то он свободен сделать еще один шаг, и познать дивный новый мир равиоли, в котором нет синхронных вызовов — все работает через нотификации, прям как в физическом мире. Это — царство наносервисов, также недавно описанное как машина Кузьмина.

Из интересного, но не нарисованного: у хорошего монолита выше пропускная способность (эффективность числомолотилки на одно ядро проца), а у акторов — проще управлять временем отклика (шустрые реал-тайм системы).

Сложность

Сложность — это то, насколько тяжело изменить систему требуемым образом, и не внести при этом в ее поведение неожиданных эффектов. Обычно при росте модели сложность будет расти более чем линейно, то есть, при любых умственных способностях где-то недалеко есть предел размера проекта, за которым фиг сможешь долго нормально работать (можно работать недолго, пока новые баги еще не стали очевидны для пользователей). Различимы следующие основные виды сложности:

  1. Сложность кода (спагетти): поведение кода, определяющего любую конкретную функциональность, зависит от кода, определяющего другую функциональность. В результате, чтобы понять, как работает один кусок кода, надо разобраться в нескольких других, а чтобы разобраться в них — надо понять остальной код на проекте.
  2. Сложность интеграции (равиоли): для того, чтобы модуль, определяющий любую конкретную функциональность, работал ожидаемым образом, требуется определенная конфигурация других модулей в системе. А для каждого из других — третьих. В результате, чтобы изменить интерфейс одного модуля, либо понять систему в целом, надо знать все взаимодействия между модулями в системе.

Общую сложность можно считать суммой двух предыдущих.

Для борьбы с одним видом сложности его переводят в другой. В частности, если проект начинается как моноблок, то для того, чтобы все не зависело от всего, вводят модульность: код и данные разделяются интерфейсами на компоненты. В результате уменьшается зависимость кода внутри любого компонента от кода внутри других компонентов (в идеале, для понимания работы компонента достаточно видеть его код, интерфейс и контракт), но появляется сложность интеграции компонентов (как за деревьями увидеть лес? Что вообще вот на этой огромной системной диаграмме нарисовано?). Чем равномернее мы можем распределить сложность между кодом внутри модулей и интеграцией между модулями — тем быстрее будет разработка на большом проекте, и тем большего размера проект мы сможем сносно поддерживать.

Здесь можно упомянуть дядю Боба с рекомендацией делать методы по 7 действий на метод. Такой подход борется со сложностью кода (спагетти), перенося ее в сложность интеграции (равиоли). Проблема в том, что когда у нас на проекте в несколько миллионов строк кода получаются сотни тысяч методов — любой программист просто запутается между этими методами, вне зависимости от того, они лежат в нескольких классах по 10 000 методов в каждом (God Objects) или там 10 000 разных классов по 10 методов у каждого (Ravioli code). От применения правила лучше не стало — стало невозможно работать. Лучше станет, когда при росте проекта мы одновременно и примерно равномерно наращиваем и количество кода в методах, и количество методов в классах, и количество классов в модулях, и количество модулей в сервисе, и количество сервисов в системе. В таком идеализированном случае общая сложность равномерно распределяется по нескольким уровням(типам), и ни на одном из уровней не зашкаливает.

Теперь сравним приведенные выше типы систем (по количеству и жесткости интерфейсов) и метрики сложности:

Получается, что для больших проектов идеальной будет серединка между модульным ООП и асинхронными акторами. Эта common wisdom называется Микросервисы: ООП используется (и берет на себя часть общей сложности) внутри сервиса, асинхронные интерфейсы — между сервисами.

Распределяемость

Еще один фактор, обеспечивший успех микросервисов при разумном подходе (голова может помочь избегать правил (в идеале — любых, обычно — неприменимых к текущей ситуации), если ею привыкнуть думать (или, как любил повторять классик, ‘Think’ is not a four-letter word)) — легкость распределения по (физически) независимым серверам сравнительно с модульным приложением.

Распределенная система — это когда один компонент не имеет эффективного доступа к памяти других компонентов. При этом нет разницы, мы искусственно ввели такое ограничение для акторов, живущих в одном процессе, или у нас взаимодействуют процессы на одной машине, или процессы раскиданы по разным машинам в сети (худший вариант из-за нестабильности связи и расхождения системного времени [DDIA]). Свойства распределенной системы более-менее одинаковы для всех случаев:

  1. В системе логически (часто и физически) одновременно производится много действий — каждый компонент делает свою работу.
  2. Взаимодействие компонентов (часто очень) дорогое по сравнению с действиями внутри одного компонента.
  3. Синхронизация состояния компонентов исключительно дорогая. То есть, при нормальной работе системы никакой компонент не может знать текущее состояние никакого другого компонента.

Когда компоненты связаны синхронно (RPC между ООП модулями — распределенный монолит), недоступность одного из компонентов, вероятно, остановит работу остальных эффектом домино. Любой вызов метода недоступного компонента блокирует вызывающий модуль (скорее всего, с таймаутом и исключением после таймаута, но все равно — скорость работы сильно нарушена, и может произойти распространение ошибки (таймаута) по цепочке вызовов между компонентами.

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

Вот эта разница в отказоустойчивости тоже сделала вклад в переход от синхронных модульных систем к асинхронным сервисам.

Принцип построения распределенных систем

Чтобы пытаться делать задачу просто, и не делать сложные задачи (KISS, правило 20/80), нужно примерно понимать, что для нас просто, а что — сложно (кэп).

Отсортируем взаимодействия в системе акторов (асинхронной распределенной системе) по возрастанию сложности:

  1. Локальная обработка входящего события / нотификации. Очень быстро, без заметных побочных эффектов, занимает одно ядро процессора и не мешает другим акторам. Событие может либо прийти локальным прерыванием / колбеком от железа или ОСи, и так попасть в очередь сообщений, либо как нотификация / подписка от внешнего агента. Идеал.
  2. Обработка запроса с отсылкой ответа. Тоже хорошо, чуть больше забот для формирования обратного сообщения (confirm) с результатами обработки входящего запроса (request).
  3. Событие, вызывающее multicast нотификации. Произошло что-то важное, о чем нужно сообщить другим компонентам (акторам). Мы делаем свою часть работы (вероятно, преобразуем логику и данные, присланные внешним источником, в наш внутренний удобный формат — (AntiCorruption Layer [DDD])), а дальше — отсылаем высокоуровневое сообщение с полезной информацией тем, кому она нужна (всем подписчикам или конкретному модулю). Так как в данном случае сообщения распространяются по системе, он дороже предыдущих по затратам ресурсов, и больше времени пройдет, пока вся система адаптируется к / изменит состояние соответственно входящей нотификации (все views отобразят данные из event source, eventual consistency [DDIA]).
  4. Цепочка обработки / распределенный сценарий. Входим в область хореографии [MP]. Нам пришел запрос, но мы не компетентны решить, как на его отреагировать. Мы перерабатываем формат данных в принятый для нашей системы [DDD], отрезаем лишние данные [EIP], добавляем полезную информацию из нашего локального состояния [EIP], и форвардим получившийся высокоуровневый запрос в модуль, который заведует соответствующей функциональностью. Процесс может повторяться, пока кто-то не примет ответственности за решение и не пошлет ответ обратно по цепочке. Результат — медленная обработка запроса (много асинхронных шагов, акторы в цепочке могут быть заняты чем-то другим и запрос провисит в очереди). Но тут еще спасает то, что решение (логика) принимается локально в одном конкретном акторе.
    Вариант: актор, принявший решение, не отсылает обратно по цепочке ответ, а рассылает нотификации об изменении состояния системы. Мы подписаны на такие нотификации, видим, что состояние изменилось в степени свободы, связанной с оригинальным запросом, и отправляем на него ответ. Цикл в графе рассылки.
  5. Решение требует информации о состоянии других акторов. Мы не можем найти кого-то, кто более компетентен для принятия решения, чем текущий актор, но в нем нет всех нужных данных. Для решения потребуется разослать всем вовлеченным акторам запрос на получение данных, запомнить ответы, объединить полученную информацию, и принять на основе ее решение. По сути — получаем распределенный join / создаем views [DDIA]. Сценарий осложняется тем, что пока мы опрашиваем соседей, их состояние может меняться, то есть — наша выборка (join) может состоять из данных (views), относящихся к несовместимым снепшотам общего состояния системы (базы) [DDIA]. Ну и скорость и стабильность сценария будет хуже, чем у самого худшего из участников. И много ненужного копирования данных по системе. И нам надо наворотить много кода и потратить много рамы, чтобы такое взаимодействие реализовать.
    Вывод: это красный флаг. Как минимум — новые требования пошли вразрез с нашим изначальным пониманием домена, исходя из которого писалась архитектура (неправильная граница разбития на субдомены), и мы делаем в 10 раз сложнее то, что в монолите — один вызов метода объекта. Как максимум — мы захотели попробовать микросервисы, и теперь начинаем огребать (слишком мелкие субдомены).
  6. Обработка запроса должна синхронизировать состояние нескольких акторов. Если будет запоздание между изменениями их состояний, нарушится системный инвариант, и вся система начнет принимать неверные решения.
    Либо: нам надо собрать данные с нескольких акторов (компонентов) и их отредактировать. При этом недопустимо, чтобы кто-то другой изменил эти данные, пока мы их редактируем (конфликт версий).
    Поздравляю (R.I.P.): у нас распределенная транзакция [DDIA]. Это вообще не к акторам. Архитектура летит нафиг. Мы пишем распределенный монолит [MP]. В принципе, каждый конкретный случай патчится оркестратором [MP], но очень очень вероятно, что систему разбили на слишком мелкие куски, и пора или переписать все (говорят, нормальный код получается после двух-трех итераций), или уволиться.
    Исключение — энтерпрайзные микро(?)сервисы с сотнями программистов, когда объем кода и нагрузка для даже одного субдомена разрослись настолько, что субдомен приходится резать как угодно, лишь бы разделить на части — с целым субдоменом уже организационно нереально справиться. Тут или все будет плохо (шарик испортится, и прочие оркестраторы), или проект можно сразу закапывать из-за сложности домена и количества требований. Но это тоже не значит, что не пора уволиться.

Разновидности Акторов

Идея акторов слишком проста и очевидна, чтобы не вызвать множество извращений. Вот наиболее распространенные варианты:

Использование потоков

  1. Ванильные акторы (любой язык с потоками и даже Unix pipes). Каждому актору выделен свой поток или процесс, скедюлингом занимается ось. При этом при создании акторов их потокам можно задать реал-тайм приоритеты, настроив скедюлинг под нужды системы.
  2. Общий пул потоков aka fibers (Эрланг/Эликсир из коробки, можно написать на С/С++). Здесь мы создаем по потоку на ядро процессора, прибиваем их гвоздями (CPU affinity), и пишем свой скедюлер. Свободный поток проходится по известным акторам и запускает среди них того, у которого в очереди лежит сообщение. Можно складывать готовых к работе (имеющих сообщение) акторов в очередь с приоритетами по реалтаймовости (насколько важна быстрая реакция) данного актора. Мы не переключаемся в ядро ОС, когда у текущего актора закончились сообщения в очереди. Также интересно то, что здесь можно плодить миллионы акторов так как пассивный (ждущий пока дадут проца) актор не имеет своего стека, соответственно, вообще не использует лишней памяти. Актор весит как обычный объект + текущие сообщения в очереди. Только вот зачем нам эти миллионы?
  3. Пул потоков с квотами времени (Эрланг/Эликсир). Скедюлер не ждет, пока текущий актор обработает сообщение, а может переключаться между акторами по таймеру. В результате, если есть прожорливый или зависший актор, то он не мешает остальным — прожорливому достается только незанятый более быстрыми соседями остаток времени процессора.
  4. Многопоточные акторы (часто микросервисы). Если у актора есть состояние — то жертвуем простотой кода (отсутствием мютексов) ради того, чтобы актор мог одновременно обрабатывать несколько входящих запросов. Вероятно, влом выносить базу данных в отдельный актор, или хочется упрощения распределенных сценариев через RPC вместо событий/корутин. Вместе с простотой пожертвовали еще и воспроизводимостью событий при проигрывании входящих сообщений.
  5. Акторы на корутинах. Код в акторе состоит из верхней синхронной половины корутин-сценариев и нижней — асинхронных обработчиков событий (Half-Sync/Half-Async [POSA2]). Легковесный по ресурсам вариант многопоточного реактора [POSA2] ценой лишних усилий на асинхронную часть — по сути, мы реализовали RPC движок ручками. Вернемся к ним во второй части статьи.

Работа с состоянием

  1. У каждого актора свое состояние в раме (стандартный вариант).
  2. Сериализуемые акторы (Акка). Состояние актора дампится на диск или в сеть, и его можно восстановить в случае креша, и можно выгружать из памяти, если ее мало осталось. Очередной event soursing.
  3. Распределенные акторы (Эрланг/Эликсир, Акка). Рантайм сам раскидывает акторы по сети, решает, как кому куда переслать сообщение, и поднимает скоропостижно скрешившихся или бутнувшихся. Цена: появляются конфиг файлы с магией, и можно словить нежданчики, когда актора положили на одном компе и еще не подняли на другом.
  4. Акторы без состояния (CGI/fCGI/FaaS). Кусок кода с интерфейсом (который может состоять из Init() и Handle(message)), получающий все нужное для работы внутри message, а чего не хватает — вынимается из глобальных переменных или сервисов. Можно спавнить в большом количестве, не задумываясь о синхронизации.

Прочие приколы

  1. Тормозные акторы (работа с периферией). Эти ребята получают запрос, лезут куда-то читать / писать / юзать / считать данные, отправляют ответ, и берут следующий запрос. Получается, поток выполнения блокируется на работе с железом. Примеры: адаптер базы в гексагональной архитектуре, управление манипулятором у робота, рендерер какой-нибудь.
  2. Акторы с приоритетами (сетевой трафик). Входящие сообщения в зависимости от типа, значения какого-то поля, или функции-фильтра распределяются в одну из очередей разных приоритетов, либо сортируются внутри единой очереди с приоритетами. Далее возможно или выполнение высокоприоритетных запросов одновременно с нормальными (в этом случае, например, можно сообщением отменить выполнение текущей задачи), или следующим обрабатывается сообщение максимального приоритета (можно отменить еще не взятую в работу задачу; можно пустить управляющий реал-тайм трафик одновременно с потоком данных; можно давать квоты времени разным заказчикам).
  3. Иерархии акторов (Акка). Один актор может в рантайме создавать других акторов, передавать им каналы для отсылки сообщений (адреса акторов) и подписываться на извещение о смерти. Цена: куча сложности с непонятным профитом.

Это все комбинируется под свои капризы и бюджет. Но если перебдеть — то из шустрой моськи с двумя методами сделаете слона с кривой обучения.

Итого:

  • Посмотрели дихотомию связность-гранулярность и увидели, что оптимум для разработчиков где-то посредине
  • Сравнили объекты и акторы
  • Прикинули, что акторам делать просто, а что — сложно
  • Ознакомились с вариантами акторов

Теперь давайте попробуем собрать из этих акторов что-то полезное. Продолжение следует...

Литература

[DDD] Domain-Driven Design: Tackling Complexity in the Heart of Software. Eric Evans. Addison-Wesley (2003).

[DDIA] Designing Data-Intensive Applications: The Big Ideas Behind Reliable, Scalable, and Maintainable Systems. Martin Kleppmann. O’Reilly Media, Inc. (2017).

[EIP] Enterprise Integration Patterns. Gregor Hohpe and Bobby Woolf. Addison-Wesley (2003).

[MP] Microservices Patterns: With Examples in Java. Chris Richardson. Manning Publications (2018).

[POSA2] Pattern-Oriented Software Architecture Volume 2: Patterns for Concurrent and Networked Objects. Douglas C. Schmidt, Michael Stal, Hans Rohnert, Frank Buschmann. John Wiley & Sons, Inc. (2000)

👍ПодобаєтьсяСподобалось36
До обраногоВ обраному25
LinkedIn

Найкращі коментарі пропустити

Сильный и очень грамотный материал, спасибо большое !

Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Следующая часть: реактор, проактор, half-sync/half-async dou.ua/forums/topic/36374

Похоже, вторая часть разрослась — надо делить пополам, чтобы была читаемой за один подход. Есть надежда за пару недель задеплоить первую половину.

Хорошее интро.

Было бы хорошо упомянуть что не только дебажить тяжело но еще и тестировать.
А еще в каких случаях противопоказанно применять акторов.

По-мелочи, но важно.

Различимы следующие основные виды сложности

Есть еще accidential и essential сложность. Любая распределенная система обладает повышенной accidential сложностью, но иногда да, для куда более технических доменов это уже ближе к essential complexity (телеком тот же)

модульность: код и данные разделяются интерфейсами на компоненты.
Это — модульность, также известная как ООП.

Модульность чаще объединяет и код и данные и не тождественна ООП. Модули были еще до ООП. В ООП — инкапсуляция, а та же инкапсуляция в структурном или функцмональном мире это как раз модульность. А бывает и так что код это данные, а данные это код.

Это — акторы, или (микро-)сервисы.

Тут тоже одно не равно другому. Все это сервисы, но то что понимают под микросервисами нынче, решает другие задачи. Микросервисы это о том как масштабировать организацию, дать автономию командам, разделить владение и как-нибудь разрулить зависимости между ними. Акторы все же больше о параллелизации вычислений и кодовая база там скорее будет общей как и ЯП/фреймворк. Хотя никто не мешает конечно юзать акторов в микросервисах, тот же Dapr sidecar в .NET на базе MS Orleans яркий тому пример.

И кстати, это вообще о чем?

Данные идеально расположены в плоскую структуру, и все правильно работает. Это — мир идей, в простонародье известный как спагетти, функциональное или процедурное программирование.

Причем тут процедурное или ф-е программрование? Спагетти и равиоли в ООП по определению готовятся бытрей и проще — там столько рецептов что лечге плюнуть и сварить спагетти чем заморачиваться как идеальную лазанью сделать. И это везде и всюду наблюдаю.
В стурктурных или ФП языках разбить методы по модулям и собрать из меньшего в большее интуитивно и на практике проще, чем спроектировать класс с правильной инкапсуляцией, интерфейсами и ненафакапить где-нибудь в иерархии с наследованием.

Любая распределенная система обладает повышенной accidential сложностью

Степень повышения будет сильно зависеть от того, через сколько связей из essential сложности прошли границы разделения. Если части, которые разнесли по нескольким процессам или боксам, были слабо связаны между собой, то большого прироста сложности из-за распределения мы не получим.

Модульность чаще объединяет и код и данные и не тождественна ООП.

На С обычно используют модули вместо объектов. В таком варианте модуль может:
1) иметь свои статические данные и функции, недоступные извне
2) предоставлять наружу интерфейс, который может совпадать с интерфейсом другого модуля
3) использовать при имплементации своего интерфейса другие модули или их интерфейсы.
найдите одно отличие от объекта.

Микросервисы это о том как масштабировать организацию

но при этом архитектурные грабли при разбитии системы на микросервисы или на акторы общие. По крайней мере — в доменной области. Разрежешь сильносвязный кусок — огребешь, когда нужно будет этими связями воспользоваться martinfowler.com/...​oservice-verdict/path.png Если был неосторожен, и таких проблем несколько — проекту конец, потому что кодить стало невозможно. Только вот, чтобы ощутить вживую вот это вот все на микросервисах — надо быть архитектором с кучелетним опытом. А у акторов — пушной зверек намного ближе и шустрее. Поэтому его проще исследовать.

Спагетти и равиоли в ООП по определению готовятся бытрей и проще

В ООП одинаково проходят границы модулей у кода и у данных. В ФП и процедурщине, насколько я понимаю, такого правила нет. Соответсвенно, в каждом месте надо ощущать 2 разных модуля: модуль кода и сдвинутый в домене относительно него модуль данных. А практика подтверждает большее удобство ООП тем, что на нем есть большие старые проекты. Много. По сравнению с процедурными и ФП. Значит, этот код можно поддерживать и развивать, даже когда его много, и уже костыли со всех сторон вываливаются.

На С обычно используют модули вместо объектов. В таком варианте модуль может:
1) иметь свои статические данные и функции, недоступные извне
2) предоставлять наружу интерфейс, который может совпадать с интерфейсом другого модуля
3) использовать при имплементации своего интерфейса другие модули или их интерфейсы.
найдите одно отличие от объекта.

Уже помнится обсуждали это. Инкапсуляция (модульность) сущевствует вне ООП. Отличия или схожести отношения к делу не имеют. Так можно и статический метод в классе сравнить с ф-й в функциональном ЯП. Наследование и инкапсуляция в ООП всего лишь прелюдия к главному свойству — полиморфизму. Как-то же на Си , Эрланге и Хаскеле умудрялись тонны кода в спагетти не смешать.

Только вот, чтобы ощутить вживую вот это вот все на микросервисах — надо быть архитектором с кучелетним опытом. А у акторов — пушной зверек намного ближе и шустрее. Поэтому его проще исследовать.

Ну проектирование все равно никто не отменял. Разрезать на правильные кусочки это нужно уметь. High cohesion и low coupling в акторах стираются и это непривычно, а это одни из основопологающих принципов архитектуры. Но играться с актороми сплошное удовольствие.

В ООП одинаково проходят границы модулей у кода и у данных. В ФП и процедурщине, насколько я понимаю, такого правила нет.

В ООП как и везде границы там где нарисуешь. Но по сути в ООП есть поведение (код), состояние (данные) и это все завернуто в коробочку (объект). Есть конечно сторонники анемичных моделей и rich, но сути сильно не меняет.
В ФП все просто, функции = данные. Есть еще ADT которые данные. Ну и можно всю программу выразить в виде структуры данных и интерпретировать ее. У того же LINQ оттуда ноги растут.

Соответсвенно, в каждом месте надо ощущать 2 разных модуля: модуль кода и сдвинутый в домене относительно него модуль данных

Что такое модуль данных? Вы говорите о статических данных или динамических?

А практика подтверждает большее удобство ООП тем, что на нем есть большие старые проекты. Много. По сравнению с процедурными и ФП. Значит, этот код можно поддерживать и развивать, даже когда его много, и уже костыли со всех сторон вываливаются.

А кода на процедурном и структурном больше чем кода на ООП и ФП вместе взятых :) И он поддерживается вполне себе... Даже КОБОЛисты-ветераны еще есть живые и где-то да поддерживают американский бизнес на плаву. Тут больше вопрос к квалификации чем к парадигме

Наследование и инкапсуляция в ООП всего лишь прелюдия к главному свойству — полиморфизму.

Полиморфизм между модулями достигается одинаковым интерфейсом. Как в линухе ядро подгружает драйвера, не зная заранее, какой ему драйвер подсунут? Это и есть полиморфизм. Работает через структуры указателей на методы. Как и в С++, только там оно под капотом. В результате модуль — такой же объект.

High cohesion и low coupling в акторах стираются и это непривычно

Почему они должны стираться? У нас же нет цели создать 500 разных акторов. У нас цель — разрезать домен наиболее удобным для будущей разработки, переделки и поддержки образом. Пускай получим на выходе 1, 3 или даже 5 акторов. Они будут выполнять свои задачи. ithare.com/...​ntent/uploads/Fig-V-6.png

В ФП все просто, функции = данные.

А где сами данные?) Вот надо картинки из папки в формате JPEG преобразовать в формат BMP и отослать почтой (или наоборот). Картинки же не являются функциями? И структура картинки или письма включает в себя всякие там заголовки. И еще есть где-то список мейлов, на которые это разослать. Про ADT не знаю, и как ФП должно разруливать картинки.

Что такое модуль данных? Вы говорите о статических данных или динамических?

Я говорю о структуре заголовков фотографий и мейла. Чтобы послать мейл, надо набить его заголовки. Где лежит понимание этих заголовков и того, как их набивать? Перемешано с логикой того, какие данные юзер хочет написать в заголовки? Или бизнес-логика отдельно, а формат заголовков мейла — отдельно?

А кода на процедурном и структурном больше чем кода на ООП и ФП вместе взятых :)

Это какой такой код? На Джаве за последние лет 20 написали больше, чем все, что до этого существовало. А еще добавили Питон и С++ и джаваскрипт.

Полиморфизм между модулями достигается одинаковым интерфейсом

Полиморфизм есть суть ООП и только ему присуще. Между модулями может все же сводится к вызову функций по тому же указателю подгруженного модуля если говорить о Си?

Как в линухе ядро подгружает драйвера, не зная заранее, какой ему драйвер подсунут?
#include <linux/init.h>
#include <linux/module.h>

static int my_init(void)
{
    return  0;
}
    
static void my_exit(void)
{
    return;
}
    
module_init(my_init);
module_exit(my_exit);
Ну и поскольку драйвер может вызывать ф-и определенные в ядре то на этапе linking-а все внешние ссылки и резолвятся. Потом это все либо компилият вместе с ядром (монолит) либо готовят в виде kernel module без рекомпиляции ядра.
Потом готовят makefile и на выходе имеем определенный object file (*.ko) который подгружаем через make load и вуаля имеем модуль в /proc/modules.

Извините, може чего упустил, очень давно под линукс не программировал, но суть думаю осталась.

Но это же не полиморфизм! Всего лишь API и соглашения.

Почему они должны стираться? У нас же нет цели создать 500 разных акторов.

Ну почему же, в нетфликсе или убере там на сотни счет идет как раз если не на тысячи микросервисов. Тут уже нужно правильно их сгруппировать и взимодействие определить. Акторы мне кажется сложней по коробочкам доменных зон ответственностей распихать. Это как много отдельных объектов и каждый со своим мейлбоксом и изолирован. На уровне исходников группировать конечно нужно, опять же , по модулям. Но наверное у меня просто практики не хватает если чесно по архитектуре акторов.

Пускай получим на выходе 1, 3 или даже 5 акторов. Они будут выполнять свои задачи

Ну такие мелкие системы вообще в проектировании мало нуждаются, хоть на акторах, хоть где. Вопрос в масштабе ведь.

А где сами данные?

Ну вот функцию можно передать в функцию. Это в простонародье называют функции высших порядков. И это уже данные для принимаемой функции (как указатель на структуру в Си например).

Вот надо картинки из папки в формате JPEG преобразовать в формат BMP и отослать почтой (или наоборот). Картинки же не являются функциями?

Это вообще чисто практическая задача. Ф-я преобразования и отправки будет выглядеть примерно так:

getFiles JPEG |> transformTo BMP |> sendTo '[email protected]'

Каждая из этих функций определена например в модуле IO, Pictures, Email и со своими вспомогательными ф-ми. Здесь уже сборка пайплайна. Вообще функциональщину можно выразить как Pipeline Oriented Programming.

А вот конкретный и рабочий пример траверсала по папкам и принта всех файлов с разруливанием вложенности:

open System
open System.IO

let rec SafeFileHierarchy startDir =
   let TryEnumerateFiles dir = 
      try 
         System.IO.Directory.EnumerateFiles(startDir)
      with _ -> Seq.empty
   
   let TryEnumerateDirs dir = 
      try 
         System.IO.Directory.EnumerateDirectories(startDir)
      with _ -> Seq.empty

   seq { 
      yield! TryEnumerateFiles startDir
      for dir in TryEnumerateDirs startDir do
         yield! (SafeFileHierarchy dir)
   }

Ничего нигде не перемешано и все красиво.

Это какой такой код? На Джаве за последние лет 20 написали больше, чем все, что до этого существовало

Ну не знаю. Больше ли? Не имею статистики. Все таки 50-60 лет коболов, фортранов и паскалей были.

джаваскрипт

Этот зверь вообще в сторонке :) Он и ООП и ФП и процедурный если сильно захочется :)

Полиморфизм есть суть ООП и только ему присуще.

Нет, это просто когда мы можем в одну штуку подставить разные аргументы с разным кодом en.wikipedia.org/...​rphism_(computer_science

Но это же не полиморфизм! Всего лишь API и соглашения.

Это как раз настоящий полиморфизм. И даже dependency injection + inversion of control).
Но суть не в этом. Суть в том, что модуль является объектом — он выглядит как объект и крякает как объект.

в нетфликсе или убере там на сотни счет идет как раз если не на тысячи микросервисов

Так у них сотни человек десятки лет это писали. Домен сложный и, вероятно, не настолько связный, чтобы его было проблематичным разделить на 100 частей. Понимаете, если США могут прислать авианосец и навалять Ираку — это не значит, что Украина может пытаться сделать то же самое. Авианосец-то построить сначала надо!

Ну такие мелкие системы вообще в проектировании мало нуждаются, хоть на акторах, хоть где

 То есть, Вы считаете, что гексагональная архитектура появилась на ровном месте — ведь там почти вся логика на одном сервере лежит? Или что базы данных (тот же MySQL) не нуждаются в проектировании и архитектуре — взял, написал что-то как-нибудь, и оно заработало? Я имею в виду собственно код MySQL, который даже на акторы или микросервисы не разбит, кажется.

Ну вот функцию можно передать в функцию. Это в простонародье называют функции высших порядков. И это уже данные для принимаемой функции (как указатель на структуру в Си например).

В С это указатель на функцию. Не надо никаких структур. Просто адрес скомпилированной функции. С стал функциональным языком?

Каждая из этих функций определена например в модуле IO, Pictures, Email и со своими вспомогательными ф-ми.

И вот вдруг обнаруживается, что модуль Email содержит:
1) кучу настроек коннекшнов, сервера, форматов данных. Хуже того — он использует пул коннекшнов чтобы быстрее работать. То есть — у этого модуля есть энкапсулированное состояние.
2) внутренний код, работающий с этим внутренним состоянием. А наружу торчит только АПИ. И это АПИ еще и полиморфно для пайплайна, присылающего данные — то есть, вместо мейла можно подставить другой модуль
3) и еще мейл использует третий модуль для работы с пайплайнами, или с коннекшнами. и при получении данных из сети они направляются в модуль связности, который потом дергает подписку модуля мейлов
Вот мы и получили ООП во вроде бы функциональной системе.

Ничего нигде не перемешано и все красиво.

Пока сюда не попадает бизнес логика о том, какие именно файлы нам нужны, а какие — нет.
* Если Вы напишете фильтр файлов прямо в этом куске кода — то код будет запутан
* Если Вы отделите фильтр в другой модуль — получится, что Вы группируете данные (файлы и директории) с кодом, который их обрабатывает (траверс). А код, задающий логику фильтрования — ничего толком не знает о траверсе. Снова ООП.

«давайте договариваться о терминах»

дело ж в том что многие академические термины из CS на практике значат уже не совсем то, а бывает и совсем не то
например:

Просто адрес скомпилированной функции. С стал функциональным языком?

дело ж в том что с точки зрения функциональщины — и в С, и везде в императивных языках функций нет. есть — процедуры с возращаемым значением :)
(в джс — это вообще объект с специальной пометочкой, спека:
9.2 ECMAScript Function Objects
ECMAScript function objects encapsulate parameterized ECMAScript code closed over a lexical environment and support the dynamic evaluation of that code.
...
All ECMAScript function objects have the [[Call]] internal method defined here
)
в PHP магическим методом __invoke() можно добавить объекту возможность быть вызванным как «процедура»

Нет, это просто когда мы можем в одну штуку подставить разные аргументы с разным кодом

как бы да, вызов «процедуры» по указателю — оно и есть

то есть на практике полифоморфизм это когда мы по строчке кода вызова не может сказать что будет вызвано, какой конкретно код будет выполнен :)

модуль является объектом — он выглядит как объект и крякает как объект.

слово «если» пропущено. если он выглядит как объект

а это не везде так. например в пыхе модули — это всего лишь неймспейсы. и разруливаются они спец штукой — автолоадером
в Java — их тоже нет, модулей как объектов. есть — пакеты классов, то есть — без состояния

в Node.js да, CommonJS — это модули как синглтон объекты

вобщем когда говорим модуль — о каком ЯП и его экосистеме речь?

Снова ООП.

ООП — термин скорей философский, чем CSный.
поэтому и холивары о нем — бесконечны

Нет, это просто когда мы можем в одну штуку подставить разные аргументы с разным кодом

Прикольно, не знал что впервые ad hoc полиморфизм появился в ALGOL 68. Но и там же далее в статье все таки отсылка к Simula и наследованию и тому что мы все хорошо знаем по ООП — полиморфизму через function overloading или operator overloading.

Это как раз настоящий полиморфизм.

Ну все равно не убедили что это полиморфизм. В модуле функции не переопределяют функции другого модуля а только определяют его границы и функционал. В случае с ядром линукса мы подключили хедер файл (интерфейс) и определили имплементацию по вполне прописанной сигнатуре. Мы ничего не переопределяли. Где тут полиморфизм?

И даже dependency injection + inversion of control).

Это как? В Си? Не, ну я могу понять фабрику там, но IoC и DI. Может пример покажете?

То есть, Вы считаете, что гексагональная архитектура появилась на ровном месте — ведь там почти вся логика на одном сервере лежит? Или что базы данных (тот же MySQL) не нуждаются в проектировании и архитектуре — взял, написал что-то как-нибудь, и оно заработало?

Ну там же и не 1,3 или даже 5 ответственностей, правда? Изначально разговор был про это.

В С это указатель на функцию. Не надо никаких структур. Просто адрес скомпилированной функции. С стал функциональным языком?

Вот именно, просто указатель. Ф-я там не является то что называют first-class citizen. Нет никаких гарантий от системы типов и компилятора. В том же C# или Java нет поддержки partial application и каррирования на уровне конструктов языка. Но там это можно на уровне приложения или библиотеки с небольшими танцами. В ф-х языках есть.

И вот вдруг обнаруживается, что модуль Email содержит:
1) кучу настроек коннекшнов, сервера, форматов данных. Хуже того — он использует пул коннекшнов чтобы быстрее работать. То есть — у этого модуля есть энкапсулированное состояние.

А что здесь не так? Да, инкапсуляция. Это хорошо. А вы это оборачиваете все это и пользуете как нужно. Разве в остальных ЯП не так?

2) внутренний код, работающий с этим внутренним состоянием. А наружу торчит только АПИ. И это АПИ еще и полиморфно для пайплайна, присылающего данные — то есть, вместо мейла можно подставить другой модуль

Что-то загнули тут. Подставить другой модуль это хорошо или плохо? :)

3) и еще мейл использует третий модуль для работы с пайплайнами, или с коннекшнами. и при получении данных из сети они направляются в модуль связности, который потом дергает подписку модуля мейлов
Вот мы и получили ООП во вроде бы функциональной системе.

Так и наоборот работает же :) Я частенько в C# подключал библиотеки из F#. Есть такая одна известная в мире .NET — FsCheck для property-based testing — чисто F# либа, которая порт с Haskell. И прекрасно работает.
Это называется уже реюзабилити на уровне компонентов. Какая там внутренняя кухня меня не интересует. А учитывая что все это .NET байт-код и ранится в виртуальной машине то вообще фиолетово.

Пока сюда не попадает бизнес логика о том, какие именно файлы нам нужны, а какие — нет.
* Если Вы напишете фильтр файлов прямо в этом куске кода — то код будет запутан
* Если Вы отделите фильтр в другой модуль — получится, что Вы группируете данные (файлы и директории) с кодом, который их обрабатывает (траверс). А код, задающий логику фильтрования — ничего толком не знает о траверсе. Снова ООП.

Вам везде мерещится ООП :) Никакого ООП в структурировании и разделении кода и поведения нет. Уже говорил — хексагоналка в ФП она почти по умолчанию без танцев с бубнами. Попробуйте мыслить на каждый ваш запрос — что это ф-я.

Фильтр файлов? — предикат, т.е. ф-я. Можно параметром принимать

Ну все равно не убедили что это полиморфизм. В модуле функции не переопределяют функции другого модуля а только определяют его границы и функционал.

А полиморфизм это что? „Многоформенность” если перевести. Ну или из вики: polymorphism is the provision of a single interface to entities of different types or the use of a single symbol to represent multiple different types. То есть, ядро ни разу не знает, какой именно драйвер видяшки (или терминала) оно вызывает. Ему пофиг. Оно дергает у этого модуля метод „сделать зашибись” в интерфейсной структуре. А каждый модуль (драйвер) подставляет в эту структуру свою какую хочет функцию:
Operations that one can perform on files depend on the drivers that manage those files.
Such operations are defined in the kernel as instances of struct file_operations.
struct file_operations exposes a set of callbacks that will handle any user-space
system call on a file. For example, if one wants users to be able to perform a write on the
file representing our device, one must implement the callback corresponding to that write
function and add it into the struct file_operations that will be tied to your device.
Let’s fill in a file operations structure:

struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t*);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
int (*flock) (struct file *, int, struct file_lock *);
[...]
};
The preceding excerpt only lists important methods of the structure, especially the ones that
are relevant for the needs of this book. One can find the full description in
include/linux/fs.h in kernel sources. Each of these callbacks is linked with a system
call, and none of them is mandatory. When a user code calls a files-related system call on a
given file, the kernel looks for the driver responsible for that file (especially the one that
created the file), locates its struct file_operations structure, and checks whether the
method that matches the system call is defined or not. If yes, it simply runs it. If not, it
returns an error code that varies depending on the system call. For example, an undefined
(*mmap) method will return -ENODEV to user, whereas an undefined (*write) method
will return -EINVAL.
Вот у нас структура в качестве интерфейса драйвера, ядро дергает члены этой структуры не зная, что там внутри и как заимплеменчено. Прям по определению полиморфизма, не?
Прям по определению полиморфизма, не?

Не, это интерфейс (АПИ) и его имплементация которая живет в отдельном модуле и подгружаема. Из выдержи в вики:

polymorphism is the provision of a single interface to entities of different types or the use of a single symbol to represent multiple different types

Присутствует понятие типа. В CS тип это

a data type or simply type is an attribute of data which tells the compiler or interpreter how the programmer intends to use the data

А не просто указатель на функцию который ядро может дёрнуть. Прямая аналогия в ООП будет опрелить интерфейс и имплементировать его 500 разными классами. Это полиморфизм уже или еще не?

Ну или зайдем с другой стороны, дайте тогда определение тому какой это тип полиморфизма и почему?

type is an attribute of data which tells the compiler or interpreter how the programmer intends to use the data

А модуль ядра или скомпилированное приложение является данными? Что не так?

Прямая аналогия в ООП будет опрелить интерфейс и имплементировать его 500 разными классами. Это полиморфизм уже или еще не?

Да, полиморфизм. en.wikipedia.org/wiki/Ad_hoc_polymorphism

Ну или зайдем с другой стороны, дайте тогда определение тому какой это тип полиморфизма и почему?

Какой тип — не знаю, у меня нет мат образования, чтобы туда закапываться. Поведение решается наполнением интерфейсной структуры, то есть полиморфизм опирается на данные.

А модуль ядра или скомпилированное приложение является данными? Что не так?

Модуль ядра это модуль ядра. Микро-кернел архитектура, plug-in pattern, как угодно.

Да, полиморфизм. en.wikipedia.org/wiki/Ad_hoc_polymorphism

И где там об этом написано? Опять же, сказано о типах и о ООП. Про ad hoc только сказано что оно не является фундаментальной чатью системы типов, в том смысле что и без ООП имеет место. Но и о модулях ни слова.

Имплементация интерфейса не меняет формы — данных. Меняется поведение. Тут та же аналогия с микро-кернел архитектурой.

Но наверное это уже диалектика :) Каждому свое

Модуль ядра это модуль ядра. Микро-кернел архитектура, plug-in pattern, как угодно.

Для тебя — это модуль ядра. А для рантайма — это данные.

Это как? В Си? Не, ну я могу понять фабрику там, но IoC и DI. Может пример покажете?

Ну вот в соседнем коммента цитата из LDD как оно реально работает. Собственно, так же, как под капотом в С++, только тут таблицы виртуальных методов написаны руками в явном виде. Вот реальные примеры не из ядра:
www.pjsip.org/...​structpjsua__callback.htm
libusb.sourceforge.io/...​19794cd7aa40c0814702b0c88

Ну там же и не 1,3 или даже 5 ответственностей, правда? Изначально разговор был про это.

Это где? Изначально разговор был о том, что нам надо разрезать домен на столько кусков, сколько угодно. А не следовать примеру Нетфликса просто потому, что ворона — птица молодая, сильная. И как мы эти куски после разрезания назовем — микросервисами или акторами — не суть важно.

Вот именно, просто указатель. Ф-я там не является то что называют first-class citizen. Нет никаких гарантий от системы типов и компилятора.

Как раз есть. Не скомпилится, если сигнатура функции отличается.

Подставить другой модуль это хорошо или плохо? :)

Не хорошо и не плохо, а полиморфизм)

хексагоналка в ФП она почти по умолчанию без танцев с бубнами

 И у адаптеров нет внутреннего состояния, вроде пулов коннекшнов или настроек или таймаутов при ошибках? А если у них внутренне состояние (то есть, адаптер уже вдруг оказывается FSM, и результат запроса в адаптер зависит от этого его состояния — например, есть у нас связь с базой, или сокет отвалился) — то какое тогда тут функциональное программирование?

Фильтр файлов? — предикат, т.е. ф-я. Можно параметром принимать

О! Мы разделили данные и логику одинаково. Данные по обходу файловой системы (дерево директорий) оказались доступны только служебному коду обхода ФС. А данные о том, какие именно файлы юзер хоче отпроцессить доступны только логике фильтра, а не логике обхода дерева. Видите связь данных и кода? Инкапсуляция же, но эта инкапсуляция у нас одновременно затрагивает и данные, и код. ООП)

Ну вот в соседнем коммента цитата из LDD как оно реально работает. Собственно, так же, как под капотом в С++, только тут таблицы виртуальных методов написаны руками в явном виде. Вот реальные примеры не из ядра:

Ну я вижу какие-то сигнатуры и массу методов. Инверсию контроля можно делать уймой способов, от фабрики или template method паттерна до DI. Но DI предполагает наличие DI фреймворка который инжектит зависимости (да, мы не говорим про Pure DI). Т.е. они в одном месте описываются и все, далее только интерфейсы используются везде. Если есть че-то подобное на Си в качестве либ или еще чего то конечно хорошо. Прогресс.

Это где? Изначально разговор был о том, что нам надо разрезать домен на столько кусков, сколько угодно

Пускай так. Суть что чем больше тем сложней нарезать: мясо толще и тверже. И нож большой нужен и умение. Простые системы они же пет проекты на коленке там не так об этом думаешь.

Как раз есть. Не скомпилится, если сигнатура функции отличается.

Ну сигнатура это да. Вот например выдержка из F# по ф-ям. Там чуток более возможностей:

F# also supports functional programming constructs such as treating functions as values, using unnamed functions in expressions, composition of functions to form new functions, curried functions, and the implicit definition of functions by way of the partial application of function arguments.

Как-то так

И у адаптеров нет внутреннего состояния, вроде пулов коннекшнов или настроек или таймаутов при ошибках? А если у них внутренне состояние (то есть, адаптер уже вдруг оказывается FSM, и результат запроса в адаптер зависит от этого его состояния — например, есть у нас связь с базой, или сокет отвалился) — то какое тогда тут функциональное программирование?

FSM в F# это просто радость, всегда пользую.
Вот — fsharpforfunandprofit.com/...​ypes-representing-states
и вот — www.marccostello.com/...​mple-state-machines-in-f
и вот — gist.github.com/...​ab10d8b567b89b1b078c02a2f
У вас какое-то предубеждение что со стейтом в ФП как-то трудно работать. Вот серия статей по дизайну и имплементации калькулятора например (в 4-й части вводится FSM) — fsharpforfunandprofit.com/posts/calculator-design

Инкапсуляция же, но эта инкапсуляция у нас одновременно затрагивает и данные, и код. ООП)

Интересный вы собеседник — у вас все ООП получается: и Си и Haskell :)
Инкапсуляция, модульность и как благодаря вам выяснилось, ad hoc полиморфизм (и параметрический тоже), они были задолго до ООП.

Интересный вы собеседник — у вас все ООП получается: и Си и Haskell :)
Инкапсуляция, модульность и как благодаря вам выяснилось, ad hoc полиморфизм (и параметрический тоже), они были задолго до ООП.
The phrase «object-oriented» means a lot of things. Half are obvious, and the other half are mistakes.

Дебажити просто, коли все логується

))) Коли воно логується, то:
1) Потрібно стягнути логи з усіх пристроїв
2) Знайти в котрому з логів вперше проявилась помилка
3) Відтворити події, що плюнули помилку в лог, на цьому пристрої (програвання логів) щоб подивитись під дебагером де та як заплутався стан
4) Потім почати відкручувати назад по логах інших пристроїв, щоб спробувати зрозуміти, що до цієї помилки призвело (вже знаємо, яка саме ситуація не покрита логікою коду, але хто нам надіслав оці криві дані?)
5) Тепер спробувати відтворити події на отому пристрої, що нам плюнув криві дані. Переходимо до пункту (3)
6) Коли за кілька ітерацій нарешті знайшли першопричину, треба її залатати, а потім довго стресс-тестити усю систему — бо коли події прийдуть в іншому порядку (а вони завжди в асинхронній системі прийдуть в іншому порядку) то баг не захоче відтворюватись, навіть якщо ми його не зафіксили. І навіть коли ми там сегфолт написали — то дізнаємось за це лише від кастомерів.

PS: для моноліту зазвичай треба лише віддебажити. Що, здається, трохи простіше)

Але яка розумная цьому альтернатіва? ©

В нас крайнє падіння дрона вбило флешку, на як писалися логи. Тому про причину тиждень тільки здогадувалися. Але знайшли. Було стидно. Зате нарешті зробили правильного.

У свій час наш модуль стояв на біржі NASDAQ, і дебажити можна було тільки за логами. Логувалися всі входи, виходи і галуження.

Ну з проблемами бажано розбиратись по черзі. З першою — розібрались, здається:

Дебажити просто, коли все логується
Але яка розумная цьому альтернатіва? ©

Тепер розбираємось із альтернативою:
Якщо дозволяють (зазвичай нефункціональні) реквайрменти — запхати усю логіку в один актор з детерміністичною поведінкою (коли він однопоточний та не працює з периферією — детермінізм не має буть десь далеко). 95% багів в логіці, і так нам хоча б не доведеться перемикатися між проектами та дебагерами й відкривати 100500 логів одночасно. Якщо зробили детерміністичним та багато інженерів в команді — написати емулятор для відтворення логів (event sourcing) ithare.com/...​-systems-with-transcript Воно й для тестування дуже згодиться, щоб менше паяти. Тоді, маючи детермінізм + replay, та усе через одне місце, можна дебажити. Не так просто, як синхронний моноліт, але вже щось dou.ua/...​cles/telecom-application

Неможливо написати емулятор невідомої херні, яка крутиться на біржі і написана альтернативно обдарованими програмістами.

В нас зараз все так і логується — на щастя, час поки що синхронізується, вся логіка зав’язанана час події, бо ріалтайм і контролери, на відміну від хадупів різних.

Щось не розумію зв’язку між біржею та контролерами

Ця архітектура ідеально лягає на Qt фреймворк через підтримку сигналів та слотів.
Фактично, там кожен обʼєкт можна перетворити на Актор просто написавши відповідний інтерфейс та зʼєднуючи його з іншими обʼєктами через Qt::QueuedConnection.

Якби я не бачив акторів на 16-бітному DSP з 4 кілобайтами оперативи під усе — то думав би про Qt)

Ну так, з Qt можна бавитись якщо є хоча б 5 мегабайт.

Я не знав що про такий простий концепт можна написати так багато.

В нього довга історія, і він розповзся усюди (те ж ООП, про котре ще більше написано, є похідним від акторів).
Коли цікаво — тут набагато більше практики ithare.com/category/reactors

кроме того, что если акторы живут в одном процессе (шарят адресное пространство), можно не сериализовать сообщения (отсылать древовидные структуры в явном виде, не копировать большие куски памяти).

кстати, даже если в разных потоках живут
если есть гарантии или соглашения что после создания сообщения никто его не изменяет — то тоже можно слать всем указатель на область памяти. читай кто хошь.
как понял — так и поступают трансляторы настоящих функциональных ЯП — на уровне ЯП тотальная иммутабельность, а потому маш код можно генерить без защиты/контроля обновления данных в разных потоках
но надо конечно посмотреть их код, трансляторов...

Теперь давайте попробуем собрать из этих акторов что-то полезное

в петпроекте недавней осени использовал идею акторов

С целью:
не хочу проектировать, хочу сразу ваять.
И проектирую — на ходу
А чтобы кодовая база не пришла бытенько в гуано-месиво, на каждое изменение требующая рефакторинга всего и вся, решение:
независимые, слабосвязанные объекты которые шлют сообщения сами не знают кому и слушают сами не знают от кого.

то есть та же концепция что описал уже и пробовал-применял частично в других проектах

Но в чистом виде — не советую.
Такой подход принесет пользу, учитывая «стоимость» работы с сообщениями, только в случаях невозможности проектирования, неопределенности, неформализованных целей, «без ТЗ, но чтобы результат все же был не ХЗ», ...
И от программистов такой подход требует — ответственности, вдумчивости. а это редкие скилы, на саппорт такой системы обыщетесь программистов

Ну и с автотестами будет проблема. Юнит тесты будут ни ап чем, а полные, интеграционные будут ну очень дорогими.

Сделаю серверную часть петпроекта, там еще и многопоток добавится, может расскажу что вышло. Но пока — опять некогда. Работать надо :)

если есть гарантии или соглашения что после создания сообщения никто его не изменяет — то тоже можно слать всем указатель на область памяти. читай кто хошь.

Мне один раз пришлось нарушать такую гарантию. То есть, по умолчанию, ты создал сообщение, напихал в него данных, и пульнул другому актору в очередь событий. Ну как бы и все. О сообщении никто не знает и не помнит, пока его новый хозяин не начнет его обрабатывать. Когда до него дойдет очередь. Вот как-то в этом сообщении была вся телефонная книга, которую то ли сортануть надо было, то ли заменить какие-то идентификаторы типа [email protected] на имена. Короче, выделять память под копию данных — ну дофига, и медленно копировать. В результате снял конст с этой телефонной книги, отредактировал что надо прямо на месте, и послал указатель обратно другим сообщением. Это один раз, когда начинка сообщения редактировалась. Обычно — сообщение считается константным.

если есть гарантии или соглашения что после создания сообщения никто его не изменяет — то тоже можно слать всем указатель на область памяти. читай кто хошь.

О, тут есть интересный изврат. ithare.com/...​-small-16×9-copy.031.jpeg
To the best of my knowledge, (Re)Actor-with-Extractors was first used by Bungie for their AAA game «Halo:Reach» [Tatarchuk]. The idea behind (Re)Actor-with-Extractors is simple:
* essentially, we have two phases in the processing (in games, both these phases usually fit into one single frame).
* during phase one (between dashed lines), state of our (Re)Actor stays CONSTANT (just because all the parties agreed not to modify it); as a result — it is perfectly safe to read it from multiple threads. In particular, «Extractors» may «extract» information-which-they-will-need-for-further-processing.
* as soon as each of extractors is done with extraction — it notifies Main Thread that it is done, and can process extracted data in its own thread, with no interaction with the state of our (Re)Actor.
* and as soon as ALL the extractors are done extracting data, Main Thread can proceed with modifying part of the (Re)Actor::react() (or the whole (Re)Actor::react() if we didn’t separate its read-only part).
ithare.com/...​hreading-with-a-script/3

независимые, слабосвязанные объекты которые шлют сообщения сами не знают кому и слушают сами не знают от кого.

А вот это — как раз причина, по которой не надо пытаться начать проект сразу с микросервисов martinfowler.com/...​oservice-verdict/path.png о чем говорит и дядя Мартин (который Фаулер), и дядя Крис (который Ричардсон). Суть:
1) пока у тебя монолит, ты относительно свободно можешь двигать границы модулей (меняя интерфейсы и копипастя код). Поэтому монолит хорошо переживает изменения требований и изменения нашего представления о том, чего вообще от нас хотят и как это порешать. А вот передвинуть кусок ответственности между двумя микросервисами, которые бегут в разных системах и написаны разными командами в разных стилях (или на разных языках) — фиг. Тут реально надо «переписать все на». Итого: вначале, пока мы полностью не осознали, чем именно занимаемся, микросервисы ни-ни.
2) в монолите кускам кода проще общаться друг с другом. И нет ops-проблем типа как это все поднять и почему оно разных версий и это все ваше разгребание логов и конфигов и кто за что виноват. То есть, пока этого самого кода мало, и он окончательно не запутался — монолит намного быстрее проще и дешевле развивать. А когда запутается — начнем от него отрезать кусочки и выделять в сервисы. Если доживем. www.hillside.net/...​lop/2020/papers/yoder.pdf

Такой подход принесет пользу, учитывая «стоимость» работы с сообщениями, только в случаях невозможности проектирования, неопределенности, неформализованных целей, «без ТЗ, но чтобы результат все же был не ХЗ», ...

Вот с точностью до наоборот.

пока у тебя монолит, ты относительно свободно можешь двигать границы модулей

не можешь уже сводобно.
у тебя уже есть публичный API модулей и их иерархии
у тебя уже прописаны ответственности модулей — в коде.
и двигать границы — это переписывать этот код

в случае ж отсутствия иерархий и структуры — изменять придется только связывание объектов-акторов. и что-то в них самих

ответственность же при этом — изначально непонятно чья. и в идеале должно быть — да и не важно чья
есть сообщение — есть ожидаемая реакция, ответ на него — системы.
а кто и как ее обеспечивает — неинтересно.

не надо пытаться начать проект сразу с микросервисов

микросервисы тут вообще ни при чем :)
они совсем не акторы по стоимости. для них сразу нужно решать инфраструктурные вопросы

Поэтому монолит хорошо переживает изменения требований

зависит от того — как внутри он сделан

помимо балагана акторов, я знаю еще парочку методов проектирования, чтобы монолит хорошо переживал изменения требований. но это уже — проектировать сразу так надо. И точно — держаться подальше от DDD и подобных :)

и самое кощунственное — не по «инженерному». втихаря так бывает и проектируют. встречал. и сам так стараюсь делать. но об этом нельзя говорить вслух — набожная айти тусовка заплюет :)

А вот передвинуть кусок ответственности между двумя микросервисами

это да, будет тяжелее, чем даже в жестком монолите. там хоть ifов нагородил и «порядок»! :)

в монолите кускам кода проще общаться друг с другом

естественно.
вопрос же в том что способов общения много.
и разные способы имеют разные достоинства. они же недостатки.

А когда запутается — начнем от него отрезать кусочки и выделять в сервисы

так все и делают. это естественный, нормальный процесс.

Вот с точностью до наоборот.

ну, у каждого свой опыт
я видел очень мало коллег которые хотя бы понимают о чем речь, не говоря о том чтобы использовать «акторы» — то есть другую парадигму, чем мейнстримовую
фаулеры с эвансами этому не учат, а косвенно — еще и запрещают :)

в случае ж отсутствия иерархий и структуры — изменять придется только связывание объектов-акторов. и что-то в них самих

Ровно до тех пор, пока ты можешь найти одного актора, который может дать ответ на твой вопрос. А когда обнаруживаешь, что ни одного нет, обладающего нужной информацией и компетенцией, а оно размазано по нескольким, а эти несколько (как правильные акторы) никак не могут синхронизироваться — вот тут ты приехал. Совсем приехал. Пиши руками 3-phase commit, и пускай весь мир обождет.

микросервисы тут вообще ни при чем :)
они совсем не акторы по стоимости.

Микросервисы — это акторы по сути. У них те же проблемы с синхронизацией, то же отсутствие общего состояния, и то же общение через интерфейсы. Проблемы огребаются одинаковые, только у микросерсисов еще и девопс свою долю получит.

Ровно до тех пор, пока ты можешь найти одного актора, который может дать ответ на твой вопрос.

если это не богообъект — то такого в системе не окажется :)

а эти несколько (как правильные акторы) никак не могут синхронизироваться — вот тут ты приехал

именно.
если акторы мелкие премелкие=простые — то так и будет — у тебя нет в системе того одного актора который может дать ответ.
ответ выдает — коллегия :)

Микросервисы — это акторы по сути

как по мне — это просто сервисы. а микро — больше хайповая приставка, чем содержательная.
и сравнивать их с акторами, как по мне, это как сравнивать здания и атомы

ну да, общего много — например у них есть характеристики — вес, занимаемый в пространстве объем

то же отсутствие общего состояния

общее состояние есть всегда :)
система без состояния уже написана, это /dev/null

если бы его не было — система не могла бы выдавать осмысленные, полезные ответы.

ответ выдает — коллегия :)

А откуда эта коллегия ответ знает? Например, кто-то хочет узнать, чего больше в системе: яблок или груш. Вот за яблоки отвечает один актор, за груши — другой. Как узнать, чего больше, когда постоянно подвозятся новые яблоки и груши? А в монолите такое делается как два пальца в розетку — через синхронные вызовы.

А откуда эта коллегия ответ знает?

если не захардкожено, то она его формирует, на основе состояния своих членов и правил

Например, кто-то хочет узнать, чего больше в системе: яблок или груш.

перед этим мы должны в систему это положить. например — поштучно

а система у нас так устроена — кладется яблоко — создается актор которому присваивается — ты яблоко! или ты груша!

а на вопрос — сколько яблок — никто не знает ответа, и все просто друг дружке шлют — я яблоко, пошлю другим что вот, 1 уже есть
другое яблоко — аха, я тоже яблоко, добавлю 1 и пошлю дальше
другая груша — аха, а я НЕ яблоко, пошлю дальше без изменений

когда все акторы отреагируют — ну вот и ответ :)
как узнать что все проголосовали?
так кто-то ж создавал акторов?
этот кто-то — тоже может хранить только id тех, которых он создал
и тоже коллегиально система может ответить на этот вопрос

ессно — это утрированный пример.

Вот за яблоки отвечает один актор

так что не обязательно, отвечает один актор :)
это как спроектируешь

А в монолите такое делается как два пальца в розетку — через синхронные вызовы.

в монолите можно и через асинхронные

почему это обязательно внутри него все системы — синхронные?

как написан — так и будет.

когда все акторы отреагируют — ну вот и ответ :)

Нет, это не ответ. Пока там сообщение бегало — часть старых яблок уже съели, и несколько новых — добавили))) То есть, для акторов невозможно получить снепшот состояния системы. А для монолита — возможно. В результате все, что полагается на общее состояние, в том числе — вопрос «сколько чего-то штук» или «чего больше» или «есть ли А или Б или С в наличии» не решается. Та самая CAP теорема. А в монолите — оно решается в 5 строчек кода вызовом методов.

в монолите можно и через асинхронные

Это уже не монолит, а монстр Франкен-мистера.

Пока там сообщение бегало

система не прнимает запросов

в условии ж не было сказано о куче всего

ты попросил промер — я тебе привел — коллегиального ответа
надуманый ессно. чтобы уж точно — коллегиальный :)

То есть, для акторов невозможно получить снепшот состояния системы.

невозможность получения снепшота состояния не означает его, состояния отсутствия

А для монолита — возможно

зависит от монолита

Та самая CAP теорема.

на которую можно забить. в разных случаях
и на которую и забивают, даже в случаях где вобщем-то как бы и нельзя

А в монолите — оно решается в 5 строчек кода вызовом методов.

зависит от монолита

но если бы так решалось — не было самого мемасика — кровавый энтерпрайз, и стонов о саппорте легаси

Это уже не монолит, а монстр Франкен-мистера.

ну, не знаю. навскидку — я только такие монолиты и знаю
особенно в Джава мире. и сам такое в них, монолитах этих и писал — многопоточное и асинхронное — внутри монолита.

система не прнимает запросов
в условии ж не было сказано о куче всего
Например, кто-то хочет узнать, чего больше в системе: яблок или груш. Вот за яблоки отвечает один актор, за груши — другой. Как узнать, чего больше, когда постоянно подвозятся новые яблоки и груши?

Так что — было сказано. Да и какой вообще смысл на акторах делать не реал-тайм систему? Оно же будет просто адово тормозить из-за месседжинга, если пропускать сообщения по-одному, а не все сразу.

невозможность получения снепшота состояния не означает его, состояния отсутствия

 Но означает, что ты не сможешь ответить на простые вопросы заказчика. То есть — специально подложил грабли выбранной модной молодежной архитектурой, сам по ним потоптался, а теперь говоришь покупателю: «за твои деньги я построил такую клевую систему, в которой никто не сможет сказать, у тебя на складе больше груш, яблок, или поровну».

Да и какой вообще смысл на акторах делать не реал-тайм систему?

проектировать меньше нужно. уже написал — зачем я использую

Оно же будет просто адово тормозить из-за месседжинга

ну если такие нагрузки что тормозит — то делать по другому

Но означает, что ты не сможешь ответить на простые вопросы заказчика

не вижу связи
но, заказчики конечно разные бывают.
обычно — они ничего не поймут в технарском изложении

а теперь говоришь покупателю

а зачем ему такое говорить?

второе — а ему критично, если он сам просит ввести на том уже уровне абстракции еще и «фрукт»?
и т.д.

а если взять бух учет — то вообще-то до закрытия периода — «сведения баланса» точное финансовое состояние предприятия неизвестно :)

и ничего, мир же как-то не один век живет :)

то есть непонятно — почему твои примеры обобщаются в качестве эталона
если только на своей практике я видел всякое, не вписывающееся в них. и это невписываемое скорее похоже на норму, а твои примеры — на исключения из нее :)

проектировать меньше нужно

ну вот я видел, как готовую систему с 20-летней историей релизов пришлось переписать с акторов на гексагоналку из-за того, что подкатили более новый стандарт со связной логикой.

ну если такие нагрузки что тормозит

ну ты же писал что

система не прнимает запросов

пока обрабатывает статистику. Вот меня такой подход смущает.

почему твои примеры обобщаются в качестве эталона

Это примеры того, как простые задачки становятся невозможными из-за выбора акторов вместо монолита. Есть тип задачек наоборот — невозможных в монолите, но возможных на акторах. Это когда надо, чтобы у разных частей программы были разные свойства. Например, одновременно двигать роборукой и распознавать изображение.

ну вот я видел, как готовую систему с 20-летней историей релизов пришлось переписать с акторов на гексагоналку из-за того, что подкатили более новый стандарт со связной логикой.

ну правильно :)
я ж написал когда они полезны :)

как раз когда — нет ни стандарта, ни связной логики, и т.п.
для меня
для проблем с многопотоком и асинхронного кода — лучше другие методы

Это примеры того, как простые задачки становятся невозможными из-за выбора акторов вместо монолита.

у нас видать термины разные
монолит может быть написан как угодно. акторы тут ни при чем. может быть на них, может не на них.

Есть тип задачек наоборот — невозможных в монолите, но возможных на акторах

значит что-то пропустил,
как декомпозиция на классы — не может решить какой-то тип задачек, а вот если декомпозиция на акторах — то может?
и то и то — может.
и чисто процедурный стиль — может.

одновременно двигать роборукой и распознавать изображение.

а акторы тут при чем?
тут вопрос в
«одновременно». если железо позволяет это одновременно — то хоть на ассемблере пиши

акторы ж — это способ декомпозиции кода. они ж не делают с однопотокового железа — многопотокового

ну то есть
потоки, акторы, монолит, для меня звучат как

красное, соленое, тяжелое

какая между ними связь... почему соленое не может быть красным,
а если тяжелое — то точно соленое...

акторы ж — это способ декомпозиции кода.

Да вроде раньше были моделью выполнения задачи en.wikipedia.org/wiki/Actor_model которая естественна для ситуации с независимыми задачами, и неестественна для связных систем. Обычное ООП — наоборот, синхронные вызовы не дают возможности одновременно выполнять задачи с разными свойствами (нефункциональными требованиями).

Да вроде раньше были моделью выполнения задачи

когда я рассуждаю об архитектуре программы

я рассуждаю о том кто что будет делать, и потоках и видах данных между ними

а то что в вики:
The actor model in computer science is a mathematical model of concurrent computation that treats actor as the universal primitive of concurrent computation

читаю как
акторами можно завернуть многопоточное выполнение

которая естественна для ситуации с независимыми задачами, и неестественна для связных систем

ну так я ж и пишу — что когда хочу уменьшить связность — беру акторы.

Обычное ООП — наоборот, синхронные вызовы

не знаю что такое обычное ООП

ООП никак не навязывает синхронность
просто — не всегда удобно
было, до появления в ЯП
async await

синхронные вызовы не дают возможности одновременно выполнять задачи

ну так не делай синхронные.
не жди.

я ж писал, всего лишь два вида кода
ждешь
не ждешь

как надо — так и делай.
что запрещает? при чем тут ООП? и т.п.

ООП никак не навязывает синхронность
просто — не всегда удобно
было, до появления в ЯП
async await

Так асинхронное ООП это и есть акторы. Но надо же как-то называть один и другой вариант?

я и называю
ждать
не ждать

ну или синхронный и асинхронный

а ООП, акторы, то уже такое
могут быть, могут не быть. могут быть такие, могут быть сякие

главное же не это

главное:
ждем
не ждем

ну так я ж и пишу — что когда хочу уменьшить связность — беру акторы.

«Задачи нужно решать просто, но не проще, чем возможно». Тут грабли, если попытаться уменьшить связность кода сильнее, чем присущая домену связность.

в общем случае связность домена и связность кода — никак не связаны

это в DDD и подобных надо их прибивать гвоздями друг к дружке, и желательно еще и клеем залить

Если несвязным кодом попытаться накрыть связный домен — получишь либо спагетти, где вроде бы независимые по коду части обращаются друг к другу — потому что в домене там есть зависимости, а в структуре кода их забыли отобразить. Поэтому вместо нормального одного модуля получается 10 модульков, имеющих друг друга 500 разными способами.
Либо, для асинхронной системы, получишь оркестраторов, хореографию, и трехфазные коммиты.

Первый вариант фиг поймешь, а второй кроме фиг поймешь еще и сложно писать, и тормозит. Поэтому — разбитие на модули должно соответствовать какому-то из разбитий домена по какому-то измерению связности. Чтобы поменьше связей разрезать. Тогда между модулями меньше зависимостей.

Если несвязным кодом попытаться накрыть связный домен — получишь либо спагетти

зависит от домена, кода, и понималки — зачем ты и что делаешь

потому что в домене там есть зависимости,

зависимости в домене — это зависимости в домене

а надо создать ПО которое одни байты превращает в другие.
и способов его создания — тьма

Либо, для асинхронной системы, получишь оркестраторов, хореографию, и трехфазные коммиты.

зависит от декомпизиции на сервисы.

Поэтому — разбитие на модули должно соответствовать какому-то из разбитий домена по какому-то измерению связности.

никто ничего никому не должен.

Поэтому вместо нормального одного модуля получается 10 модульков, имеющих друг друга 500 разными способами.

у кого так получается, тот пусть и разбирается, как так у него получается.

модули — это для меня еще одна категория
квадратное

которая дополняет список
красное, соленое, тяжелое

почему сладкое должно быть квадратным, а синее — круглым — знать не знаю такого закона в разработке :)

и сам такое в них, монолитах этих и писал — многопоточное и асинхронное — внутри монолита.

А расскажи — зачем асинхронность в монолите. Я же из эмбедеда, и там асинхронность обычно акторами делают.

зачем асинхронность в монолите

затем что
1. количество входящих в него запросов на порядки больше чем ядер проца, и мы не можем тупо каждый запрос положить в отдельный тред

в php так делается. поэтому — там проще чем везде разрабатывать — там все синхронно в коде. если не юзаем ReactPHP, AMP или Swoole

а уже на питоне и ноде — асинхронщины хватает

на джаве и сишарпе — самостоятельно запускаются потоки

2. монолит живет не один, а обращается к другим сервисам. минимум — базе данных. Для формирования ответа — ему надо.

но самое то — асинхронность акторов можно использовать для — слабой связанности и «одноранговости», неирерахического построения системы

платить придется. за все нужно — платить.

Синхронізація за часом.

Ти можеш знати, скільки в тебе чого на певний час.

Синхронні виклики не потрібні (розробники Ada і QNX можуть вважати інакше, але на першому я нічого практично не писав, а на другому в нас реалізоване очікування на зміну хоч чогось у вхідних даних, а потім вже аналізується, що саме змінилося ­— тобто той же ж select/epoll/WaitForMultipleObjects в ідеології QNX. Ну і в них чудовий механізм розблокування за таймаутом чи імпульсом).

Може бути синхронізація за подією.

Ти можеш знати, скільки в тебе чого на певний час.

Для цього потрібно підтримувати view стану цілої системи в окремому акторі. І бути певним, що до нього вже надійшли усі дані від усіх інших акторів, бо ж там eventual consistency. Себто, ми будуємо синхронну симуляцію зверху над асинхронною системою.
І от нафіга ці складнощі, коли можна зробить моноліт, і в ньому — просто синхронний запит? KISS

Периферія вся асинхронна за своєю природою, фільтри додають до цього свої затримки — нема фізичної точки синхронізації

Для ефективної роботи з периферією складається програмна модель стану цієї периферії www.hillside.net/...​/2020/papers/poltorak.pdf в результаті чого з’являється можливість синхронно оцінити снепшот стану усієї системи. І це потрібно саме тому, що периферія асинхронна за своєю природою.

А подивіться що вище в гілці — нам пропонують софтову систему спочатку перетворити на асинхронний меш, а потім щоб зрозуміти, що відбувається от у тій каші, що ми самі собі наробили — будувати зверху отого асинхронного меша синхронну вьюшку. От нафіга оці усі складності, коли можна ж було зразу робить синхронний моноліт, і не вигадувати собі та людям перешкоди, щоб потім їх героїчно долати?

Дебажили колись ZeroMQ, нав’язаний клієнтом, поки не викинули і не зробили по-людськи. Never again. Без нормальних інструментів працювати важко.

Розбиття на асинхронні компоненти в теорії мало б ефективніше використовувати ресурси і зменшувати лейтенсі. На практиці не у всіх виходить.

Розбиття на асинхронні компоненти в теорії мало б ефективніше використовувати ресурси і зменшувати лейтенсі. На практиці не у всіх виходить.

Навпаки. Що більше асинхронності, то більше перемикань контекстів і, зазвичай, динамічних алокацій під меседжі. Обидві штуки недешеві. + втрачається можливість лінкера оптимізувати систему як ціле (наприклад, заінлайнити чи покаласти поруч методи різних модулів). Відповідно, синхронний варіант обробки події швидший за умови, що в системі не відбувається інших активностей (не потрібно лочити дані). А коли інші активності є — то або йти на атоміки (lock free data structures), або в актори. І при цьому найшвидшими будут події, що не виходять за межі одного актора (вилетіло з заліза, обробилиось на місці, і полетіло назад в залізо). А коли потрібен меседжинг між акторами — то вже не знаєш, скільки часу піде. Переходимо до soft real time.

«Multi-Coring» and «Non-Blocking» instead of «Multi-Threading»

по хорошему — этого вообще не должно быть в коде.
тредов в смысле.
это машинерия — и вся она должна быть упрятана глубоко-глубоко, и интересна только «разработчикам драйверов» или частей ядра операционки или вмки

если она выплывает на прикладной уровень — то это протечка абстракции
такая же, как если нам приходится учитывать количество регистров в процессоре на котором будет выполняться наш код

то есть уровни абстракций кода — тоже имеют градацию на манер сетевой модели OSI

Мощно задвинул, спасибо.
Вроде как материал понятный, но очень хорошо систематизирован и изложен.

Оце було потужно! Пиши ще!

Текст майже готовий (за винятком розбору реальних прикладів архітектур у третій частині). Але треба малюнки зробити. Десь 2-4 тижні, бо ночами та на вихідних.

Для С++ есть очень не плохой фреймворк SObjectizer (habr.com/ru/post/304386), но у меня никак не дойдут руки посмотреть его. Спасибо за статью, хоть вникну немного в идею такого подхода.

SObjectizer

выглядит монстровито по сравнению с простым интерфейсом актора:

class Handler {
public:
	virtual void Post(Message* const msg) = 0;
};
Часть прелести акторов в том, что фреймворка никакого-то и не надо — просто каждому потоку дается очередь, и он из нее читает по одному сообщению, если там что-то есть. А если нету — то ждет, пока будет. Все. Больше никакой магии. И никакого другого взаимодействия между потоками. Надо отослать информацию — или скопируйте ее в сообщение, и положите его в чужую (или в свою собственную, чтобы позже обработать) очередь. Или положите в сообщение указатель на данные, но сами про эти данные забудьте — отдали их другому актору.

Я как-то было сделал такое, на Java, захотелось без явно выделенной шины, и без явной адресации
Акторы знают только о ближних соседях
Актор пуляет сообщение ближним соседям. Если принявший не знает что с ним делать — пуляет его своим ближним соседям, кроме автора.

Проблема раз — сообщение может гулять по кругу, и возвращаться через «третьих лиц» к «четвертым».
Поэтому принявший отмечает у себя что такое сообщение он уже получал, и в таком случае ничего с ним делать не надо.
Вторая — автору обычно интересен ответ. Чтобы ответ не шел таким же неизвестным окружным путем — автор указывает куда отправить. Проблемка — автор и ответчик могут жить в разных потоках, поэтому «callback» должен вбрасывать ответ в поток автора

Не доделал.
И кажется главной сложностью в разработке при таком подходе был бы даже не оверхед, с ним можно бороться, а — сами виды сообщений, их наименование, агрегация ответов если одно сообщение порождает несколько, и т.п. То есть сложность просто уходит в работу с сообщениями, их типизацию, отладку, и т.п. толку что акторы — простые, изолированые, самодостаточные, слабосвязанные, живут в разных потоках. голова начинает болеть не о них, а о — самих сообщениях :)

Вобщем — однопоточный синхронный код — всяко лучше многопоточного асинхронного
И если можно обойтись — то обойдитесь :)

Вобщем — однопоточный синхронный код — всяко лучше многопоточного асинхронного

 Не всяко. Бывают ситуации, когда одновременно надо делать несколько дел. Ну типа читать с дискеты и показывать видео. И вот тут многопоточный асинхронный лучше. Ну или там опрашивать пожарные датчики, стримить видео с камер безопасности, и качать обновление для виндовс.
Ну или тупой пример: одновременно писать в базу новые покупки, и выполнять тяжелую выборку.

Вот для таких случаев как раз акторы прописаны — мы можем нескольким кускам придать разные свойства, запустить их одновременно, и они будут работать, не мешая друг другу.
А логику — это да, лучше всю пихнуть вместе рядом, если там нет очевидной разницы в свойствах того, как эта логика должна выживать в окружающей среде.

И вот тут многопоточный асинхронный лучше

Который лучше упрятать.
и чтобы код остался на вид однопоточным, и только немножко асинхронным.

В описанном примере, с дискетой я бы использовал не «делать несколько дел одновременно» а
«не надо ждать когда вот это — ответит»

Одновременности обычно то и нет.
Сколько ядер то в проце?
А есть непрогнозируемый порядок, асинхронность, ответов.

С той же дискетой — DMA контроллер работает. Работал с ним. И со звуком то же — кинул 64к звука и указатель на функцию что ещё кинет след порцию, которую прочтёт так же, DMA контроллером, и пошёл себе дальше. В однопоточной ms dos

То есть — а неважно что там внутри вызываемого. Важно — ты ждёшь ответа, или не ждешь.

Вот для таких случаев как раз акторы прописаны

И думаю что акторы нужны для — удобства и единообразия.
Ведь тот же ерланг долго был — однопоточный, с зелёными тредами.
А какая тебе, посылающему сообщение, разница? Никакой, треды там или дреды у принимающего. Главное — ты пишешь код который ждёт, или который — не ждёт.

Очень хорошо это можно почувствовать на джс коде под ноду.
недавно очередь простенькую сделал, надо поберечь конечную систему, и не бомбить некоторыми видам запросов, а выстроить их друг за дружкой.
и вначале потупил — не хватает мьютексов, черт....
Потом, чувак, какие мьютексы тебе?? у тебя ж один поток!

Потому что ощущение от асинхронных вещей как от многопоточных. Ты ж понимаешь что реально будет — одновременно обработка запросов. Просто — не в твоём коде, не твоим кодом.
Отправил — и не ждешь.а отправляешь следующий запрос.

И вот тут приходим к тому что нет разницы между иллюзорностью одновременности и действительной параллельностью.
В использовании.

Ну смотри. У тебя в базу висит какой-то злой джойн по нескольким таблицам на миллионі записей с сортировкой выдачи по какой-то фигне, которая расчитывается из джойна. И он будет час считаться. Все остальное должно ждать? Или код джойна должен после каждой сотни записей выходить из функции и спрашивать, а нет ли у нас вот прям щас ждущих инсертов? Получается, тут настоящая многопоточность. Джойн работает в одном потоке, а инсерты — в другом. Или как?

И заметь, что код джойна и код инсертов реально независимы. Они пересекаются только где-то там в кишках базы. Поэтому мы можем их спокойно разнести в разные потоки, и они не поругаются и почти не будут друг другу мешать (если база в такое умеет). Вот в этом цель акторов — распихивать независимые штуки по разным местам.

А то, о чем ты говоришь — вот уже написали несколько лет назад
(2015) ithare.com/...​el-is-considered-harmful
(2010) ithare.com/...​ading-back-to-the-future
при этом он же распихивает независимые задачи по акторам
ithare.com/...​am-threads-and-game-loop

Все остальное должно ждать?

Так я ж об этом и написал уже :)
Что код бывает только двух видов — который ждёт, и который не ждёт.

Получается, тут настоящая многопоточность

А настоящая она или фальшивая — не имеет значения для кода который — не ждёт.

Ну вот на пыхе — как делается —

И он будет час считаться

Он же рождён чтобы умирать.
А делается же ж — не ждуший, когда там база или кто выродит ответ :)

при этом он же распихивает независимые задачи по акторам

С мобилы не хочется читать.

Да и обзор ты уже сделал хороший.
Вполне с очевидным выводом — нафик они не нужны, актор эти.
А, если нужны, то вы просто не поняли слова нафик :)

Утрирую конечно, но они нужны так редко...

настоящая многопоточность. Джойн работает в одном потоке, а инсерты — в другом . Или как?

Инсерты поставили семафоры на таблицу, диапазоны индексов, и т.п.- и стоят эти джойны, ждут :)
Так что может она и настоящая, многопоточность, да вот параллельность-одновременнрсть часто — фальшивая. Закон Амдала и т.п.

А шустрейший Redis — принципиально однопоточный.

Вторая — автору обычно интересен ответ.

...и вот здесь ты совершил принципиальную ошибку, поэтому и...

Не доделал.

Суть в том, что акторы — про событийный способ видения мира. В такой картине мира не существует такого поняти как «ответ». Его пытается натянуть на глобус твой человеческий мозг, которому хочется вернуться в мир синхронной коммуникации, потому что в асинхронном мире ему некомфортно. И отсюда возникает принципиальная проблема — ты пытаешь сэмулировать синхронность в мире, в котором нет синхронности.

Остюда возникают проблемы. Моя любимая ошибка всех горе-строителей говно-интеграций — две очереди сообщений something-to-do-request и убийственная, беспощадная и бессмысленная something-done-response, которые призваны «в лоб» сделать «типа запрос-ответ» поверх асинхронных ивентов.

Ну это тоже зависит от мира, в котором мы живем.
Например, при работе с железом как раз без этого запрос-ответ никуда. Запрос: переместить руку на 1 метр вперед. Ответ: переместили (или попали в стенку). Запрос: начать проигрывать мелодию будильника. Ответ: начали (или не найден файл, или генератор сигналов уже занят чем-то).
Еще прикол. Если строить пайплайн обработки данных из акторов, и в пайплайн пихать много всего — то может быть боттлнек на акторе, который самый медленный. Так же, как при отсылке дампа базы по ТСР. Соответственно, нужен обратный канал связи с фидбеком (congestion control). Форма фидбека уже произвольная, но для больших пакетов вполне может быть ответ для каждого обработанного пакета (тот же ТСР).
Так что — новое — это давно забытое старое, и вот так вот отметать сразу не стоит.

поэтому и...

нет, не поэтому не доделал.

но телепатам виднее конечно, как оно было....

В такой картине мира не существует такого поняти как «ответ».

да, телеоглогия чужда миру.

И отсюда возникает принципиальная проблема — ты пытаешь сэмулировать синхронность в мире

и отсюда вытекает проблема
если система на событиях не телеогична по своей сути — то на кой она нужна тогда вообще?
какая в ней ценность?

сделать «типа запрос-ответ» поверх асинхронных ивентов.

А потому что ценность информационной системы в том что
ей вопрос — она ответ
вместо дефиса может много чего быть, например
ей вопрос. а она, потом, когда-нибудь — ответ

но ценность системы это когда есть
вопрос-ответ

ты пытаешь сэмулировать синхронность в мире, в котором нет синхронности.

и вы пытаетесь

все пытаются :)
в этом и заключается смысл деятельности — разработка программного обеспечения:
научить компьютер выдавать ответы, на вопросы.

Очень советую посмотреть на Orleans От MS это Virtual actors Также интересная тема ето Proactors. Также отдельно тема с wasmCloud и акторами на wasm

Proactor — это первоначальная имплементация акторов, стейт-машины без блокирующих операций. Хорош для control flow систем. Reactor — это блокирующее извращение, часто многопоточное, но код проще. Можно применять для data flow, если запросы независимы (не меняют состояние актора, либо этого состояния нет). Еще есть их смесь — реактор на корутинах, описан вот тут ithare.com/...​hreading-with-a-script/2 У него код сценариев тоже линейный, но морока в написании движка корутин. Зато одна корутина может вбросить исключение в другую, если движок умный (какой напишем, такой и будет). Ну и корутины по ресурсам куда легче, чем потоки.

Сильный и очень грамотный материал, спасибо большое !

Статтю ще не читав, але хочу сказати, що саме так бачив ООП Alan Key.

В нас подібна архітектура в автопілоті на дроні.

І близьке до того в системі для оцінки страхових ризиків.

Я навіть не можу сказати, чи інші архітектури мають сенс.

В нас подібна архітектура в автопілоті на дроні.

Яка саме подібна?

Я навіть не можу сказати, чи інші архітектури мають сенс.

Ось поціновувач акторів вважає, що як мінімум для hard real-time систем, на зразок HFT, актори не підходять ithare.com/...​hreading-with-a-script/4 Також вони не дуже впишуться в data layer — бо потрібно підтримувати консистентність та снепшоти. Та й взагалі, складна бізнес-логіка на акторах — то трешовий треш. Тому — головою треба думати.

Там сказано про write load і HFT.

В нас класичний pub/sub — сенсори генерують значення, вони проходять через фільтри, рахуються різноманітні стани, в результаті це все крутить моторами.

На всю систему два м’ютекси.

Імовірно, черги меседжів для кожного актора у Вас також на мютексах з кондварами, лише туди ніхто не лазить.

Саме там, і тільки там.

І туди лазить тільки автор і дуже рідко. Крайній раз — щоб викинути фічу, яка призводила до інверсії пріоритетів.

Всі потоки комунікують виключно через пабсаб.

Я навіть не можу сказати, чи інші архітектури мають сенс.

Есть такая. Обращайся.. Акторы это часть ее. Я б даже сказал баловство. Но, в правильном направлении.

Теорію Акторів сформулював Hewitt після того як працював над Smalltalk
Тому там одне джерело

Тому там одне джерело

Джерело -объективная реальность. Сама жизнь предложила эти принципы. Только я их развил глубже.

Це дуже самовпевнене твердженя)) й претензія на революцію яка зачасту при більш детальному розгляді виявляється вже існуючою й добре обгрунтованою теорією. Взагалі важко щось вигадати після 80х. Все нове якісно забуте старе

вже існуючою й добре обгрунтованою теорією
Четыре года спустя на лице журналистки Агаповой появится глубокий шрам от удара металлической рейсшиной. На нее с безумным воплем кинется архитектор-самоучка Дегтяренко, герой публицистической радиопередачи «Ясность», так и не запущенной в эфир. За шесть недель до этой безобразной сцены журналистке впервые расскажут о проекте «Мобиле кооперато» и его гениальном творце, чернорабочем одной из таллиннских фабрик. Агапова напишет очерк под рубрикой «Встреча с интересным человеком». Технический отдел затребует чертежи. Эксперт Чубаров минуту подержит в холеных руках две грязные трепещущие кальки и выскажется следующим образом:

— Оригинально! Весьма оригинально!

Журналистка с облегчением и гордостью воскликнет:

— У него четыре класса образования!

— А у вас? — брезгливо поинтересуется эксперт. — Вы знаете, что это такое?

— Мобиле кооперато. Подвижный дом. Жилище будущего...

— Это вагон, — прервет ее Чубаров, — обыкновенный вагон. А вашего Ле Корбюзье нужно срочно госпитализировать...

© Довлатов «Компромисс»

Я навіть не можу сказати, чи інші архітектури мають сенс.

Мають.
Якщо у вас складна бізнес-логіка, а не взяти ентітю аккаунта з бази, подивитись чи там шось більше нуля чи менше і відправити флажок-відповідь, то бізнес-логіка на акторах перетвориться в пекло, а колегам оформлять безлімітні абонементи в дурку )

Дуже потужно
youtu.be/a7L59q-scAY тут також розповідь про акторні системи. в описі відео є посилання на сучасні розподілені системи типу dapr

Дякую. Там автор заходить з боку наносервісів. Я в них не вірю, бо будь-яка серйозна бізнес-логіка огрібає від спроб її розподілити — або тормоза, або нестабільність, або складність. Чи усе зразу.
Відповідно, заходжу навпаки — від моноліту martinfowler.com/bliki/MonolithFirst.html котрий потім потроху ріжеться на шматки: шардінгом, чи за субдоменами, чи за рівнями абстракції. Ось як його ділити, і які властивості за умови якого розподілу отримуємо — це тема наступної частини.
Для неї треба перековиряти літературу (подивитись, котрі забув з патернів), та малюнки зробить.

В дапр так . вони зразу дизайнять з розрахунком на куби . от Orlaens та віртуальні актори цікава тема там дуже цікаво побудована система. нажаль на дотнеті но якщо пейпери почитати то дуже допомагає. А що ви на С++ використовуєте
Про моноліти poliyth також окрема тема напобалакати

А можна лінки?
На С++ та на С в ембедеді нічого не використовується — просто береться потік, втикається йому черга меседжів з мютексом та кондваром — і готово. Фактично

class Handler {
public:
	virtual void Post(Message* const msg) = 0;
};
й пишеться воно за 2 тижні, якщо треба абстрагуватись від pthread. Якщо не треба, і є std:: то ще швидше, мабуть. Звичайно, там нема усякого розмноження акторів та обробки помилок, але в ембедеді я й не зустрічав потреби у такому. Актори квазістатичні, створюються на старті системи для обслуговування заліза (периферії) та логіки субдоменів. В хайлоаді теж досвідчений автор жодного фреймвоку не рекомендував, здається — бо воно реально руками велосипедиться як два пальці до розетки ithare.com/category/reactors

Ще там в відео щось казали про дерева акторів для фронтенду. Це нагадує PAC herbertograca.com/...​cal-model-view-controller і я буду вдячний за лінк на доки чи оглядову інфу — таке як раз гарно було б для третьої частини статті, де PAC описується — я думав, що він наразі мертвий, аж ось ні.

coggle.it/...​241c6226afd36481aeeafa107 то моя майдмапа по темі
на фронті є xstate там актори лімітовані но є
xstate.js.org

Підписатись на коментарі