In both approaches I stuck on How to map processes by given set of ids or groups and then map stored struct to filter data.
%{group => [users]}
implementation.I realized that groups will be limited in opposite to users, so I've created one process module that uses groups names as keys.
I'm afraid that in future there will be to many users in few groups, so my question is how can I split current UserGroupServer
module to keep many separated processes identified by group names? I would like to keep functionality of current module, within init processes by groups list, additionally I don't know how to map each process to get groups by user_id?
Currently I start only one process in Phoenix lib/myapp.ex
by including module in children tree list, so I can call UserGroupServer
in Channels directly.
defmodule UserGroupServer do
use GenServer
## Client API
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, :ok, opts)
end
def update_user_groups_state(server, data) do
{groups, user_id} = data
GenServer.call(server, {:clean_groups, user_id}, :infinity)
users = Enum.map(groups, fn(group) ->
GenServer.call(server, {:add_user_group, group, user_id}, :infinity)
end)
Enum.count(Enum.uniq(List.flatten(users)))
end
def get_user_groups(server, user_id) do
GenServer.call(server, {:get_user_groups, user_id})
end
def users_count_in_gorup(server, group) do
GenServer.call(server, {:users_count_in_gorup, group})
end
## Callbacks (Server API)
def init(_) do
{:ok, Map.new}
end
def handle_call({:clean_groups, user_id}, _from, user_group_dict) do
user_group_dict = user_group_dict
|> Enum.map(fn({gr, users}) -> {gr, List.delete(users, user_id)} end)
|> Enum.into(%{})
{:reply, user_group_dict, user_group_dict}
end
def handle_call({:add_user_group, group, user_id}, _from, user_group_dict) do
user_group_dict = if Map.has_key?(user_group_dict, group) do
Map.update!(user_group_dict, group, fn(users) -> [user_id | users] end)
else
Map.put(user_group_dict, group, [user_id])
end
{:reply, Map.fetch(user_group_dict, group), user_group_dict}
end
end
test:
defmodule MyappUserGroupServerTest do
use ExUnit.Case, async: false
setup do
{:ok, server_pid} = UserGroupServer.start_link
{:ok, server_pid: server_pid}
end
test "add users", context do
c1 = UserGroupServer.update_user_groups_state(context[:server_pid], {[:a, :b, :c], 1})
assert(1 == c1)
c2 = UserGroupServer.update_user_groups_state(context[:server_pid], {[:c, :d], 2})
assert(2 == c2)
c3 = UserGroupServer.update_user_groups_state(context[:server_pid], {[:x], 2})
assert(1 == c3)
c4 = UserGroupServer.update_user_groups_state(context[:server_pid], {[:d], 1})
assert(1 == c4)
c5 = UserGroupServer.update_user_groups_state(context[:server_pid], {[:d, :c], 2})
assert(2 == c5)
end
end
%{user => [groups]}
Monitor stores groups list assigned to user_id. How to find users that are in given group? Have I to create separate processes that will handle m..n relation between groups and user ids? What should I change to get each user groups and then Map them?
Server implementation:
defmodule Myapp.Monitor do
use GenServer
def create(user_id) do
case GenServer.whereis(ref(user_id)) do
nil -> Myapp.Supervisor.start_child(user_id)
end
end
def start_link(user_id) do
GenServer.start_link(__MODULE__, [], name: ref(user_id))
end
def set_groups(user_pid, groups) do
try_call user_pid, {:set_groups, groups}
end
def handle_call({:set_groups, groups}, _from, state) do
{ :reply, groups, groups } # reset user groups on each set_group call.
end
defp ref(user_id) do
{:global, {:user, user_id}}
end
defp try_call(user_id, call_function) do
case GenServer.whereis(ref(user_id)) do
nil -> {:error, :invalid_user}
user_pid -> GenServer.call(user_pid, call_function)
end
end
end
Supervisor:
defmodule Myapp.Supervisor do
use Supervisor
def start_link do
Supervisor.start_link(__MODULE__, :ok, name: __MODULE__)
end
def start_child(user_id) do
Supervisor.start_child(__MODULE__, [user_id])
end
def init(:ok) do
supervise([worker(Myapp.Monitor, [], restart: :temporary)], strategy: :simple_one_for_one)
end
end
Example:
Monitor.create(5)
Monitor.set_groups(5, ['a', 'b', 'c'])
Monitor.create(6)
Monitor.set_groups(6, ['a', 'b'])
Monitor.set_groups(6, ['a', 'c'])
# Monitor.users_in_gorup('a') # -> 2
# Monitor.users_in_gorup('b') # -> 1
# Monitor.users_in_gorup('c') # -> 2
# or eventually more desired:
# Monitor.unique_users_in_groups(['a', 'b', 'c']) # -> 2
# or return in set_groups unique_users_in_groups result
Before jumping to processes and gen_servers you always need to think about data structure.
How are you going to add the data? How often? How are you going to query it? How often?
In your example you mention three operations:
Using the most basic types in Elixir (lists and maps) you can arrange your data in two ways:
%{user => [groups]}
) %{group => [users]}
)For those two implementations you can asses how fast are the operations. For %{user => [groups]}
:
O(1)
(just update the key in map)O(n*m)
where n
is the number of users and m
is number of groups (for all n
users you need to check if it is in the group by scanning potentially m
group names)For the implementation with %{group => [users]}
:
O(n*m)
(you need to scan all groups if the user is there, delete it and then set it only for the new ones) If set groups only added user to new groups without deleting it first, it would just add the user in time proportional to number of groups in the input (not all groups)O(1)
- just query the mapThis shows that first implementation is much better if your monitor is updated rapidly and queried less often. Second one is much better if you are updating it less frequently, but query it all the time.
After you implement one of those solutions without any actors or gen_server and can tell that it works you might want to treat pids as map keys and rewrite the algorithms. You may also consider using only one process to store all the data. It also depends on your exact problem. Good luck!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With