drip 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +4 -0
- data/Gemfile +6 -0
- data/README +6 -0
- data/Rakefile +2 -0
- data/drip.gemspec +20 -0
- data/drip.txt +380 -0
- data/install.rb +10 -0
- data/lib/drip.rb +294 -0
- data/lib/drip/version.rb +3 -0
- data/lib/my_drip.rb +64 -0
- data/sample/copocopo.rb +82 -0
- data/sample/drip_tw.rb +255 -0
- data/sample/gca.rb +70 -0
- data/sample/hello_tw.rb +5 -0
- data/sample/my_status.rb +52 -0
- data/sample/simple-oauth.rb +140 -0
- data/sample/tw_markov.rb +154 -0
- data/test/basic.rb +156 -0
- metadata +71 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README
ADDED
data/Rakefile
ADDED
data/drip.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "drip/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "drip"
|
7
|
+
s.version = Drip::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Masatoshi Seki"]
|
10
|
+
s.homepage = "https://github.com/seki/Drip"
|
11
|
+
s.summary = %q{Simple RD-Stream for Rinda::TupleSpace lovers.}
|
12
|
+
s.description = ""
|
13
|
+
|
14
|
+
s.rubyforge_project = "drip"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
end
|
data/drip.txt
ADDED
@@ -0,0 +1,380 @@
|
|
1
|
+
*ストリーム指向のストレージDrip
|
2
|
+
|
3
|
+
**TupleSpaceの永続化とその制約
|
4
|
+
|
5
|
+
この節ではRinda::TupleSpaceをおさらいしながら、TupleSpaceに永続化したPTupleSpaceの概要を紹介し、その制約について考えます。
|
6
|
+
PTupleSpaceはTupleSpaceのサブクラスです。タプルの状態の変化を逐次二次記憶にログして、次回の起動に備えます。PTupleSpaceを再起動すると最後の(最新の)タプルの状態のままに復元されます。
|
7
|
+
|
8
|
+
タプルは実世界の「伝票」によく似ています。タプルをプロセス間でリレーしながら仕事を進めていく様子は、「伝票」を持ち回って仕事を行うのにそっくりです。Rindaの世界では「伝票」はTupleSpaceを介してプロセスからプロセスへ渡り歩きます。
|
9
|
+
|
10
|
+
PTupleSpaceの提供する永続化は、TupleSpaceに蓄えられた伝票の束にのみ作用します。プロセスが持っている伝票をPTupleSpaceが知ることはできず、永続化されません。また、待合せている様子も永続化の対象ではありません。プロセスがある伝票を待っている、という状況までは再現できないのです。
|
11
|
+
|
12
|
+
TupleSpaceに期待する機能が伝票の貯蔵庫であると考えた場合には、これで充分と言えるでしょう。PTupleSpaceにwriteした情報は再起動後もそのまま手に入ります。多くのアプリケーションではこれで間に合うかもしれません。ArrayやHashをそのままdRubyで公開する、あるいはログ付きで公開するのに比べて、TupleSpaceはどのくらい便利なのでしょうか。おそらく、RindaのTupleSpaceの強力なパターンマッチングにはある程度のアドバンテージがあるでしょう。そのパターンマッチングと引き換えに、あまり効率のよいデータ構造を使うことができませんでした。実装には線形探索が残っていて、要素数が増えたときに不安があります。
|
13
|
+
|
14
|
+
TupleSpaceの本来の役割であるプロセス間の協調についてはどうでしょうか。PTupleSpaceに異常が起きてクラッシュしてしまった、再起動が必要になった、といった状況を想像してみましょう。まず、PTupleSpaceプロセスが停止することにより、readやtakeなどの待合せのRMIを実行していたプロセスではdRubyの例外があがります。PTupleSpaceが再起動されるとタプル群の最後の状態に復元されます。待合せをしていたプロセスは再起動したことを(知るのは難しいのですが)知ったのち、例外が発生した操作をやり直すことになります。しかし、そのように再開するスクリプトを書くのは難しく面倒です。
|
15
|
+
|
16
|
+
また、RMIのために抱え込む厄介な問題もあります。writeやtakeなど、タプルの状態を変える操作を考えてみましょう。通常のメソッド呼び出しでは処理が終われば呼び出した側に直ちに制御がもどりますが、RMIではサーバ側のメソッドの終了と、RMIの終了の間にソケット通信が行われます。つまり、処理が終わる前に例外が発生したのか、結果を伝える間に例外が発生したのか知ることができません。PTupleSpaceが二次記憶にタプルの操作をログしたあとに、クライアントにその完了が届く前にクラッシュしてしまう可能性があります。(全てがうまくいってからログする実装を選んでも、クライアントにタプルが届いたのち、ログするまえにクラッシュする可能性があります)
|
17
|
+
|
18
|
+
異常終了といえば、プロセス側のクラッシュも考えられますね。PTupleSpaceの対象外ですがちょっと想像してみましょう。伝票をプロセスが取り出したままクラッシュしてしまうと、復元する方法がありません。次の短いスクリプトを見てみましょう。
|
19
|
+
|
20
|
+
>|ruby|
|
21
|
+
def succ
|
22
|
+
_, count = @ts.take([:count, nil])
|
23
|
+
count += 1
|
24
|
+
yield(count)
|
25
|
+
ensure
|
26
|
+
@ts.write([:count, count) if count
|
27
|
+
end
|
28
|
+
||<
|
29
|
+
|
30
|
+
これは[:count, 整数]のタプルを取り出し、一つ大きくしてまた書き込むスクリプトです。伝票を取り出し、カウンタを一つ進め、最後にTupleSpaceに書き戻します。伝票がプロセスにある間は、別のプロセスは伝票をTupleSpaceから読んだり、取り出したりすることはできないので安全にカウンタを操作できます。さて、もしも伝票がプロセスにある間にそのプロセスがクラッシュしたらどうなるでしょう。PTupleSpaceは自身の中にある伝票しか復元できませんから、その伝票は失われたままです。このカウンタを操作するプロセス群は全て停まってしまいます。こういった使い方(協調に使うケースの多くはそうなんだと思うのですが)をする場合、TupleSpaceだけでなく関係するプロセス群も再起動する必要があるだけでなく、TupleSpace内のタプルも初期状態にする必要があります。せっかくタプルの状態を復元できるようにしたというのに‥。
|
31
|
+
|
32
|
+
PTupleSpaceはTupleSpace自体の永続化を目的としたもので、それ自体はおそらく期待した通りに動作すると思います(そういうつもりで作ったので)。しかし、それだけでは協調するプロセス群をもとに戻すことはできません。ちょっとだまされた気分ですよね。
|
33
|
+
|
34
|
+
**PTupleSpaceの使い方
|
35
|
+
|
36
|
+
skip
|
37
|
+
|
38
|
+
|
39
|
+
**ストレージとしてのTupleSpace
|
40
|
+
|
41
|
+
APIの視点からストレージとしてのTupleSpaceをおさらいします。
|
42
|
+
TupleSpaceはタプル群を扱う集合構造です。同じ情報を複数持つことができるので、Bagと言えるでしょう。
|
43
|
+
最近の流行言葉にKVSという言葉ありますね。キーと値で表現するなら、同じキーを持つ要素の重複を許すストレージです。キーしかなくて、キーが値、にも見えますが。
|
44
|
+
|
45
|
+
これに対してHashは一つのキーに一つの値が関連付けられる辞書です。
|
46
|
+
|
47
|
+
TupleSpaceで辞書を模倣するのはやっかいです。[キー, 値]というタプルで辞書を構成仕様とした場合を考えてみましょう。まずデータを読むのは次のように書けそうです。
|
48
|
+
>|ruby|
|
49
|
+
@ts.read([キー, nil])
|
50
|
+
||<
|
51
|
+
では要素の追加はどうでしょう。
|
52
|
+
>|ruby|
|
53
|
+
@ts.write([キー, 値])
|
54
|
+
||<
|
55
|
+
このような単純なwriteでは重複を防ぐことはできません。全体をロックして、そのキーのタプルを削除してからwriteする必要があります。
|
56
|
+
>|ruby|
|
57
|
+
def []=(key, value)
|
58
|
+
lock = @ts.take([:global_lock])
|
59
|
+
@ts.take([key, nil], 0) rescue nil
|
60
|
+
@ts.write([key, value])
|
61
|
+
ensure
|
62
|
+
@ts.write(lock) if lock
|
63
|
+
end
|
64
|
+
||<
|
65
|
+
このグローバルなロックは実はデータを読むときにも必要です。なぜなら、そのキーの情報を別のスレッドが更新中かもしれないからです。
|
66
|
+
|
67
|
+
>|ruby|
|
68
|
+
def [](key)
|
69
|
+
lock = @ts.take([:global_lock])
|
70
|
+
_, value = @ts.read([key, nil], 0) rescue nil
|
71
|
+
return value
|
72
|
+
ensure
|
73
|
+
@ts.write(lock) if lock
|
74
|
+
end
|
75
|
+
||<
|
76
|
+
|
77
|
+
要素の増減がないケースでは前章で示した通り、グローバルなロックは不要です。だれかが更新中はその要素は取り出せませんが、更新が終わればまた書き戻されるはずです。ですから、単に要素が読めるまでreadで待ってしまえば良いことになり、局所的なロックとなります。
|
78
|
+
|
79
|
+
eachはどのように実装したらよいでしょう。TupleSpace全体を順に走査するうまい方法はありません。read_allで全ての要素のArrayを生成して、その配列にeachを委譲することになります。
|
80
|
+
|
81
|
+
>|ruby|
|
82
|
+
def each(&blk)
|
83
|
+
lock = @ts.take([:global_lock])
|
84
|
+
@ts.read_all([nil, nil]).each(&blk)
|
85
|
+
ensure
|
86
|
+
@ts.write(lock) if lock
|
87
|
+
end
|
88
|
+
||<
|
89
|
+
|
90
|
+
要素数が少ないうちは気になりませんが、多くなると損している気がしますね。
|
91
|
+
分散ハッシュテーブルなどでもeachやkeysを低コストで実装するのは難しいかもしれません。
|
92
|
+
|
93
|
+
流行のストレージには、常にキーでソートされているシーケンスを持つものがあります。並んでいることを利用して、大きな空間をブラウズするのが得意です。キーを工夫することでバージョン付きの情報を蓄えることもできます。RindaのTupleSpaceには、タプルを順序付けて並べることはできませんから、これを低コストで模倣するのは難しいです。
|
94
|
+
|
95
|
+
ところであなたが欲しかった集合は本当にHashでしたか?
|
96
|
+
|
97
|
+
**ストリーム指向のストレージDrip
|
98
|
+
|
99
|
+
この節で紹介するのは前章で説明したRD stream(消えないストリーム)のようなデータ構造を持つライブラリDripです。Dripはオブジェクトを書き込まれた順に蓄えるだけでなく、イベント通知メカニズムとしてプロセス間の同期にも使えます。Drip自体の機能は非常に小さいものですが、視点によっていろいろなものに見えます。ストレージにもバッチ処理のミドルウェアにも見えます。ひとことで説明するのは難しいですが、流行のサービスではTwitterのタイムラインがよく似ています。Rubyの標準ライブラリでたとえると、消えないQueue、消えないRindaです。たとえてもやっぱりよくわかりませんね。
|
100
|
+
|
101
|
+
簡単なスクリプトを使ってDripを使ってみましょう。
|
102
|
+
|
103
|
+
***Dripをインストールする
|
104
|
+
|
105
|
+
(井上さんgems化してくださーい。)
|
106
|
+
|
107
|
+
***Dripを作る
|
108
|
+
|
109
|
+
Dripは二次記憶としてプレーンなファイルを使います。Dripを生成するにはファイルを置くディレクトリを指定します。次のスクリプト(drip_s.rb)はDripを生成しdRubyでサービスするものです。
|
110
|
+
|
111
|
+
>|ruby|
|
112
|
+
require 'drip'
|
113
|
+
require 'drb'
|
114
|
+
|
115
|
+
class Drip
|
116
|
+
def quit
|
117
|
+
Thread.new do
|
118
|
+
synchronize do |key|
|
119
|
+
exit(0)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
drip = Drip.new('drip_dir')
|
126
|
+
DRb.start_service('druby://localhost:54321', drip)
|
127
|
+
DRb.thread.join
|
128
|
+
||<
|
129
|
+
|
130
|
+
Dripにquitメソッドを追加しています。これはRMI経由でこのプロセスを終了させるもので、Dripが二次記憶への操作をしていないとき(synchronize中)を待ってから終わらせます。
|
131
|
+
|
132
|
+
このあとの操作のために起動しておきましょう。
|
133
|
+
|
134
|
+
ターミナル1
|
135
|
+
>||
|
136
|
+
% ruby drip_s.rb
|
137
|
+
||<
|
138
|
+
|
139
|
+
***オブジェクトを覚える
|
140
|
+
|
141
|
+
Dripの状態を変更する唯一のメソッドはwriteメソッドです。writeメソッドには覚えたいオブジェクトを指定します。ちょっと書いてみましょう。
|
142
|
+
|
143
|
+
ターミナル2
|
144
|
+
>||
|
145
|
+
% irb -r drb --simple-prompt
|
146
|
+
>> drip = DRbObject.new_with_uri('druby://localhost:54321')
|
147
|
+
=> #<Drip:...>
|
148
|
+
>> drip.write('Hello, World.')
|
149
|
+
=> 1308221059579470
|
150
|
+
||<
|
151
|
+
|
152
|
+
ターミナル1で起動したDripサーバへの参照を作り、dripという変数に覚えます。次に'Hello, World.'という文字列をwriteします。writeの戻り値は書き込んだオブジェクトに対応するキーです。キーは単調増加する整数で、たいてい時刻から生成されます。時刻と最新のキーを比べて、最新のキーの方が大きい場合には+1したものをキーとします。多くの場合時刻と相互に変換できると思いますが、「ユーザが時計を設定してしまった問題」や時刻の分解能よりも細かい単位で書き込まれた場合などにはその限りではありません。いずれにしろ、このようなケースでもキーはいつも単調増加となります。
|
153
|
+
キーがおおよその時刻に変換できるのは、人間があとでデータを調べるときに多少は便利です。Dripには同時にただ一つのオブジェクトだけが書き込めます。さまざまな事象は同時にいくつも発生しますが、Dripがそれを観測するのは同時にはただ一つです。事象が発生した時刻ではなく、Dripがそれを知った時刻と考えて下さい。
|
154
|
+
|
155
|
+
writeメソッドはStringに限らず、どんなオブジェクトでも保存できます。ただし、MarshalできないオブジェクトはDRbObjectで保存されますから、取り出して使えるようにするには多少の工夫が必要です。また、あとでオブジェクトを取り出すときのヒントとなる複数のタグを指定できます。タグはStringでなければいけません。
|
156
|
+
|
157
|
+
>||
|
158
|
+
>> drip.write({:text => 'Hello, World', :user => 'm_seki'}, 'greeting', 'test')
|
159
|
+
=> 1308221460421676
|
160
|
+
>> drip.write(1308221460421676, 'test')
|
161
|
+
=> 1308221907348161
|
162
|
+
||<
|
163
|
+
|
164
|
+
この操作では、Hashのオブジェクトに二つのタグ('greeting, 'test')を付けて書き込み、次に整数に'test'というタグを付けて書き込んでいます。writeの際に、一つのオブジェクトに複数のタグをつけることができます。タグはDripで一意なものでなく、同じタグを持つオブジェクトがすでにwriteされていても問題ありません。
|
165
|
+
|
166
|
+
***read
|
167
|
+
|
168
|
+
Dripからデータを読む方法はたくさんありますが、基本となるのはreadです。readの引数は意外なほど多いです。
|
169
|
+
|
170
|
+
>|ruby|
|
171
|
+
read(key, n=1, at_least=1, timeout=nil)
|
172
|
+
||<
|
173
|
+
|
174
|
+
戻り値はキーとオブジェクト、タグからできたArrayのArrayです。読みたい要素数が1であってもArrayが返ります。keyよりも大きなキーを持つ要素をn個返します。at_leastは最低限返して欲しい要素数です。読める要素数がat_leastに達するまで、readはブロックします。timeoutはブロックの期限を指定します。最低でもtimeout秒は待ち、それを越えると例外があがります。nilを指定すると無限に待ちます。
|
175
|
+
|
176
|
+
先頭から一つずつ読み進めてみましょう。Dripのキーは正の整数ですから、先頭はキー0の次の要素です。0から二つの要素を読んでみましょう。
|
177
|
+
|
178
|
+
>||
|
179
|
+
>> drip.read(0, 2)
|
180
|
+
=> [[1308221059579470, "Hello, World."], [1308221460421676, {:text=>"Hello, World", :user=>"m_seki"}, "greeting", "test"]]
|
181
|
+
||<
|
182
|
+
|
183
|
+
0の次のキーを持つ要素(つまり先頭、一番旧い要素)から二つ分の要素が集まってできたArrayが返りました。
|
184
|
+
|
185
|
+
次に先頭から順に一つずつ読んでみます。注目点のキーをkとして、kをずらしながらreadしていきましょう。
|
186
|
+
|
187
|
+
>||
|
188
|
+
>> k = 0
|
189
|
+
=> 0
|
190
|
+
>> k, v, *tag = drip.read(k)[0]
|
191
|
+
=> [1308221059579470, "Hello, World."]
|
192
|
+
>> k, v, *tag = drip.read(k)[0]
|
193
|
+
=> [1308221460421676, {:text=>"Hello, World", :user=>"m_seki"}, "greeting", "test"]
|
194
|
+
>> k, v, *tag = drip.read(k)[0]
|
195
|
+
=> [1308221907348161, 1308221460421676, "test"]
|
196
|
+
>> k, v, *tag = drip.read(k)[0]
|
197
|
+
||<
|
198
|
+
|
199
|
+
どうでしょうか?ひとつずつ順に取り出せている様子がわかりますか?Dripにおけるデータのブラウズは、キーをずらすことで表現します。readは与えられたキーのすぐ後の要素を返しますから、さっき読んだオブジェクトのキーを与えてreadすることで全ての要素を順に辿ることができます。C言語のstdioライブラリでいうとfseek()とfread()を一度に行うイメージですね。
|
200
|
+
|
201
|
+
さて、4回目のreadでブロックしてしまいました。これは注目点のキーより新しい要素が存在しないからです。もう一つ端末を用意して、要素を追加してみましょう。
|
202
|
+
|
203
|
+
ターミナル3
|
204
|
+
>||
|
205
|
+
% irb -r drb --simple-prompt
|
206
|
+
>> drip = DRbObject.new_with_uri('druby://localhost:54321')
|
207
|
+
=> #<Drip:0x0000010086b130>
|
208
|
+
>> drip.write('Hello, Again.', 'test')
|
209
|
+
=> 1308222915958300
|
210
|
+
||<
|
211
|
+
|
212
|
+
ターミナル2
|
213
|
+
>||
|
214
|
+
=> [1308222915958300, "Hello, Again.", "test"]
|
215
|
+
||<
|
216
|
+
|
217
|
+
うん。ターミナル2のブロックは解け、新しい要素が届いたのがわかります。
|
218
|
+
|
219
|
+
ここでdrip_s.rbを停止させたらどうなるか実験してみましょう。まずターミナル2でreadを行ってブロックさせます。
|
220
|
+
|
221
|
+
ターミナル2
|
222
|
+
>||
|
223
|
+
>> k, v, *tag = drip.read(k)[0]
|
224
|
+
||<
|
225
|
+
|
226
|
+
続いてdrip_s.rbを[control]+Cなどで停止させます。
|
227
|
+
|
228
|
+
ターミナル1
|
229
|
+
>||
|
230
|
+
% ruby drip_s.rb
|
231
|
+
^Cdrip_s.rb:16:in `join': Interrupt
|
232
|
+
from drip_s.rb:16:in `<main>'
|
233
|
+
||<
|
234
|
+
|
235
|
+
ターミナル2ではreadの最中にdrip_s.rbが終了したので例外があがります。待ち合わせしているときにサーバが終了すると、待合せは解除されることになります。
|
236
|
+
|
237
|
+
>||
|
238
|
+
DRb::DRbConnError: connection closed
|
239
|
+
....
|
240
|
+
||<
|
241
|
+
|
242
|
+
再びdrip_s.rbを起動してから、readを試してみましょう。
|
243
|
+
|
244
|
+
ターミナル1
|
245
|
+
>||
|
246
|
+
% ruby drip_s.rb
|
247
|
+
||<
|
248
|
+
|
249
|
+
ターミナル2
|
250
|
+
>||
|
251
|
+
>> k, v, *tag = drip.read(k)[0]
|
252
|
+
||<
|
253
|
+
|
254
|
+
そしてターミナル3からもう一つ要素を追加します。ターミナル2のreadが完了し、その要素を読み出すことができるはずです。
|
255
|
+
|
256
|
+
ターミナル3
|
257
|
+
>||
|
258
|
+
>> drip.write('drip drop')
|
259
|
+
=> 1308304037358423
|
260
|
+
||<
|
261
|
+
|
262
|
+
ターミナル2
|
263
|
+
>||
|
264
|
+
=> [1308304037358423, "drip drop"]
|
265
|
+
||<
|
266
|
+
|
267
|
+
Dripは要素を二次記憶にwriteされたオブジェクトをログしており、次回の起動に備えています。これまでの実験の中でwriteされた5つの要素が本当に残っているかためしましょう。先頭から5つの要素をreadします。keyの最小値から5つの要素をreadするには、read(0, 5)とします。
|
268
|
+
|
269
|
+
ターミナル2
|
270
|
+
>||
|
271
|
+
>> drip.read(0, 5)
|
272
|
+
=> [[1308221059579470, "Hello, World."], [1308221460421676, {:text=>"Hello, World", :user=>"m_seki"}, "greeting", "test"], [1308221907348161, 1308221460421676, "test"], [1308222915958300, "Hello, Again.", "test"], [1308304037358423, "drip drop"]]
|
273
|
+
||<
|
274
|
+
|
275
|
+
drip_s.rbが一度終了しても内容が失われてないことが確認できます。
|
276
|
+
|
277
|
+
**APIの設計指針
|
278
|
+
|
279
|
+
DripはdRubyと組み合わせて使うのを前提としてAPIを設計しました。dRubyの弱点はいくつかありますが、特に苦手なのはサーバ側のオブジェクトの寿命と排他制御の管理、そしてRMIの遅さです。サーバ側に状態をもつオブジェクトを作らないこと、RMIの回数を減らすことはAPIの選択の指針となります。
|
280
|
+
|
281
|
+
さきほどのreadメソッドに与えるキーについて、もう一度よく見てみましょう。readのキーは、データベース中の視点、カーソル、ページといった概念に近いものです。よくあるデータベースのAPIでは「カーソル」はコンテキストの中に隠されています。例えばRubyのFileオブジェクトは現在の位置を覚えていて、ファイル中のその位置から読んだり、その位置へ書いたりします。これに対し、DripではFileオブジェクトのような状態/コンテキストをもつオブジェクトを用いません。Dripへの質問は状態の変化を伴わない、関数のようになっています。位置などのコンテキストを管理するオブジェクトの代わりに、注目点となるキーを使うのです。このAPIを選択した理由は、コンテキストを管理するオブジェクトをDripサーバの中で生成しないためです。DripはdRubyを経由したRMIで利用されることを前提としています。生成と消滅(beginとend、openとclose)があるようなコンテキストを導入すると、その寿命をサーバが気にする必要が生まれます。分散環境でのGCといった難しい問題に向かい合わなくてはなりません。このため、Dripではそのような面倒を嫌ってInteger(キー)だけの付き合いとなるようにAPIを設計しました。
|
282
|
+
この節で示した通り、コンテキストを管理するオブジェクトを使う代わりに、readのたびに返されるキーを使ってアクセスすることで、同様な操作を実現できます。もしこのAPIでの操作が面倒と感じるなら、ローカルなプロセスの中でキーを隠すようなコンテキストを準備することを勧めます。間違ってDripサーバ側にコンテキストを用意しないよう注意して下さいね。
|
283
|
+
|
284
|
+
readでは、自分の知らない情報を一度に最大n個、少なくともm個を返せ、と指示します。n回のreadで構成すると、RMIの回数が増えてしまいますが、このように一度に転送すればRMIの回数を削減できます。応答時間よりも処理時間が重要なバッチ処理などのケースで有効です。「少なくともm個」を指定することで、イベントの(データの)発生の都度RMIを発生させずにすみます。ほどほどにデータがたまるのを待って一度に転送することができるからです。
|
285
|
+
|
286
|
+
Dripはストレージに関する一連の習作の経験から、「作りすぎない」ことに留意しました。「作る」ことは楽しいので、請われるままに機能を増やしてしまうことがしばしば起こります(私はそういう経験があります)。Dripのポリシーを明確にして、機能を増やしてしまう誘惑と戦いました。
|
287
|
+
|
288
|
+
**タグとその他のread系API
|
289
|
+
|
290
|
+
writeの際につけたタグを使って、読み出す情報をフィルタすることができます。writeでは、一つの情報に複数のタグをつけることができ、read_tagはタグを一つ指定してそのタグを持つ要素だけを読み出すことができます。その他の引数はreadと同じです。
|
291
|
+
|
292
|
+
>|ruby|
|
293
|
+
read_tag(key, tag, n=1, at_least=1, timeout=nil)
|
294
|
+
||<
|
295
|
+
|
296
|
+
Dripでは全ての要素は一直線のストリームとして管理されます。要素はキーと値、複数のタグで構成されて、キーの順に並んでいます。readやread_tagは小さいキーから大きいキーの順にアクセスします。キーはwriteされた時刻を元に計算され、たいてい連続していません。readやread_tagの際に、存在しないキーを与えても問題ありません。そのキーよりも大きなキーを持つ、直近の要素からアクセスを開始します。readは与えたキーよりも大きなキーを持つ、直近の要素を返します。read_tagも同様に直近の要素を返しますが、タグがマッチしない要素はスキップされ、マッチした要素だけが返されます。タグを使うと一つのDripをタグで分類されたたくさんのDripのように見立てることもできます。
|
297
|
+
また、readとread_tagのキーは共通ですから二つを組み合わせることもできます。例えば、read_tagで狙ったタグを持つ要素を取得して、それ以降の要素をreadで順に全て集める、といった操作です。
|
298
|
+
|
299
|
+
read_tagもreadと同様に要素が取り出せない場合に、ブロックして新しい要素の到着を待つことができます。
|
300
|
+
|
301
|
+
read系のAPIは他にも用意されています。
|
302
|
+
|
303
|
+
>|ruby|
|
304
|
+
older(key, tag=nil)
|
305
|
+
|
306
|
+
head(n=1, tag=nil)
|
307
|
+
||<
|
308
|
+
|
309
|
+
readやread_tagは過去から未来へ走査するAPIですが、olderとheadは過去方向への操作を補助するAPIです。
|
310
|
+
olderはkeyで指定した要素の一つ旧い要素を返します。tagを使って要素をフィルタすることもできます。keyにnilを与えると最新を指定したことになります。older(nil)は最新の要素を一つ返します。
|
311
|
+
|
312
|
+
headはolderを使ったコンビニエンスメソッドです。一番新しい要素までのn個を返します。tagを使って要素をフィルタすることも可能です。nが1の場合、一番新しい要素だけが入ったArrayを返します。nが2の場合には、一番新しい要素の一つ旧い要素と、一番新しい要素の入ったArrayを返します。Arrayの中は旧いから新しいものへと並んでいます。
|
313
|
+
|
314
|
+
過去へ向かってのアクセスはブロックすることはありません。なぜなら、過去には情報が追加できないからです。(要素が増えるのは未来方向のみ)
|
315
|
+
|
316
|
+
コンビニエンスメソッドとしてはolderの対になるnewerもあります。
|
317
|
+
|
318
|
+
>|ruby|
|
319
|
+
newer(key, tag=nil)
|
320
|
+
||<
|
321
|
+
|
322
|
+
これは単なるread/read_tagのラッパーで、read(key, 1, 0)[0]あるいはread_tag(key, tag, 1, 0)[0]を簡単に呼べるようにしたものです。
|
323
|
+
|
324
|
+
|
325
|
+
|
326
|
+
**とりあえず使ってみる
|
327
|
+
|
328
|
+
DripはPRb、KoyaなどのOODBへの挑戦(そして失敗)とRindaを実アプリケーションで使った経験、分散ファイルシステムへの憧れから書かれた小さなライブラリです。2009のRubyKaigiでの講演のテーマ、世界を並べるとも通じます。
|
329
|
+
|
330
|
+
|
331
|
+
**できること
|
332
|
+
|
333
|
+
Dripはオブジェクトを保存することができます。オブジェクトは時系列整理されます。新しいオブジェクトを追加することはできますが、削除や変更はできません。KVSのようにキーと値を組にして覚えるのではなく、単に時系列に追加されます。KVSのキーとして、追加を受け付けた瞬間の時刻を使っている、と考えてもよいでしょう。
|
334
|
+
Dripのキーは単調増加する整数です。この整数と時刻(Time)とは相互に変換することができます。オブジェクトを追加された時刻に従って順にブラウズすることができます。
|
335
|
+
オブジェクトにはStringで表現したタグを複数つけることができます。同じタグを持つオブジェクトの集合を、時系列にブラウズすることができます。タグの付け方を工夫することで、履歴付きのKVSのように使うことができます。
|
336
|
+
オブジェクトを書くためのAPIはwriteメソッドです。引数は保存したいオブジェクトとそのタグです。writeはオブジェクトのキーを返します。ひとつのDripの中ではキーはユニークです。あるキーに関連しているオブジェクトはただ一つです。
|
337
|
+
オブジェクトを読むためのAPIは複数用意されています。基本となるのは、あるキーよりも後のオブジェクトを複数取得するreadメソッドです。アプリケーションは自分の知っているキーよりも後に追加されたオブジェクトをまとめて取得できます。もし、指定するキーよりも後にオブジェクトが追加されていなければ、このメソッドはブロックされ新しいオブジェクトが届くのを待ちます。
|
338
|
+
|
339
|
+
readメソッドにはいくつかバリエーションがあります。read_tagを使うとタグでフィルタしたストリームをブラウズできます。また、older, newer, headなどのread, read_tagをラップした簡易メソッドが用意されます。
|
340
|
+
|
341
|
+
**それでなにするの?
|
342
|
+
|
343
|
+
**とりあえず保存する
|
344
|
+
|
345
|
+
Dripではデータを保存するためにキーを考える必要がありません。Objectをnewするように単にwriteするだけです。(OODBのうれしさのひとつはオブジェクトの生成がnewでできることです。)
|
346
|
+
オブジェクトを保存するとキーが返るので、それを使って思い出すことができますし、だいたいの時間がわかればキーを忘れてもなんとかなります。補助的な情報としてタグを使うこともできます。Dripではオブジェクトを変更したり削除したりすることはできませんから、他のプロセスやスレッドがオブジェクトを変えてしまう心配はありません。そうDripでは過去を変えることはできないのです。
|
347
|
+
|
348
|
+
KoyaやPRbの経験から「オブジェクトの生成」のためにユニークなキーを生成するのは私の中で大きなテーマでした。ここで紹介するDripではキーとして時刻を選びました。あるDripにとって、ある瞬間に同時に生成されるオブジェクトはありません。生成という処理を受け入れるのは同時にただ一つのスレッドだけです。
|
349
|
+
|
350
|
+
ロディアにメモするように、使い方はあとで考えるけどとりあえず保存しておいてあとで取り出す、という使い方の可能性についてはのちに(?)議論したいと思います。
|
351
|
+
|
352
|
+
|
353
|
+
**ストリームとして使う
|
354
|
+
|
355
|
+
Dripではあるキーよりも後に追加されたオブジェクトを取得することができます。カーソルをアプリケーション側が管理してすべての情報を順に辿れることを意味します。Rubyでよく使われるEnumerableとは違いますね。もしeachを実装するならアプリケーション側に書くとよいでしょう。この方式の大きなメリットは、ブラウズの中断や再開が容易な点です。
|
356
|
+
実運用でのバッチ処理アプリケーションなどでは、(とくに開発中など)処理が途中で失敗してしまったり、もっと効率のよいアルゴリズムを思いついてしまったりすることが珍しくありません。そんなとき、処理がうまく進んだ場所をメモしておいて、そこから再開することができます。また、失敗時に待ち行列から要素を紛失してしまうことも避けられます。
|
357
|
+
|
358
|
+
>|ruby|
|
359
|
+
#典型的なDripのreadのループ
|
360
|
+
def drip_each(drip, key=0)
|
361
|
+
while true
|
362
|
+
ary = drip.read(key, 10, 1)
|
363
|
+
ary.each do |kv|
|
364
|
+
yield(*kv)
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
368
|
+
||<
|
369
|
+
|
370
|
+
|
371
|
+
Dripの集合の先端(そのキーよりも新しいオブジェクトがまだ書かれてないところ)まで読み進めた場合、次のread操作はブロックされ、次のオブジェクトが届くまで休眠状態となります。この挙動はイベントの通知にDripを使うのに適しています。自分が知っているイベントよりも後に発生したイベントを読み尽したら、今度は新しいイベントが発生するまで休んでいることができます。
|
372
|
+
|
373
|
+
RMIの性質の点から観察してみましょう。DripのアプリケーションはすべてDripへのメソッド呼び出しとその結果の取得という向きだけで構成されます。ブロック付きメソッド(イテレータなど)を使用していません。readとカーソルによるデータのブラウズ方法を思い出してください。ブラウズもイテレータは使わずメソッド呼び出しの繰り返しで行われます。このためDripからアプリケーションを呼び返す、アプリケーションの処理を待ってしまう状況はありません。典型的なオブザーバーパターンをナイーブに実現してしまうと、遅いアプリケーションに律速してしまいますが、DripのAPIではそのような状況に陥りにくいです。RMIの回数についても最適化されます。キーよりあとのN個のオブジェクトをまとめて取得することができるので、アプリケーションが仕事中に溜まってしまったオブジェクトを一度のRMIで読みだすことが可能です。
|
374
|
+
|
375
|
+
**世界をログから編み上げる
|
376
|
+
|
377
|
+
多くのリビジョン管理システムではすべての変更を保持していて、情報の追加・削除・更新は「変更ログ」への追記として表現されます。(Gitのようにオブジェクトのスナップショットを保持する作戦もありますが、この場合も情報は増える一方です。)Dripの提供する保存機能もこれらの変更ログと同じようなものと考えることができます。
|
378
|
+
|
379
|
+
(とちゅう)
|
380
|
+
|
data/install.rb
ADDED
data/lib/drip.rb
ADDED
@@ -0,0 +1,294 @@
|
|
1
|
+
require 'rbtree'
|
2
|
+
require 'drb/drb'
|
3
|
+
require 'rinda/tuplespace'
|
4
|
+
require 'enumerator'
|
5
|
+
|
6
|
+
class Drip
|
7
|
+
include DRbUndumped
|
8
|
+
def inspect; to_s; end
|
9
|
+
|
10
|
+
def initialize(dir, option={})
|
11
|
+
@pool = RBTree.new
|
12
|
+
@tag = RBTree.new
|
13
|
+
@event = Rinda::TupleSpace.new(5)
|
14
|
+
@event.write([:last, 0])
|
15
|
+
prepare_store(dir, option)
|
16
|
+
end
|
17
|
+
|
18
|
+
def write(*value)
|
19
|
+
write_after(Time.now, *value)
|
20
|
+
end
|
21
|
+
|
22
|
+
def write_after(at, *value)
|
23
|
+
make_key(at) do |key|
|
24
|
+
do_write(key, value)
|
25
|
+
@pool[key] = @store.write(key, value)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def write_at(at, *value)
|
30
|
+
make_key_at(at) do |key|
|
31
|
+
do_write(key, value)
|
32
|
+
@pool[key] = @store.write(key, value)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def fetch(key)
|
37
|
+
@pool[key].to_a
|
38
|
+
end
|
39
|
+
alias [] fetch
|
40
|
+
|
41
|
+
def make_renewer(timeout)
|
42
|
+
case timeout
|
43
|
+
when 0
|
44
|
+
return 0
|
45
|
+
when Numeric
|
46
|
+
return Renewer.new(timeout)
|
47
|
+
else
|
48
|
+
nil
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def read(key, n=1, at_least=1, timeout=nil)
|
53
|
+
renewer = make_renewer(timeout)
|
54
|
+
key = time_to_key(Time.now) unless key
|
55
|
+
ary = []
|
56
|
+
n.times do
|
57
|
+
begin
|
58
|
+
wait(key, renewer) if at_least > ary.size
|
59
|
+
rescue Rinda::RequestExpiredError
|
60
|
+
return ary
|
61
|
+
end
|
62
|
+
key, value = @pool.lower_bound(key + 1)
|
63
|
+
return ary unless key
|
64
|
+
ary << [key] + value.to_a
|
65
|
+
end
|
66
|
+
ary
|
67
|
+
end
|
68
|
+
|
69
|
+
def read_tag(key, tag, n=1, at_least=1, timeout=nil)
|
70
|
+
renewer = make_renewer(timeout)
|
71
|
+
key = time_to_key(Time.now) unless key
|
72
|
+
ary = []
|
73
|
+
n.times do
|
74
|
+
begin
|
75
|
+
wait_tag(key, tag, renewer) if at_least > ary.size
|
76
|
+
rescue Rinda::RequestExpiredError
|
77
|
+
return ary
|
78
|
+
end
|
79
|
+
it ,= @tag.lower_bound([tag, key + 1])
|
80
|
+
return ary unless it && it[0] == tag
|
81
|
+
key = it[1]
|
82
|
+
ary << [key] + fetch(key)
|
83
|
+
end
|
84
|
+
ary
|
85
|
+
end
|
86
|
+
|
87
|
+
def head(n=1, tag=nil)
|
88
|
+
ary = []
|
89
|
+
key = nil
|
90
|
+
while it = older(key, tag)
|
91
|
+
break if n <= 0
|
92
|
+
ary.unshift(it)
|
93
|
+
key = it[0]
|
94
|
+
n -= 1
|
95
|
+
end
|
96
|
+
ary
|
97
|
+
end
|
98
|
+
|
99
|
+
def older(key, tag=nil)
|
100
|
+
key = time_to_key(Time.now) unless key
|
101
|
+
unless tag
|
102
|
+
k, v = @pool.upper_bound(key - 1)
|
103
|
+
return k ? [k] + v.to_a : nil
|
104
|
+
end
|
105
|
+
|
106
|
+
it ,= @tag.upper_bound([tag, key - 1])
|
107
|
+
return nil unless it && it[0] == tag
|
108
|
+
[it[1]] + fetch(it[1])
|
109
|
+
end
|
110
|
+
|
111
|
+
def newer(key, tag=nil)
|
112
|
+
return read(key, 1, 0)[0] unless tag
|
113
|
+
read_tag(key, tag, 1, 0)[0]
|
114
|
+
end
|
115
|
+
|
116
|
+
def time_to_key(time)
|
117
|
+
time.tv_sec * 1000000 + time.tv_usec
|
118
|
+
end
|
119
|
+
|
120
|
+
def key_to_time(key)
|
121
|
+
Time.at(*key.divmod(1000000))
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
class SimpleStore
|
126
|
+
Attic = Struct.new(:fname, :fpos, :value)
|
127
|
+
class Attic
|
128
|
+
def to_a
|
129
|
+
value || retrieve
|
130
|
+
end
|
131
|
+
|
132
|
+
def forget
|
133
|
+
self.value = nil
|
134
|
+
end
|
135
|
+
|
136
|
+
def retrieve
|
137
|
+
File.open(fname) do |fp|
|
138
|
+
fp.seek(fpos)
|
139
|
+
kv = Marshal.load(fp)
|
140
|
+
kv[1]
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
class AtticCache
|
146
|
+
def initialize(n)
|
147
|
+
@size = n
|
148
|
+
@tail = 0
|
149
|
+
@ary = Array.new(n)
|
150
|
+
end
|
151
|
+
|
152
|
+
def push(attic)
|
153
|
+
@ary[@tail].forget if @ary[@tail]
|
154
|
+
@ary[@tail] = attic
|
155
|
+
@tail = (@tail + 1) % @size
|
156
|
+
attic
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def self.reader(name)
|
161
|
+
self.to_enum(:each, name)
|
162
|
+
end
|
163
|
+
|
164
|
+
def self.each(name)
|
165
|
+
file = File.open(name, 'rb')
|
166
|
+
while true
|
167
|
+
pos = file.pos
|
168
|
+
key, value = Marshal.load(file)
|
169
|
+
yield(key, value, Attic.new(name, pos, value))
|
170
|
+
end
|
171
|
+
rescue EOFError
|
172
|
+
ensure
|
173
|
+
file.close if file
|
174
|
+
end
|
175
|
+
|
176
|
+
def initialize(name, option={})
|
177
|
+
@name = name
|
178
|
+
@file = nil
|
179
|
+
cache_size = option.fetch(:cache_size, 8)
|
180
|
+
@cache = AtticCache.new(cache_size) if @name
|
181
|
+
end
|
182
|
+
|
183
|
+
def write(key, value)
|
184
|
+
return value unless @name
|
185
|
+
@file = File.open(@name, 'a+b') unless @file
|
186
|
+
pos = @file.pos
|
187
|
+
Marshal.dump([key, value], @file)
|
188
|
+
@file.flush
|
189
|
+
@cache.push(Attic.new(@name, pos, value))
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def prepare_store(dir, option={})
|
194
|
+
if dir.nil?
|
195
|
+
@store = SimpleStore.new(nil, option)
|
196
|
+
return
|
197
|
+
end
|
198
|
+
|
199
|
+
Dir.mkdir(dir) rescue nil
|
200
|
+
Dir.glob(File.join(dir, '*.log')) do |fn|
|
201
|
+
begin
|
202
|
+
store = SimpleStore.reader(fn)
|
203
|
+
restore(store)
|
204
|
+
rescue
|
205
|
+
end
|
206
|
+
end
|
207
|
+
name = time_to_key(Time.now).to_s(36) + '.log'
|
208
|
+
@store = SimpleStore.new(File.join(dir, name))
|
209
|
+
end
|
210
|
+
|
211
|
+
def shared_text(str)
|
212
|
+
key, value = @tag.lower_bound([str, 0])
|
213
|
+
if key && key[0] == str
|
214
|
+
key[0]
|
215
|
+
else
|
216
|
+
str
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def do_write(key, value)
|
221
|
+
(1...value.size).each do |n|
|
222
|
+
k = value[n]
|
223
|
+
next unless String === k
|
224
|
+
tag = shared_text(k)
|
225
|
+
@tag[[tag, key]] = key
|
226
|
+
end
|
227
|
+
@pool[key] = value
|
228
|
+
end
|
229
|
+
|
230
|
+
def restore(store)
|
231
|
+
_, last = @event.take([:last, nil])
|
232
|
+
store.each do |k, v, attic|
|
233
|
+
do_write(k, v)
|
234
|
+
@pool[k] = attic
|
235
|
+
@pool[k].forget
|
236
|
+
end
|
237
|
+
last ,= @pool.last
|
238
|
+
ensure
|
239
|
+
@event.write([:last, last || 0])
|
240
|
+
end
|
241
|
+
|
242
|
+
def make_key(at=Time.now)
|
243
|
+
synchronize do |last|
|
244
|
+
key = [time_to_key(at), last + 1].max
|
245
|
+
yield(key)
|
246
|
+
key
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def make_key_at(at)
|
251
|
+
synchronize do |last|
|
252
|
+
key = time_to_key(at)
|
253
|
+
raise 'InvalidTimeError' if key <= last
|
254
|
+
yield(key)
|
255
|
+
key
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
def synchronize
|
260
|
+
_, last = @event.take([:last, nil])
|
261
|
+
last = yield(last)
|
262
|
+
ensure
|
263
|
+
@event.write([:last, last])
|
264
|
+
end
|
265
|
+
|
266
|
+
INF = 1.0/0.0
|
267
|
+
def wait(key, renewer)
|
268
|
+
@event.read([:last, key+1 .. INF], renewer)[1]
|
269
|
+
end
|
270
|
+
|
271
|
+
def wait_tag(key, tag, renewer)
|
272
|
+
wait(key, renewer)
|
273
|
+
okey = key + 1
|
274
|
+
begin
|
275
|
+
it ,= @tag.lower_bound([tag, okey])
|
276
|
+
return if it && it[0] == tag
|
277
|
+
end while key = wait(key, renewer)
|
278
|
+
end
|
279
|
+
|
280
|
+
class Renewer
|
281
|
+
def initialize(timeout)
|
282
|
+
@at = Time.now + timeout
|
283
|
+
end
|
284
|
+
|
285
|
+
def renew
|
286
|
+
@at - Time.now
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
if __FILE__ == $0
|
292
|
+
require 'my_drip'
|
293
|
+
MyDrip.invoke
|
294
|
+
end
|