Skip to content
This repository has been archived by the owner on Aug 28, 2023. It is now read-only.

Commit

Permalink
Store Rates in ETS (#9) (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
maennchen committed Dec 2, 2019
1 parent 39e9713 commit cdee695
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 57 deletions.
17 changes: 9 additions & 8 deletions lib/currency_conversion/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,19 @@ defmodule CurrencyConversion.Application do

use Application

alias CurrencyConversion.UpdateWorker

@spec start(Application.start_type(), start_args :: term) ::
{:ok, pid}
| {:ok, pid, Application.state()}
| {:error, reason :: term}
def start(_type, _args) do
import Supervisor.Spec, warn: false

children = [
worker(CurrencyConversion.UpdateWorker, [], restart: :permanent)
]

opts = [strategy: :one_for_one, name: CurrencyConversion.Supervisor]
Supervisor.start_link(children, opts)
Supervisor.start_link(
[
{UpdateWorker, Application.get_all_env(:currency_conversion)}
],
strategy: :one_for_one,
name: CurrencyConversion.Supervisor
)
end
end
23 changes: 23 additions & 0 deletions lib/currency_conversion/rates.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,27 @@ defmodule CurrencyConversion.Rates do
"""
@enforce_keys [:base, :rates]
defstruct [:base, :rates]

@doc false
@spec to_list(CurrencyConversion.Rates.t()) :: [{atom, float} | {:base, atom}]
def to_list(%__MODULE__{base: base, rates: rates}) do
[{:base, base} | Enum.to_list(rates)]
end

@doc false
@spec from_list(list :: [{atom, float} | {:base, atom}]) :: CurrencyConversion.Rates.t()
def from_list(list) when is_list(list) do
Enum.reduce(
list,
%__MODULE__{base: nil, rates: %{}},
fn
{:base, base}, %__MODULE__{rates: rates} when is_atom(base) ->
%__MODULE__{base: base, rates: rates}

{currency, rate}, %__MODULE__{base: base, rates: rates}
when is_atom(currency) and is_float(rate) ->
%__MODULE__{base: base, rates: Map.put_new(rates, currency, rate)}
end
)
end
end
75 changes: 43 additions & 32 deletions lib/currency_conversion/update_worker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,62 +6,73 @@ defmodule CurrencyConversion.UpdateWorker do

require Logger

@update_worker CurrencyConversion.UpdateWorker
@default_refresh_interval 1000 * 60 * 60 * 24

@doc """
Starts the update worker.
"""
def start_link do
GenServer.start_link(__MODULE__, :ok, name: @update_worker)
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: Keyword.get(opts, :name, __MODULE__))
end

@spec init(:ok) :: {:ok, Rates.t()} | {:stop, any}
def init(:ok) do
Process.send_after(self(), :refresh, get_refresh_interval())
@impl GenServer
def init(opts) do
table_identifier =
opts
|> Keyword.get(:name, __MODULE__)
|> table_name
|> :ets.new([:protected, :ordered_set, :named_table])

case refresh() do
{:ok, rates} -> {:ok, rates}
opts = Keyword.put(opts, :table_identifier, table_identifier)

refresh_interval = Keyword.get(opts, :refresh_interval, @default_refresh_interval)

case refresh(opts) do
:ok -> {:ok, opts, refresh_interval}
{:error, binary} -> {:stop, {:error, binary}}
end
end

@spec handle_call(:get, any, Rates.t()) :: {:reply, Rates.t(), Rates.t()}
def handle_call(:get, _options, state) do
{:reply, state, state}
end
@impl GenServer
def handle_info(:timeout, opts) do
refresh_interval = Keyword.get(opts, :refresh_interval, @default_refresh_interval)

@spec handle_info(:refresh, Rates.t()) :: {:noreply, Rates.t()}
def handle_info(:refresh, state) do
Process.send_after(self(), :refresh, get_refresh_interval())

case refresh() do
{:ok, rates} -> {:noreply, rates}
{:error, _} -> {:noreply, state}
case refresh(opts) do
:ok -> {:noreply, opts, refresh_interval}
{:error, binary} -> {:stop, {:error, binary}}
end
end

defp refresh do
case get_source().load() do
@spec refresh(opts :: Keyword.t()) :: :ok | {:error, binary}
defp refresh(opts) do
table_identifier = Keyword.fetch!(opts, :table_identifier)
source = Keyword.get(opts, :source, CurrencyConversion.Source.Fixer)

case source.load() do
{:ok, rates} ->
Logger.info("Refreshed currency rates.")
Logger.debug(inspect(rates))
{:ok, rates}

for entry <- Rates.to_list(rates) do
:ets.insert(table_identifier, entry)
end

:ok

{:error, error} ->
Logger.error("An error occured while rereshing currency rates. " <> inspect(error))
{:error, error}
end
end

@spec get_source() :: atom
defp get_source,
do: Application.get_env(:currency_conversion, :source, CurrencyConversion.Source.Fixer)

# Default: One Day
@spec get_refresh_interval() :: integer
defp get_refresh_interval,
do: Application.get_env(:currency_conversion, :refresh_interval, 1000 * 60 * 60 * 24)
@spec get_rates(worker_name :: atom()) :: Rates.t()
def get_rates(worker_name \\ __MODULE__),
do:
worker_name
|> table_name
|> :ets.tab2list()
|> Rates.from_list()

@spec get_rates() :: Rates.t()
def get_rates, do: GenServer.call(@update_worker, :get)
@spec table_name(worker_name :: atom()) :: atom()
defp table_name(worker_name), do: Module.concat(worker_name, Table)
end
5 changes: 4 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ defmodule CurrencyConversion.Mixfile do
end

def application do
[applications: [:logger, :httpotion], mod: {CurrencyConversion.Application, []}]
[
extra_applications: [:logger],
mod: {CurrencyConversion.Application, []}
]
end

defp deps do
Expand Down
42 changes: 27 additions & 15 deletions test/currency_conversion/update_worker_test.exs
Original file line number Diff line number Diff line change
@@ -1,33 +1,45 @@
defmodule CurrencyConversion.UpdateWorkerTest do
use ExUnit.Case, async: false
doctest CurrencyConversion.UpdateWorker

import CurrencyConversion.UpdateWorker
alias CurrencyConversion.UpdateWorker

import ExUnit.CaptureLog
doctest UpdateWorker

import Mock

defmodule Source do
@moduledoc false

@behaviour CurrencyConversion.Source

def load do
{:ok, %CurrencyConversion.Rates{base: :CHF, rates: %{}}}
end
end

test "initial load called" do
capture_log(fn ->
Application.stop(:currency_conversion)
test "initial load called", %{test: test_name} do
name = Module.concat(__MODULE__, test_name)
start_supervised!({UpdateWorker, source: Source, name: name})

Application.put_env(
:currency_conversion,
:source,
CurrencyConversion.UpdateWorkerTest.Source
)
assert UpdateWorker.get_rates(name) == %CurrencyConversion.Rates{base: :CHF, rates: %{}}
end

Application.ensure_started(:logger)
Application.ensure_all_started(:currency_conversion)
end)
test "refresh load called", %{test: test_name} do
test_pid = self()
name = Module.concat(__MODULE__, test_name)

with_mock CurrencyConversion.Source.Test,
load: fn ->
send(test_pid, :load)
{:ok, %CurrencyConversion.Rates{base: :CHF, rates: %{}}}
end do
start_supervised!(
{UpdateWorker,
source: CurrencyConversion.Source.Test, name: name, refresh_interval: 1_000}
)

assert get_rates() == %CurrencyConversion.Rates{base: :CHF, rates: %{}}
assert_received :load
assert_receive :load, 1_100
end
end
end
2 changes: 1 addition & 1 deletion test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
ExUnit.start()
ExUnit.start(capture_log: true)

0 comments on commit cdee695

Please sign in to comment.