Skip to content

Commit

Permalink
Russian translate GenStage lesson (#845)
Browse files Browse the repository at this point in the history
  • Loading branch information
saneery authored and brain-geek committed Dec 14, 2016
1 parent 6e1c116 commit 6ee6c19
Showing 1 changed file with 261 additions and 0 deletions.
261 changes: 261 additions & 0 deletions ru/lessons/advanced/gen-stage.md
@@ -0,0 +1,261 @@
---
layout: page
title: GenStage
category: advanced
order: 11
lang: ru
---

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

{% include toc.html %}

## Вступление

Так что же такое GenStage? В официальной документации написано, что это "спецификация и поток вычислений для Elixir", но что это означает для нас?

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

Чтобы лучше понять, как это работает, давайте визуализируем простой поток производитель-потребитель:

```
[A] -> [B] -> [C]
```

В этом примере у нас три стадии: `A` — производитель, `B` — производитель-потребитель, и `C` — потребитель. `A` производит значение, которое потребляет `B`, `B` выполняет какую-то работу и возвращает значение, которое отправляется нашему потребителю `C`. Роль стадии важна, что мы увидим в следующей секции.

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

Чтобы лучше проиллюстрировать эти понятия, мы построим конвейер с помощью GenStage, но сначала давайте рассмотрим чуть подробнее роли, на которые GenStage опирается.

## Производитель и потребитель

Как мы уже рассмотрели, роль которую мы даем нашей стадии важна. Спецификация GenStage принимает три роли:

+ `:producer` — Источник данных. Производители ждут требования от потребителей и реагируют, выполняя запрошенные действия.

+ `:producer_consumer` — И источник, и потребитель. Производитель-потребитель может как реагировать на требования от потребителей, так и запрашивать выполнение действий от производителей.

+ `:consumer` — Потребитель. Потребитель запрашивает и получает данные от производителя.

Заметили, что производители _ожидают_ требования? Благодаря GenStage наши потребители посылают требование вверх по потоку и обрабатывают данные от производителя. Это облегчает использование механизма, который называется обратное давление. Обратное давление перекладывает ответственность на неперегруженного производителя, когда потребители заняты.

Теперь, когда мы рассмотрели роли в GenStage, займёмся нашим приложением.

## Начало работы

В этом примере мы напишем GenStage приложение, которое генерирует числа, выбирает четные и в конце печатает их.

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

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

```shell
$ mix new genstage_example --sup
$ cd genstage_example
```

Давайте обновим наши зависимости в `mix.exs`, добавив `gen_stage`:

```elixir
defp deps do
[
{:gen_stage, "~> 0.7"},
]
end
```

Мы должны скачать зависимости и скомпилировать их прежде чем двигаться дальше:

```shell
$ mix do deps.get, compile
```

Теперь мы готовы к созданию нашего приложения!

## Производитель

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

```shell
$ mkdir lib/genstage_example
$ touch lib/genstage_example/producer.ex
```

Теперь мы можем добавить код:

```elixir
defmodule GenstageExample.Producer do
alias Experimental.GenStage

use GenStage

def start_link(initial \\ 0) do
GenStage.start_link(__MODULE__, initial, name: __MODULE__)
end

def init(counter), do: {:producer, counter}

def handle_demand(demand, state) do
events = Enum.to_list(state..state + demand - 1)
{:noreply, events, (state + demand)}
end
end
```

Две важные части, которые нужно принять к сведению — `init/1` и `handle_demand/2`. В `init/1` мы устанавливаем начальное состояние, как мы делали в наших GenServer, но, что более важно, мы помечаем запускаемый процесс производителем. На основе результата вызова функции `init/1` GenStage классифицирует процесс.

В функции `handle_demand/2` находится основная часть нашего производителя, и она должна быть реализована всеми производителями GenStage. Здесь мы возвращаем множество чисел, затребованных потребителями, и увеличиваем счетчик. Требование от потребителей (`demand` в нашем коде) представляется в виде целого числа, соответствующего числу событий, которые они могут обрабатывать, по умолчанию 1000.

## Производитель-потребитель

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

```shell
$ touch lib/genstage_example/producer_consumer.ex
```

Обновим наш файл:

```elixir
defmodule GenstageExample.ProducerConsumer do
alias Experimental.GenStage
use GenStage

require Integer

def start_link do
GenStage.start_link(__MODULE__, :state_doesnt_matter, name: __MODULE__)
end

def init(state) do
{:producer_consumer, state, subscribe_to: [GenstageExample.Producer]}
end

def handle_events(events, _from, state) do
numbers =
events
|> Enum.filter(&Integer.is_even/1)

{:noreply, numbers, state}
end
end
```

Вы могли заметить, что вместе с нашим производителем-потребителем мы ввели новую функцию `handle_events/3` и опцию в `init/1`. С помощью опции `subscribe_to` мы поручаем GenStage связать нас с конкретным производителем.

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

## Потребитель

Остался последний по порядку, но не по важности процесс — потребитель. Начнём:

```shell
$ touch lib/genstage_example/consumer.ex
```

Поскольку потребитель и производитель-потребитель очень схожи, наш код не будет сильно отличаться:

```elixir
defmodule GenstageExample.Consumer do
alias Experimental.GenStage
use GenStage

def start_link do
GenStage.start_link(__MODULE__, :state_doesnt_matter)
end

def init(state) do
{:consumer, state, subscribe_to: [GenstageExample.ProducerConsumer]}
end

def handle_events(events, _from, state) do
for event <- events do
IO.inspect {self(), event, state}
end

# Так как мы потребители, мы не создаем события
{:noreply, [], state}
end
end
```

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

## Собираем все вместе

Теперь, когда мы создали производителя, производитель-потребителя и потребителя, мы готовы соединить это все вместе.

Давайте откроем файл `lib/genstage_example.ex` и добавим наши новые процессы в дерево надзора:

```elixir
def start(_type, _args) do
import Supervisor.Spec, warn: false

children = [
worker(GenstageExample.Producer, [0]),
worker(GenstageExample.ProducerConsumer, []),
worker(GenstageExample.Consumer, []),
]

opts = [strategy: :one_for_one, name: GenstageExample.Supervisor]
Supervisor.start_link(children, opts)
end
```

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

```shell
$ mix run --no-halt
{#PID<0.109.0>, 2, :state_doesnt_matter}
{#PID<0.109.0>, 4, :state_doesnt_matter}
{#PID<0.109.0>, 6, :state_doesnt_matter}
...
{#PID<0.109.0>, 229062, :state_doesnt_matter}
{#PID<0.109.0>, 229064, :state_doesnt_matter}
{#PID<0.109.0>, 229066, :state_doesnt_matter}
```

Мы сделали это! Как мы и ожидали, наше приложение пропускает только четные числа и делает это _быстро_.

На данный момент у нас есть рабочий конвейер. Производитель генерирует числа, производитель-потребитель отбрасывает нечетные числа и потребитель отображает все это и продолжает процесс. Мы уже упоминали во вступлении, что возможно реализовать более одного производителя или потребителя. Давайте взглянем на это.

Если мы рассмотрим вывод `IO.inspect/1` из нашего приложения, мы увидим, что все события обрабатываются одним PID. Давайте внесем некоторые корректировки для нескольких работников путем изменения `lib/genstage_example.ex`:

```elixir
children = [
worker(GenstageExample.Producer, [0]),
worker(GenstageExample.ProducerConsumer, []),
worker(GenstageExample.Consumer, [], id: 1),
worker(GenstageExample.Consumer, [], id: 2),
]
```

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

```shell
$ mix run --no-halt
{#PID<0.120.0>, 2, :state_doesnt_matter}
{#PID<0.121.0>, 4, :state_doesnt_matter}
{#PID<0.120.0>, 6, :state_doesnt_matter}
{#PID<0.120.0>, 8, :state_doesnt_matter}
...
{#PID<0.120.0>, 86478, :state_doesnt_matter}
{#PID<0.121.0>, 87338, :state_doesnt_matter}
{#PID<0.120.0>, 86480, :state_doesnt_matter}
{#PID<0.120.0>, 86482, :state_doesnt_matter}
```

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

## Сценарии использования

Только что мы рассмотрели GenStage и построили наше первое приложение. А в каких случаях можно _реально_ использовать GenStage?

+ Конвейер преобразования данных — производителям не обязательно быть простыми генераторами чисел, мы могли бы создавать события из базы данных или даже из другого источника, например из Apache Kafka. Благодаря сочетанию производителей-потребителей и потребителей мы могли бы обрабатывать, сортировать, каталогизировать и хранить метрики как только они становятся доступными.

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

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

Это всего лишь _некоторые_ возможности GenStage.

0 comments on commit 6ee6c19

Please sign in to comment.