Question

I am having a hard time wrapping my head around the correct way to make calls against a gen_server instance dynamically created by a supervisor with a simple_one_for_one child strategy. I am attempting to create data access controls as gen_servers. Each entity will have its own supervisor, and that supervisor will create gen_server instances as needed to actually perform CRUD operations on the database. I understand the process for defining the child processes, as well as the process for creating them as needed.

Initially, my plan was to abstract the child creation process into custom functions in the gen_server module that created a child, fired off the requested operation (e.g. find, store, delete) on that child using gen_server:call(), and then returning the operation results back to the calling process. Unless I am mistaken, though, that will block any other processes attempting to use those functions until the call returns. That is definitely not what I have in mind.

I may be stuck in OO mode (my background is Java), but it seems like there should be a clean way of allowing a function in one module to obtain a reference to a child process and then make calls against that process without leaking the internals of that child. In other words, I do not want to have to call the create_child() method on an entity supervisor and then have my application code make gen_server:calls against that child PID (i.e. gen_sever:call(Pid, {find_by_id, Id})). I would instead like to be able to call a function more like Child:find_by_id(Id).

Was it helpful?

Solution

when you create, modify or delete a record, you don't need to wait for an answer. You can use a gen_server:cast for this, but you don't need a gen_server for this, as I said in my first comment, a simple call to an interface function executed in the client process will save time.

If you want to read, 2 cases:

  • you can do something else while waiting the answer, then a gen_server call is ok, but a simple spawned process waiting for the answer and sending it back to the client will provide the same service.

  • you cannot do anything before getting the answer, then there is no blocking issue, and I think that it is really preferable to use as less code as possible so again a simple function call will be enough.

gen_server is meant to be persistent and react to messages. I don't see in your example the need to be persistent.

-module(access).
-export([add/2,get/1]).
-record(foo, {bar, baz}).

add(A,B) ->
  F = fun() ->
    mnesia:write(#foo{bar=A,baz=B})
  end,
  spawn(mnesia,activity,[transaction, F]). %% the function return immediately,
                                           %% but you will not know if the transaction failed

get(Bar) ->
  F = fun() ->
    case mnesia:read({foo, Bar}) of
      [#foo{baz=Baz}] -> Baz;
      [] -> undefined
    end
  end,
  Pid = self(),
  Ref = make_ref(),
  Get = fun() ->
    R = mnesia:activity(transaction, F),
    Pid ! {Ref,baz,R}
  end,
  spawn(Get),
  Ref. %% the function return immediately a ref, and will send later the message {Ref,baz,Baz}.

OTHER TIPS

A full answer is highly dependent on your application — for example, one gen_server might suffice, or you might really need a pool of database connections instead. But one thing you should be aware of is that a gen_server can return from a handle_call callback before it actually has a reply ready for the client by returning {noreply, NewState} and then later, once it has a client reply ready, calling gen_server:reply/2 to send it back to the client. This allows the gen_server to service calls from other clients without blocking on the first call. Note though that this requires that the gen_server has a way of sending a request into the database without having to block waiting for a reply; this is often achieved by having the database send a reply that arrives in the gen_server:handle_info/2 callback, passing enough info back that the gen_server can associate the database reply with the correct client request. Note also that gen_server:call/2,3 has a default timeout of 5 seconds, so you'll need to deal with that if you expect the duration of database calls to exceed the default.

If the problem you see is that you are leaking that the internal implementation of your db-process is a gen_server, you could implement the api such that it takes the pid as argument as well.

-module(user).

-behaviour(gen_server).

-export([find_by_id/2]).

find_by_id(Pid, Id) ->
    gen_server:call(Pid, {find_by_id, Id}).

%% Lots of code omitted

handle_call({find_by_id, Id}, From, State) ->
    ok.

%% Lots more code omitted.

This way you don't tell clients that the implementation is in fact a gen_server (although someone could use gen_server:call as well).

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top