6. Adding a GenServer Part 3 - abarr/remote GitHub Wiki

After ensuring I have a working test for the timer in the GenServer it's back to the requirements.

Requirments

As it stands, the updated list looks like this

  1. :remote app launches GenServer when the application starts - DONE
  2. Holds state %{max_number: :integer, timestamp: :etc_datetime} - DONE
  • :timestamp starts as nil and is updated each time someone queries the GenServer - DONE
  • :timestamp represents the last time someone queried the GenServer - TODO
  1. Every 60 seconds, the GenServer runs a job - DONE
  • The GenServer updates every user's points value with a random value between 0..100 - DONE
  • The Genserver updates :max_number in the state to a new random number between 0..100 - DONE
  1. The GenServer accepts a call to return Users with points greater than :max_number with a limit of 2 and the :timestamp from the previous call. - TODO

The :timestamp in the `GenServer' does not update unless someone makes a call i.e. requirement number 4.

Adding a Call to get Users

  1. First I add a configurable :users_returned_limit value and refactor GenServer to hold all configuration in state
"/lib/remote/users/user_server.ex"

defmodule Remote.Users.UserServer do
  @moduledoc false
  use GenServer

  import Ecto.Query, warn: false
  alias Remote.Repo

  require Logger

  @update_interval Application.get_env(:remote, :update_interval) || 60_000
  @max_num_range Application.get_env(:remote, :update_interval) || 100
  @min_num_range Application.get_env(:remote, :update_interval) || 0
  @users_returned_limit Application.get_env(:remote, :limit) || 2

  def start_link(opts) do
    name = Access.get(opts, :name, __MODULE__)
    update_interval = Access.get(opts, :update_interval, @update_interval)
    max_num_range = Access.get(opts, :max_num_range , @max_num_range)
    min_num_range = Access.get(opts, :max_num_range , @min_num_range)
    users_returned_limit = Access.get(opts, :max_num_range , @users_returned_limit)
    GenServer.start_link(
      __MODULE__,
      %{
        update_interval: update_interval,
        max_num_range: max_num_range,
        min_num_range: min_num_range,
        users_returned_limit: users_returned_limit
      },
      name: name
    )
  end

  @impl true
  def init(config) do
    Logger.info("#{__MODULE__} - successfully created")
    schedule_update(config.update_interval)
    {:ok, {%{max_number: Enum.random(0..100), timestamp: nil}, config}}
  end

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

  @impl true
  def handle_info(:update_user_points, {state, config}) do
    with num_rows <- Repo.one(from u in "users", select: count(u.id)),
         {:ok, %{num_rows: rows}} when rows == num_rows <-
           Ecto.Adapters.SQL.query(
             Repo,
             """
             UPDATE users
             SET
              points = floor(random() * (#{config.max_num_range} - #{config.min_num_range})) + #{config.min_num_range},
              updated_at = now() at time zone 'utc';
             """
           ) do
      Logger.info("User rows updated with new random points value: #{num_rows}")
    else
      _ ->
        Logger.error("User points update failed!")
        raise "User points update failed!"
    end

    schedule_update(config.update_interval)
    {:noreply, {%{state | max_number: Enum.random(0..100)}, config}}
  end

  # set timer
  defp schedule_update(interval) do
    Process.send_after(self(), :update_user_points, interval)
  end
end

  1. Add a handle_call/2 callback to return the configured limit of Users with points greater than the GenServer :max_number
"/lib/remote/users/user_server.ex"

def handle_call(:get_users_points_greater_than_max, _from, {state, config}) do
  query =
    from u in User,
      where: u.points > ^state.max_number,
      limit: ^config.users_returned_limit,
      select: %{id: u.id, points: u.points}

  users = Repo.all(query)

  result =
    %{
      users: users,
      timestamp: state.timestamp
    }

  {:reply, result, {%{state | timestamp: DateTime.utc_now()}, config}}
end

Calling the new callback I get the results I expect (timestamp: nil for the first call and ~U[2022-01-11 03:09:20.317057Z] on the second.

$ iex -S mix

Erlang/OTP 24 [erts-12.0] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1]

Compiling 1 file (.ex)
[info] Elixir.Remote.Users.UserServer - successfully created
Interactive Elixir (1.13.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> GenServer.call(Remote.Users.UserServer, :get_users_points_greater_than_max)
[debug] QUERY OK source="users" db=0.8ms decode=1.4ms queue=1.2ms idle=1742.7ms
SELECT u0."id", u0."points" FROM "users" AS u0 WHERE (u0."points" > $1) LIMIT $2 [5, 2]
[info] Remote.Users.UserServer :max_number updated and Users returned
%{timestamp: nil, users: [%{id: 203032, points: 66}, %{id: 203070, points: 10}]}
iex(2)> GenServer.call(Remote.Users.UserServer, :get_users_points_greater_than_max)
[debug] QUERY OK source="users" db=1.0ms idle=1918.9ms
SELECT u0."id", u0."points" FROM "users" AS u0 WHERE (u0."points" > $1) LIMIT $2 [5, 2]
[info] Remote.Users.UserServer :max_number updated and Users returned
%{
  timestamp: ~U[2022-01-11 03:09:20.317057Z],
  users: [%{id: 203032, points: 66}, %{id: 203070, points: 10}]
}
iex(3)>

After letting it run I am getting different values for the Users

[debug] QUERY OK source="users" db=108.9ms queue=0.1ms idle=937.2ms
SELECT count(u0."id") FROM "users" AS u0 []
[debug] QUERY OK db=4093.1ms queue=0.8ms idle=1046.6ms
UPDATE users
SET
 points = floor(random() * (100 - 0)) + 0,
 updated_at = now() at time zone 'utc';
 []
[info] User rows updated with new random points value: 1000000

iex(4)> GenServer.call(Remote.Users.UserServer, :get_users_points_greater_than_max)
[debug] QUERY OK source="users" db=1.5ms idle=1016.2ms
SELECT u0."id", u0."points" FROM "users" AS u0 WHERE (u0."points" > $1) LIMIT $2 [63, 2]
%{
  timestamp: ~U[2022-01-11 03:09:23.468374Z],
  users: [%{id: 203071, points: 83}, %{id: 203073, points: 99}]
}
[info] Remote.Users.UserServer :max_number updated and Users returned
iex(5)>