まえがき
今まで作ったトランザクションデータベースを操作するクライアントプログラムを作成し、実行してみます。
動作確認レベルのクライアント
-module(client_query_exec).
-compile(export_all).
exec() ->
%% start supervisor of transaction_db including query_exec_sup.
sup:start_link(),
%% start query_exec process and get its Pid.
{ok, Pid} = gen_connection:connect(),
Price1 = 999,
Price2 = 100,
io:format("ClientPid: ~p, QueryExecutorPid: ~p~n", [self(), Pid]),
query_exec:exec_query(Pid, {create_table, fruit, [name, price]}),
%% Tx1
Txid = query_exec:exec_query(Pid, {begin_tx}),
query_exec:exec_query(Pid, {insert, fruit, [apple, Price1]}),
query_exec:exec_query(Pid, {insert, fruit, [apple, Price2]}),
io:format("[Sel1]ClientPid: ~p, SelectData: ~p~n", [self(), query_exec:exec_query(Pid, {select, fruit, name, apple})]),
query_exec:exec_query(Pid, {update, fruit, [{price, 150}], price, Price1}),
query_exec:exec_query(Pid, {update, fruit, [{name, banana}], price, 150}),
io:format("[Sel2]ClientPid: ~p, SelectData: ~p~n", [self(), query_exec:exec_query(Pid, {select, fruit, name, banana})]),
query_exec:exec_query(Pid, {delete, fruit, name, banana}),
io:format("Client Txid: ~p~n", [Txid]),
io:format("[Sel3]ClientPid: ~p, SelectData: ~p~n", [self(), query_exec:exec_query(Pid, {select, fruit, name, banana})]),
query_exec:exec_query(Pid, {commit_tx}),
%% Tx2
query_exec:exec_query(Pid, {begin_tx}),
io:format("[Sel4]ClientPid: ~p, SelectData: ~p~n", [self(), query_exec:exec_query(Pid, {select, fruit, name, banana})]),
io:format("[Sel5]ClientPid: ~p, SelectData: ~p~n", [self(), query_exec:exec_query(Pid, {select, fruit, name, apple})]),
query_exec:exec_query(Pid, {commit_tx}),
query_exec:exec_query(Pid, {drop_table, fruit}).
テーブルを作って、DMLをいくつか実行します。
1つ目のトランザクションでは
begin_tx→insert→insert→select→update→update→select→delete→select→commit_tx
と進む。2つ目のトランザクションでは
begin_tx→select→commit_tx
を行う。
2つ目のトランザクションで[apple, 100]というデータが取得できれば、1つめのトランザクションで実行した結果をきちんとコミットできていそうという感じである。
トランザクションの同時実行テストクライアント
続いて、複数のトランザクションを並行に実行して、それらの実行がシリアライザブルな分離レベルで、シーケンシャルに実行できることを確認してみる。
-module(client_query_tx_perf).
-compile(export_all).
exec() ->
Count = 2000,
%% start supervisor of transaction_db including query_exec_sup.
sup:start_link(),
{ok, Pid} = gen_connection:connect(),
query_exec:exec_query(Pid, {create_table, fruit, [name, price]}),
query_exec:exec_query(Pid, {begin_tx}),
query_exec:exec_query(Pid, {insert, fruit, [apple, 0]}),
query_exec:exec_query(Pid, {commit_tx}),
ok = loop(fun() -> child() end, Count),
timer:sleep(100),
query_exec:exec_query(Pid, {begin_tx}),
[[apple, Price]] = query_exec:exec_query(Pid, {select, fruit, name, apple}),
io:format("Final Price: ~p~n", [Price]),
query_exec:exec_query(Pid, {commit_tx}),
query_exec:exec_query(Pid, {drop_table, fruit}).
child() ->
%% start query_exec process and get its Pid.
{ok, Pid} = gen_connection:connect(),
query_exec:exec_query(Pid, {begin_tx}),
[[apple, Price]] = query_exec:exec_query(Pid, {select, fruit, name, apple}),
query_exec:exec_query(Pid, {update, fruit, [{price, Price + 1}], price, Price}),
query_exec:exec_query(Pid, {commit_tx}).
loop(_Fun, 0) ->
ok;
loop(Fun, Count) ->
spawn_link(Fun),
loop(Fun, Count - 1).
exec関数の中で、child関数を別プロセスで並列に実行する。各child関数は、fruitテーブルにあるappleの値段を1円だけ値上げする。すなわち、begin_tx→select→update→commit_txをする。
appleの初期値段は0円なので、もし、トランザクションがそれぞれシリアライザブルに実行されるとしたら、child関数が呼ばれた回数とappleの値段が等しくなるはずである。
実行するプロセスの数を2000に設定して実行してみると1秒弱で終わった。結果は問題なく各プロセスの実行ごとにトランザクションが確立され、実行ごとに1円ずつ値上げしていき、最終的には2000円になることが確かめられた。
全プロセスの起動自体は100ms以内に終わっていたので、シーケンシャルに実行された結果、それぞれのプロセスが自分の順番をwaitingさせられたこともわかる。
ちなみに、プロセスの起動数を10000にすると15秒ぐらいかかった。おそらくETSのルックアップなどに時間がかかっているのだろう。
あとがき
さて、クライアントプログラムを作成して実行してみました。
これで、シリアライザブルなトランザクションデータベースは完成したので、その振り返りをした後に、耐障害性と同時実行性を上げる方法を考えてみます。