Mix.install([
{:reactor, "~> 0.15.5"},
{:kino, "~> 0.16.0"},
{:nanoid, "~> 2.1"}
])
defmodule HotelService do
require Logger
@failure_rate 0.2
def book() do
Logger.info("Booking hotel")
if :rand.uniform() < @failure_rate do
Logger.error("[#{__MODULE__}] failed to reserve hotel room")
{:error, "Failed to reserve room"}
else
{:ok, confirmation_code()}
end
end
def cancel(code) do
Logger.warning("[#{__MODULE__}] canceling hotel reservation #{code}")
:ok
end
defp confirmation_code() do
"HOTEL-#{Nanoid.generate()}"
end
end
defmodule FlightService do
require Logger
@failure_rate 0.05
def find_flight_number() do
Logger.info("Finding flight")
if :rand.uniform() < @failure_rate do
Logger.error("[#{__MODULE__}] failed to reserve flight")
{:error, "Failed to reserve flight"}
else
{:ok, "CS-2025"}
end
end
def book(flight_number) do
Logger.info("Booking flight, #{flight_number}")
if :rand.uniform() < @failure_rate do
Logger.error("[#{__MODULE__}] failed to reserve flight")
{:error, "Failed to reserve flight"}
else
{:ok, confirmation_code()}
end
end
def cancel(code) do
Logger.warning("[#{__MODULE__}] canceling flight reservation #{code}")
:ok
end
defp confirmation_code() do
"FLY-#{Nanoid.generate()}"
end
end
defmodule EmailService do
require Logger
@timeout_rate 0.5
def send(destination, message) do
Logger.info("Sending email to #{destination}: #{message}")
cond do
:rand.uniform() < @timeout_rate ->
:timer.sleep(1_000)
Logger.warning("[#{__MODULE__}] timeout")
{:error, :timeout}
String.contains?(destination, "example.net") ->
Logger.error("[#{__MODULE__}] bad destination address")
{:error, :bad_destination}
true ->
{:ok, :sent}
end
end
end
{:ok, flight_number} = FlightService.find_flight_number()
{:ok, flight_confirmation} = FlightService.book(flight_number)
{:ok, hotel_confirmation} = HotelService.book()
{:ok, :sent} = EmailService.send("[email protected]", """
Your hotel and flight have been booked!
Flight confirmation: #{flight_confirmation}
Hotel confirmation: #{hotel_confirmation}
""")
defmodule SimpleTripReactor do
use Reactor
input(:user_email)
step :find_flight_number do
run(fn _ -> FlightService.find_flight_number() end)
end
step :book_hotel do
run(fn _ ->
HotelService.book()
end)
end
step :book_flight do
argument(:flight_number, result(:find_flight_number))
run(fn %{flight_number: flight_number} ->
FlightService.book(flight_number)
end)
end
step :email_user do
argument(:user_email, input(:user_email))
argument(:hotel_code, result(:book_hotel))
argument(:flight_code, result(:book_flight))
run(fn inputs ->
message = """
Your hotel and flight have been booked!
Flight confirmation: #{inputs.flight_code}
Hotel confirmation: #{inputs.hotel_code}
"""
EmailService.send(inputs.user_email, message)
end)
end
collect :booking_results do
argument(:hotel_code, result(:book_hotel))
argument(:flight_number, result(:find_flight_number))
argument(:flight_code, result(:book_flight))
wait_for(:email_user)
end
return(:booking_results)
end
Reactor.Mermaid.to_mermaid!(SimpleTripReactor, output: :binary)
|> Kino.Mermaid.new()
defmodule BookFlightStep do
use Reactor.Step
@impl Reactor.Step
def run(%{flight_number: flight_number}, _context, _options) do
FlightService.book(flight_number)
end
@impl Reactor.Step
def undo(confirmation_code, _arguments, _context, _options) do
FlightService.cancel(confirmation_code)
end
end
defmodule TripReactor do
use Reactor
input(:user_email)
step :find_flight_number do
run(fn _ -> FlightService.find_flight_number() end)
end
step :book_hotel do
run(fn _ -> HotelService.book() end)
undo(fn confirmation -> HotelService.cancel(confirmation) end)
end
step :book_flight, BookFlightStep do
argument(:flight_number, result(:find_flight_number))
end
step :email_user do
argument(:user_email, input(:user_email))
argument(:hotel_code, result(:book_hotel))
argument(:flight_code, result(:book_flight))
max_retries(2)
run(fn inputs ->
message = """
Your hotel and flight have been booked!
Flight confirmation: #{inputs.flight_code}
Hotel confirmation: #{inputs.hotel_code}
"""
EmailService.send(inputs.user_email, message)
end)
compensate(fn
:bad_destination -> :ok
:timeout -> :retry
end)
end
collect :booking_results do
argument(:hotel_code, result(:book_hotel))
argument(:flight_number, result(:find_flight_number))
argument(:flight_code, result(:book_flight))
wait_for(:email_user)
end
return(:booking_results)
end
Reactor.Mermaid.to_mermaid!(TripReactor, output: :binary)
|> Kino.Mermaid.new()