(整理)用Elixir做一个多人扑克游戏 3

原创
2016/10/05 21:07
阅读数 296

今天我们将为德州扑克游戏添加故障恢复能力。

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 中。

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部