こんばんは、キャスレーコンサルティングSI部の反田(そった)です。

今回はRuby製のストリーム指向のストレージのDripを使って自前のWikiを作成します。

Dripの説明は以下のgithubでの公式テキストが詳しいですが
https://github.com/seki/Drip/blob/master/drip.txt

ざっくり説明すると

  • 簡単に永続化できる
  • 簡単に排他制御できる

という特徴があります。

一方で

  • トランザクションがない
  • スケールしない

という面も持っていますので注意が必要です。今回の使用方法(自分で使うwikiのストレージ)では全く問題ありませんね。
この記事ではDripを初めて知ったという人向けにDripの使い方の紹介をいたします。
最初に簡単にDripの説明をした後にDripをストレージとして簡単なWikiを作りながら実際のDripの使い方を紹介します。

Dripってなに?

上記のテキストによると

Dripは追記型のストレージの一種で、Rubyオブジェクトを時系列にログします。Dripにはオブジェクトの追記のみ可能で、削除や更新はできません。

また、Dripはプロセス間の同期メカニズムでもあります。新しいオブジェクトの到着を待合せることができます。Dripでは一度保存されたオブジェクトは変化することはありません。複数のプロセスがばらばらの時刻に読み出した情報はどれも同じものですし、誰かが読んだオブジェクトを別の誰かが変更することはありません。この特性は分散ファイルシステムでよく見られる特性で、情報を排他的にアクセスしなくてはならない状況を減らすことができます。

と説明がされています。失礼を承知で乱暴に説明するとDripにはRDBの代わりをつとめてもらっています。
DripにWikiの内容を保存するとSQLなしで状態の保存、取得ができるのでめちゃくちゃ気軽にWikiが作れました。

ちゃんとした説明は上記公式テキストと、↓このあたりも参考になると思います。

http://www.druby.org/Drip.pdf
http://d.hatena.ne.jp/m_seki/20130119

インストール方法

    % gem install drip

gem install dripするだけです。(内部的に赤黒木のライブラリを使っているそうで、それもインストールされます)


    gem list drip

    *** LOCAL GEMS ***

    drip (0.0.2)

Dripの起動

インストールが出来たらDripを起動します。
DripはRDBの様に別プロセスで起動しdRubyを通してアクセスします。
公式テキストを参考に以下の様にDripの起動スクリプトを書いてRubyスクリプト(storage.rb)として起動します。


	require 'drip'
	require 'drb'

	class Drip
	  def quit
	    Thread.new do
	      synchronize do |key|
	        exit(0)
	      end
	    end
	  end
	end

	drip = Drip.new('drip_dir')
	DRb.start_service('druby://localhost:54321', drip)
	DRb.thread.join

 

ruby storage.rb

Drip.new('drip_dir')している部分でDripの保存用フォルダを指定しています。
DRb.start_service('druby://localhost:54321', drip)で54321ポートでdRubyのアクセスを受け付けています。

Wikiをつくる

さてDripが起動できたので実際にWikiを作っていきましょう。
Wikiとはなにかわからない・・・という方はこちらをどうぞ。
Wikipediaでのwikiの説明(WikipediaもWiki形式で作られていますね。)

今回作るWikiの名前はシンプルにdrip_wikiと名付けました。
また、以下を達成することを目指しています。

  • Wikiとしてコンテンツの追加/変更/削除ができること
  • (多少なりとも)独自の記法を導入すること

Dripの役割はwikiページの表示が要求されたら、その内容を返すということと
wikiページが変更されたら変更内容を保持するということになります。

UIには前回の記事で説明したSinatraを使用しているのでそちらも参考にして下さい。

drip_wikiのファイル構成

drip_wikiは以下のファイル構成で作成しました。

wiki_core.rb
storage.rb
drip_dir
views
– wiki_page.erb
– wiki_edit.erb
– wiki_list.erb
– wiki_history.erb

wiki_core.rbにwikiとしての処理をすべて書いています。これ以降はこのファイルを説明していきます。
storage.rbは先ほど説明した通りDripの起動スクリプトです。
drip_dirフォルダはstorage.rbが使用する保存用フォルダです。
viewsフォルダはSinatraの標準命名規約のフォルダです。erbテンプレートを置いています。

