Skip to content

Commit a2ddb6a

Browse files
author
José Valim
committed
Add Agent MFAs
1 parent ac629c8 commit a2ddb6a

File tree

5 files changed

+126
-39
lines changed

5 files changed

+126
-39
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## v0.15.0-dev
44

55
* Enhancements
6+
* [Agent] Improve the Agent API to also accept functions that receive explicit module, function and arguments
67
* [IEx] Support `--werl` call on Windows
78
* [Logger] Add `Logger`
89
* [Map] Add `Map.from_struct/1`

lib/elixir/lib/agent.ex

Lines changed: 91 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -68,22 +68,22 @@ defmodule Agent do
6868
## A word on distributed agents
6969
7070
It is important to consider the limitations of distributed agents. Agents
71-
work by sending anonymous functions between the caller and the agent.
72-
In a distributed setup with multiple nodes, agents only work if the caller
73-
(client) and the agent have the same version of a given module.
74-
75-
This setup may exhibit issues when doing "rolling upgrades". By rolling
76-
upgrades we mean the following situation: you wish to deploy a new version of
77-
your software by *shutting down* some of your nodes and replacing them with
78-
nodes running a new version of the software. In this setup, part of your
79-
environment will have one version of a given module and the other part
80-
another version (the newer one) of the same module; this may cause agents to
81-
crash. That said, if you plan to run in distributed environments, agents
82-
should likely be avoided.
83-
84-
Note, however, that agents work fine if you want to perform hot code
85-
swapping, as it keeps both the old and new versions of a given module.
86-
We detail how to do hot code swapping with agents in the next section.
71+
provides two APIs, one that works with anonymous functions and another
72+
that expects explicit module, function and arguments.
73+
74+
In a distributed setup with multiple nodes, the API that accepts anonymous
75+
functions only works if the caller (client) and the agent have the same
76+
version of the caller module.
77+
78+
Keep in mind this issue also shows up when performing "rolling upgrades"
79+
with agents. By rolling upgrades we mean the following situation: you wish
80+
to deploy a new version of your software by *shutting down* some of your
81+
nodes and replacing them with nodes running a new version of the software.
82+
In this setup, part of your environment will have one version of a given
83+
module and the other part another version (the newer one) of the same module.
84+
85+
The best solution is to simply use the explicit module, function and arguments
86+
APIs when working with distributed agents.
8787
8888
## Hot code swapping
8989
@@ -111,7 +111,7 @@ defmodule Agent do
111111
@type state :: term
112112

113113
@doc """
114-
Starts an agent linked to the current process.
114+
Starts an agent linked to the current process with the given function.
115115
116116
This is often used to start the agent as part of a supervision tree.
117117
@@ -149,6 +149,18 @@ defmodule Agent do
149149
GenServer.start_link(Agent.Server, fun, options)
150150
end
151151

152+
@doc """
153+
Starts an agent linked to the current process with the given module
154+
function and arguments.
155+
156+
Same as `start_link/2` but a module, function and args are expected
157+
instead of an anonymous function.
158+
"""
159+
@spec start_link(module, atom, [any], GenServer.options) :: on_start
160+
def start_link(module, fun, args, options \\ []) do
161+
GenServer.start_link(Agent.Server, {module, fun, args}, options)
162+
end
163+
152164
@doc """
153165
Starts an agent process without links (outside of a supervision tree).
154166
@@ -160,7 +172,18 @@ defmodule Agent do
160172
end
161173

162174
@doc """
163-
Gets the agent value and executes the given function.
175+
Starts an agent with the given module function and arguments.
176+
177+
Similar to `start/2` but a module, function and args are expected
178+
instead of an anonymous function.
179+
"""
180+
@spec start(module, atom, [any], GenServer.options) :: on_start
181+
def start(module, fun, args, options \\ []) do
182+
GenServer.start(Agent.Server, {module, fun, args}, options)
183+
end
184+
185+
@doc """
186+
Gets an agent value via the given function.
164187
165188
The function `fun` is sent to the `agent` which invokes the function
166189
passing the agent state. The result of the function invocation is
@@ -173,6 +196,18 @@ defmodule Agent do
173196
GenServer.call(agent, {:get, fun}, timeout)
174197
end
175198

199+
@doc """
200+
Gets an agent value via the given function.
201+
202+
Same as `get/3` but a module, function and args are expected
203+
instead of an anonymous function. The state is added as first
204+
argument to the given list of args.
205+
"""
206+
@spec get(agent, module, atom, [term], timeout) :: any
207+
def get(agent, module, fun, args, timeout \\ 5000) do
208+
GenServer.call(agent, {:get, {module, fun, args}}, timeout)
209+
end
210+
176211
@doc """
177212
Gets and updates the agent state in one operation.
178213
@@ -188,6 +223,18 @@ defmodule Agent do
188223
GenServer.call(agent, {:get_and_update, fun}, timeout)
189224
end
190225

