From 9a213e621d822347296f23d12d8c70e3660489e3 Mon Sep 17 00:00:00 2001 From: Boon Low Date: Fri, 4 Sep 2020 10:28:06 +0100 Subject: [PATCH 1/3] create a bare-bones GenServer-based request rate calculator --- lib/origin_simulator/rate_calculator.ex | 19 +++++++++++++++++++ lib/origin_simulator/supervisor.ex | 3 ++- .../origin_simulator/rate_calculator_test.exs | 9 +++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 lib/origin_simulator/rate_calculator.ex create mode 100644 test/origin_simulator/rate_calculator_test.exs diff --git a/lib/origin_simulator/rate_calculator.ex b/lib/origin_simulator/rate_calculator.ex new file mode 100644 index 0000000..33a9c59 --- /dev/null +++ b/lib/origin_simulator/rate_calculator.ex @@ -0,0 +1,19 @@ +defmodule OriginSimulator.RateCalculator do + use GenServer + + def start_link(opts) do + GenServer.start_link(__MODULE__, opts, name: __MODULE__) + end + + def rate() do + GenServer.call(__MODULE__, :rate) + end + + ## Server Callbacks + + @impl true + def init(_), do: {:ok, 0} + + @impl true + def handle_call(:rate, _from, rate), do: {:reply, rate, rate} +end diff --git a/lib/origin_simulator/supervisor.ex b/lib/origin_simulator/supervisor.ex index 007eb0e..7bf7194 100644 --- a/lib/origin_simulator/supervisor.ex +++ b/lib/origin_simulator/supervisor.ex @@ -10,7 +10,8 @@ defmodule OriginSimulator.Supervisor do children = [ OriginSimulator.Simulation, OriginSimulator.Payload, - OriginSimulator.Counter + OriginSimulator.Counter, + OriginSimulator.RateCalculator ] opts = [ diff --git a/test/origin_simulator/rate_calculator_test.exs b/test/origin_simulator/rate_calculator_test.exs new file mode 100644 index 0000000..fa0654b --- /dev/null +++ b/test/origin_simulator/rate_calculator_test.exs @@ -0,0 +1,9 @@ +defmodule OriginSimulator.RateCalculatorTest do + use ExUnit.Case + + alias OriginSimulator.RateCalculator + + test "state() returns a valid integer request per-second rate" do + assert RateCalculator.rate() |> is_integer() + end +end From d1ee10b06f7b9b8c538c9a4903e4812b251461d2 Mon Sep 17 00:00:00 2001 From: Boon Low Date: Fri, 4 Sep 2020 12:24:28 +0100 Subject: [PATCH 2/3] refactor Counter: parameterised naming, add unit tests (not longer a singleton and testable in isolation) --- lib/origin_simulator/counter.ex | 17 +++++++------ test/origin_simulator/counter_test.exs | 34 ++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 test/origin_simulator/counter_test.exs diff --git a/lib/origin_simulator/counter.ex b/lib/origin_simulator/counter.ex index b644697..b58bb37 100644 --- a/lib/origin_simulator/counter.ex +++ b/lib/origin_simulator/counter.ex @@ -3,22 +3,23 @@ defmodule OriginSimulator.Counter do @initial_state %{total_requests: 0} - def start_link(_opts) do - Agent.start_link(fn -> @initial_state end, name: __MODULE__) + def start_link(opts) do + name = Keyword.get(opts, :name) + Agent.start_link(fn -> @initial_state end, name: if(name, do: name, else: __MODULE__)) end - def value do - Agent.get(__MODULE__, & &1) + def value(agent \\ __MODULE__) do + Agent.get(agent, & &1) end - def clear do - Agent.update(__MODULE__, fn _state -> + def clear(agent \\ __MODULE__) do + Agent.update(agent, fn _state -> @initial_state end) end - def increment(status_code) do - Agent.update(__MODULE__, fn state -> + def increment(status_code, agent \\ __MODULE__) do + Agent.update(agent, fn state -> state |> increment_key(:total_requests) |> increment_key(status_code) diff --git a/test/origin_simulator/counter_test.exs b/test/origin_simulator/counter_test.exs new file mode 100644 index 0000000..7f1452b --- /dev/null +++ b/test/origin_simulator/counter_test.exs @@ -0,0 +1,34 @@ +defmodule OriginSimulator.CounterTest do + use ExUnit.Case, async: true + + alias OriginSimulator.Counter + + setup_all do + {:ok, _} = start_supervised({Counter, name: TestCounter}) + :ok + end + + setup do + Counter.clear(TestCounter) + :ok + end + + test "clear/1 resets total requests to zero initial state" do + assert Counter.value(TestCounter).total_requests == 0 + end + + test "value/1 returns the current count" do + assert Counter.value(TestCounter).total_requests == 0 + end + + test "increment/1 adds to the current count" do + Counter.increment(200, TestCounter) + Counter.increment(200, TestCounter) + assert Counter.value(TestCounter).total_requests == 2 + end + + test "increment/1 adds to the current count for the status" do + Counter.increment(404, TestCounter) + assert Counter.value(TestCounter)[404] == 1 + end +end From 4cfe7c7ee5a62bc8c1f8765a8fe3a7353054793a Mon Sep 17 00:00:00 2001 From: Boon Low Date: Fri, 4 Sep 2020 13:09:04 +0100 Subject: [PATCH 3/3] implement per-second request rate calculation via GenServer event loop --- lib/origin_simulator/rate_calculator.ex | 32 +++++++++++++--- .../origin_simulator/rate_calculator_test.exs | 38 +++++++++++++++++-- 2 files changed, 61 insertions(+), 9 deletions(-) diff --git a/lib/origin_simulator/rate_calculator.ex b/lib/origin_simulator/rate_calculator.ex index 33a9c59..4fc5d4d 100644 --- a/lib/origin_simulator/rate_calculator.ex +++ b/lib/origin_simulator/rate_calculator.ex @@ -1,19 +1,39 @@ defmodule OriginSimulator.RateCalculator do use GenServer + alias OriginSimulator.Counter + def start_link(opts) do - GenServer.start_link(__MODULE__, opts, name: __MODULE__) + name = Keyword.get(opts, :name) + GenServer.start_link(__MODULE__, opts, name: if(name, do: name, else: __MODULE__)) end - def rate() do - GenServer.call(__MODULE__, :rate) - end + def rate(calculator \\ __MODULE__), do: GenServer.call(calculator, :rate) + def state(calculator \\ __MODULE__), do: GenServer.call(calculator, :state) + + def current_count(counter \\ Counter), do: Counter.value(counter).total_requests ## Server Callbacks @impl true - def init(_), do: {:ok, 0} + def init(opts) do + counter = Keyword.get(opts, :counter) + send(self(), {:calculate, if(counter, do: counter, else: Counter)}) + + {:ok, %{rate: 0, current_count: 0}} + end @impl true - def handle_call(:rate, _from, rate), do: {:reply, rate, rate} + def handle_call(:rate, _from, state), do: {:reply, state.rate, state} + + @impl true + def handle_call(:state, _from, state), do: {:reply, state, state} + + @impl true + def handle_info({:calculate, counter}, state) do + Process.send_after(self(), {:calculate, counter}, 1000) + + new_count = current_count(counter) + {:noreply, %{rate: new_count - state.current_count, current_count: new_count}} + end end diff --git a/test/origin_simulator/rate_calculator_test.exs b/test/origin_simulator/rate_calculator_test.exs index fa0654b..a3d7572 100644 --- a/test/origin_simulator/rate_calculator_test.exs +++ b/test/origin_simulator/rate_calculator_test.exs @@ -1,9 +1,41 @@ defmodule OriginSimulator.RateCalculatorTest do - use ExUnit.Case + use ExUnit.Case, async: true - alias OriginSimulator.RateCalculator + alias OriginSimulator.{Counter, RateCalculator} - test "state() returns a valid integer request per-second rate" do + @test_counter TestCounterForRateCalculator + + setup_all do + {:ok, _} = start_supervised({Counter, name: TestCounterForRateCalculator}) + :ok + end + + setup do + Counter.clear(@test_counter) + end + + test "rate() returns a valid integer request per-second rate" do assert RateCalculator.rate() |> is_integer() end + + test "state() returns the calculator state" do + assert RateCalculator.state() == %{current_count: 0, rate: 0} + end + + test "current_count/1 returns the current request count" do + Counter.increment(200, @test_counter) + Counter.increment(200, @test_counter) + assert RateCalculator.current_count(@test_counter) == 2 + end + + test "handles request rate calculation" do + current_calculator_state = %{rate: 0, current_count: 0} + + for _request <- 1..10 do + Counter.increment(200, @test_counter) + end + + {:noreply, new_calculator_state} = RateCalculator.handle_info({:calculate, @test_counter}, current_calculator_state) + assert new_calculator_state.rate == 10 + end end