こちらに全ソースを置いてあります。
https://github.com/swallow-life/drip_wiki

Dripを起動した後に
ruby wiki_core.rbすれば起動できます。
http://localhost:4567/wiki/listでアクセスします。

Dripに接続する

HOST_NAME = "localhost"
PORT_NUMBER = "54321"
configure do
    set :drip, DRbObject.new_with_uri("druby://#{HOST_NAME}:#{PORT_NUMBER}")
end

dRubyを利用してlocalhostの54321ポートにアクセスしてdripのインスタンスを取得しています。
configureはSinatraの機能で起動時に一度だけ実行されます。

Dripに書き込む

Dripでは状態を変化させるメソッドはwriteだけしか用意されていません。
必然的にwikiページを新しく作ったり、変更した場合はwriteメソッドを呼び出します。
その際、引数にタグを使用できます。drip_wikiではタグとしてWikiのページ名(name)を設定しています。
drip.write(contents, name)
Dripに書き込むと書き込んだ時間を基に正の整数のkeyが生成されvalueとtagと合わせて保存されます。
writeタグの戻り値はkeyが返されます。


WIKI_NAMES_TAG = "__wiki_nemes__"

post '/wiki/*' do |name|
        drip = settings.drip
        #wikiページの作成処理
        contents = params[:contents]
        drip.write(contents, name)
        if drip.head(1, WIKI_NAMES_TAG).empty?
                wiki_names = Hash.new
        else
                _, wiki_names = drip.head(1, WIKI_NAMES_TAG)[0]
        end

        unless wiki_names.key? name
                wiki_names.store(name, true)
                drip.write(wiki_names, WIKI_NAMES_TAG)
        end

        redirect "wiki/#{name}"
end

Dripから読みだす

書き込みと違いDripから目的の内容を読み出すメソッドは幾つかあります。

  • read
  • read_tag
  • head
  • newer
  • older

このうちreadとread_tagは読み出す対象の要素がない場合にはブロックして追加されるまでか
タイムアウトになるまで待つという動作をします。
headは読み出す対象の最新n件を取得します。要素がなくてもブロックしません。
newerとolderはそれぞれkeyで指定した要素より1つ新しい/1つ古い要素を返します。

drip_wikiではwikiページが要求された場合、最後に追加・変更された状態で表示したいので
headメソッドを使用して最新1件を情報を取得するようにしました。
また、headメソッはタグ(下記のnameのこと)を指定できます。書き込む際にタグを指定しておくと読み出す際に指定したタグと一致する要素をだけを取得できます。
今回はwikiページ名をタグとして使用しました。

_, @value = drip.head(1, name)[0]

headの戻り値は[key, value, tag]という3要素の配列が配列で返るので1つ目の要素から3要素のうちのvalueのみを取得するようにしています。配列から配列を取り出すのですが、ここは少しややこしいですね。
valueが取得できない場合はまだwikiページ名に対応するページが存在しないということなので新規ページ作成用の画面を表示します。
ちなみに、valueが取得できた場合はエスケープ、独自記法の解釈、マークダウンの解釈をしてから画面に表示しています。

get '/wiki/*' do |name|
        @name = name
        edit = params[:edit]
        #該当のwikiページを表示する
        drip = settings.drip
        _, @value = drip.head(1, name)[0]
        html_escaped = us h @value
        @contents = markdown(make_link(html_escaped, request.path.sub("/wiki/", "")))
        if @value and not edit
                erb :wiki_page
        else
                #まだ存在しない場合はwikiページ作成用のページを表示する
                @button_name = !edit ? "ページを作成" : "ページを編集"
                @previous_page = request.referer if edit
                @edit = edit
                erb :wiki_edit
        end
end

まとめ

Dripを使ってWikiを作ってみました。
Drip#writeで変更の保存、Drip#headで内容の取得をしています。
200行に満たないコードでwikiの基本的な機能を実装できたのにびっくりしました。
最低限の機能を実装するまでの時間は半日程でした。
以前からwikiを実装してみたかったのですが実際にこんなに簡単に実装できたのは
DripとSinatraの力と感じました。
特にDripはまだまだ、使い道が多そうなのでこれからも使い込んでいきたいですね。
皆さんも使ってみて下さい!

以上、駆け足の紹介になってしましましたが
お読みいただきありがとうございました。