elixir 高可用系列(二) GenServer

wang_yb發表於2016-05-15

概述

如果我們需要管理多個程式,那麼,就需要一個專門的 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: 這個函式接受非同步訊息並處理

處理這3個常用的函式之外,GenServer 中的函式也不是很多,其他的函式,屬性以及每個函式返回的值說明請參見:http://elixir-lang.org/docs/stable/elixir/GenServer.html

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

來源:http://blog.iotalabs.io/

相關文章