drip 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in Drip.gemspec
4
+ gemspec
5
+
6
+ gem 'rbtree'
data/README ADDED
@@ -0,0 +1,6 @@
1
+ = Drop
2
+
3
+ Simple RD-Stream for Rinda::TupleSpace lovers.
4
+
5
+ See "sample" directory for examples.
6
+
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -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
@@ -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
+
@@ -0,0 +1,10 @@
1
+ require 'rbconfig'
2
+ require 'fileutils'
3
+
4
+ dest = RbConfig::CONFIG['sitelibdir']
5
+ src = ['lib/drip.rb', 'lib/my_drip.rb']
6
+
7
+ src.each do |s|
8
+ FileUtils.install(s, dest, {:verbose => true, :mode => 0644})
9
+ end
10
+
@@ -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