1. 程式人生 > >elixir 高可用系列(二) GenServer

elixir 高可用系列(二) GenServer

概述

如果我們需要管理多個程序,那麼,就需要一個專門的 server 來集中監控和控制這些程序的狀態,啟停等。
OTP 平臺中的 GenServer 就是對這個 server 通用部分的抽象。

利用 GenServer 中已經提供的通用操作, 可以很方便的開發出可靠,健壯的程式。
下面首先通過一個示例演示 GenServer 的方便和強大之處,然後再對其進行介紹。

GenServer 示例

這是一個 GenServer 管理多個程序的示例,模擬控制各個程序的啟動,停止,以及狀態查詢。

defmodule ProcessMonitor do
  use GenServer

  #====================================================
  # api for clients
  #====================================================
  # start GenServer
  def start(data, opt \\ []) do
    GenServer.start_link(__MODULE__, data, opt)
  end

  # add process which is controled by this GenServer
  def process_add(server, name) do
    GenServer.call(server, {:add, name})
  end

  # get process status
  def process_status(server, name) do
    GenServer.call(server, {:status, name})
  end

  # start a process by name
  def process_start(server, name) do
    GenServer.cast(server, {:start, name})
  end

  # stop a process by name
  def process_stop(server, name) do
    GenServer.cast(server, {:stop, name})
  end

  #====================================================
  # callbacks for server
  #====================================================
  def init(data) do
    {:ok, data}
  end

  # handle status message synchronization
  def handle_call({:status, name}, _from, data) do
    val = Map.get(data, name, nil)
    {:reply, val, data}
  end

  # handle add message synchronization
  def handle_call({:add, name}, _from, data) do
    data = Map.put(data, name, "stopped")
    {:reply, name, data}
  end

  # handle start message asynchronization
  def handle_cast({:start, name}, data) do
    data = Map.put(data, name, "running")
    {:noreply, data}
  end

  # handle stop message asynchronization
  def handle_cast({:stop, name}, data) do
    data = Map.put(data, name, "stopped")
    {:noreply, data}
  end

end

上面程式碼測試方法如下:

$ iex -S mix
Erlang/OTP 18 [erts-7.3] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]

Interactive Elixir (1.2.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> {:ok, server} = ProcessMonitor.start(Map.new)               # 建立 GenServer,並初始化一個 map 用於儲存此server管理的 process 資訊
{:ok, #PID<0.87.0>}
iex(2)> ProcessMonitor.process_status(server, "process01")          # 建立 GenServer 後,預設沒有管理任何程序,所以沒有 process01 的資訊
nil
iex(3)> ProcessMonitor.process_add(server, "process01")             # 給 GenServer 增加一個被管理程序 process01
"process01"
iex(4)> ProcessMonitor.process_status(server, "process01")          # 新加入的程序預設狀態是 stopped,示例程式碼預設這麼實現
"stopped"
iex(5)> ProcessMonitor.process_start(server, "process01")           # 啟動 process01
:ok
iex(6)> ProcessMonitor.process_status(server, "process01")          # process01 狀態變為 running
"running"
iex(7)> ProcessMonitor.process_stop(server, "process01")            # 停止 process01
:ok
iex(8)> ProcessMonitor.process_status(server, "process01")          # process01 狀態變為 stopped
"stopped"
iex(9)> ProcessMonitor.process_add(server, "process02")             # 再增加一個被管理程序 process02
"process02"
iex(10)> ProcessMonitor.process_start(server, "process02")          # 啟動 process02
:ok
iex(11)> ProcessMonitor.process_status(server, "process02")         # process02 狀態變為 running
"running"
iex(12)> ProcessMonitor.process_status(server, "process01")         # process01 狀態仍然是 stopped,不受 process02 的影響
"stopped"
iex(13)> ProcessMonitor.stop(server)                                # 停止 GenServer

上面的程式碼是用 mix 建立工程來執行的,mix 的使用方法可以參見 blog:mix 構建工具

GenServer 通用抽象簡介

示例程式碼使用了 GenServer 中的幾個關鍵函式: init handle_call handle_case

  • init: 這個函式在 GenServer.start_link 時執行,對 start_link 中的引數進行處理
  • handle_call: 這個函式接受同步訊息並處理
  • handle_cast: 這個函式接受非同步訊息並處理

在上面的示例中,其實 client 也可以直接呼叫 GenServer 的 handle_call/handle_cast 來發送同步/非同步訊息,
我之所以封裝了一些 api 給 client 呼叫,一方面,是為了簡化客戶端的呼叫(client 的 api 中引數更加簡潔直觀),
另一方面,將處理訊息的程式碼和 傳送訊息的程式碼分開,便於以後擴充套件(因為,可能存在多個傳送訊息的處理都對應同一個訊息處理)。