ROMAプラグインの作り方

# 対象者

このドキュメントは、ROMAを自力で起動でき、ROMAのためのプラグインを開発してみたい、またはROMAにプラグインを追加し遊んでみたい人を対象にしています。

ROMAのお試し起動の方法は[getting_started](http://roma-kvs.org/getting_started.html)にありますので、こちらを参照してください。

また、プラグインを開発するに辺りROMAをgemでインストールしている事を前提にしています。

# プラグインとは

[ROMA プラグイン](http://roma-kvs.org/docs/plugin.html)とは小さなプログラムを書く事で、ROMAの機能を拡張できる機構です。プラグインを利用することで、新しいROMAコマンドを実装することもできます。

実は、alist、mapなどのコマンドもこのプラグインの機構を利用することで作成されています。

# なぜプラグインを作るのか

例えば、alistで実装されているリスト処理について考えてみます。この機能は、keyに対してリスト形式のデータが登録してあり、登録されたリストに対し

- リストにデータを追加
- リストの中の特定の値のみ削除
- リストの特定の値のみ取り出し

という事が出来る物です。

わざわざROMAにプラグインを追加しなくても、JSON形式などで値を登録しておき、keyに対し値を取り出しリスト操作しても良さそうに思えますが、リクエストが多い場合にちょっとした問題が発生します。

例えば、

- key:1 value: [1 ,2]

という値が保存されていたとします。
このとき、クライアントAは、リストにに'3' を、クライアントBはリストに'4'を追加しようとしたとします。この場合は、期待する結果は、

- key:1 value: [1, 2, 3, 4]

となるわけですが、クライアントAとクライアントBが同時に値を取得し、セットしたらどうなるでしょうか？

1. クライアントAが値を取り出す (key:1 value: [1,2])
2. クライアントBが値を取り出す (key:1 value: [1,2])
3. クライアントAが値を更新する (key:1 value: [1,2,3])
4. クライアントBが値を更新する (key:1 value: [1,2,4])

この結果、最終的に保存されるデータは、key:1 value: [1,2,4]となってしまいます。このような、リスト操作などの値の一部を操作または、取り出した値を加工するなどの処理を行う場合は、一度クライアント側に取り出して処理を行うと期待する結果にならない事があります。このような状況を避ける方法の一つとして、プラグインを利用出来ます。

# プラグインの実装

それでは、ROMAにJSON形式でデータを保存し、JSONの一部のデータを取り出せるというプラグインを作ってみましょう。

## コマンドの仕様

コマンドは以下の物をサポートする事とします

| コマンド | パラメータ | 説明 |
|------------|-------------|--------------|
| json_set   | \<key> \<json_key> \<flag> \<expire_time> \<bytes> |      jsonに値をセットする |
| json_get   | \<key> \<json_key> 0 | jsonから値を取得する |
| json_delete | \<key> \<json_key> 0 | json内の特定の値を削除する |
| json_clear | \<key> \<json_key> 0 | jsonオブジェクトの中身をクリアする |

パラメータに登場する値は以下の通りです。

|パラメータ|説明|
|------------|-------------|
|\<key>|ROMAにストアする値のキー|
|\<json_key>|value内のjsonの為のキー|
|\<flag>|ROMAコマンドの為のフラグ。詳細は[commands](http://roma-kvs.org/commands.html)ページを参照してください|
|\<expire_time>|ROMAにストアする値のexpire time。詳細は[commands](http://roma-kvs.org/commands.html)ページを参照してください|
|\<bytes>|valueのバイト数|

## 設定とソースの準備

それでは、プラグインの開発を始めます。まず初めに、今回はjsonプラグインですので、プラグインのファイル名は、plugin_json.rbとします。

config.rbのPLUGIN_FILESの項目を探し、"plugin_json.rb"を追加します。
PLUGIN_FILESは、Array形式となっていますので、以下のように設定します。

```rb
PLUGIN_FILES = ["plugin_storage.rb", "plugin_json.rb"]
```

本来であれば、このplugin_json.rbをroma本体のpluginフォルダに入れるわけですが、それではソース管理が厄介な事になってしまいます。そのためプラグイン読み込みを行っているコード(roma本体の中で以下のromad.rb initialize_pluginメソッドにある)

```rb
Roma::Config::PLUGIN_FILES.each do|f|
  require "roma/plugin/#{f}"
  @log.info("roma/plugin/#{f} loaded")
end
```

を確認すると、requireパスのroma/pluginディレクトリ以下にファイルがあればあれば良いことが分かるため、

    plugin_test/roma/plugin/plugin_json.rb

というパスにファイルを配置し、通常であれば

```bash
romad localhost -p 10001 -d --replication_in_host --config config.rb
```

のようにROMAを起動する所を

```bash
ruby -I./plugin_test `which romad` localhost -p 10001 -d --replication_in_host --config config.rb
```
とrequireパスを指定して、pluginを探せるようにし、ROMAを起動すると解決出来ます。


## プラグインのお約束

それでは、プラグインのテンプレートを記載します。

```rb
require 'roma/messaging/con_pool'
require 'roma/command/command_definition'

module Roma
  module CommandPlugin
    # ROMA plugin
    module PluginJson
      include Roma::CommandPlugin
      include Roma::Command::Definition
    end
  end
end
```

module Roma::CommandPluginの中に、新しくmoduleを定義します。今回は、jsonプラグインですので、PluginJsonという名前にしています。このモジュールの中で、Roma::CommandPluginおよびRoma::Command::Definitionをincludeしておきます。

## json_set


それでは、json_setコマンドの実装をします。
JSONライブラリも利用しているので、ファイルの先頭で、

```rb
require 'json'
```

も記載しておいてください。

json_getコマンドを実装するには、以下のDSLをPluginJsonの中に記載します。

```rb
def_write_command_with_key_value :json_set, 5 do |ctx|
  v = {}
  v = JSON.parse(ctx.stored.value) if ctx.stored

  v[ctx.argv[2]] = ctx.params.value
  expt = chg_time_expt(ctx.argv[4].to_i)

  # [flags, expire time, value, kind of counter(:write/:delete), result message]
  [0, expt, v.to_json, :write, 'STORED']
end
```

- def_write_command_with_key_value は、key, value形式のパラメータを受け取るROMAコマンドを追加するための宣言です
- :json_setは、このコマンドをjson_setというコマンド名で呼び出せるようにするための宣言です
- 5はjson_setコマンドが引数を5個受け取る宣言です

ブロック引数に登場するcxtは、CommandContextクラスのインスタンスです。
これは、[command_definition.rb:56](https://github.com/roma/roma/blob/master/lib/roma/command/command_definition.rb#L56)に宣言されています。こう言ってはなんですが、わかりにく実装ですね。クラス宣言にすれば良いのに。ともかく、CommandContextは、argv, params, storedというパラメータを持っています。

**argv** コマンドに引数で渡された文字列のArrayとなっています。
例えば、

```bash
json_set data1 hoge 0 0 4
```

というコマンドを渡した場合は、

```ruby
["json_set", "data1", "hoge", "0", "0", "4"]
```

という値が格納されています。

**params** コマンドのパラメータ。詳細は、[command_definition.rb:210](https://github.com/roma/roma/blob/master/lib/roma/command/command_definition.rb#L210)を確認してください
今回はprams.value にコマンドに渡されたvalueが入っているという事だけを覚えておいて下さい

**stored** は、コマンド実行前に同じ名前のキーの値が格納されていた場合は、その値を格納したオブジェクトが入っています。ない場合は、nilです。また、stored.valueとする事で、以前に格納されていたオブジェクトを取り出すことができます。

それでは、以上の値を元に各行の詳細を見ていきたいと思います。

```rb
v = {}
v = JSON.parse(ctx.stored.value) if ctx.stored
```

この部分は、空のHashを準備し、以前の値が存在した場合はその値をロードしておきます。
今回は、データをJSON形式でROMAに格納しているので、JSON.parseを使い値をロードしています。本番運用も考えるならJSONパースエラーの補足も考慮する必要がありますが、今回はシンプルにするために省略しています。

```rb
v[ctx.argv[2]] = ctx.params.value
```

これは、内部で準備したHashに引数の2番目の値をキーにvalueを詰めています。

```rb
expt = chg_time_expt(ctx.argv[4].to_i)
```

これは、expire timeの加工のための処理です。決まり事だと思って追加しておいてください。
最後に、


```rb
# [flags, expire time, value, kind of counter(:write/:delete), result message]
[0, expt, v.to_json, :write, 'STORED']
```

これは、このブロックからの戻り値Arrayを元に、ROMA本体にデータの保存処理をしたもらう為のコードです。

Arrayの変数の詳細は、コメントにあるように

- 1番目がフラグ。これは、ROMAないで利用するフラグで取りあえず 0 を指定しておけば問題ありません
- 2番目は、expire time
- 3番目は、保存する値
- 4番目は、keyに対し、値を保存するのか削除するのかを指定するフラグ
- 5番目はコマンドの応答として返す文字列です

それでは、このコードを読み込んでROMAを再起動し、コマンドを投げてみましょう

```bash
telnet localhost 10001
ROMA$ json_set data1 hoge 0 0 4[return]
fuga[return]
```

正しく実装されていれば

```bash
STORED
```

と応答が返ってくるはずです。起動に失敗したり、エラーが起きる場合はプラグインのソースやconfig.rbを見直して下さい。

では、storeした値を取り出してみます。通常のgetコマンドを使い

```bash
telnet localhost 10001
ROMA$ get data1
{"hoge":"fuga"}
```

上記のようなJSONが返ってくるはずです。

## json_get

次にjson_getです。それでは早速コードから

```rb
def_read_command_with_key :json_get, :multi_line do |ctx|
  if ctx.stored
    v = JSON.parse(ctx.stored.value)[ctx.argv[2]]
    send_data("VALUE #{ctx.params.key} 0 #{v.length}\r\n#{v}\r\n") if v
  end
  send_data("END\r\n")
end
```

それでは、各行を見ていきます。

```rb
def_read_command_with_key :json_get, :multi_line do |ctx|
```

def_read_command_with_keyは、keyに対しあたいを読み込むコマンドの為の宣言です。:json_getは、コマンド名になります。:multi_lineはvalueに改行を含む文字列が含まれる可能性がある場合に宣言します。改行が含まれないことが分かっている場合は、:one_lineを指定します。詳細は[command_definition.rb:97](https://github.com/roma/roma/blob/master/lib/roma/command/command_definition.rb#L97)を参照して下さい。ctxは、json_setと同じ、CommandContextのインスタンスになります。

```rb
if ctx.stored
```

こちらは、json_setと同様ROMAに既にデータが保存されているのかを確認しています。

```rb
v = JSON.parse(ctx.stored.value)[ctx.argv[2]]
send_data("VALUE #{ctx.params.key} 0 #{v.length}\r\n#{v}\r\n") if v
```

この2行で、

- 保存されているJSON文字列からオブジェクトへ変換
- JSONのキーに該当するオブジェクトの取り出し
- 値がある場合は値を返す

という処理になります。

send_dataコマンドで、クライアントに文字列の送信を行います。応答の書式は VALUEを返す事を示すVALUE、キーの値、フラグ、valueの長さと改行、実際のvalueと改行、という書式になります。キーが見つからなかった場合は、これらの行が送られません。

```rb
send_data("END\r\n")
```

最後に、応答の終了を表すENDを返します。

それでは、ROMAの再起動を行いjson_setで最後値をセットし(RubyHashStorageを使っている場合は)、以下のコマンドを実行してみましょう。

```bash
ROMA$ json_get data1 hoge
```

以下の結果が得られると思います。

```bash
VALUE data1 0 4
fuga
END
```

また、存在しないキーまたは、JSON内にキーのない値を指定し、json_getコマンドを実行すると

```bash
END
```

のみが返ってきます。

## json_delete, json_clear

これらのコマンドの実装は、この記事を読んだ皆さんの宿題にしておきます。

# 最後に
ここまでのコードは[roma_plugin_json](https://github.com/byplayer/roma_plugin_json)に上げてあります。

このコードは[plugin_map](https://github.com/roma/roma/blob/master/lib/roma/plugin/plugin_map.rb)のコードとほぼ同様です。json_delete, json_clear共に、plugin_mapを参考にしていただければ実装できると思います。

また、途中でも記載しましたが、本番運用を考えた場合は、JSONのパースエラーについても考慮する必要があると思います。また、多階層のJSONサポートについても考える必要があるでしょう。

ROMAプラグインは、それほど多くのコードを書くことなくROMAの機能を拡張し、サービス独自の機能を組み込むことが出来ます。是非みなさんもチャレンジしてみてください。
また、ROMAプラグインを作成した場合は是非pullリクエストを下さい！