Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

GenServer

12-genservers.livemd

GenServer

Lets create a simple GenServer

GenServer is commonly used to store complex states with a client/server like API

> __MODULE__ is a special macro to refer current module

use GenServer brings all necessary stuffs to manage a gen server. its also a macro

defmodule Counter do
  use GenServer

  # Client API

  # Starts the GenServer
  def start_link(initial_value \\ 0) do
    GenServer.start_link(__MODULE__, initial_value, name: __MODULE__)
  end

  # Gets the current count
  def get_count do
    GenServer.call(__MODULE__, :get_count)
  end

  # Increments the count
  def increment do
    GenServer.cast(__MODULE__, :increment)
  end

  # Server (GenServer) Callbacks

  # Initializes the state with an initial value
  @impl true
  def init(initial_value) do
    {:ok, initial_value}
  end

  # Handles the synchronous call to get the count
  @impl true
  def handle_call(:get_count, _from, state) do
    {:reply, state, state}
  end

  # Handles the asynchronous cast to increment the count
  @impl true
  def handle_cast(:increment, state) do
    {:noreply, state + 1}
  end
end

Lets use this gen server

{:ok, _pid} = Counter.start_link(10)    # Start the server with an initial value of 10
Counter.get_count()                                        
Counter.increment()
Counter.get_count()

What is happening? ๐Ÿคทโ€โ™‚๏ธ

In the above definition we are starting a GenServer with the Module name(Counter) The name you see in the start_link allows us to idnetify the Process easily

What is Client/Server APIs

For our business logic we dont want to involve GenServer, instead we use thin wrappers to do GenServer calls without letting the caller knows whats inside.

  # Gets the current count
  def get_count do
    GenServer.call(__MODULE__, :get_count)
  end

in here we are simply abstracting our logic which calls a function call/2 on GenServer

What is call/2 and cast/2 ?

  • call/2 is used to do a synchronous call to the server, so caller will wait for the reply from server

> ๐Ÿ’ก In JS this is similar to a async/await call. > > javascript > await fetchUserCount() >

  • cast/2 is doing an async call. Now caller doesnt wait for reply from the server.

We need to decide what to use based on our application requirements ๐Ÿ™Œ

Server callbacks

Now when you do call or cast from client API, GenServer receive messages to the process and handle_call is invoked by GenServer process

> ๐Ÿ’ก In our previous bare bone Process creation, we got messages with receive. This is just a nicer way to handle the same thing

  @impl true
  def handle_call(:get_count, _from, state) do
    {:reply, state, state}
  end

Above snippet is handling a call made to the GenServer and using Pattern Matching to identify the message(as you already know we can pattern match on function signature)

for a call the server must reply to client with :reply

{:reply, reply_to_caller, new_state}

When we have a cast, the handle_cast function is invoked inside the GenServer

Now we dont need to reply back, but we can update our internal state or do whatever we want

{:noreply, new_state}

> Lot of concepts in Elixir/Erlang is built around GenServers > > Its like a building block

Handling other messages

A GenServer is just a process, it can also receive any message from anywhere. So its not limited to just call or cast

To handle anything else there is an another server callback called handle_info

defmodule PeriodicLogger do
  use GenServer

  # Starts the GenServer with an initial state and schedules a periodic timeout
  def start_link(initial_message) do
    GenServer.start_link(__MODULE__, initial_message, name: __MODULE__)
  end

  ## Client API

  # Public function to change the logging message
  def change_message(new_message) do
    GenServer.cast(__MODULE__, {:change_message, new_message})
  end

  ## Server Callbacks

  @impl true
  def init(initial_message) do
    # Set an initial message and schedule the first timeout for 5 seconds
    schedule_timeout()
    {:ok, initial_message}
  end

  @impl true
  def handle_cast({:change_message, new_message}, _state) do
    {:noreply, new_message}
  end

  @impl true
  def handle_info(:timeout, current_message) do
    # Log the current message when the timeout triggers
    IO.puts("Logging message: #{current_message}")

    # Reschedule the timeout for another 5 seconds
    schedule_timeout()
    {:noreply, current_message}
  end

  # Helper function to schedule a timeout in 5 seconds
  defp schedule_timeout do
    Process.send_after(self(), :timeout, 5000)
  end
end

In here we are using Process module to send a message to current process after a timeout of 5s

This message will be recevied by the GenServer and forward to handle_info as its not a call or cast

{:ok, pid} = PeriodicLogger.start_link("Hello, world!")
PeriodicLogger.change_message("New message!")

Lets kill the process

GenServer.stop(PeriodicLogger)