AgentMap v1.1.2 AgentMap
AgentMap can be seen as a stateful Map that parallelize operations made on
different keys. Basically, it can be used as a cache, memoization,
computational framework and, sometimes, and as an alternative to GenServer.
AgentMap supports operations made on a group of keys (“multi-key”
calls).
Examples
Create and use it as an ordinary Map (new/0, new/1 and new/2):
iex> am = AgentMap.new(a: 42, b: 24)
...>
iex> AgentMap.get(am, :a)
42
iex> AgentMap.keys(am)
[:a, :b]
iex> am
...> |> AgentMap.update(:a, & &1 + 1)
...> |> AgentMap.update(:b, & &1 - 1)
...> |> AgentMap.to_map()
%{a: 43, b: 23}
#
iex> Enum.count(am)
2
or in an Agent manner (start/2, start_link/2):
iex> {:ok, pid} = AgentMap.start_link()
iex> pid
...> |> AgentMap.put(:a, 1)
...> |> AgentMap.get(:a)
1
You can even use it with Agent module interface:
iex> {:ok, am_pid} = AgentMap.start_link()
...>
iex> Agent.update(am_pid, &Map.put(&1, :a, 1))
:ok
iex> Agent.get(am_pid, & &1[:a])
1
iex> Agent.get_and_update(am_pid, & {&1, %{}})
%{a: 1}
iex> Agent.get(am_pid, & &1[:a])
nil
Struct %AgentMap{} allows to use Enumerable and Collectable protocols:
iex> {:ok, pid} = AgentMap.start_link()
iex> am = AgentMap.new(pid)
...> Enum.empty?(am)
true
#
iex> Enum.into([a: 1, b: 2], am)
iex> AgentMap.to_map(am)
%{a: 1, b: 2}
See README for memoization and accounting examples.
Priority (:!)
Most of the functions support !: priority option to make out-of-turn
(“priority”) calls.
Priority can be given as a non-negative integer or alias. Aliases are: :min | :low = 0, :avg | :mid = 255, :max | :high = 65535. Relative value
are also acceptable, for ex.: {:max, -1} = 65534.
iex> AgentMap.new(state: :ready)
...> |> sleep(:state, 10) # 0 — creates a worker
...> |> cast(:state, fn :go! -> :stop end) # : ↱ 3
...> |> cast(:state, fn :steady -> :go! end, !: :max) # : ↱ 2 :
...> |> cast(:state, fn :ready -> :steady end, !: {:max, +1}) # ↳ 1 :
...> |> get(:state) # ↳ 4
:stop
Also, !: :now option can be given in get/4, get_lazy/4 or take/3 to
instruct AgentMap to make execution using a separate Task and current
values. Calls fetch!/3, fetch/3, values/2, to_map/2 and has_key?/3
use this option by default:
iex> am =
...> AgentMap.new(key: 1)
iex> am
...> |> sleep(:key, 20) # 0 — creates a sleepy worker
...> |> put(:key, 42) # ↳ 1 ┐
...> # :
...> |> fetch(:key) # 0 :
{:ok, 1} # :
iex> get(am, :key, & &1 * 99, !: :now) # 0 :
99 # :
iex> get(am, :key, & &1 * 99) # ↳ 2
4158
How it works
Underneath it’s a GenServer that holds a Map. When a state changing call
(update/4, get_and_update/4, …) is first made for a key, a special
temporary worker-process is spawned. All subsequent calls are forwarded to the
message queue of this worker, which respects the order of incoming new calls.
Worker executes them in a sequence, except for get/4 calls, which are
processed as a parallel Tasks. A worker will die after ~20 ms of
inactivity.
For example:
iex> am = AgentMap.new(a: 1)
iex> am
...> |> cast(:a, & &1 + 1) # new worker is spawned for `:a` and …
...> # callback `& &1 + 1` is added to its queue
...> |> get(:a) # callback `& &1` is added to the same queue
2 # now all callbacks are invoked
iex> sleep(40) # worker is inactive for > 20 ms…
...> # …so it dies
iex> get(am, :a, & &1 + 1) # `& &1 + 1` is executed in a Task process…
...> # …no worker is spawned
3
or:
iex> am = AgentMap.new(a: 1) # server worker queue
iex> am # %{a: 1} — —
...> |> cast(:a, & &1 + 1) # %{} 1 [& &1 + 1]
...> |> get(:a) # %{} 1 [& &1 + 1, get]
2 # %{} 2 []
#
iex> sleep(40) # …worker dies
iex> get(am, :a, & &1 + 1) # %{a: 2} — —
3
Sometimes, like in the above example, it’s more expensive to spawn a new
worker and handle logic related to its death than to execute callback on
server. For this particular case tiny: true option can be provided to
get_and_update/4, update/4, update!/4, put_new_lazy/4 and cast/4
calls. When it’s given, callback is invoked inside GenServer’s loop and no
additional workers are spawned:
iex> am = AgentMap.new(a: 1)
iex> am
...> |> cast(:a, & &1 + 1, tiny: true) # callback `& &1 + 1` is executed by server
...> |> get(:a) # server returns the current value
2 #
iex> get(am, :a, & &1 + 1) # `& &1 + 1` is executed in a Task process
3
Be aware, that when invoking callbacks, server could not handle any other requests. For example, this:
iex> am = AgentMap.new(a: 42) # {:ok, pid} = Agent.start(fn -> %{a: 42} end)
iex> am # pid
...> |> cast(:b, fn _ -> # |> Agent.cast(fn _ ->
...> sleep(50) # sleep(50)
...> 24 # %{a: 42, b: 24}
...> end, tiny: true) # end)
...> |> get(:a) #
42 #
iex> get(am, :b) # Agent.get(pid, & &1)
24 # %{a: 42, b: 24}
will be handled in around of 50 ms. In this case AgentMap behaves exactly
like Agent with a map given as a state.
Other
AgentMap is bound to the same name registration rules as GenServers, see
GenServer documentation for details.
Finally, note that use AgentMap defines a child_spec/1 function, allowing
the defined module to be put under a supervision tree. The generated
child_spec/1 can be customized with the following options:
:id- the child specification id, defauts to the current module;:start- how to start the child process (defaults to calling__MODULE__.start_link/1);:restart- when the child should be restarted, defaults to:permanent;:shutdown- how to shut down the child.
For example:
use AgentMap, restart: :transient, shutdown: 10_000
See Supervisor docs.
Link to this section Summary
Types
Return values for start and start_link functions
Functions
Performs “fire and forget” update/4 call with GenServer.cast/2
See the “Child specification” section in the Supervisor module for more
detailed information
Deletes entry for key
Drops given keys
Fetches the value for a specific key, erroring out if instance doesn’t
contain key at the moment
Fetches the current value for a specific key
Returns the value for a specific key
Gets a value via the given fun
Gets the value for key and updates it, all in one pass
Returns whether the given key exists at the moment
Returns all keys
Starts an AgentMap via start_link/1 function
Creates an AgentMap instance from enumerable via the given transformation
function
Removes and returns the value associated with key
Puts the given value under key
Puts the given value under key, unless the entry already exists
Evaluates fun and puts the result under key, unless it is already present
Alters the value stored under key, but only if key already exists
Starts an unlinked AgentMap instance
Starts a linked AgentMap instance
Synchronously stops the AgentMap instance with the given reason
Returns a map with keys and current values
Returns a current Map representation
Updates known key with the given function
Updates key with the given fun
Returns current values stored by AgentMap instance
Link to this section Types
Link to this section Functions
Performs “fire and forget” update/4 call with GenServer.cast/2.
Returns without waiting for the actual update to finish.
Options
:initial,nil— value forkeyif it’s missing;!: priority,:avg;tiny: true— to executefunon server if it’s possible.
Examples
iex> AgentMap.new(a: 1)
...> |> sleep(:a, 20) # 0 — creates a worker
...> |> cast(:a, fn 2 -> 3 end) # : ↱ 2
...> |> cast(:a, fn 1 -> 2 end, !: :max) # ↳ 1 :
...> |> cast(:a, fn 3 -> 4 end, !: :min) # ↳ 3
...> |> get(:a) # ↳ 4
4
See the “Child specification” section in the Supervisor module for more
detailed information.
Deletes entry for key.
Returns without waiting for the actual delete to occur.
Default priority for this call is :max.
Options
cast: false— to return only after the actual removal;!: priority,:max;:timeout,5000.
Examples
iex> %{a: 1, b: 2}
...> |> AgentMap.new()
...> |> delete(:a)
...> |> to_map()
%{b: 2}
drop(am(), Enumerable.t(), keyword()) :: am()
Drops given keys.
Returns without waiting for the actual drop to occur.
This call has a fixed priority {:avg, +1}.
Options
cast: false— to return only after delete is finished for allkeys;:timeout,5000.
Examples
iex> %{a: 1, b: 2, c: 3}
...> |> AgentMap.new()
...> |> drop([:b, :d], cast: false)
...> |> to_map()
%{a: 1, c: 3}
Fetches the value for a specific key, erroring out if instance doesn’t
contain key at the moment.
Returns current value or raises a KeyError.
See fetch/3.
Options
!: priority,:now— to return only when calls with higher priorities are finished for thiskey;:timeout,5000.
Examples
iex> am = AgentMap.new(a: 1)
iex> fetch!(am, :a)
1
iex> fetch!(am, :b)
** (KeyError) key :b not found
Fetches the current value for a specific key.
Returns {:ok, value} or :error if key is not present.
Options
!: priority,:now— to return only when calls with higher priorities are finished for thiskey;:timeout,5000.
Examples
iex> am = AgentMap.new(a: 1)
iex> fetch(am, :a)
{:ok, 1}
iex> fetch(am, :b)
:error
By default, this call returns the current key value. As so, any pending
operations will have no effect:
iex> am = AgentMap.new()
iex> am
...> |> sleep(:a, 20) # 0 — creates a worker for this key
...> |> put(:a, 42) # ↳ 1 ┐ — …pending
...> # :
...> |> fetch(:a) # 0 :
:error # :
# :
iex> fetch(am, :a, !: :avg) # ↳ 2
{:ok, 42}
Returns the value for a specific key.
This call has the :min priority. As so, the value is retrived only after all
other calls for key are completed.
See get/4, AgentMap.Multi.get/3.
Options
:default,nil— value to return ifkeyis missing;!: priority,:min;:timeout,5000.
Examples
iex> am = AgentMap.new(Alice: 42)
iex> get(am, :Alice)
42
iex> get(am, :Bob)
nil
By default, get/4 has the :min priority:
iex> AgentMap.new(a: 42)
...> |> sleep(:a, 20) # 0 — creates a worker
...> |> put(:a, 0) # : ↱ 2 — !: :max
...> |> get(:a, !: {:max, +1}) # ↳ 1
42
Gets a value via the given fun.
A callback fun is sent to an instance that invokes it, passing as an
argument the value associated with key. The result of an invocation is
returned from this function. This call does not change value, and so, workers
execute a series of get-calls as a parallel Tasks.
Options
:default,nil— value forkeyif it’s missing;!: priority,:avg;!: :now— to execute call in a separateTask(passing current value), spawned from server:iex> am = AgentMap.new() iex> sleep(am, :key, 40) iex> for _ <- 1..100 do ...> Task.async(fn -> ...> get(am, :key, fn _ -> sleep(40) end, !: :now) ...> end) ...> end iex> sleep(10) iex> meta(am, :processes) 102:timeout,5000.
Examples
iex> am = AgentMap.new()
iex> get(am, :Alice, & &1)
nil
iex> am
...> |> put(:Alice, 42)
...> |> get(:Alice, & &1 + 1)
43
iex> get(am, :Bob, & &1 + 1, default: 0)
1
Gets the value for key and updates it, all in one pass.
The fun is sent to an AgentMap that invokes it, passing the value for
key. A callback can return:
{ret, new value}— to set new value and retrive “ret”;{ret}— to retrive “ret” value;:pop— to retrive current value and removekey;:id— to just retrive current value.
For example, get_and_update(account, :Alice, &{&1, &1 + 1_000_000}) returns
the balance of :Alice and makes the deposit, while get_and_update(account, :Alice, &{&1}) just returns the balance.
This call creates a temporary worker that is responsible for holding queue of
calls awaiting execution for key. If such a worker exists, call is added to
the end of its queue. Priority can be given (:!), to process call out of
turn.
See Map.get_and_update/3.
Options
:initial,nil— value forkeyif it’s missing;tiny: true— to executefunon server if it’s possible;!: priority,:avg;:timeout,5000.
Examples
iex> am = AgentMap.new(a: 42)
...>
iex> get_and_update(am, :a, &{&1, &1 + 1})
42
iex> get(am, :a)
43
iex> get_and_update(am, :a, fn _ -> :pop end)
43
iex> has_key?(am, :a)
false
iex> get_and_update(am, :a, fn _ -> :id end)
nil
iex> has_key?(am, :a)
false
iex> get_and_update(am, :a, &{&1, &1})
nil
iex> has_key?(am, :a)
true
iex> get_and_update(am, :b, &{&1, &1}, initial: 42)
42
iex> has_key?(am, :b)
true
Returns whether the given key exists at the moment.
See fetch/3.
Options
!: priority,:now;:timeout,5000.
Examples
iex> am = AgentMap.new(a: 1)
iex> has_key?(am, :a)
true
iex> has_key?(am, :b)
false
iex> am
...> |> delete(:a)
...> |> has_key?(:a)
false
By default, this call returns a key value at the moment. As so, any pending
operations will have no effect:
iex> AgentMap.new(a: 1)
...> |> sleep(:a, 20) # — creates a worker for this key
...> |> delete(:a) # — …pending
...> |> has_key?(:a)
true
use :! option to bypass:
iex> AgentMap.new(a: 1)
...> |> sleep(:a, 20) # 0
...> |> delete(:a) # ↳ 1
...> |> has_key?(:a, !: :min) # ↳ 2
false
Returns all keys.
Examples
iex> %{a: 1, b: nil, c: 3}
...> |> AgentMap.new()
...> |> keys()
[:a, :b, :c]
Returns a new instance of AgentMap.
Examples
iex> AgentMap.new()
...> |> Enum.empty?()
true
Starts an AgentMap via start_link/1 function.
As an argument, enumerable with keys and values may be provided or the PID of
an already started AgentMap.
Returns a new instance of AgentMap wrapped in a %AgentMap{}. This struct
allows to use Enumerable and Collectable protocols.
Examples
iex> am = AgentMap.new(a: 42, b: 24)
iex> get(am, :a)
42
iex> keys(am)
[:a, :b]
iex> {:ok, pid}
...> = AgentMap.start_link()
iex> pid
...> |> AgentMap.new()
...> |> put(:a, 1)
...> |> get(:a)
1
iex> {:ok, pid}
...> = AgentMap.start_link()
iex> pid
...> |> AgentMap.new()
...> |> Enum.empty?()
true
iex> {:ok, pid}
...> = AgentMap.start_link()
...>
iex> %{a: 2, b: 3}
...> |> Enum.into(AgentMap.new(pid))
...> |> to_map()
%{a: 2, b: 3}
new(Enumerable.t(), (term() -> {key(), value()})) :: am()
Creates an AgentMap instance from enumerable via the given transformation
function.
Duplicated keys are removed; the latest one prevails.
Examples
iex> [:a, :b]
...> |> AgentMap.new(&{&1, to_string(&1)})
...> |> to_map()
%{a: "a", b: "b"}
Removes and returns the value associated with key.
If there is no such key, default is returned (nil).
Options
!: priority,:avg;:timeout,5000.
Examples
iex> am =
...> AgentMap.new(a: 42, b: nil)
...>
iex> pop(am, :a)
42
iex> pop(am, :a)
nil
iex> pop(am, :a, :error)
:error
iex> pop(am, :b, :error)
nil
iex> pop(am, :b, :error)
:error
iex> Enum.empty?(am)
true
Puts the given value under key.
Returns without waiting for the actual put.
Default priority for this call is :max.
Options
cast: false— to return after the actual put;!: priority,:max;:timeout,5000.
Examples
iex> AgentMap.new()
...> |> put(:a, 42)
...> |> put(:b, 42)
...> |> to_map()
%{a: 42, b: 42}
By default, put/4 has the :max priority:
iex> AgentMap.new(a: 42) # server worker queue
...> |> sleep(:a, 20) # %{a: 42} — —
...> #
...> |> put(:a, 1, !: :min) # ↱ 3a ┐ %{} 42 [min]
...> |> put(:a, 2, !: :avg) # ↱ 2a : %{} 42 [avg, min]
...> |> put(:a, 3) # 1a : %{} 42 [max, avg, min]
...> |> get(:a) # ↳ 4a %{} 42 [max, avg, min, get]
1 # %{} 1 []
#
# the worker dies after ~20 ms:
# %{a: 1} — —
but:
iex> AgentMap.new() # %{} — —
...> |> put(:a, 1, !: :min) # 1 %{a: 1} — —
...> |> put(:a, 2, !: :avg) # ↳ 2 %{a: 2} — —
...> |> put(:a, 3) # ↳ 3 %{a: 3} — —
...> |> get(:a) # ↳ 4 %{a: 3} — —
3
Puts the given value under key, unless the entry already exists.
Returns without waiting for the actual put.
Default priority for this call is :max.
See put/4.
Options
cast: false— to return after the actual put;!: priority,:max;:timeout,5000.
Examples
iex> %{a: 1}
...> |> AgentMap.new()
...> |> put_new(:a, 42)
...> |> put_new(:b, 42)
...> |> to_map()
%{a: 1, b: 42}
Evaluates fun and puts the result under key, unless it is already present.
Returns without waiting for the actual put.
This function is useful in case you want to compute the value to put under
key only if it is not already present (e.g., the value is expensive to
calculate or generally difficult to setup and teardown again).
Default priority for this call is :max.
See put_new/4.
Options
cast: false— to return after the actual put;!: priority,:max;tiny: true— to executefunon server if it’s possible;:timeout,5000.
Examples
iex> fun = fn ->
...> # some expensive operation
...> 42
...> end
...>
iex> %{a: 1}
...> |> AgentMap.new()
...> |> put_new_lazy(:a, fun, cast: false)
...> |> put_new_lazy(:b, fun, cast: false)
...> |> to_map()
%{a: 1, b: 42}
Alters the value stored under key, but only if key already exists.
If key is not present, a KeyError exception is raised.
See update!/4.
Options
!: priority,:avg;:timeout,5000.
Examples
iex> am = AgentMap.new(a: 1, b: 2)
iex> am
...> |> replace!(:a, 3)
...> |> values()
[3, 2]
iex> replace!(am, :c, 2)
** (KeyError) key :c not found
Starts an unlinked AgentMap instance.
See start_link/2 for details.
Examples
iex> err =
...> AgentMap.start([a: 42,
...> b: fn -> sleep(:infinity) end,
...> c: fn -> raise "oops" end,
...> d: fn -> :ok end],
...> timeout: 10)
...>
iex> {:error, a: :badfun, b: :timeout, c: {e, _st}} = err
iex> e
%RuntimeError{message: "oops"}
Starts a linked AgentMap instance.
Argument funs is a keyword that contains pairs {key, fun/0}. Each fun is
executed in a separate Task and return an initial value for key.
Options
name: term— is used for registration as described in the module documentation;:debug— is used to invoke the corresponding function in:sysmodule;:spawn_opt— is passed as options to the underlying process as inProcess.spawn/4;:hibernate_after— if present, the GenServer process awaits any message for the given number of milliseconds and if no message is received, the process goes into hibernation automatically (by calling:proc_lib.hibernate/3);:timeout,5000—AgentMapis allowed to spend at most the given number of milliseconds on the whole process of initialization or it will be terminated and the start function will return{:error, :timeout};max_concurrency: pos_integer | :infinity | {pos_integer, pos_integer},5000— a maximum number of processes instance can have. Limit can be “soft” — if it is exceeded, optimizations are applied; or “hard” — if it’s exceeded and no optimization can be made to spawn workers, all requests are handled in a server process, one by one. For example,max_concurrency: 100≅max_concurrency: {100, :infinity}mean99processes can be spawned before optimizations are made, and no hard-limitation is given;:meta,[]— a keyword list of metadata to be attached to theAgentMapinstance. SeeAgentMap.Utils.meta/2,AgentMap.Utils.put_meta/3orAgentMap.Utils.upd_meta/4.
Return values
If an instance is successfully created and initialized, the function returns
{:ok, pid}, where pid is the PID of the server. If a server with the
specified name already exists, the function returns {:error, {:already_started, pid}} with the PID of that process.
If one of the callbacks fails, the function returns {:error, [{key, reason}]}, where reason is :timeout, :badfun, :badarity, {:exit, reason} or an arbitrary exception.
Examples
To start server with a single key :k.
iex> {:ok, pid} =
...> AgentMap.start_link(k: fn -> 42 end)
iex> get(pid, :k)
42
iex> meta(pid, :max_concurrency)
{5000, :infinity}
The following will not work:
iex> AgentMap.start(k: 3)
{:error, k: :badfun}
#
iex> AgentMap.start(k: & &1)
{:error, k: :badarity}
#
iex> {:error, k: {e, _st}} =
...> AgentMap.start(k: fn -> raise "oops" end)
iex> e
%RuntimeError{message: "oops"}
To start server without any keys use:
iex> AgentMap.start([], name: Account)
iex> Account
...> |> put(:a, 42)
...> |> get(:a)
42
Synchronously stops the AgentMap instance with the given reason.
Returns :ok if terminated with the given reason. If it terminates with
another reason, the call will exit.
This function keeps OTP semantics regarding error reporting. If the reason is
any other than :normal, :shutdown or {:shutdown, _}, an error report
will be logged.
Examples
iex> {:ok, pid} = AgentMap.start_link()
iex> AgentMap.stop(pid)
:ok
Returns a map with keys and current values.
Options
!: priority,:now;:timeout,5000.
Examples
iex> %{a: 1, b: 2, c: 3}
...> |> AgentMap.new()
...> |> put(:a, 42)
...> |> put(:b, 42)
...> |> take([:a, :b, :d])
%{a: 42, b: 42}
This call returns a representation at the moment, so any pending operations will have no effect:
iex> am =
...> AgentMap.new(a: 1, b: 2, c: 3)
iex> am
...> |> sleep(:a, 20) # 0a — spawns a worker for the :a key
...> |> put(:a, 42) # ↳ 1a ┐ …pending
...> # :
...> |> put(:b, 42) # 0b : — is performed on the server
...> |> take([:a, :b]) # 0 :
%{a: 1, b: 42} # :
# :
iex> take(am, [:a, :b], !: :avg) # ↳ 2
%{a: 42, b: 42}
Returns a current Map representation.
Examples
iex> %{a: 1, b: 2, c: nil}
...> |> AgentMap.new()
iex> |> to_map()
%{a: 1, b: 2, c: nil}
This call returns a representation at the moment, so any pending operations will have no effect:
iex> %{a: 1, b: 2, c: nil}
...> |> AgentMap.new()
...> |> sleep(:a, 20) # 0a — spawns a worker for the :ay
...> |> put(:a, 42, !: :avg) # ↳ 1a — delayed for 20 ms
...> #
...> |> to_map() # 0
%{a: 1, b: 2, c: nil}
Updates known key with the given function.
If key is present, fun is invoked with value as argument and its result is
used as the new value. Otherwise, a KeyError exception is raised.
See update/4.
Options
!: priority,:avg;tiny: true— to executefunon server if it’s possible;:timeout,5000.
Examples
iex> AgentMap.new(a: 1)
...> |> sleep(:a, 20) # 0a
...> |> put(:a, 3) # : ↱ 2a ┐
...> |> update!(:a, fn 1 -> 2 end, !: {:max, +1}) # ↳ 1a ↓
...> |> update!(:a, fn 3 -> 4 end) # 3a
...> |> update!(:b, & &1) # 0b
** (KeyError) key :b not found
Updates key with the given fun.
See get_and_update/4.
Options
:initial,nil— value forkeyif it’s missing;tiny: true— to executefunon server if it’s possible;!: priority,:avg;:timeout,5000.
Examples
iex> AgentMap.new(Alice: 24)
...> |> update(:Alice, & &1 + 1_000)
...> |> get(:Alice)
1024
iex> AgentMap.new()
...> |> update(:a, fn nil -> 42 end)
...> |> get(:a)
42
iex> AgentMap.new()
...> |> update(:a, & &1 + 18, initial: 24)
...> |> get(:a)
42
Returns current values stored by AgentMap instance.
Examples
iex> %{a: 1, b: 2, c: 3}
...> |> AgentMap.new()
...> |> values()
[1, 2, 3]
This call returns values as they are at the moment, so any pending operations will have no effect:
iex> AgentMap.new(a: 1, b: 2, c: 3)
...> |> sleep(:a, 20)
...> |> put(:a, 0) # pending
...> |> values()
[1, 2, 3]