/
gen_server.ex
319 lines (245 loc) · 10.3 KB
/
gen_server.ex
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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
defmodule Parent.GenServer do
@moduledoc """
A GenServer extension which simplifies parenting of child processes.
This behaviour helps implementing a GenServer which also needs to directly
start child processes and handle their termination.
## Starting the process
The usage is similar to GenServer. You need to use the module and start the
process:
```
def MyParentProcess do
use Parent.GenServer
def start_link(arg) do
Parent.GenServer.start_link(__MODULE__, arg, options \\\\ [])
end
end
```
The expression `use Parent.GenServer` will also inject `use GenServer` into
your code. Your parent process is a GenServer, and this behaviour doesn't try
to hide it. Except when starting the process, you work with the parent exactly
as you work with any GenServer, using the same functions, and writing the same
callbacks:
```
def MyParentProcess do
use Parent.GenServer
def do_something(pid, arg), do: GenServer.call(pid, {:do_something, arg})
...
@impl GenServer
def init(arg), do: {:ok, initial_state(arg)}
@impl GenServer
def handle_call({:do_something, arg}, _from, state),
do: {:reply, response(state, arg), next_state(state, arg)}
end
```
Compared to plain GenServer, there are following differences:
- A Parent.GenServer traps exits by default.
- The generated `child_spec/1` has the `:shutdown` configured to `:infinity`.
- The generated `child_spec/1` specifies the `:type` configured to `:supervisor`
## Starting child processes
To start a child process, you can invoke `start_child/1` in the parent process:
```
def handle_call(...) do
Parent.GenServer.start_child(child_spec)
...
end
```
The function takes a child spec map which is similar to Supervisor child
specs. The map has the following keys:
- `:id` (required) - a term uniquely identifying the child
- `:start` (required) - an MFA, or a zero arity lambda invoked to start the child
- `:meta` (optional) - a term associated with the started child, defaults to `nil`
- `:shutdown` (optional) - same as with `Supervisor`, defaults to 5000
- `:timeout` (optional) - timeout after which the child is killed by the parent,
see the timeout section below, defaults to `:infinity`
The function described with `:start` needs to start a linked process and return
the result as `{:ok, pid}`. For example:
```
Parent.GenServer.start_child(%{
id: :hello_world,
start: {Task, :start_link, [fn -> IO.puts "Hello, World!" end]}
})
```
You can also pass a zero-arity lambda for `:start`:
```
Parent.GenServer.start_child(%{
id: :hello_world,
start: fn -> Task.start_link(fn -> IO.puts "Hello, World!" end) end
})
```
Finally, a child spec can also be a module, or a `{module, arg}` function.
This works similarly to supervisor specs, invoking `module.child_spec/1`
is which must provide the final child specification.
## Handling child termination
When a child process terminates, `handle_child_terminated/5` will be invoked.
The default implementation is injected into the module, but you can of course
override it:
```
@impl Parent.GenServer
def handle_child_terminated(id, child_meta, pid, reason, state) do
...
{:noreply, state}
end
```
The return value of `handle_child_terminated` is the same as for `handle_info`.
## Timeout
If a positive integer is provided via the `:timeout` option, the parent will
terminate the child if it doesn't stop within the given time. In this case,
`handle_child_terminated/5` will be invoked with the exit reason `:timeout`.
## Working with child processes
This module provide various functions for managing child processes. For example,
you can enumerate running children with `children/0`, fetch child meta with
`child_meta/1`, or terminate a child process with `shutdown_child/1`.
## Termination
The behaviour takes down the child processes during termination, to ensure that
no child process is running after the parent has terminated. This happens after
the `terminate/1` callback returns. Therefore in `terminate/1` the child
processes are still running, and you can interact with them.
## Supervisor compliance
A process powered by `Parent.GenServer` can handle supervisor specific
messages, which means that for all intents and purposes, such process is
treated as a supervisor. As a result, children of parent will be included in
the hot code reload process.
"""
use GenServer
use Parent.PublicTypes
@type state :: term
@doc "Invoked when a child has terminated."
@callback handle_child_terminated(id, child_meta, pid, reason :: term, state) ::
{:noreply, new_state}
| {:noreply, new_state, timeout | :hibernate}
| {:stop, reason :: term, new_state}
when new_state: state
@doc "Starts the parent process."
@spec start_link(module, arg :: term, GenServer.options()) :: GenServer.on_start()
def start_link(module, arg, options \\ []) do
GenServer.start_link(__MODULE__, {module, arg}, options)
end
@doc "Starts the child described by the specification."
@spec start_child(child_spec | module | {module, term}) :: on_start_child
defdelegate start_child(child_spec), to: Parent.Procdict
@doc """
Terminates the child.
This function waits for the child to terminate. In the case of explicit
termination, `handle_child_terminated/5` will not be invoked.
"""
@spec shutdown_child(id) :: :ok
defdelegate shutdown_child(child_id), to: Parent.Procdict
@doc """
Terminates all running child processes.
The order in which processes are taken down is not guaranteed.
The function returns after all of the processes have been terminated.
"""
@spec shutdown_all(reason :: term) :: :ok
defdelegate shutdown_all(reason \\ :shutdown), to: Parent.Procdict
@doc "Returns the list of running child processes."
@spec children :: [child]
defdelegate children(), to: Parent.Procdict, as: :entries
@doc "Returns the count of running child processes."
@spec num_children() :: non_neg_integer
defdelegate num_children(), to: Parent.Procdict, as: :size
@doc "Returns the id of a child process with the given pid."
@spec child_id(pid) :: {:ok, id} | :error
defdelegate child_id(pid), to: Parent.Procdict, as: :id
@doc "Returns the pid of a child process with the given id."
@spec child_pid(id) :: {:ok, pid} | :error
defdelegate child_pid(id), to: Parent.Procdict, as: :pid
@doc "Returns the meta associated with the given child id."
@spec child_meta(id) :: {:ok, child_meta} | :error
defdelegate child_meta(id), to: Parent.Procdict, as: :meta
@doc "Updates the meta of the given child process."
@spec update_child_meta(id, (child_meta -> child_meta)) :: :ok | :error
defdelegate update_child_meta(id, updater), to: Parent.Procdict, as: :update_meta
@doc """
Awaits for the child to terminate.
If the function succeeds, `handle_child_terminated/5` will not be invoked.
"""
@spec await_child_termination(id, non_neg_integer() | :infinity) ::
{pid, child_meta, reason :: term} | :timeout
defdelegate await_child_termination(id, timeout), to: Parent.Procdict, as: :await_termination
@doc """
Returns true if the child process is still running, false otherwise.
Note that this function might return true even if the child has terminated.
This can happen if the corresponding `:EXIT` message still hasn't been
processed.
"""
@spec child?(id) :: boolean
def child?(id), do: match?({:ok, _}, child_pid(id))
@impl GenServer
def init({callback, arg}) do
# needed to simulate a supervisor
Process.put(:"$initial_call", {:supervisor, callback, 1})
Process.put({__MODULE__, :callback}, callback)
Parent.Procdict.initialize()
invoke_callback(:init, [arg])
end
@impl GenServer
def handle_info(message, state) do
case Parent.Procdict.handle_message(message) do
:ignore ->
{:noreply, state}
{:EXIT, pid, id, meta, reason} ->
invoke_callback(:handle_child_terminated, [id, meta, pid, reason, state])
:error ->
invoke_callback(:handle_info, [message, state])
end
end
@impl GenServer
def handle_call(:which_children, _from, state),
do: {:reply, Parent.Procdict.supervisor_which_children(), state}
def handle_call(:count_children, _from, state),
do: {:reply, Parent.Procdict.supervisor_count_children(), state}
def handle_call(message, from, state), do: invoke_callback(:handle_call, [message, from, state])
@impl GenServer
def handle_cast(message, state), do: invoke_callback(:handle_cast, [message, state])
@impl GenServer
# Needed to support `:supervisor.get_callback_module`
def format_status(:normal, [_pdict, state]) do
[
data: [{~c"State", state}],
supervisor: [{~c"Callback", Process.get({__MODULE__, :callback})}]
]
end
def format_status(:terminate, pdict_and_state),
do: invoke_callback(:format_status, [:terminate, pdict_and_state])
@impl GenServer
def code_change(old_vsn, state, extra),
do: invoke_callback(:code_change, [old_vsn, state, extra])
@impl GenServer
def terminate(reason, state) do
invoke_callback(:terminate, [reason, state])
after
Parent.Procdict.shutdown_all(reason)
end
unless Version.compare(System.version(), "1.7.0") == :lt do
@impl GenServer
def handle_continue(continue, state), do: invoke_callback(:handle_continue, [continue, state])
end
defp invoke_callback(fun, arg), do: apply(Process.get({__MODULE__, :callback}), fun, arg)
@doc false
def child_spec(_arg) do
raise("#{__MODULE__} can't be used in a child spec.")
end
@doc false
defmacro __using__(opts) do
quote location: :keep, bind_quoted: [opts: opts, behaviour: __MODULE__] do
use GenServer, opts
@behaviour behaviour
@doc """
Returns a specification to start this module under a supervisor.
See `Supervisor`.
"""
def child_spec(arg) do
default = %{
id: __MODULE__,
start: {__MODULE__, :start_link, [arg]},
shutdown: :infinity,
type: :supervisor
}
Supervisor.child_spec(default, unquote(Macro.escape(opts)))
end
@impl behaviour
def handle_child_terminated(_id, _meta, _pid, _reason, state), do: {:noreply, state}
defoverridable handle_child_terminated: 5, child_spec: 1
end
end
end