LoginSignup
136
134

More than 5 years have passed since last update.

Elixir のプロセスを使ってフェイルセーフなアプリケーションを作る ─ 失敗は恐れず泥水にダイブ

Last updated at Posted at 2015-08-01

[] Elixir   Elixir 

   ×  ! Elixir  (調) 

 Supevisor 使


:  HTTP GET





 URL  HTML  title 

 URL  HTTP 




URL  title  HashDict 

: HTTP


 URL  HTML  title 
lib/web_archive/fetcher.ex
defmodule WebArchive.Fetcher do
  def fetch(url) do
    HTTPotion.start
    HTTPotion.get(url)
  end

  def process_response(%{status_code: 200, body: body}) do
    body
    |> to_string
  end

  def extract_title(url) do
    [{"title", [], [title]}] = fetch(url)
    |> process_response
    |> Floki.find "title"
    title
  end
end

HTTP クライアントには HTTPotion、HTML の parse に Floki を使った。

iex> WebArchive.Fetcher.extract_title("https://twitter.com/")
"Twitterへようこそ - ログインまたは新規登録"

このように、extract_title/1 で twitter.com の title 文字列が返ってくる。

泥沼に構わず突っ込む

さて、このコードで特徴的なのは正常系のことしか気にしてないところである。

  def process_response(%{status_code: 200, body: body}) do
    body
    |> to_string
  end

例えば HTTP GET 後の処理は process_response/1 に渡すが、パターンマッチでステータスコード 200 の時しかみてない。従って 200 以外が返ってくると、FunctionClauseError でクラッシュする。

iex(5)> WebArchive.Fetcher.extract_title("http://example.com/404")
** (FunctionClauseError) no function clause matching in WebArchive.Fetcher.process_response/1

 url  :cat  URL  HTTPotion  HTML  Floki HTML  title 



Supervisor 

 Supervisor 





iex(5)> WebArchive.Server.extract_title("https://twitter.com/") # HTTP アクセス
"Twitterへようこそ - ログインまたは新規登録"
iex(6)> WebArchive.Server.extract_title("https://twitter.com/") # キャッシュから返る
"Twitterへようこそ - ログインまたは新規登録"



 GenServer 使GenServer  Elixir  OTP (GenServer )  Supervisor  HTTP GET 

!!



  URL GenServer  HashDict 
iex(12)> WebArchive.Server.extract_title(:cat) # クラッシュ!
10:18:04.900 [error] GenServer WebArchive.Server terminating
Last message: {:extract_title, :cat}
State: #PID<0.153.0>
...
iex(13)> WebArchive.Server.extract_title("https://twitter.com/") # HTTP GET してしまう
"Twitterへようこそ - ログインまたは新規登録"




Supervison Tree 


 Stash 

Supervisor 

4f82f5998c4d538b44579ddcab5b000e2b1137d4.png
 Server  Stash !



Stash (Agent)


Stash  GenServer HashDict OTP  Agent 使
lib/web_archive/stash.ex
defmodule WebArchive.Stash do
  def start_link do
    Agent.start_link(fn -> HashDict.new end)
  end

  def save_title(pid, url, title) do
    Agent.update pid, fn(dict) -> Dict.put(dict, url, title) end
  end

  def get_title(pid, url) do
    Agent.get pid, fn(dict) -> dict[url] end
  end
end

 Agent  API  Stash Stash  Agent 使

Server (GenServer)


Server  Fetcher  Stash 使 extract_title/1 

 HashDict  Stash  Stash  pid 

(1)  extract_title/1  (2) (1) (2)  GenServer 使Stash  pid  GenServer 使
lib/web_archive/server.ex
defmodule WebArchive.Server do
  use GenServer

  def start_link(stash_pid) do
    GenServer.start_link(__MODULE__, stash_pid, name: __MODULE__)
  end

  def extract_title(url) do
    GenServer.call __MODULE__, {:extract_title, url}
  end

  def handle_call({:extract_title, url}, _from, stash_pid) do
    {:reply, fetch_title(stash_pid, url), stash_pid}
  end

  defp fetch_title(stash_pid, url) do
    case WebArchive.Stash.get_title(stash_pid, url) do
      nil   ->
        title = WebArchive.Fetcher.extract_title(url)
        :ok = WebArchive.Stash.save_title(stash_pid, url, title)
        title
      title -> title
    end
  end
end

fetch_title/2  Fetcher  Stash 使Stash  HTTP GET

Supervisor


 Stash  Server Supervisor Programming Elixir

SubSupervisor  Stash  pid Stash  SubSupervisor  supervise/3  start_child/3 
lib/web_archive/supervisor.ex
defmodule WebArchive.Supervisor do
  use Supervisor

  def start_link do
    res = {:ok, sup} = Supervisor.start_link(__MODULE__, [])
    {:ok, stash_pid} = 
      Supervisor.start_child(sup, worker(WebArchive.Stash, []))
    Supervisor.start_child(sup, supervisor(WebArchive.SubSupervisor, [stash_pid]))
    res
  end

  def init(_) do
    supervise [], strategy: :one_for_one
  end
end

サブの Supervisor の方は何の変哲もない。

lib/web_archive/subsupervisor.ex
defmodule WebArchive.SubSupervisor do
  use Supervisor

  def start_link(stash_pid) do
    Supervisor.start_link(__MODULE__, stash_pid)
  end

  def init(stash_pid) do
    supervise [ worker(WebArchive.Server, [stash_pid]) ], strategy: :one_for_one
  end
end

Application

これで部品は揃ったので、アプリケーションのエントリポイントを書く。

lib/web_archive.ex
defmodule WebArchive do
  use Application
  def start(_type, _args) do
    WebArchive.Supervisor.start_link
  end
end

親の Supervisor を起動するだけですな。

これで親 Supervisor → Stash → SubSupervisor → Server の順に起動する。完成。

iex(15)> WebArchive.Server.extract_title("https://twitter.com/")
"Twitterへようこそ - ログインまたは新規登録"
iex(16)> WebArchive.Server.extract_title(:cat)
** (exit) exited in: GenServer.call(WebArchive.Server, {:extract_title, :cat}, 5000)
    ** (EXIT) an exception was raised:
    ...
iex(17)> WebArchive.Server.extract_title("https://twitter.com/")
"Twitterへようこそ - ログインまたは新規登録"

クラッシュさせてもアプリケーションは終了せず、また、クラッシュ後の extract_title/1 もキャッシュから返ってくる。ほんとにそうなってるか知りたい場合は extract_title/1 で HTTP GET してくるところにデバッグメッセージを追加すれば良いだろう。

おまけ: Task の async/await で並行処理

せっかくキャッシュ付きで HTTP リクエストを飛ばす仕組みを作ったので、複数のサイトからまとめてタイトルを取ってくる API も作ってみよう。

iex> WebArchive.Server.extract_titles(["https://twitter.com/", "https://github.com/", "https://kaizenplatform.com/"])
["Twitterへようこそ - ログインまたは新規登録",
 "GitHub · Build software better, together.",
 "Kaizen Platform, Inc."]



 URL 

OTP  Task  async/await 使
lib/web_archive/server.ex
defmodule WebArchive.Server
  use GenServer

  ...

  def extract_titles(urls) when is_list(urls) do
    urls
    |> Enum.map(&(Task.async(fn -> extract_title(&1) end)))
    |> Enum.map(&(Task.await/1))
  end

!

async/await  C#  API Elixir  async/await 



Elixir  Supervisor 

 Supervision Tree 

 OTP 使GenServerAgent etc.

Task 使


 Elixir 
136
134
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up

136
134