-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(commuter_rail_occupancies): read commuter rail occupancies from …
…S3 instead of firebase (#858) Problem: Keolis needs to decommission their Firebase feed Solution: We're having them move the contents of that Firebase feed to S3.
- Loading branch information
Showing
8 changed files
with
242 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
defmodule StateMediator.S3Mediator do | ||
@moduledoc """ | ||
S3Mediator is responsible for reading files from an S3 bucket and | ||
sending messages to the state module. | ||
""" | ||
|
||
defstruct [ | ||
:module, | ||
:bucket_arn, | ||
:object, | ||
:sync_timeout, | ||
:interval | ||
] | ||
|
||
@opaque t :: %__MODULE__{ | ||
module: module, | ||
bucket_arn: String.t(), | ||
object: String.t(), | ||
sync_timeout: pos_integer() | ||
} | ||
|
||
use GenServer | ||
require Logger | ||
alias ExAws.S3 | ||
|
||
def child_spec(opts) do | ||
{spec_id, opts} = Keyword.pop!(opts, :spec_id) | ||
|
||
%{ | ||
id: spec_id, | ||
start: {__MODULE__, :start_link, [opts]} | ||
} | ||
end | ||
|
||
@spec start_link(Keyword.t()) :: {:ok, pid} | ||
def start_link(options) do | ||
GenServer.start_link(__MODULE__, options) | ||
end | ||
|
||
@spec stop(pid) :: :ok | ||
def stop(pid) do | ||
GenServer.stop(pid) | ||
end | ||
|
||
@spec init(Keyword.t()) :: {:ok, __MODULE__.t(), {:continue, any()}} | ||
def init(options) do | ||
state_module = Keyword.fetch!(options, :state) | ||
|
||
bucket_arn = Keyword.fetch!(options, :bucket_arn) | ||
object = Keyword.fetch!(options, :object) | ||
sync_timeout = options |> Keyword.get(:sync_timeout, 5000) | ||
interval = options |> Keyword.get(:interval, 5000) | ||
|
||
state = %__MODULE__{ | ||
interval: interval, | ||
module: state_module, | ||
bucket_arn: bucket_arn, | ||
object: object, | ||
sync_timeout: sync_timeout | ||
} | ||
|
||
{:ok, state, {:continue, nil}} | ||
end | ||
|
||
@spec handle_continue(any, t) :: {:noreply, t} | {:noreply, t, :hibernate} | ||
def handle_continue(_, %{module: state_module} = state) do | ||
_ = Logger.debug(fn -> "#{__MODULE__} #{state_module} initial sync starting" end) | ||
fetch(state) | ||
end | ||
|
||
def handle_info(:timeout, %{module: state_module} = state) do | ||
_ = Logger.debug(fn -> "#{__MODULE__} #{state_module} timeout sync starting" end) | ||
fetch(state) | ||
end | ||
|
||
defp fetch(%{bucket_arn: bucket_arn, object: object} = state) do | ||
bucket_arn | ||
|> S3.get_object(object) | ||
|> ExAws.request() | ||
|> handle_response(state) | ||
end | ||
|
||
defp handle_response( | ||
{:ok, %{body: body}}, | ||
%{sync_timeout: sync_timeout, module: state_module} = state | ||
) do | ||
debug_time("#{state_module} new state", fn -> state_module.new_state(body, sync_timeout) end) | ||
|
||
schedule_update(state) | ||
end | ||
|
||
defp handle_response( | ||
response, | ||
state | ||
) do | ||
Logger.warning( | ||
"Received unknown response when getting commuter rail occupancies from S3: #{inspect(response)}" | ||
) | ||
|
||
schedule_update(state) | ||
end | ||
|
||
defp schedule_update(%{interval: interval} = state) when interval != nil do | ||
{:noreply, state, interval} | ||
end | ||
|
||
defp debug_time(description, func) do | ||
State.Logger.debug_time(func, fn milliseconds -> | ||
"#{__MODULE__} #{description} took #{milliseconds}ms" | ||
end) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
58 changes: 58 additions & 0 deletions
58
apps/state_mediator/test/state_mediator/s3_mediator_test.exs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
defmodule StateMediator.S3MediatorTest do | ||
use ExUnit.Case, async: true | ||
|
||
import Mox | ||
import StateMediator.S3Mediator | ||
|
||
defmodule StateModule do | ||
def size do | ||
0 | ||
end | ||
|
||
def new_state(pid, _timeout) do | ||
send(pid, :received_new_state) | ||
end | ||
end | ||
|
||
@moduletag capture_log: true | ||
@opts [ | ||
bucket_arn: "mbta-gtfs-boom-shakalaka", | ||
object: "objection", | ||
state: __MODULE__.StateModule, | ||
interval: 1_000 | ||
] | ||
|
||
describe "init/1" do | ||
test "fires a continue" do | ||
assert {:ok, _, {:continue, _}} = init(@opts) | ||
end | ||
|
||
test "builds an initial state" do | ||
assert {:ok, state, {:continue, _}} = init(@opts) | ||
assert %StateMediator.S3Mediator{} = state | ||
assert state.module == @opts[:state] | ||
assert state.bucket_arn == @opts[:bucket_arn] | ||
assert state.sync_timeout == 5_000 | ||
assert state.interval == 1_000 | ||
end | ||
end | ||
|
||
describe "handle_info/2" do | ||
test "on body: schedules an update" do | ||
{:ok, state, {:continue, _}} = init(@opts) | ||
assert {:noreply, ^state, 1_000} = handle_info(:timeout, state) | ||
end | ||
|
||
test "on error: schedules an update" do | ||
{:ok, state, {:continue, _}} = init(@opts) | ||
Mox.defmock(FakeAws, for: ExAws.Behaviour) | ||
|
||
test_pid = self() | ||
monitor_pid = GenServer.whereis(StateMediator.S3Mediator) | ||
allow(FakeAws, test_pid, monitor_pid) | ||
stub(FakeAws, :request, fn _ -> {:error, %{body: "your transit isn't rapid enough"}} end) | ||
|
||
assert {:noreply, ^state, 1_000} = handle_info(:timeout, state) | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters