- Basic Approach without Elixir/OTP - Anatomy of an Elixir Process (optional) - Fault Tolerance Systems with Supervision Tree Strategies (optional) - Approach 1 / with Supervision Tree + GenServer - Approach 2 / with Supervision Tree + GenServer + Partial Pooling - Approach 3 / with Supervision Tree + GenServer + Pooling - Metrics - Source Codes - Questions
:: {:ok, binary} | charlist @callback write(charlist, binary) :: {:ok, binary} end defmodule ImgOut.StorageInterface do @callback read({:ok, binary}) @callback read({:error, integer, map}) @callback read(charlist) end defmodule ImgOut.ImageInterface do @callback thumb({:error, integer, map}, any) @callback thumb({:ok, binary}, map) end defmodule MicroserviceController do def generate_thumb(id, %{} = dimensions) do id |> Cache.read |> Storage.read |> Image.thumb(dimensions) end end defmodule AlternativeApproachController do def generate_thumb(id, %{} = dimensions) do id |> Storage.read |> Image.thumb(dimensions) end end
{:ok, binary} | charlist @callback write(charlist, binary) :: {:ok, binary} end defmodule ImgOut.CacheService do @behaviour ImgOut.CacheInterface def read(key) do response = Memcache.Client.get(key) case response.status do :ok -> {:ok, response.value} _ -> key end end def write(key, val) do Memcache.Client.set(key, val) {:ok, val} end end
cache (w) Task.start(..) defmodule ImgOut.CacheService do ... # way 1: to make async write to cache def write(key, val) do Task.start(fn -> Memcache.Client.set(key, val) end) {:ok, val} end end
W 1 S W 2 W 3 W 1 S W 2 W 3 W 1 if one worker dies supervisor respawns all killed down signal restart all killed workers supervisor kills rest of the workers with start order (not W1) Note: Assumed start order of workers are like W1, W2, W3 and W4 W 4 W 4 W 4 S W 3 W 1 W 4 W 4
implement Supervision.Spec - You need to specify only one entry for a child - Every child spawned by this strategy is same kind of process, can not be mixed.
Str (S) Ch (S) Str (w) Im (w) Srv (w) Ch (w) defmodule ImgOut do use Application def start(_type, _args) do import Supervisor.Spec, warn: false children = [ supervisor(ImgOut.WebServerSupervisor, []), supervisor(ImgOut.ImageSupervisor, []), supervisor(ImgOut.StorageSupervisor, []) ] opts = [strategy: :one_for_one, name: ImgOut.Supervisor] Supervisor.start_link(children, opts) end end
GenServer.start_link(__MODULE__, [], name: __MODULE__) … def thumb({:ok, img}, dimensions), do: GenServer.call(__MODULE__, {:thumb, {:ok, img}, dimensions}) … def handle_call({:thumb, {:ok, img}, dimensions}, _from, state) do data = ImgOut.ImageService.thumb({:ok, img}, dimensions) {:reply, data, state} end … Cons: Spawn 1 Process Per GenServer Pros: No need to store pid to call process funcs
more workers on GenServer: - Process Registry - We can save pid on creation and deregister on exit - Too manual work - Reinventing wheels(special for this service) - Gproc Package - Supports pooling - Has advanced techniques to register, deregister, monitor etc... - Poolboy Package - Supports pooling - Easy
timeouts Spawned Multiple ImageWorker(s) with Poolboy - Process Registry handled by poolboy - We can change max, min spawned processes thumb thumb thumb
storage + cache thumb no timeouts Since we solved the timeout problem for Thumbnail processor - Now storage and cache worker getting too many request - but not processing that fast with 1 instance! thumb thumb thumb timeouts
more workers on GenServer: - Process Registry - We can save pid on creation and deregister on exit - Too manual work - Reinventing wheels(special for this service) - Gproc Package - Supports pooling - Has advanced techniques to register, deregister, monitor etc... - Poolboy Package - Supports pooling - Easy
- If you hit connection limit, you can’t get positive response Storage - Remote storage servers might have some limitation on requests - Req/per second - If you hit more, you will get error from remote