Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 27 additions & 16 deletions upgrade_host/lib/upgrade_host/loans.ex
Original file line number Diff line number Diff line change
Expand Up @@ -91,31 +91,42 @@ defmodule UpgradeHost.Loans do

The document's steps write no invoke id, so each is generated as
`<state id>.<suffix>` under the state its `<invoke>` sits in. That state
maps back to its block through the compile's provenance, the block's
type to its module through the palette, and the module's invoke type to
the handler. Anything else is `:error`.
maps back to its block through the compile's provenance, and the block
to its handler through the step handler map. Anything else is `:error`.

The step handler map is built once per node from the document and the
palette and kept beside the compile, so a cancel does not author the
document again.
"""
@spec handler_for_invocation(String.t()) :: {:ok, module()} | :error
def handler_for_invocation(invoke_id) when is_binary(invoke_id) do
[state_id | _suffix] = String.split(invoke_id, ".", parts: 2)
%Compiled{provenance: provenance} = compiled()

with {:ok, %{block_id: block_id}} <- Provenance.owner_of_state(provenance, state_id),
{:ok, type} <- block_type(block_id),
{:ok, module} when is_atom(module) <- Palette.fetch(Blocks.palette(), type),
true <- function_exported?(module, :invoke_type, 0) do
Map.fetch(@invoke_handlers, module.invoke_type())
else
_not_a_step -> :error
with {:ok, %{block_id: block_id}} <- Provenance.owner_of_state(provenance, state_id) do
Map.fetch(step_handlers(), block_id)
end
end

@spec block_type(String.t()) :: {:ok, String.t()} | :error
defp block_type(block_id) do
case Enum.find(Document.blocks(LoanDocument.document()), &(&1.id == block_id)) do
nil -> :error
block -> {:ok, block.type}
end
# The handler serving each invoke step of the loan document, by the
# step's block id: the block's type to its module through the palette,
# and the module's invoke type to the handler. A block that is not an
# invoke step, or whose invoke type no handler serves, is absent. Each
# module is loaded before it is asked, because the map is kept and a
# miss would be kept with it.
@spec step_handlers() :: %{String.t() => module()}
defp step_handlers do
cached(:step_handlers, fn ->
palette = Blocks.palette()

for block <- Document.blocks(LoanDocument.document()),
{:ok, module} when is_atom(module) <- [Palette.fetch(palette, block.type)],
Code.ensure_loaded?(module),
function_exported?(module, :invoke_type, 0),
{:ok, handler} <- [Map.fetch(@invoke_handlers, module.invoke_type())],
into: %{},
do: {block.id, handler}
end)
end

@spec cached(atom(), (-> value)) :: value when value: term()
Expand Down
39 changes: 39 additions & 0 deletions upgrade_host/test/upgrade_host/loan_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,32 @@ defmodule UpgradeHost.LoanTest do
end
end

describe "the handler lookup" do
# sabotage: the lookup built the step handler map from the document on
# every call and kept nothing -> red, no map was kept for the node.
# sabotage: the lookup dropped the kept map and built it again on every
# call -> red, the fine's invocation named AssessFine over the kept map.
test "reads each step's handler from a map kept once per node, not from the document" do
key = {Loans, :step_handlers}
fine = LoanDocument.block_id(:fine)
notice = LoanDocument.block_id(:notice)
fine_invocation = state_of(:fine) <> ".inv_1"
notice_invocation = state_of(:notice) <> ".inv_1"

assert {:ok, AssessFine} = Loans.handler_for_invocation(fine_invocation)
assert :persistent_term.get(key, nil) == %{fine => AssessFine, notice => NotifyPatron}

# The lookup answers from the kept map: a map that says otherwise is
# what it answers, without the document being read again.
kept = :persistent_term.get(key)
on_exit(fn -> :persistent_term.put(key, kept) end)
:persistent_term.put(key, %{fine => NotifyPatron})

assert {:ok, NotifyPatron} = Loans.handler_for_invocation(fine_invocation)
assert :error = Loans.handler_for_invocation(notice_invocation)
end
end

describe "a loan" do
# sabotage: the executor's cancel clause returned :ok without cancelling
# -> red, two scheduled timers after the renewal.
Expand Down Expand Up @@ -237,6 +263,19 @@ defmodule UpgradeHost.LoanTest do
|> Enum.sort()
end

# The state the block `name` emitted as its own, read from the compile's
# provenance.
defp state_of(name) do
block_id = LoanDocument.block_id(name)

[state_id] =
for {state_id, %{block_id: ^block_id, role: nil}} <-
Loans.compiled().provenance.by_state_id,
do: state_id

state_id
end

# The one live invocation of a loan, by its generated invoke id.
defp invocation(loan_id) do
[invoke_id] = loan_id |> position() |> Map.fetch!(:active_invocations) |> Map.values()
Expand Down
Loading