今天我们将为德州扑克游戏添加故障恢复能力。
OTP为我们准备好了构建容错程序所需要的工具。我们只需要定义正确的behavior 行为。
Supervisor
有了Supervisor,我们就只需要关心当进程崩溃时如何反应。首先,我们使用顶层的Supervisor——Application:
defmodule GenPoker do
use Application
def start(_type, _args) do
import Supervisor.Spec
children = [
worker(Poker.Bank, [])
]
opts = [strategy: :one_for_one, name: GenPoker.Supervisor]
Supervisor.start_link(children, opts)
end
end
在mix.exs中注册我们的应用模块:
def application do
[mod: {GenPoker, []}]
end
当工作中的进程崩溃后,会新建一个新的进程,打开 iex -S mix 测试一下:
iex(1)> Process.whereis(Poker.Bank)
#PID<0.93.0>
iex(2)> Process.whereis(Poker.Bank) |> Process.exit(:kill)
true
iex(3)> Process.whereis(Poker.Bank)
#PID<0.97.0>
The Table Supervisor
我们可以把牌桌和牌局进程放在同一个Supervisor下:
defmodule Poker.Table.Supervisor do
use Supervisor
def start_link(table_name, num_players) do
Supervisor.start_link(__MODULE__, [table_name, num_players])
end
def init([table_name, num_players]) do
children = [
worker(Poker.Table, [table_name, num_players])
]
supervise children, strategy: :one_for_one
end
end
把这个Supervisor添加到顶层的Supervisor下:
def start(_type, _args) do
import Supervisor.Spec
children = [
worker(Poker.Bank, []),
supervisor(Poker.Table.Supervisor, [:table_one, 6])
]
opts = [strategy: :one_for_one, name: GenPoker.Supervisor]
Supervisor.start_link(children, opts)
end
添加hand 牌局
我们不希望牌局在玩家准备好之前自动启动,也不希望在牌局结束之后重启。首先,向 Table Supervisor 中添加一个函数:
def start_hand(supervisor, table, players, config \\ []) do
Supervisor.start_child(supervisor,
supervisor(Poker.Hand.Supervisor,
[table, players, config], restart: :transient, id: :hand_sup
)
)
end
我们使用了 transient 暂时策略,也就是它不会在普通的退出之后被重启。子进程是牌局的Supervisor:
defmodule Poker.Hand.Supervisor do
use Supervisor
def start_link(table, players, config) do
Supervisor.start_link(__MODULE__, [table, players, config])
end
def init([table, players, config]) do
hand_name = String.to_atom("#{table}_hand")
children = [
worker(Poker.Hand, [table, players, config, [name: hand_name]], restart: :transient)
]
supervise children, strategy: :one_for_one
end
end
之后我们会解释多加这一层Supervisor的原因。我们需要对Table Supervisor的init稍作修改:
def init([table_name, num_players]) do
children = [
worker(Poker.Table, [self, table_name, num_players])
]
supervise children, strategy: :one_for_one
end
以及对deal 发牌消息的 handle_call:
def handle_call(:deal, _from, state = %{hand_sup: nil}) do
players = get_players(state) |> Enum.map(&(&1.id))
case Poker.Table.Supervisor.start_hand(
state.sup, state.table_name, players
) do
{:ok, hand_sup} ->
Process.monitor(hand_sup)
{:reply, {:ok, hand_sup}, %{state | hand_sup: hand_sup}}
error ->
{:reply, error, state}
end
end
我们在收到deal消息后启动hand牌局,并使用之前创建的Hand Supervisor 来监控。
现在我们的Supervisor 树已经有了雏形,但我们的state 状态信息无法保存,它会在进程崩溃时消失。所以我们需要 ETS 来保存state。当崩溃次数达到一定限度,Supervisor就会放弃,并由上一级Supervisor来重启。
下一篇中,我们将把已有的程序导入Phoenix Channel 中。