/
description.ru.yml
158 lines (119 loc) · 9.99 KB
/
description.ru.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
---
name: Связь процессов
theory: |
Так как процессы изолированы друг от друга, то возникновение ошибки в одном процессе никак не отражается на работе остальных:
```elixir
spawn(fn -> raise "boom" end)
# => 17:27:46.980 [error] Process #PID<0.277.0> raised an exception
# => ** (RuntimeError) boom
# => iex:156: (file)
```
Однако, в некоторых ситуациях нужно, чтобы процессы были связаны между собой для обработки ошибок. Создание связи происходит через функцию `spawn_link`:
```elixir
self()
# => #PID<0.105.0>
spawn_link(fn -> raise "boom" end)
# => ** (EXIT from #PID<0.105.0>) shell process exited with reason: an exception was raised:
# => ** (RuntimeError) boom
# => iex:158: (file)
# => 17:30:45.109 [error] Process #PID<0.280.0> raised an exception
# => ** (RuntimeError) boom
# => iex:158: (file)
```
Теперь, когда процессы связаны, возникновение ошибки в созданном процессе так же вызвало ошибку и в процессе, с которым он был связан. По сути, связанный процесс получил сигнал `EXIT` и аварийно завершился.
Благодаря связям, можно создавать процессы *супервизоры* (supervisors), которые следят за порожденными процессами *рабочими* (workers). При возникновении внештатных ситуаций процесс супервизор перезапускает наблюдаемый им процесс. Такая архитектура лежит в основе Elixir и Erlang приложений и называется *дерево супервизии*, внутри которого отдельные процессы супервизоры запускают другие процессы и следят за их работой, перезапуская при необходимости. Корнем в этом дереве выступает само приложение, внутри которого запущены остальные процессы. Выглядит дерево супервизии примерно так:
```elixir
# супервизор игровой сессии -- обработчики игровых сессий
# /
# процесс приложения -- супервизор обработки платежей -- обработчики платежей
# \
# супервизор рассылки уведомлений -- отправители уведомлений
```
Теперь рассмотрим причины завершения процессов. Когда процесс заканчивает свою работу, он *завершается* (finishes) в *нормальном* (normal) режиме, по сути нормальное завершение процесса выглядит как прямой вызов функции `exit(:normal)` из модуля `Process`:
```elixir
spawn_link(fn -> exit(:normal) end)
# => #PID<0.282.0>
spawn(fn -> exit(:normal) end)
# => #PID<0.283.0>
```
Результат работы функций идентичный из-за завершения процесса в режиме `:normal`. Если же причина будет другой, то завершение процесса будет расцениваться как *аварийное*:
```elixir
spawn(fn -> exit(:boom_reason) end)
# => #PID<0.284.0>
spawn_link(fn -> exit(:boom_reason) end)
# => ** (EXIT from #PID<0.281.0>) shell process exited with reason: :boom_reason
#
# => Interactive Elixir (1.15.0) - press Ctrl+C to exit (type h() ENTER for help)
```
Так как процесс интерактивной оболочки был связан напрямую с новым процессом, который аварийно завершился, то процесс интерактивной оболочки тоже аварийно завершился. А дальше *супервизор интерактивной оболочки* перехватил аварийное завершение *процесса-оболочки* и перезапустил его.
Именно так и работает дерево супервизии. Но для перезапуска нужно как-то понять, каким образом завершился наблюдаемый процесс. Для этих целей используется перехват выхода или перехват завершения процесса (trap exits) через специальный флаг, который выставляется для настройки процесса. Процессы можно конфигурировать, например размер кучи, приоритет, перехват завершения и многое другое. Нас интересует флаг перехвата, который конвертирует все сигналы завершения процесса в сообщения вида `{'EXIT', from, reason}`. Настройка процессов таким образом не является стандартной практикой, так как фреймворк OTP предоставляет специальную абстракцию под это, которая и называется `Supervisor`. С OTP фреймворком познакомимся чуть позже, когда разберемся как процессы работают на более низком уровне.
Теперь настроим процесс на перехват сигналов о завершении:
```elixir
Process.flag(:trap_exit, true)
# => false
spawn_link(fn -> 1 + 1 end)
# => #PID<0.287.0>
Process.info(self(), :messages)
# => {:messages, [{:EXIT, #PID<0.287.0>, :normal}]}
```
Как видно из кода, процесс посчитал выражение и завершился в штатном режиме. Теперь завершим процесс с другим сигналом:
```elixir
spawn_link(fn -> exit(:not_okay_reason) end)
# => #PID<0.288.0>
Process.info(self(), :messages)
# =>{:messages, [{:EXIT, #PID<0.288.0>, :not_okay_reason}]}
```
Теперь мы можем перехватывать любые сообщения о завершении и реагировать на них:
```elixir
receive do
{:EXIT, pid, :normal} -> "Process #{inspect(pid)} exited normally"
{:EXIT, pid, reason} -> "Process #{inspect(pid)} exited abnormally #{reason}"
end
```
Более того, теперь мы можем перехватывать и исключения:
```elixir
spawn_link(fn -> raise "boom!" end)
# => #PID<0.107.0>
#
# => 02:15:35.982 [error] Process #PID<0.107.0> raised an exception
# => ** (RuntimeError) boom!
# => iex:4: (file)
Process.info(self(), :messages)
# => {:messages, [{:EXIT, #PID<0.107.0>, {%RuntimeError{message: "boom!"}, [{:elixir_eval, :__FILE__, 1, [file: ~c"iex", line: 4]}]}}]}
```
instructions: |
В файле с решением описан модуль `Worker`. Для работы с ним, нужно создать и связать процесс с этим модулем следующим образом:
```elixir
number = 5
spawn_link(fn -> Worker.work(number) end)
```
После этого, `Worker` проверяет число по правилам классической задачи с собеседований `FooBar` и процесс завершается с соответствующим сигналом:
- число кратно 3 и 5 - `:foobar`;
- число кратно 3 - `:foo`;
- число кратно 3 - `:bar`;
- в ином случае процесс завершается в штатном режиме `:normal`.
Создайте функцию `supervise_foobar` которая принимает число, вызывает `Worker` и на основе сигнала о завершении формирует строку, где вместо переданного числа подставляется `Foo`, `Bar`, `FooBar` либо ничего, затем число увеличивается на *единицу* и процесс проверки числа и сбора строки идет дальше.
Если переданное число больше 100 или меньше 1, то продолжать сбор строки не нужно. Не забудьте о `Process.flag(:trap_exit, true)`, так как иначе не получится собрать информацию о сигналах завершения `Worker`. Примеры:
```elixir
Solution.supervise_foobar(0)
# => ""
Solution.supervise_foobar(-10)
# => ""
Solution.supervise_foobar(11123)
# => ""
Solution.supervise_foobar(80)
# => "Bar Foo Foo Bar Foo FooBar Foo Bar Foo Foo Bar"
Solution.supervise_foobar(100)
# => "Bar"
Solution.supervise_foobar(75)
# => "FooBar Foo Bar Foo Foo Bar Foo FooBar Foo Bar Foo Foo Bar"
```
tips:
- |
[Отказоустойчивость в Elixir](https://www.youtube.com/watch?v=mkGq1WoEvI4)
- |
[Официальная документация](https://hexdocs.pm/elixir/Process.html#exit/2)
- |
[Документация по настройке процессов](https://www.erlang.org/doc/man/erlang.html#process_flag-2)
- |
[Документация по супервизорам](https://hexdocs.pm/elixir/Supervisor.html)