226+
@doc """
227+
Gets and updates the agent state in one operation.
228+
229+
Same as `get_and_update/3` but a module, function and args are expected
230+
instead of an anonymous function. The state is added as first
231+
argument to the given list of args.
232+
"""
233+
@spec get_and_update(agent, module, atom, [term], timeout) :: any
234+
def get_and_update(agent, module, fun, args, timeout \\ 5000) do
235+
GenServer.call(agent, {:get_and_update, {module, fun, args}}, timeout)
236+
end
237+
191238
@doc """
192239
Updates the agent state.
193240
@@ -197,11 +244,23 @@ defmodule Agent do
197244
A timeout can also be specified (it has a default value of 5000).
198245
This function always returns `:ok`.
199246
"""
200-
@spec update(agent, (state -> state)) :: :ok
247+
@spec update(agent, (state -> state), timeout) :: :ok
201248
def update(agent, fun, timeout \\ 5000) when is_function(fun, 1) do
202249
GenServer.call(agent, {:update, fun}, timeout)
203250
end
204251

252+
@doc """
253+
Updates the agent state.
254+
255+
Same as `update/3` but a module, function and args are expected
256+
instead of an anonymous function. The state is added as first
257+
argument to the given list of args.
258+
"""
259+
@spec update(agent, module, atom, [term], timeout) :: :ok
260+
def update(agent, module, fun, args, timeout \\ 5000) do
261+
GenServer.call(agent, {:update, {module, fun, args}}, timeout)
262+
end
263+
205264
@doc """
206265
Performs a cast (fire and forget) operation on the agent state.
207266
@@ -213,7 +272,19 @@ defmodule Agent do
213272
"""
214273
@spec cast(agent, (state -> state)) :: :ok
215274
def cast(agent, fun) when is_function(fun, 1) do
216-
GenServer.cast(agent, fun)
275+
GenServer.cast(agent, {:cast, fun})
276+
end
277+
278+
@doc """
279+
Performs a cast (fire and forget) operation on the agent state.
280+
281+
Same as `cast/2` but a module, function and args are expected
282+
instead of an anonymous function. The state is added as first
283+
argument to the given list of args.
284+
"""
285+
@spec cast(agent, module, atom, [term]) :: :ok
286+
def cast(agent, module, fun, args) do
287+
GenServer.cast(agent, {:cast, {module, fun, args}})
217288
end
218289

