2
votes

I was reading this article about how microservices can be extracted out from existing phoenix app. Author refactored one of the phoenix app controller and moved one of its method to a Genserver ,after that he moved that Genserver to a separate application and added it a a dependency in the main project.

But I'm little confused here because GenServer allows you to have only two server callbacks (which are handle call and handle cast). But if I want a functionality to move out as a microservice we would end up creating as many Genserver as the endpoints involved in that service, because a single genserver would allow a one/two method call. Is genserver an ideal approach for extracting services ?

2

2 Answers

7
votes

GenServer has 3 main callbacks: handle_cast, handle_call, and handle_info. Each of these functions can have multiple clauses, which each implement a specific piece of functionality.

For example, let's assume I have a UserService module implemented as a GenServer, with 5 operations: create, show, index, update, and delete. Let's further assume that I'm implementing delete as an async operation (so using handle_cast), and the rest as synchronous operations (using handle_call).

def handle_call({:create, user_data}, _from, state) do
  new_user = User.create(user_data) // (However you create a user)
  {:reply, new_user, state}
end

def handle_call(:index, _from, state) do
  users = User.all
  {:reply, users, state}
end

def handle_call({:update, user_changes}, _from, state) do
  updated_user = User.update(user_changes)
  {:reply, updated_user, state}
end

def handle_call({:show, user_id}, _from, state) do
  user = User.get(user_id)
  {:reply, user, state}
end

def handle_cast({:delete, user_id}, state) do
  User.delete(user_id)
  {:no_reply, state)
end

A client module can call user = GenServer.call(pid, {:show, user_id}) to use the show clause. The main takeaway is that even though there are only a few "functions" defined, you can define as many clauses of that function as you need, and pattern matching will dispatch to the correct clause.

4
votes

I'm a little confused by your question, but I don't think you've got a correct picture of GenServers.

The 2 callbacks, handle_call and handle_cast are for receiving messages that are sent via GenServer.call or GenServer.cast functions. There is also handle_info which can receive any non OTP message sent to the GenServer's process.

Call is for synchronous messages which will block for a response. I would guess that's what you want for a microservice. (Cast is for asynchronous messages.)

A single GenServer implementation can receive any number of messages sent via call.

The handle_call callback takes 3 arguments. The first one is the message that it's handling. Typically that message is a tagged tuple, which means it's first item identifies which type of message it is. You can handle multiple different messages and use pattern matching to execute the correct function clause within your GenServer like this:

def handle_call({:message_one, foo}, _from, state)
    # create some_response from foo or modify state here
    {:reply, some_respone, state}
end
def handle_call({:message_two, bar}, _from, state)
    # create some_response from bar or modify state here 
    {:reply, some_response, state}
end
def handle_call({:message_three, buzz}, _from, state)
    # create some_response from buzz or modify state here
    {:reply, some_response, state}
end

Your GenServer can have as many of these function clauses as you need. You'd typically also keep those message tag atoms as an implementation detail and create public API functions that wrap your calls to GenServer.call.