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