LoginSignup
7
8

More than 5 years have passed since last update.

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

Posted at

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.
+++++++++++++
7
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
8