Elixir GenServer is a behaviour module for implementing concurrent and stateful processes.

Table of contents

    It builds upon the concepts introduced by the gen_server module in Erlang's OTP framework, offering a more idiomatic and developer-friendly interface.

    The term "GenServer" is short for "Generic Server," representing abstract code that handles the complexities of managing server processes running on Erlang VM.

    How to use it?

    We will write some code for the simple example. Let's start by opening up the iex shell. Type iex in your terminal to enter the Elixir interactive shell. Then, we'll define a callback module that utilizes the GenServer behaviour:

    iex(1)> defmodule Game do
     use GenServer
    end

    The use macro injects code into the calling module during compilation. You can verify this by typing:

    iex(2)> Game.__info__(:functions)

    These functions must be implemented in the calling module in order to use the gen_server behaviour. You can overwrite existing functions by using the same name and arity of the function.

    To start the process, run:

    iex(3)> GenServer.start(Game)
    {:ok, #PID<0.117.0>}

    Here, we are passing the module name and an argument that will be passed to the init/1 function (more on that later). This returns {:ok, pid}, where pid is a unique identifier for the spawned process. You can utilize this pid to identify the process. At this point, the process is up and running.

    Our example represents a basic implementation of the GenServer behaviour, which may not be practical in real-world scenarios due to the lack of a usable API. To enhance the functionality of our GenServer, we need to define and implement the following callbacks:

    • init/1: invoked when the GenServer process is started. It sets initial state of the process.
    • handle_cast/2: receive messages that are asynchronous sent to the GenServer using the cast function. It processes the message and may update the state of the GenServer without sending a response.
    • handle_call/2: handles synchronous messages sent to the GenServer using the call function. It processes the message, may update the state of the GenServer, and sends a response back to the caller.
    • handle_info/2: invoked whenever the GenServer receives a message that doesn't match any of the defined synchronous or asynchronous message patterns (handled by handle_call/2 and handle_cast/2, respectively)

    Let's add some code and implement these callbacks in our example Game module to create a more useful GenServer:

    defmodule Game do
      use GenServer
    
      def init(state) do
        {:ok, state}
      end
    
      def handle_cast({:join, player_id}, state) do
        updated_state = [player_id | state]
        {:noreply, updated_state}
      end
    
      def handle_call(:get_players, _from, state) do
        {:reply, state, state}
      end
    
      def handle_info({:remove_player, player_id}, state) do
        updated_state = List.delete(state, player_id)
        {:noreply, updated_state}
      end
    end

    After implementing callbacks such as init, handle_cast, handle_call and handle_info to manage the game state, we are now able to send messages to Game server process:

    iex(4)> {:ok, pid} = GenServer.start(Game, [])
    iex(5)> GenServer.cast(pid, {:join, 1})
    iex(6)> GenServer.cast(pid, {:join, 2})
    iex(7)> GenServer.call(pid, :get_players)
    # Output: [2, 1]
    iex(8)> send(pid, {:remove_player, 1})
    iex(9)> GenServer.call(pid, :get_players)
    # Output: [2]

    Notice how we invoke handle_info/2 by using send/2.

    Everything works, but we can enhance the Game module for better readability and user-friendliness by incorporating interface functions in the same module:

    defmodule Game do
      use GenServer
    
     def start_link(state \\ []) do
        GenServer.start_link(__MODULE__, state)
      end
    
      def join(pid, player_id) do
        GenServer.cast(pid, {:join, player_id})
      end
    
      def get_players(pid) do
        GenServer.call(pid, :get_players)
      end
    
      def remove_player(pid, player_id) do
        send(pid, {:remove_player, player_id})
      end
    
      # Callbacks...
    end

    In this extension we have introduced interface functions that provide a clear and intuitive API for users to interact with the Game module.

    Now, let’s test the server:

    iex(1)> {:ok, pid} = Game.start_link()
    iex(2)> Game.join(pid, 1)
    iex(3)> Game.join(pid, 2)
    iex(4)> Game.get_players(pid)
    # Output: [2, 1]
    iex(5)> Game.remove_player(pid, 1)
    iex(6)> Game.get_players(pid)
    # Output: [2]

    It works as expected!

    GenServer.start/2 function returns a tuple {:ok, pid}. If in init/1 you decide that something went wrong and new process should not start, you can return return {:stop, reason}, then the result of start/2 is {:error, reason}.

    It's important to note that GenServer.start/2 works synchronously, meaning start/2 function returns only once the init/1 callback has completed execution within the server process. So you don't want to perform actions that could potentially take too long because it will block the GenServer from completing start-up.

    For instance, consider a scenario where we're starting a game server to accommodate a large number of players waiting to join. If the initialization of the game server takes an excessive amount of time, players may become impatient and decide to leave. Therefore, it's essential to prioritize a quick start-up of game processes to minimize waiting times and maintain player engagement.

    To ilustrate this problem, add prepare_game function that simulates long-running operations and update init/1 function:

    def init(state) do
        prepare_game()
        {:ok, state}
    end
    
    defp prepare_game do
      :timer.sleep(5000)
    end

    Now, if you start new game via Game.start, you'll experience a 5-second delay before being able to proceed to start another game. During this time, the calling process is temporarily blocked and unable to perform any additional actions until the initialization process completes.

    To address the issue of potentially lengthy initialization processes blocking the start-up of multiple games, we can use the handle_continue/2 callback. This callback is invoked by the GenServer process whenever the previous callback returns {:continue, message}. By returning this value from our init/1 callback, we ensure that the server can immediately proceed with additional initialization steps asynchronously, without delaying the start-up of subsequent games.

    An alternative solution to handle non-blocking initialization is to utilize a self-sent message within the GenServer process. In this approach, the process sends a message to itself to trigger additional initialization steps asynchronously. However, this method may introduce unpredictability, as there's no guarantee that the self-sent message will be the first message in the process's message queue, potentially leading to delays in processing.

    Now, we'll update the init/1 function and add the handle_continue/2 callback:

    def init(state) do
      {:ok, state, {:continue, :prepare_game}}
    end
    
    def handle_continue(:prepare_game, state) do
      prepare_game()
      {:noreply, state}
    end

    With these changes, when you start new process again, you'll notice that the function returns immediately, indicating that the process is executing the prepare_game/0 function asynchronously. This allows for smoother and non-blocking initialization of game processes.

    Imagine that the Game process is running smoothly, with players actively engaged in the game. However, as with any software, there may come a time when updates to the existing code base are necessary, perhaps to introduce new features. In such scenarios, interrupting the game by stopping the process and removing all players to perform the update is far from ideal. This is not a problem for GenServer because GenServer process is calling out to the most recent verion of the Game module.

    To demonstrate this concept, let's restart the iex shell and initiate a new game process. Then, we'll proceed to update the join/2 function to notify when a user joins:

    def join(pid, player_id) do
      IO.puts("Player #{player_id} joined the game")
      GenServer.cast(pid, {:join, player_id})
    end

    Now, if you save the file, recompile the Game module, and invoke the join function, you will see an output:

    iex(1)> r Game
    iex(2)> Game.join(pid, 2)
    # Output: Player 2 joined the game

    This implies that the game server was still running while the code has changed, demonstrating the seamless update capability of GenServer processes.

    But what if there is a need to change the structure of the state? For example, there is a need to hold players under the key in a map like this: %{players: players}.

    This is where the code_change callback comes into play. This callback will be invoked in the event of a code change, so we can transform the state to the correct structure:

    def init(state) do
      {:ok, %{players: state}, {:continue, :prepare_game}}
    end
    
    def handle_cast({:join, player_id}, state) do
      updated_state = %{state | players: [player_id | state.players]}
      {:noreply, updated_state}
    end
    
    def handle_call(:get_players, _from, state) do
      {:reply, state.players, state}
    end
    
    def handle_info({:remove_player, player_id}, state) do
      updated_state = %{state | players: List.delete(state.players, player_id)}
      {:noreply, updated_state}
    end
    
    def code_change(_old_vsn, state, _extra) do
      {:ok, %{players: state}}
    end

    Without the code_change callback, after recompiling the Game module, if we attempt to retrieve all players by invoking the get_players function, we would encounter an error: "(KeyError) key :players not found in []". This occurs because the state was not adjusted to reflect the new structure introduced in the updated code.

    To test if this process will work, we need to explicitly change the code for a running process, which involves the following steps:

    1. Temporarily suspend the process.
    2. Implement the code change.
    3. Resume the process.
    4. Verify if the state has been updated correctly.
    iex(3)> :sys.suspend(pid)
    iex(4)> r Game
    iex(5)> :sys.change_code(pid, Game, nil, [])
    iex(5)> :sys.resume(pid)
    iex(6)> :sys.resume(pid)
    iex(7)> :sys.get_state(pid)
    # Output: %{players: [1,2]}

    This output confirms that the state is adjusted as we want it to be, so game servers can run without any interruption even if we want to change the code or the state structure

    In implementing the simple example of the Game module, we've demonstrated the power and versatility of GenServer in Elixir. GenServer offers robust support for building concurrent and stateful processes, providing developers with a user-friendly interface for efficient server process management. By leveraging initialization callbacks and interface functions, such as those illustrated in the Game module, developers can create scalable and responsive server modules tailored to their application's needs.

    When to use it?

    GenServer is a versatile tool in the Elixir ecosystem, suitable for various scenarios where you need to manage concurrent and stateful processes efficiently. Here are some real-life use cases where GenServer shines:

    Real-time Collaboration Apps

    GenServer can be used to manage collaborative features in applications such as real-time document editing, chat applications, or collaborative drawing tools.

    Building Real-time Chat Systems

    Real-time communication systems, such as chat applications, require efficient handling of message delivery and user presence tracking. GenServer can be employed to manage chat room state, handle message broadcasting, and track online/offline status of users. This ensures timely delivery of messages and seamless user interaction in the chat system.

    Building Game Servers

    Game servers require efficient management of game state, handling player interactions, and maintaining game world consistency. GenServer can be leveraged to implement game server logic, managing player sessions, processing game events, and updating game state in real-time. This facilitates the development of scalable and responsive multiplayer game servers.

    Implementing Rate Limiting Mechanisms

    Rate limiting is essential for preventing abuse and ensuring fair usage of resources in networked applications. GenServer can be used to implement rate limiting mechanisms, tracking request counts and enforcing limits per user or IP address. This helps protect the system from excessive traffic and potential denial-of-service attacks.

    These are just a few examples of how GenServer can be used effectively in real-world applications. By leveraging its capabilities to manage concurrent processes and state, you can build robust and scalable systems in Elixir.

    When NOT to use it:

    There are certain scenarios, where GenServer may not be the best choice

    Running Function Asynchronously:

    If your goal is to execute a function asynchronously, it's recommended to utilize the Task module. This approach guarantees that the function runs in a separate process, enabling the caller process to execute other tasks concurrently.

    iex(1)> do_some_work = fn -> 1 + 1 end
    iex(2)> task = Task.async(fn -> do_some_work.() end)
    iex(3)> other_work = 2 + 2
    iex(4)> result = Task.await(task) + other_work
    # Output: 6

    Simple State Management

    If your application requires only basic, non-concurrent and short-term state management, the Agent module is recommended. Built on top of GenServer, Agent abstracts certain functionalities while leveraging the underlying abstract code of GenServer. This abstraction simplifies the process of managing state, making it ideal for scenarios where complex concurrency features are unnecessary.

    iex(1)> {:ok, pid} = Agent.start(fn -> 1 end)
    iex(2)> Agent.get(pid, fn state -> state end)
    iex(3)> Agent.update(pid, fn state -> state + 1 end)
    iex(4)> Agent.get(pid, fn state -> state end)
    # Output: 2

    When Concurrency is Unnecessary

    If your application doesn't require concurrent processing or management of state across multiple processes, using GenServer might introduce unnecessary complexity. In such cases, simpler alternatives that don't involve concurrency, such as basic function calls or sequential processing, would be more appropriate.

    Understanding these limitations can help you make informed decisions about when to leverage GenServer in your Elixir applications, ensuring that you choose the right tool for the job

    FAQ

    What is Elixir GenServer and its primary use?

    Elixir GenServer is a behavior module for building concurrent and stateful processes in Elixir applications, typically used in scenarios requiring efficient management of state and concurrent operations.

    How does GenServer improve Elixir application development?

    GenServer abstracts and handles complexities of concurrent server processes, offering idiomatic and developer-friendly interfaces for building robust and scalable applications.

    What are the key functions and callbacks of GenServer in Elixir?

    Key functions include start_link and cast, while essential callbacks include init/1, handle_call/3, handle_cast/2, and handle_info/2, crucial for managing state and inter-process communication.

    How do GenServer processes handle asynchronous and synchronous communications?

    GenServer uses handle_cast/2 for handling asynchronous messages without response, and handle_call/3 for synchronous messages where a response is expected from the server.

    Can GenServer be used for tasks other than managing server processes?

    While primarily for server processes, GenServer is versatile and can be adapted for various scenarios beyond traditional server tasks, like managing shared state or asynchronous tasks.

    What are the situations where GenServer should not be used in Elixir?

    Avoid GenServer for simple, non-concurrent state management or asynchronous function execution, where modules like Task or Agent might be more appropriate.

    How does GenServer handle state changes and code updates in running applications?

    GenServer allows for hot code swapping and state updates through the code_change/3 callback, facilitating seamless updates and maintenance of running processes.

    Curiosum Elixir Developer Olaf
    Olaf Bado Elixir Developer

    Read more
    on #curiosum blog