- TCP経由で受け取ったメッセージをパースしてKVアプリケーションに送る処理を実装する。
Doctests
doctest
を使ってパースする関数を実装する。これはドキュメントからテストを作成することができ、ドキュメント内の正確なサンプルコードの提供を助けてくれる。lib/kv_server/command.ex
を新しく作ってみよう。
defmodule KVServer.Command do
@doc ~S"""
Parses the given `line` into a command.
## Examples
iex> KVServer.Command.parse "CREATE shopping\r\n"
{:ok, {:create, "shopping"}}
"""
def parse(line) do
:not_implemented
end
end
doctest
は iex>
のラインから始まる、スペース4文字分インデントが下がっているブロックのところで、コマンドと、その次の行にコマンドが返すであろう値を書く
doctest
を走らせてみよう。新しくテスト用のtest/kv_server/command_test.exs
を作成してmix test
する。
defmodule KVServer.CommandTest do
use ExUnit.Case, async: true
doctest KVServer.Command
end
- 実行すると以下のログが出て、
doctest
が実行されていることがわかる。
test/kv_server/command_test.exs:3
Doctest failed
code: KVServer.Command.parse "CREATE shopping\r\n" === {:ok, {:create, "shopping"}}
lhs: :not_implemented
stacktrace:
lib/kv_server/command.ex:11: KVServer.Command (module)
- では
parse/1
を実装してテストが通るようにしよう。stlingを受け取ってパターンマッチで"CREATE"だったら返すようにする。直した上でテストを走らせる。今度は通るはず。
def parse(line) do
case String.split(line) do
["CREATE", bucket] -> {:ok, {:create, bucket}}
end
end
iex> KVServer.Command.parse "CREATE shopping\r\n"
{:ok, {:create, "shopping"}}
iex> KVServer.Command.parse "CREATE shopping \r\n"
{:ok, {:create, "shopping"}}
iex> KVServer.Command.parse "PUT shopping milk 1\r\n"
{:ok, {:put, "shopping", "milk", "1"}}
iex> KVServer.Command.parse "GET shopping milk\r\n"
{:ok, {:get, "shopping", "milk"}}
iex> KVServer.Command.parse "DELETE shopping eggs\r\n"
{:ok, {:delete, "shopping", "eggs"}}
Unknown commands or commands with the wrong number of
arguments return an error:
iex> KVServer.Command.parse "UNKNOWN shopping eggs\r\n"
{:error, :unknown_command}
iex> KVServer.Command.parse "GET shopping\r\n"
{:error, :unknown_command}
def parse(line) do
case String.split(line) do
["CREATE", bucket] -> {:ok, {:create, bucket}}
["GET", bucket, key] -> {:ok, {:get, bucket, key}}
["PUT", bucket, key, value] -> {:ok, {:put, bucket, key, value}}
["DELETE", bucket, key] -> {:ok, {:delete, bucket, key}}
_ -> {:error, :unknown_command}
end
end
Pipelines
kv_server.ex
に作成したコマンドを組み込んでいこう。その前にcommand.ex
に以下の関数を追加する。
@doc """
Runs the given command.
"""
def run(command) do
{:ok, "OK\r\n"}
end
- そして
kv_server.ex
でクライアントからメッセージを受け取った際にコマンドを実行するようにする。
defp serve(socket) do
# 受け取ったメッセージを元にコマンドを実行する
msg =
case read_line(socket) do
{:ok, data} ->
case KVServer.Command.parse(data) do
{:ok, command} ->
KVServer.Command.run(command)
{:error, _} = err ->
err
end
{:error, _} = err ->
err
end
# コマンドの結果を出力
write_line(socket, msg)
serve(socket)
end
defp read_line(socket) do
:gen_tcp.recv(socket, 0)
end
defp write_line(socket, msg) do
:gen_tcp.send(socket, format_msg(msg))
end
defp format_msg({:ok, text}), do: text
defp format_msg({:error, :unknown_command}), do: "UNKNOWN COMMAND\r\n"
defp format_msg({:error, _}), do: "ERROR\r\n"
- ここまでできたら再び
mix run --no-halt
でサーバーを起動してtelnetで接続しよう。CREATEコマンドを打ったらOKが、未定義のコマンドを打ったらUNKNOWN COMMANDが返ってくるはず
CREATE shopping
OK
HELLO
UNKNOWN COMMAND
- ここで、ふたたびserveを見るとネストが深く見辛いコードになっているように感じる。できれば
|>
でつなげて書きたいが、コマンドが返す値は:ok
を含んだtupleかerror
を含んだtupleが返る可能性があるので返り値をうまくさばけない。{:ok, _}
が返ってくる限り、値をパイプで私続けるような機能が欲しい。
elixir-pipes
というモジュールがまさにあてはまる。mix.exs
を開いて追加しよう。
def application do
[applications: [:logger, :pipe, :kv],
mod: {KVServer, []}]
end
defp deps do
[{:kv, in_umbrella: true},
{:pipe, github: "batate/elixir-pipes"}]
end
- 保存したら
mix deps.get
して依存モジュールを取得しよう。そしてこれを使うことでserve/1
は以下のように書き直すことができる。
defp serve(socket) do
import Pipe
# pipe_matching/3は x, {:ok, x} で {:ok, value} が与えられている間はvalueを次の関数に渡し続けるように命令している
# マッチしない場合はマッチしなかった値を返す。
msg =
pipe_matching x, {:ok, x},
read_line(socket)
|> KVServer.Command.parse()
|> KVServer.Command.run()
write_line(socket, msg)
serve(socket)
end
Running commands
- 最後に
KVServer.Command.run/1
を実装してKVアプリケーションに命令を渡すようにしよう。
@doc """
Runs the given command.
"""
def run(command)
def run({:create, bucket}) do
KV.Registry.create(KV.Registry, bucket)
{:ok, "OK\r\n"}
end
def run({:get, bucket, key}) do
lookup bucket, fn pid ->
value = KV.Bucket.get(pid, key)
{:ok, "#{value}\r\nOK\r\n"}
end
end
def run({:put, bucket, key, value}) do
lookup bucket, fn pid ->
KV.Bucket.put(pid, key, value)
{:ok, "OK\r\n"}
end
end
def run({:delete, bucket, key}) do
lookup bucket, fn pid ->
KV.Bucket.delete(pid, key)
{:ok, "OK\r\n"}
end
end
defp lookup(bucket, callback) do
case KV.Registry.lookup(KV.Registry, bucket) do
{:ok, pid} -> callback.(pid)
:error -> {:error, :not_found}
end
end
- 受け取った命令に応じてbucketを操作するように直した。
lookup
を見てみよう。bucketが見つからなかったら{:error, :not_found}
を返すようにしているが、ユーザーに表示する場合もNot Foundであることを伝えるようにしたほうがよい。KV.Server
の format_msg/1
のパターンを追加する。
defp format_msg({:error, :not_found}), do: "NOT FOUND\r\n"
- これでサーバーはほとんど完成した。最後にテストを追加しよう。
defmodule KVServerTest do
use ExUnit.Case
setup do
Logger.remove_backend(:console)
# KVアプリケーションを止めてstateをリセットした上で再開
Application.stop(:kv)
:ok = Application.start(:kv)
Logger.add_backend(:console, flush: true)
:ok
end
setup do
# クライアントからの接続を作成
opts = [:binary, packet: :line, active: false]
{:ok, socket} = :gen_tcp.connect('localhost', 4040, opts)
{:ok, socket: socket}
end
test "server interaction", %{socket: socket} do
assert send_and_recv(socket, "UNKNOWN shopping\r\n") ==
"UNKNOWN COMMAND\r\n"
assert send_and_recv(socket, "GET shopping eggs\r\n") ==
"NOT FOUND\r\n"
assert send_and_recv(socket, "CREATE shopping\r\n") ==
"OK\r\n"
assert send_and_recv(socket, "PUT shopping eggs 3\r\n") ==
"OK\r\n"
# GET returns two lines
assert send_and_recv(socket, "GET shopping eggs\r\n") == "3\r\n"
assert send_and_recv(socket, "") == "OK\r\n"
assert send_and_recv(socket, "DELETE shopping eggs\r\n") ==
"OK\r\n"
# GET returns two lines
assert send_and_recv(socket, "GET shopping eggs\r\n") == "\r\n"
assert send_and_recv(socket, "") == "OK\r\n"
end
defp send_and_recv(socket, command) do
:ok = :gen_tcp.send(socket, command)
{:ok, data} = :gen_tcp.recv(socket, 0, 1000)
data
end
end