Erlang gen_server テスト駆動開発でKey-Valueストアを作る

  • 7
    Like
  • 0
    Comment
More than 1 year has passed since last update.

Erlangのサーバを簡単に実装できるOPTのgen_server ビヘイビア(behaviour)を利用して、Key-Valueストアを実装してみました。set, get, delete命令しか実装していません。

なぜ gen_serverでサーバを実装すべきなのか

実装が容易で本番稼働での実績が十分な点はもちろんですが、次のようなメリットが存在します。
名前付きプロセス、タイムアウトの設定、デバッグ情報の付与、予期しないメッセージの処理、ホットコードローディング、特定エラーへの対処、様々なサーバシャットダウン手法への対応、スーパバイザ対応。

1. サーバ起動

最低限の部品だけ実装してサーバを稼働させます。動くだけで命令できない状態

tests.erl
-module(tests).
-include_lib("eunit/include/eunit.hrl").
-author("hami").

%% API
-export([test/0]).

test() ->
  server_test().

server_test() ->
  kvs_server:start().

kvs_server.erl
-module(kvs_server).
-author("hami").
-behaviour(gen_server).

%% API
-export([start/0]).
-export([init/1]).

%%
start() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

%% genserver実装用
init([]) ->
  Storage = dict:new(),
  {ok, Storage}.