219290
@doc """

lib/elixir/lib/agent/server.ex

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,20 @@ defmodule Agent.Server do
44
use GenServer
55

66
def init(fun) do
7-
{:ok, fun.()}
7+
{:ok, run(fun, [])}
88
end
99

1010
def handle_call({:get, fun}, _from, state) do
11-
{:reply, fun.(state), state}
11+
{:reply, run(fun, [state]), state}
1212
end
1313

1414
def handle_call({:get_and_update, fun}, _from, state) do
15-
{reply, state} = fun.(state)
15+
{reply, state} = run(fun, [state])
1616
{:reply, reply, state}
1717
end
1818

1919
def handle_call({:update, fun}, _from, state) do
20-
{:reply, :ok, fun.(state)}
20+
{:reply, :ok, run(fun, [state])}
2121
end
2222

2323
def handle_call(:stop, _from, state) do
@@ -28,16 +28,16 @@ defmodule Agent.Server do
2828
super(msg, from, state)
2929
end
3030

31-
def handle_cast(fun, state) when is_function(fun, 1) do
32-
{:noreply, fun.(state)}
31+
def handle_cast({:cast, fun}, state) do
32+
{:noreply, run(fun, [state])}
3333
end
3434

3535
def handle_cast(msg, state) do
3636
super(msg, state)
3737
end
3838

39-
def code_change(_old, state, { m, f, a }) do
40-
{:ok, apply(m, f, [state|a])}
39+
def code_change(_old, state, fun) do
40+
{:ok, run(fun, [state])}
4141
end
4242

4343
def terminate(_reason, _state) do
@@ -50,4 +50,7 @@ defmodule Agent.Server do
5050
end
5151
:ok
5252
end
53+
54+
defp run({m, f, a}, extra), do: apply(m, f, extra ++ a)
55+
defp run(fun, extra), do: apply(fun, extra)
5356
end

lib/elixir/lib/task.ex

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ defmodule Task do
5858
that dynamically supervise tasks:
5959
6060
{:ok, pid} = Task.Supervisor.start_link()
61-
Task.Supervisor.async(pid, fn -> do_work() end)
61+
Task.Supervisor.async(pid, MyMod, :my_fun, [arg1, arg2, arg3])
6262
6363
`Task.Supervisor` also makes it possible to spawn tasks in remote nodes as
6464
long as the supervisor is registered locally or globally:
@@ -67,7 +67,7 @@ defmodule Task do
6767
Task.Supervisor.start_link(name: :tasks_sup)
6868
6969
# In the client
70-
Task.Supervisor.async({:tasks_sup, :remote@local}, fn -> do_work() end)
70+
Task.Supervisor.async({:tasks_sup, :remote@local}, MyMod, :my_fun, [arg1, arg2, arg3])
7171
7272
`Task.Supervisor` is more often started in your supervision tree as:
7373
@@ -77,7 +77,15 @@ defmodule Task do
7777
supervisor(Task.Supervisor, [[name: :tasks_sup]])
7878
]
7979
80-
Check `Task.Supervisor` for other operations supported by the Task supervisor.
80+
Note that, when working with distributed tasks, one should use the `async/4` API,
81+
that expects explicit module, function and arguments, instead of `async/2` that
82+
works with anonymous functions. That's because the anonymous function API expects
83+
the same module version to exist on all involved nodes. Check the `Agent` module
84+
documentation for more information on distributed processes, as the limitations
85+
described in the agents documentation apply to the whole ecosystem.
86+
87+
Finally, check `Task.Supervisor` for other operations supported by the Task
88+
supervisor.
8189
"""
8290

8391
@doc """

lib/elixir/test/elixir/agent_test.exs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ Code.require_file "test_helper.exs", __DIR__
33
defmodule AgentTest do
44
use ExUnit.Case, async: true
55

6-
test "start_link/2 workflow with unregistered name" do
6+
def identity(state) do
7+
state
8+
end
9+
10+
test "start_link/2 workflow with unregistered name and anonymous functions" do
711
{:ok, pid} = Agent.start_link(fn -> %{} end)
812

913
{:links, links} = Process.info(self, :links)
@@ -17,21 +21,21 @@ defmodule AgentTest do
1721
wait_until_dead(pid)
1822
end
1923

20-
test "start/2 workflow with registered name" do
21-
{:ok, pid} = Agent.start(fn -> %{} end, name: :agent)
24+
test "start/2 workflow with registered name and module functions" do
25+
{:ok, pid} = Agent.start(Map, :new, [], name: :agent)
2226
assert Process.info(pid, :registered_name) == {:registered_name, :agent}
23-
assert Agent.cast(:agent, &Map.put(&1, :hello, :world)) == :ok
24-
assert Agent.get(:agent, &Map.get(&1, :hello)) == :world
25-
assert Agent.get_and_update(:agent, &Map.pop(&1, :hello)) == :world
26-
assert Agent.get(:agent, &(&1)) == %{}
27+
assert Agent.cast(:agent, Map, :put, [:hello, :world]) == :ok
28+
assert Agent.get(:agent, Map, :get, [:hello]) == :world
29+
assert Agent.get_and_update(:agent, Map, :pop, [:hello]) == :world
30+
assert Agent.get(:agent, AgentTest, :identity, []) == %{}
2731
assert Agent.stop(:agent) == :ok
2832
assert Process.info(pid, :registered_name) == nil
2933
end
3034

3135
test ":sys.change_code/4 with mfa" do
3236
{ :ok, pid } = Agent.start_link(fn -> %{} end)
3337
:ok = :sys.suspend(pid)
34-
mfa = { Map, :put, [:hello, :world] }
38+
mfa = {Map, :put, [:hello, :world]}
3539
assert :sys.change_code(pid, __MODULE__, "vsn", mfa) == :ok
3640
:ok = :sys.resume(pid)
3741
assert Agent.get(pid, &Map.get(&1, :hello)) == :world

0 commit comments

Comments
 (0)