Redux-Cluster или использование redux на сервере.

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

  1. Компонент React может вызывать рендер в 2-х случаях:
    1. Изменен props (т.е. рендер был вызван в компоненте верхнего уровня).
    2. Изменен state.
  2. Т.к. рендерить весь app убивает начисто смысл react как такового, то нас интересует только п.1.2. Т.о. мы будем изменять state, чем вызовем render.
  3. Все состояния нам нужны в Redux, допустим, для использования в других компонентах. Т.о. мы ставим Handler на DOM элементы компонента React, причем этот Handler вызовет dispatch и положит новое состояние в Redux.
  4. Состояние в Redux уже обновилось, но для вызова рендера нам необходимо обновить state. Для этого, при монтировании компонента React мы подпишем (subcribe) его state на изменения в Redux. Здесь же стоит поставить проверку, т.к. subscribe вызывается при каждом изменении, а рендер нужен только при обновлении необходимых данных.
  5. В value компонента DOM можно использовать как данные из state, так и из Redux (не обязательно хранить всю копию данных в state для вызова рендера, можно хранить, например, MD5 сумму. тут, правда, стоит учитывать что расчет MD5 затратная с т.з. ЦП операция и имеет смысл только в случае раздутых данных).
  6. При удалении/отмонтировании от DOM компонента React нам необходимо отписать компонент от изменений в Redux. Если лень читать документацию, то unsubscribe (функция) есть ничто иное как результат subscribe, т.е. var unsubscribe = YouStore.subscribe(function(){}); чтобы подписать. И unsubscribe (); чтобы потом отписать.

Для чего я привел этот паттерн и какое он имеет отношение к бэкэнду? Самое прямое. Обновление данных здесь происходит таким образом:

ReactComponent вызывает Redux.dispatch -> Redux обновляет данные и вызывает событие во всех subcribe ->ReactComponent получает новый state.

А теперь представим, что Redux это наш мастер процесс, а ReactComponent — рабочие и построим аналогичную схему:

  1. Допустим, у нас есть один мастер процесс и два рабочих.
  2. Все три процесса имеют одинаковое хранилище redux.
  3. Изменения в redux может вызывать только мастер процесс.
  4. Рабочие и мастер общаются по средствам IPC.
  5. При необходимости изменений Redux рабочий отправляет мастеру сообщение, мол обнови хранилище.
  6. Мастер обновляет хранилище, при этом в мастере есть подписка на изменения. По средствам этой подписки мастер отправит рабочим снимок состояния собственного хранилища
  7. Рабочие получив снимок состояния обновят собственное хранилище Redux.

Почему в 6 пункте нужно отправлять всё хранилище, а не конкретный action? Это увеличивает объем передаваемой информации, но гарантирует, что даже при ошибках (не доставленное сообщение рабочему) не вызовет конечную рассинхронизацию состояний процессов. Точнее вызовет, но ровно до следующего события action. По-моему это лучшее решение, чем доставка action в рабочие.

Итого мы получили по сути тот же паттерн Worker -> Master -> Workers[] (схема Master -> Workers[] также работает, т.е. события могут вызываться и в мастере).

Изначально, реализовывая такие схемы в проектах я вызывал в рабочек определенные сообщение, потом обрабатывал его в мастере — приводил Redux в нужное состояние и отправлял его рабочим. Но такая архитектура не вписывается в стандарты и повторное использование. Конечно, я поискал решения, прежде чем самому городить этот «огород». И не нашел ничего адекватного (возможно плохо искал).

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

Итак, цель — реализовать архитектуру Redux в максимально приближенном к оригиналу виде, с работой в нескольких процессах. Безусловно, тут есть огромный минус из коробки: я имею по сути копию данных в каждом процессе (т.е. избыточная информация). Однако, учитывая мощности современных ПК, а также то, что в процессах я не собираюсь хранить какие-то массивные данные (БД iocommander не превышала нескольких мегабайт, как правило и столько не нужно). Устроив сам себе мозговой штурм, пришел к решению, что мне ничто не мешает вместо собственного формата данных отправлять из рабочих сам action. Далее этот action применять к Redux мастер процесса и передавать его состояние рабочим. Для этого я реализовал отправку сообщений из мастера рабочим вида {_msg:»REDUX_CLUSTER_MSGTOWORKER», _hash:hash,  _data: DATA}, где DATA — снимок состояния Redux мастер-процесса, а hash — SHA1 сумма редьюсера возвращенного в строку (для того чтобы иметь возможность создавать несколько хранилищ с одинаковыми action.type, иначе процесс не смог бы сопоставлять сообщение с конкретным Redux store)

От рабочих мастеру вида {_msg:»REDUX_CLUSTER_MSGTOMASTER», _hash:hash,  _action: ACTION}, где ACTION — это action из функции dispatch рабочего. Для того чтобы в рабочих YouStore.dispatch вызывал изменение Redux сделаем финт в виде:

  1. Присваиваю функции хранилища Redux dispatchNEW начальный вариант dispatch (чтобы сохранить возможность обновления хранилища).
  2. Присваиваю функции dispatch функцию с телом process.send (отправка сообщения, т.е. action мастеру).
  3. При получении сообщения от мастера с определенным типом, задал его как action.type === REDUX_CLUSTER_SYNC вызываю dispatchNEW в рабочем.
  4. Для того чтобы REDUX_CLUSTER_SYNC было обработано корректным способом, рабочему при создании хранилища Redux передаю собственный редьюсер.
  5. Для того, чтобы рабочий со старта получил актуальную копию Redux, отправляю служебное сообщение вида {_msg:REDUX_CLUSTER_START, _hash:hash} мастер-процессу.
  6. Получив его мастер-процесс отправит актуальную копию Redux во все рабочие.

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

https://www.npmjs.com/package/redux-cluster

https://github.com/siarheidudko/redux-cluster

Кидайте тапки в комменты, что ли. А то посещаемость 10-30 человек в день, а ни одного коммента — зря я что ли vk к wordpress прикручивал?