How to create many GenServer processes from a list and map data stored in them?

In both approaches, I focused on how to map processes by a given set of identifiers or groups, and then display the saved structure for filtering data.

%{group => [users]} .

I realized that groups would be limited as opposed to users, so I created one process module that uses group names as keys.

I am afraid that in the future there will be many users in several groups, so my question is how can I split the current UserGroupServer module to save many separate processes identified by group names? I would like to keep the functionality of the current module inside the init processes according to the list of groups, in addition, I do not know how to map each process to get groups by user_id?

Currently, I run only one process in Phoenix lib/myapp.ex , including the module in the list of child trees, so I can directly call UserGroupServer in the channels.

 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 

Old approach %{user => [groups]}

The monitor saves the list of groups assigned by user_id. How to find users in this group? Should I create separate processes that will handle the m..n relationship between groups and user IDs? What should I change to get each user group and then display 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 

Head:

 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 
+6
source share
1 answer

Before moving on to processes and gen_servers, you always need to think about the data structure.

How are you going to add data? How often? How are you going to request it? How often?

In your example, you will mention three operations:

  • set groups for the user (reset all previously defined groups)
  • return all users to the group
  • returns unique users to a set of groups

Using the most basic types in Elixir (lists and maps), you can organize your data in two ways:

  • map, where the key is a user and the value is a list of groups ( %{user => [groups]} )
  • or vice versa ( %{group => [users]} )

For these two implementations, you can find out how fast the operations are. For %{user => [groups]} :

  • set groups for user O(1) (just update the key on the map)
  • returns all users in the group O(n*m) , where n is the number of users and m is the number of groups (for all n users you need to check if it is in the group by scanning potentially m group names)
  • unique users in the group are the same as above + sorting and deduplication

For implementation with %{group => [users]} :

  • Set groups for user O(n*m) (you need to scan all groups, if the user is there, delete it, and then install it only for new ones). If the specified groups only added the user to new groups without first deleting him, he simply adds the user in time in proportion to the number of groups at the input (not in all groups)
  • return all users in the O(1) group - just request a map
  • proportional to the number of groups in the request + sorting and deduplication

This shows that the first implementation is much better if your monitor is quickly updated and requested less frequently. Secondly, it is much better if you update it less often, but request it all the time.

After you implement one of these solutions without any actors or gen_server and you can say that it works, you may want to treat pids as card keys and rewrite the algorithms. You may also consider using only one process to store all data. It also depends on your specific problem. Good luck

+4
source

All Articles