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
:remote
app launchesGenServer
when the application starts - DONE- Holds state %{max_number: :integer, timestamp: :etc_datetime} - DONE
:timestamp
starts asnil
and is updated each time someone queries theGenServer
- DONE:timestamp
represents the last time someone queried theGenServer
- TODO
- 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
- The
GenServer
accepts a call to return Users with points greater than:max_number
with a limit of2
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.
Users
Adding a Call to get - First I add a configurable
:users_returned_limit
value and refactorGenServer
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
- Add a
handle_call/2
callback to return the configured limit ofUsers
with points greater than theGenServer
: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)>