make.sh
# rm
rm -rf ./*.beam

# コンパイル
echo "+++++++++++++"
erlc ./tests.erl
erlc ./kvs_server.erl
echo "+++++++++++++"

# list
ls -lta|grep beam
echo "+++++++++++++"

# 実行
erl -noshell -pa ebin -eval "eunit:test(tests, [verbose])" -s init stop
echo "+++++++++++++"

実行結果
>>> sh ./make.sh 
+++++++++++++
kvs_server.erl:3: Warning: undefined callback function code_change/3 (behaviour 'gen_server')
kvs_server.erl:3: Warning: undefined callback function handle_call/3 (behaviour 'gen_server')
kvs_server.erl:3: Warning: undefined callback function handle_cast/2 (behaviour 'gen_server')
kvs_server.erl:3: Warning: undefined callback function handle_info/2 (behaviour 'gen_server')
kvs_server.erl:3: Warning: undefined callback function terminate/2 (behaviour 'gen_server')
+++++++++++++
======================== EUnit ========================
tests: server_test (module 'tests')...ok
=======================================================
  Test passed.
+++++++++++++

1-2. Warning対策

コンパイル時のWarning対策に必要な関数を追加します。

kvs_server.erl
%% 利用しないが、コンパイル時にwarning出るので実装
handle_cast(_Message, Storage) -> {noreply, Storage}.
handle_info(_Message, Storage) -> {noreply, Storage}.
terminate(_Reason, _Storage) -> ok.
code_change(_OldVersion, Storage, _Extra) -> {ok, Storage}.

2. set関数の実装

kvs_server.erl
%% API
-export([start/0, set/2]).

%% サーバ操作コマンド
start() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
set(Key, Value) -> gen_server:call(?MODULE, {set, Key, Value}).

%% コールバックの実装
handle_call({set, Key, Value}, _From, Storage) ->
  NewStorage = dict:append(Key, Value, Storage),
  {reply, ok, NewStorage}.

tests.erl
%% set関数のテスト
set_test() ->
  %% setと上書きset
  kvs_server:set(key1, 10),
  kvs_server:set(key1, 1),
  kvs_server:set(key2, 20),
  kvs_server:set(key3, thirty),
  kvs_server:set(key4, fourty),
  %% set時の応答がokであること
  ?assertEqual(ok, kvs_server:set(key5, 50)).

3. get関数の実装

set / get関数を実装すると、いよいよKVSっぽくなってきます。

kvs_server.erl
%% サーバ操作関数
get(Key) -> gen_server:call(?MODULE, {get, Key}).

%% コールバックの実装
handle_call({get, Key}, _From, Storage) ->
  case dict:is_key(Key, Storage) of
    true ->
      %% Keyが存在する
      Value = dict:fetch(Key, Storage),
      {reply, Value, Storage};
    false ->
      %% Keyが存在しない
      {reply, none, Storage}
  end.

tests.erl

%% get/set関数のテスト
set_get_test() ->
  %% setとget list型
  Value1 = "aaaa",
  kvs_server:set(key1, Value1),
  ?assertEqual(Value1, kvs_server:get(key1)),

  %% setとget int型
  Value2 = 10,
  kvs_server:set(key2, Value2),
  ?assertEqual(Value2, kvs_server:get(key2)),

  %% setとget atom
  Value3 = atommmm,
  kvs_server:set(key3, Value3),
  ?assertEqual(Value3, kvs_server:get(key3)),

  %% 上書き list型
  Value1_ = "bbb",
  kvs_server:set(key1, Value1_),
  ?assertEqual(Value1_, kvs_server:get(key1)),

  %% 上書き int型
  Value2_ = 600,
  kvs_server:set(key2, Value2_),
  ?assertEqual(Value2_, kvs_server:get(key2)),

  %% 存在しないkeyでget
  ?assertEqual(none, kvs_server:get(key99)),
  ok.

4. delete実装して完成

完成したKVSのコードは次の通りです。

kvs_server.erl
-module(kvs_server).
-author("hami").
-behaviour(gen_server).

%% API
-export([start/0, set/2, get/1, delete/1]).
-export([init/1, handle_call/3]).
-export([handle_cast/2, handle_info/2, terminate/2, code_change/3]).

%% サーバ操作関数
start() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
set(Key, Value) -> gen_server:call(?MODULE, {set, Key, Value}).
get(Key) -> gen_server:call(?MODULE, {get, Key}).
delete(Key) -> gen_server:call(?MODULE, {delete, Key}).

%% genserver実装用
init([]) ->
  Storage = dict:new(),
  {ok, Storage}.

%% コールバックの実装
handle_call({set, Key, Value}, _From, Storage) ->
  NewStorage = dict:store(Key, Value, Storage),
  {reply, ok, NewStorage};

handle_call({get, Key}, _From, Storage) ->
  case dict:is_key(Key, Storage) of
    true ->
      %% Keyが存在する
      Value = dict:fetch(Key, Storage),
      {reply, Value, Storage};
    false ->
      %% Keyが存在しない
      {reply, none, Storage}
  end;

handle_call({delete, Key}, _From, Storage) ->
  case dict:is_key(Key, Storage) of
    true ->
      %% Keyが存在する
      NewStorage = dict:erase(Key, Storage),
      {reply, ok, NewStorage};
    false ->
      %% Keyが存在しない
      {reply, ng, Storage}
  end.

%% 利用しないが、コンパイル時にwarning出るので実装
handle_cast(_Message, Storage) -> {noreply, Storage}.
handle_info(_Message, Storage) -> {noreply, Storage}.
terminate(_Reason, _Storage) -> ok.
code_change(_OldVersion, Storage, _Extra) -> {ok, Storage}.

tests.erl
-module(tests).
-include_lib("eunit/include/eunit.hrl").
-author("hami").

%% API
-export([test/0]).

%% テスト実行
%% erl -noshell -pa ebin -eval "eunit:test(test_module, [verbose])" -s init stop

test() ->
  server_test(),
  set_get_test().

%% サーバ起動テスト
server_test() ->
  kvs_server:start().

%% get/set関数のテスト
set_get_test() ->
  %% setとget list型
  Value1 = "aaaa",
  kvs_server:set(key1, Value1),
  ?assertEqual(Value1, kvs_server:get(key1)),

  %% setとget int型
  Value2 = 10,
  kvs_server:set(key2, Value2),
  ?assertEqual(Value2, kvs_server:get(key2)),

  %% setとget atom
  Value3 = atommmm,
  kvs_server:set(key3, Value3),
  ?assertEqual(Value3, kvs_server:get(key3)),

  %% 上書き list型
  Value1_ = "bbb",
  kvs_server:set(key1, Value1_),
  ?assertEqual(Value1_, kvs_server:get(key1)),

  %% 上書き int型
  Value2_ = 600,
  kvs_server:set(key2, Value2_),
  ?assertEqual(Value2_, kvs_server:get(key2)),

  %% 存在しないkeyでget
  ?assertEqual(none, kvs_server:get(key99)),
  ok.

%% delete関数のテスト
delete_test() ->
  %% 存在しないkeyはng
  ?assertEqual(ng, kvs_server:delete(key1001)),

  %% 存在するkeyは削除可能
  Value = aiue,
  ?assertEqual(ng, kvs_server:delete(key2001)),  %% deleteしたとき存在しないこと
  kvs_server:set(key2001, Value),
  ?assertEqual(Value, kvs_server:get(key2001)),  %% set後にgetしたとき正しい値が返ること
  ?assertEqual(ok, kvs_server:delete(key2001)),  %% delete後にokが返ること
  ?assertEqual(none, kvs_server:get(key2001)).   %% delete後にget時に値が存在しないこと

実行結果
>>> sh ./make.sh 
+++++++++++++
+++++++++++++
======================== EUnit ========================
module 'tests'
  tests: server_test...[0.001 s] ok
  tests: set_get_test...ok
  tests: delete_test...ok
  [done in 0.010 s]
=======================================================
  All 3 tests passed.
+++++++++++++