ТЛ; ДР. Перейти непосредственно на страницу ALF.

FBP и актеры

Поточное программирование — довольно старая идея (начало 70-х) представления приложения в виде сети независимых процессов (компонентов), обменивающихся данными посредством передачи сообщений. Это похоже на известную модель Актера, но с парой существенных отличий. Во-первых, соединения между процессами (какой процесс и какому может отправлять сообщения) предопределены. Во-вторых, что более важно, FBP — это подход, ориентированный на данные: приложение рассматривается как система потоков данных, трансформируемых процессами. Проще говоря, в парадигме Актера «существуют процессы, которые обмениваются данными», а в FBP —  «данные проходят через процессы».

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

В экосистеме Elixir/Erlang модель Актера является нативной. Платформа OTP предоставляет очень мощную абстракцию GenServer с простым интерфейсом для отправки/получения синхронных/асинхронных сообщений, мониторинга процессов, их перезапуска и так далее. Более того, в Эликсире есть библиотека GenStage с инструментами для определения топологии связи между процессами. Он заботится о доставке сообщений между производителями и потребителями, а также реализует механизм обратного давления, который предотвращает перегрузку процессов сообщениями. Поэтому реализация подхода FBP в Elixir становится легкой задачей.

Флоуэкс

Моя первая (довольно ограниченная) реализация подхода FBP в Эликсире была создана почти 5 лет назад. Именно библиотека Flowex предоставляет интерфейс для создания простых линейных цепочек взаимодействующих процессов (конвейеров). Я назвал этот подход Railway-FBP. Я рекомендую вам ознакомиться с Flowex Readme, чтобы получить представление о том, как пользовательский код размещается внутри Elixir GenStages и каковы варианты использования. По сути, основное внимание в библиотеке уделялось простоте и масштабируемости. Можно настроить этапы для запуска нескольких идентичных процессов на каждом этапе (таким образом достигается более высокая пропускная способность конвейера). Также можно выполнить код, написанный на других языках программирования, используя порты Erlang.

Мы использовали Flowex около 2 лет в производстве в наших «доморощенных» конвейерах данных. Приложение, поддерживаемое Flowex, было подключено ко многим базам данных внутри нашей системы и собирало/преобразовывало данные перед их отправкой в ​​озеро данных. Трубопроводы выглядели так:

defmodule MainAggregator.Leads.Pipeline do
  use Flowex.Pipeline

  defstruct [:ids, :leads, :call_logs]

  pipe :get_leads, count: 2
  pipe :preload_associations, count: 2
  pipe :get_call_logs, count: 3
  pipe :build_leads, count: 1
  pipe :insert_leads, count: 3
  pipe :delete_test_requests, count: 1
  ...
end

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

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

defmodule GetUserPipeline do
  use Flowex.Pipeline
  defstruct [:conn, :user]  
  pipe FetchParams, 
       opts: %{auth_data: ["token"], repo_data: ["user_id"]}
  pipe AuthClient
  pipe FindRecord,
       opts: %{finder: &__MODULE__.find_user/1, assign_to: :user}
  pipe :prepare_data
  pipe RenderResponse, opts: %{renderer: UserRenderer}
  pipe SendResponse
  error_pipe :handle_error
  ...
end

Основной проблемой подхода Railway-FBP в Flowex является его «железнодорожный» подход. Более или менее сложный поток не может быть представлен только прямыми потоками. Иногда нужны условные переходы, какие-то циклы, возможность клонирования пакета данных для выполнения других «аспектов» программы и т. д. Так что в случае с приложениями на основе Flowex всегда есть дополнительная обычная логика, которая добавляет недостающий функционал. Именно поэтому появился ALF.

Альф

С момента публикации Flowex в язык Elixir было добавлено много интересных функций. Есть DynamicSupervisor, Stream, Telemetry, библиотека GenStage наконец дошла до версии 1.0. Таким образом, ALF написан с нуля с той же идеей, но с более широким набором функций.

Со стороны компонентов есть не только Producer, Consumer и Stage, но и множество других компонентов:

  • Switch — для условного перехода;
  • Клонировать — для создания копии события;
  • Plug/Unplug — для вставки компонентов из других пайплайнов;
  • Decomposer/Recomposer — для создания нескольких событий на основе заданного, а также для перекомпоновки нескольких событий в одно;
  • и многие другие компоненты.

Интерфейс отправки/получения данных в/из конвейера также отличается. Основной интерфейс теперь основан на потоках. Конвейер получает поток событий и создает поток обработанных событий.

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

Важной частью проекта является язык моделирования (набор иконок/фигур для компонентов вместе с правилами их комбинирования), который можно использовать при проектировании прикладного уровня. Ниже приведена часть конвейера, который будет использоваться в онлайн-игре «крестики-нолики», над которой я сейчас работаю, чтобы получить хороший пример использования ALF.

А вот как это выглядит в коде:

defmodule Tictactoe.Pipelines.UserEnters do
  use ALF.DSL

  @components [
    stage(:validate_input),
    switch(
      :token_present?,
      branches: %{
        yes: [
          stage(:find_user_by_token),
          stage(:find_active_game),
          stage(:find_pending_game_if_no_game),
          goto(:if_there_is_game, to: :prepare_game_data_point)
        ],
        no: [
          stage(:find_users_with_the_name),
          stage(:add_postfix_if_needed),
          stage(:create_user)
        ]
      }
    ),
    stage(:find_free_game),
    switch(
      :is_there_free_game?,
      branches: %{
        yes: [stage(:assign_user_to_the_game)],
        no: [stage(:create_new_game)]
      }
    ),
    goto_point(:prepare_game_data_point),
    stage(:prepare_game_data)
  ]
  ...
end

Прелесть подхода FBP заключается в том, что то, что вы видите на диаграмме, на самом деле является тем, что вы будете иметь во время выполнения. А с ALF DSL у вас будет такое же дерево/граф и во время кодирования. Вот так и появилась модная фраза  — «наблюдаемость за дизайном, кодированием и во время выполнения»!

Надеюсь, вы заинтересовались проектом! Итак, проверьте репо и дайте мне знать, что вы думаете!