Skip to content

Commit 75d8a08

Browse files
committed
WiP
1 parent fde7789 commit 75d8a08

3 files changed

Lines changed: 86 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Changelog
22

33
## Latest
4-
* Handle removed pads properly in `Membrane.Connector` [#1075](https://github.com/membraneframework/membrane_core/pull/1075)/
4+
* Handle removed pads properly in `Membrane.Connector` [#1075](https://github.com/membraneframework/membrane_core/pull/1075)
55
* Improve remove_link action docs
66
* Deprecate `:components` option for `:unsafely_name_processes_for_observer`
77
* Deprecate `:links` option for `:unsafely_name_processes_for_observer` in favour of `:report_links_to_observer` configuration entry

guides/useful_concepts/crash_groups.md

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ The most fundamental functionality of crash groups is to separate the crash of a
1010

1111
## Defining Crash Groups
1212

13-
Crash groups are defined in the `spec` within your pipeline or bin. You assign a crash group ID to a set of children.
13+
Crash groups are defined in the `spec` within your pipeline or bin. To create a crash group, you have to define a group name and set `crash_group_mode` to `:temporary`, like in the example below:
1414

1515
```elixir
1616
defmodule MyPipeline do
@@ -23,13 +23,11 @@ defmodule MyPipeline do
2323
|> child(:filter, MyFilter)
2424
|> child(:sink, MySink)
2525

26-
{[spec: {spec, crash_group: :audio_processing}], %{}}
26+
{[spec: {spec, group: :my_group, crash_group_mode: :temporary}], %{}}
2727
end
2828
end
2929
```
3030

31-
In the case above, the crash group ID is `:audio_processing`.
32-
3331
## Behavior
3432

3533
When an element belonging to a crash group crashes:
@@ -45,14 +43,71 @@ To react to a crash group failure, implement the `handle_crash_group_down/3` cal
4543
@impl true
4644
def handle_crash_group_down(crash_group_id, context, state) do
4745
# Logic to restart the group or handle the error
48-
{[], state}
4946
end
5047
```
5148

52-
`context` passed to `handle_crash_group_down/3` callback contiains 3 additional fields, that usually don't occur in contexts of other callbacks:
53-
- `context.crash_initiator` - name or reference of the child that crashed, what caused crash group to explode.
49+
## Flow of all callbacks reated to a crash within a crash group.
50+
51+
Let's assume, that in `:filter` spawned in `MyPipeline` raised with message `"internal error"`.
52+
53+
### Handling termination of the crash itiator
54+
55+
The first callback, that will be executed in `MyPipeline`, is
56+
57+
```elixir
58+
@impl true
59+
def handle_child_terminated(:filter, context, state) do
60+
# ...
61+
end
62+
```
63+
64+
`context` passed to this callback will contain few extra fileds:
65+
* `context.exit_reason` - in this case equals `{%RuntimeError{message: "internal error"}, _stacktrace}`.
66+
* `context.group_name` - because `:filter` was spawned inside `:my_group` group, it equals `:my_group`. If a child is spawned beyond any crash group and is terminated gracefully, value of this field is `nil`.
67+
* `context.crash_initiator` - the same as child's reference, that is `:filter`.
68+
69+
`:filter` won't be present in `context.children`, so you could respawn it here, however it is suggested to do it in `handle_crash_group_down/3` later.
70+
71+
### Terminating other children within the crash group that explodes
72+
73+
Because one of children from crash group `:my_group` ungracefully crashed, the rest of children from this group will be terminated as well.
74+
75+
Therefore, Membrane will terminate `:source` and `:sink` in random order. After each termination, `MyPipeline` will execute
76+
77+
```elixir
78+
@impl true
79+
def handle_child_terminated(child, context, state) do
80+
# ...
81+
end
82+
```
83+
84+
callback. Each time, `context` will contain following extra fields:
85+
* `context.exit_reason` - for these terminations, equals `{:shutdown, :membrane_crash_group_kill}`.
86+
* `context.group_name` - equals `:my_group`.
87+
* `context.crash_initiator` - TODO: continue
88+
89+
90+
Note, that `context.children` map always contains only children that are still alive. E.g. if `:source` is terminated first, `hanlde_child_terminated(:source, context, state)` will contain only `:sink` in `context.children` map and for `hanlde_child_terminated(:sink, context, state)` `context.children` will be empty.
91+
92+
Of course, `MyPipeline` could possibly spawn another children beyond `:source`, `:filter` and `:sink`, inside different crash groups or beyond any crash group. In such a case, `context.children` would contain all of them normally and these children wouldn't be interrupted by the crash of `:my_group` members. The main goal of crash groups is to limit the consequences of the child's crash only to children with the same crash group.
93+
94+
### Handling the crash group down
95+
96+
Then, when all members of a crash group are terminated, `MyPipeline` will execute
97+
98+
```elixir
99+
@impl true
100+
def handle_crash_group_down(:my_group, context, state) do
101+
# ...
102+
end
103+
```
104+
105+
`context` passed as a third argument to `handle_crash_group_down/3` callback contains 3 additional fields, that usually don't occur in contexts of other callbacks:
106+
- `context.crash_initiator` - name or reference of the child that crashed first and caused the crash group to explode.
54107
- `context.crash_reason` - the reason with which `context.crash_initiator` crashed.
55108
- `context.members` - names/references of all children that were in the crash group.
109+
TODO: continue
110+
56111

57112
Question marks:
58113
- is crash group initiator in context.members?

test/membrane/dupa_test.exs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,33 @@ defmodule Membrane.DupaTest do
1818
flow_control: :push
1919
end
2020

21+
defmodule MockFilter do
22+
use Membrane.Filter
23+
24+
def_input_pad :input,
25+
accepted_format: _any
26+
27+
def_output_pad :output,
28+
accepted_format: _any
29+
30+
@impl true
31+
def handle_buffer(_pad, _buffer, _ctx, state), do: {[], state}
32+
33+
@impl true
34+
def handle_parent_notification(:dupa, _ctx, state), do: raise("internal error")
35+
end
36+
2137
defmodule MyPipeline do
2238
use Membrane.Pipeline
2339

2440
alias Membrane.Testing
25-
alias Membrane.DupaTest.MockSource
26-
41+
alias Membrane.DupaTest.{MockFilter, MockSource}
2742
@impl true
2843
def handle_init(_ctx, _opts) do
2944
crash_group_spec = {
3045
for i <- 2..5 do
31-
child({:connector, i}, Membrane.Connector)
46+
child_def = if i == 3, do: MockFilter, else: Membrane.Connector
47+
child({:connector, i}, child_def)
3248
end,
3349
group: :my_group, crash_group_mode: :temporary
3450
}
@@ -75,8 +91,10 @@ defmodule Membrane.DupaTest do
7591

7692
Process.sleep(1500)
7793

78-
{:ok, connector_pid} = Testing.Pipeline.get_child_pid(pipeline, {:connector, 3})
79-
Process.exit(connector_pid, :kill)
94+
# {:ok, connector_pid} = Testing.Pipeline.get_child_pid(pipeline, {:connector, 3})
95+
# Process.exit(connector_pid, :kill)
96+
97+
Testing.Pipeline.notify_child(pipeline, {:connector, 3}, :dupa)
8098

8199
Process.sleep(1500)
82100
Testing.Pipeline.terminate(pipeline)

0 commit comments

Comments
 (0)