fraggle 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2011 Blake Mizerany, Keith Rarick, Chris Moos
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # Fraggle
2
+ **An EventMachine based Doozer client**
3
+
4
+ ## Install
5
+
6
+ $ gem install fraggle
7
+
8
+ ## Use
9
+
10
+ require 'rubygems'
11
+ require 'eventmachine'
12
+ require 'fraggle'
13
+
14
+ EM.start do
15
+ c = Fraggle.connect "127.0.0.1", 8046
16
+
17
+ ## Setting a key
18
+ c.set "/foo", "bar", :missing do |e|
19
+ if ! e.err
20
+ e.cas # => "123"
21
+ end
22
+ end
23
+
24
+ c.get "/foo" do |e|
25
+ if err != nil
26
+ e.body # => "bar"
27
+ e.cas # => "123"
28
+ e.dir? # => false
29
+ end
30
+ end
31
+
32
+ watch = c.watch "/foo" do |e|
33
+ # The event has:
34
+ # ------------------------
35
+ # NOTE: `err` will be set iff the glob is bad
36
+ # e.err # => nil
37
+ # e.path # => "/foo"
38
+ # e.body # => "bar"
39
+ # e.cas # => "123"
40
+ # e.set? # => true
41
+ # e.del? # => false
42
+ # e.done? # => true
43
+ # ------------------------
44
+
45
+ if e.done?
46
+ # This watch was closed, do something if you wish.
47
+ else
48
+ done_something_with(e)
49
+
50
+ # Phoney check for example
51
+ if can_stop_watching?(path)
52
+ c.close(watch)
53
+ end
54
+ end
55
+
56
+ end
57
+
58
+ end
59
+
60
+
61
+ ## Dev
62
+
63
+ **Clone**
64
+ $ git clone http://github.com/bmizerany/fraggle.git
65
+
66
+ **Test**
67
+ $ gem install turn
68
+ $ turn
data/lib/fraggle.rb ADDED
@@ -0,0 +1,322 @@
1
+ require 'beefcake'
2
+ require 'eventmachine'
3
+ require 'fraggle/proto'
4
+
5
+ module Fraggle
6
+
7
+ MaxInt32 = (1<<31)-1
8
+ MinInt32 = -(1<<31)
9
+
10
+ ##
11
+ # Response extensions
12
+ class Response
13
+ module Flag
14
+ VALID = 1
15
+ DONE = 2
16
+ end
17
+
18
+ def valid?
19
+ (flags & Flag::VALID) > 0
20
+ end
21
+
22
+ def done?
23
+ (flags & Flag::DONE) > 0
24
+ end
25
+
26
+ # Err sugar
27
+ def ok? ; err_code == nil ; end
28
+ def other? ; err_code == Err::OTHER ; end
29
+ def tag_in_use? ; err_code == Err::TAG_IN_USE ; end
30
+ def unknown_verb? ; err_code == Err::UNKNOWN_VERB ; end
31
+ def redirect? ; err_code == Err::REDIRECT ; end
32
+ def invalid_snap? ; err_code == Err::INVALID_SNAP ; end
33
+ def mismatch? ; err_code == Err::CAS_MISMATCH ; end
34
+ def notdir? ; err_code == Err::NOTDIR ; end
35
+ def dir? ; err_code == Err::ISDIR ; end
36
+
37
+ # CAS sugar
38
+ def missing? ; cas == 0 ; end
39
+ def clobber? ; cas == -1 ; end
40
+ def dir? ; cas == -2 ; end
41
+ def dummy? ; cas == -3 ; end
42
+ end
43
+
44
+
45
+ class AssemblyError < StandardError
46
+ end
47
+
48
+
49
+ def self.connect(addr="127.0.0.1:8046", opts={})
50
+ # TODO: take a magnet link instead
51
+ host, port = addr.split(":")
52
+ EM.connect(host, port, self, addr, opts)
53
+ end
54
+
55
+ attr_reader :doozers, :addr, :opts
56
+
57
+ def initialize(addr, opts)
58
+ opts[:assemble] = opts.fetch(:assemble, true)
59
+
60
+ # TODO: take a magnet link and load into @doozers
61
+ @addr = addr
62
+ @opts = opts
63
+ @doozers = {}
64
+ end
65
+
66
+ ##
67
+ # Collect all cluster information for the event of a disconnect from the
68
+ # server; At which point we will want to attempt a connecting to them one by
69
+ # one until we have a connection or run out of options.
70
+ def assemble
71
+ return if ! opts[:assemble]
72
+
73
+ blk = Proc.new do |we|
74
+ if ! we.ok?
75
+ raise AssemblyError, we.err_detail
76
+ end
77
+
78
+ if we.value == ""
79
+ doozers.delete(we.path)
80
+ else
81
+ get "/doozer/info/#{we.value}/public-addr" do |e|
82
+ next if e.value == addr
83
+ doozers[we.path] = e.value
84
+ end
85
+ end
86
+ end
87
+
88
+ watch "/doozer/slot/*", &blk
89
+ walk "/doozer/slot/*", &blk
90
+ end
91
+
92
+ ##
93
+ # Attempts to connect to another doozer when a connection is lost
94
+ def unbind
95
+ return if ! opts[:assemble]
96
+
97
+ _, @addr = doozers.shift
98
+ if ! @addr
99
+ raise AssemblyError, "All known doozers are down"
100
+ end
101
+
102
+ host, port = @addr.split(":")
103
+ reconnect(host, port)
104
+ end
105
+
106
+
107
+
108
+
109
+ ##
110
+ # Session generation
111
+ def gen_key(name, size=16)
112
+ nibbles = "0123456789abcdef"
113
+ "#{name}." + (0...size).map { nibbles[rand(nibbles.length)].chr }.join
114
+ end
115
+
116
+ def session(name="fraggle", &blk)
117
+ raise ArgumentError, "no block given" if ! blk
118
+
119
+ id = gen_key(name)
120
+
121
+ fun = lambda do |e|
122
+ raise e.err_detail if ! e.ok?
123
+ checkin(e.cas, id, &fun)
124
+ end
125
+
126
+ established = lambda do |e|
127
+ case true
128
+ when e.mismatch?
129
+ id = gen_key(name)
130
+ checkin(0, id, &established)
131
+ when ! e.ok?
132
+ raise e.err_detail
133
+ else
134
+ blk.call
135
+ checkin(e.cas, id, &fun)
136
+ end
137
+ end
138
+
139
+ checkin(0, id, &established)
140
+ end
141
+
142
+ def checkin(cas, id, &blk)
143
+ call(
144
+ Request::Verb::CHECKIN,
145
+ :cas => casify(cas),
146
+ :path => id.to_s,
147
+ &blk
148
+ )
149
+ end
150
+
151
+ def post_init
152
+ @buf = ""
153
+ @tag = 0
154
+ @cbx = {}
155
+ @len = nil
156
+
157
+ assemble
158
+ end
159
+
160
+ def receive_data(data)
161
+ @buf << data
162
+
163
+ got = true
164
+ while got
165
+ got = false
166
+
167
+ if @len.nil? && @buf.length >= 4
168
+ @len = @buf.slice!(0, 4).unpack("N").first
169
+ end
170
+
171
+ if @len && @buf.length >= @len
172
+ bytes = @buf.slice!(0, @len)
173
+ res = Response.decode(bytes)
174
+ receive_response(res)
175
+ @len = nil
176
+ got = true
177
+ end
178
+ end
179
+ end
180
+
181
+ def receive_response(res)
182
+ blk = @cbx[res.tag]
183
+
184
+ if blk && res.valid?
185
+ if blk.arity == 2
186
+ blk.call(res, false)
187
+ else
188
+ blk.call(res)
189
+ end
190
+ end
191
+
192
+ if res.done?
193
+ if blk && blk.arity == 2
194
+ blk.call(nil, true)
195
+ end
196
+ @cbx.delete(res.tag)
197
+ end
198
+ end
199
+
200
+ def call(verb, attrs={}, &blk)
201
+ if @tag == MaxInt32
202
+ @tag = MinInt32
203
+ end
204
+
205
+ while true
206
+ break if ! @cbx.has_key?(@tag)
207
+ @tag += 1
208
+ end
209
+
210
+ attrs[:verb] = verb
211
+ attrs[:tag] = @tag
212
+ @cbx[@tag] = blk
213
+
214
+ send_request(Request.new(attrs))
215
+
216
+ @tag
217
+ end
218
+
219
+ def send_request(req)
220
+ buf = req.encode
221
+
222
+ send_data([buf.length].pack("N"))
223
+ send_data(buf)
224
+ end
225
+
226
+
227
+
228
+ ##
229
+ # Sugar
230
+ def get(path, sid=0, &blk)
231
+ call(
232
+ Request::Verb::GET,
233
+ :path => path,
234
+ :id => sid,
235
+ &blk
236
+ )
237
+ end
238
+
239
+ def set(path, body, cas, &blk)
240
+ call(
241
+ Request::Verb::SET,
242
+ :path => path,
243
+ :value => body,
244
+ :cas => casify(cas),
245
+ &blk
246
+ )
247
+ end
248
+
249
+ def del(path, cas, &blk)
250
+ call(
251
+ Request::Verb::DEL,
252
+ :path => path,
253
+ :cas => casify(cas),
254
+ &blk
255
+ )
256
+ end
257
+
258
+ def watch(glob, &blk)
259
+ call(
260
+ Request::Verb::WATCH,
261
+ :path => glob,
262
+ &blk
263
+ )
264
+ end
265
+
266
+ def walk(glob, &blk)
267
+ call(
268
+ Request::Verb::WALK,
269
+ :path => glob,
270
+ &blk
271
+ )
272
+ end
273
+
274
+ def snap(&blk)
275
+ call(
276
+ Request::Verb::SNAP,
277
+ &blk
278
+ )
279
+ end
280
+
281
+ def delsnap(id, &blk)
282
+ call(
283
+ Request::Verb::DELSNAP,
284
+ :id => id,
285
+ &blk
286
+ )
287
+ end
288
+
289
+ def noop(&blk)
290
+ call(
291
+ Request::Verb::NOOP,
292
+ &blk
293
+ )
294
+ end
295
+
296
+ def cancel(tag)
297
+ blk = lambda do |e|
298
+ if e.ok?
299
+ if blk = @cbx.delete(tag)
300
+ blk.call(nil, true)
301
+ end
302
+ end
303
+ end
304
+
305
+ call(
306
+ Request::Verb::CANCEL,
307
+ :id => tag,
308
+ &blk
309
+ )
310
+ end
311
+
312
+ private
313
+
314
+ def casify(cas)
315
+ case cas
316
+ when :missing then 0
317
+ when :clobber then -1
318
+ else cas
319
+ end
320
+ end
321
+
322
+ end
@@ -0,0 +1,75 @@
1
+ require 'beefcake'
2
+
3
+ module Fraggle
4
+
5
+ class Request
6
+ include Beefcake::Message
7
+
8
+ required :tag, :int32, 1
9
+
10
+ module Verb
11
+ CHECKIN = 0; # cas, id => cas
12
+ GET = 1; # path, id => cas, value
13
+ SET = 2; # cas, path, value => cas
14
+ DEL = 3; # cas, path => {}
15
+ ESET = 4; # cas, path => {}
16
+ SNAP = 5; # {} => seqn, id
17
+ DELSNAP = 6; # id => {}
18
+ NOOP = 7; # {} => {}
19
+ WATCH = 8; # path => {cas, path, value}+
20
+ CANCEL = 10; # id => {}
21
+
22
+ # future
23
+ GETDIR = 14; # path => {cas, value}+
24
+ MONITOR = 11; # path => {cas, path, value}+
25
+ SYNCPATH = 12; # path => cas, value
26
+ WALK = 9; # path, id => {cas, path, value}+
27
+
28
+ # deprecated
29
+ JOIN = 13;
30
+ end
31
+
32
+ required :verb, Verb, 2
33
+
34
+ optional :cas, :int64, 3
35
+ optional :path, :string, 4
36
+ optional :value, :bytes, 5
37
+ optional :id, :int32, 6
38
+
39
+ optional :offset, :int32, 7
40
+ optional :limit, :int32, 8
41
+
42
+ end
43
+
44
+
45
+ class Response
46
+ include Beefcake::Message
47
+
48
+ required :tag, :int32, 1
49
+ required :flags, :int32, 2
50
+
51
+ optional :seqn, :int64, 3
52
+ optional :cas, :int64, 4
53
+ optional :path, :string, 5
54
+ optional :value, :bytes, 6
55
+ optional :id, :int32, 7
56
+
57
+ module Err
58
+ # don't use value 0
59
+ OTHER = 127
60
+ TAG_IN_USE = 1
61
+ UNKNOWN_VERB = 2
62
+ REDIRECT = 3
63
+ INVALID_SNAP = 4
64
+ CAS_MISMATCH = 5
65
+
66
+ # match unix errno
67
+ NOTDIR = 20
68
+ ISDIR = 21
69
+ end
70
+
71
+ optional :err_code, Err, 100
72
+ optional :err_detail, :string, 101
73
+ end
74
+
75
+ end
data/test/core_test.rb ADDED
@@ -0,0 +1,215 @@
1
+ require 'fraggle'
2
+
3
+ ##
4
+ # This is used to test core functionality that the live integration tests will
5
+ # rely on.
6
+ class FakeConn
7
+ include Fraggle
8
+
9
+ attr_reader :sent, :cbx
10
+ attr_accessor :tag
11
+
12
+ def initialize
13
+ @sent = ""
14
+ super("127.0.0.1", :assemble => false)
15
+ post_init
16
+ end
17
+
18
+ def send_data(data)
19
+ @sent << data
20
+ end
21
+ end
22
+
23
+ class CoreTest < Test::Unit::TestCase
24
+
25
+ attr_reader :c
26
+
27
+ V = Fraggle::Request::Verb
28
+ F = Fraggle::Response::Flag
29
+
30
+ def setup
31
+ @c = FakeConn.new
32
+ end
33
+
34
+ def reply(attrs={})
35
+ attrs[:tag] = c.tag
36
+ attrs[:flags] ||= 0
37
+ attrs[:flags] |= F::VALID
38
+ res = Fraggle::Response.new(attrs)
39
+ c.receive_response(res)
40
+ res
41
+ end
42
+
43
+ def reply!(attrs={})
44
+ attrs[:flags] = F::DONE
45
+ reply(attrs)
46
+ end
47
+
48
+ def test_sending_data
49
+ c.call(V::NOOP)
50
+
51
+ req = Fraggle::Request.new(
52
+ :tag => c.tag,
53
+ :verb => V::NOOP
54
+ )
55
+
56
+ buf = req.encode
57
+ pre = [buf.length].pack("N")
58
+
59
+ assert_equal pre+buf, c.sent
60
+ end
61
+
62
+ def test_receive_small_buffered_data
63
+ count = 0
64
+
65
+ tag = c.call(V::WATCH, :path => "**") do |e|
66
+ count += 1
67
+ end
68
+
69
+ res = Fraggle::Response.new(
70
+ :tag => tag,
71
+ :flags => F::VALID
72
+ )
73
+
74
+ exp = 10
75
+ buf = res.encode
76
+ pre = [buf.length].pack("N")
77
+ bytes = (pre+buf)*exp
78
+
79
+ # Chunk bytes to receive_data in some arbitrary size
80
+ 0.step(bytes.length, 3) do |n|
81
+ c.receive_data(bytes.slice!(0, n))
82
+ end
83
+
84
+ assert_equal 10, count
85
+ end
86
+
87
+ def test_receive_large_buffered_data
88
+ count = 0
89
+
90
+ tag = c.call(V::WATCH, :path => "**") do |e|
91
+ count += 1
92
+ end
93
+
94
+ res = Fraggle::Response.new(
95
+ :tag => tag,
96
+ :flags => F::VALID
97
+ )
98
+
99
+ exp = 10
100
+ buf = res.encode
101
+ pre = [buf.length].pack("N")
102
+ bytes = (pre+buf)*exp
103
+
104
+ c.receive_data(bytes)
105
+
106
+ assert_equal 10, count
107
+ end
108
+
109
+ def test_callback_without_done
110
+ valid = lambda do |e|
111
+ assert_kind_of Fraggle::Response, e
112
+ end
113
+
114
+ done = lambda do |e|
115
+ assert false, "Unreachable"
116
+ end
117
+
118
+ tests = [valid, done]
119
+
120
+ c.call(V::NOOP) do |e|
121
+ tests.shift.call(e)
122
+ end
123
+ reply!
124
+
125
+ assert_equal 1, tests.length
126
+ end
127
+
128
+ def test_callback_with_done
129
+ valid = lambda do |e, done|
130
+ assert_kind_of Fraggle::Response, e
131
+ assert_equal false, done
132
+ end
133
+
134
+ done = lambda do |e, done|
135
+ assert_nil e
136
+ assert_equal true, done
137
+ end
138
+
139
+ tests = [valid, done]
140
+
141
+ c.call(V::NOOP) do |e, done|
142
+ tests.shift.call(e, done)
143
+ end
144
+
145
+ reply!
146
+ assert tests.empty?
147
+ end
148
+
149
+ def test_no_callback
150
+ c.call(V::NOOP)
151
+
152
+ assert_nothing_raised do
153
+ reply!
154
+ end
155
+ end
156
+
157
+ def test_no_callback_gc
158
+ c.call(V::NOOP)
159
+ reply!
160
+
161
+ assert ! c.cbx.has_key?(1)
162
+ end
163
+
164
+ def test_callback_gc
165
+ c.call(V::NOOP) {}
166
+ reply
167
+
168
+ assert c.cbx.has_key?(c.tag)
169
+
170
+ reply!
171
+
172
+ assert ! c.cbx.has_key?(c.tag)
173
+ end
174
+
175
+ def test_call_returns_tag
176
+ assert_equal 0, c.call(V::NOOP)
177
+ assert_equal 1, c.call(V::NOOP)
178
+ end
179
+
180
+ def test_call_increments_tag
181
+ c.call(V::NOOP)
182
+ assert_equal 0, c.tag
183
+ c.call(V::NOOP)
184
+ assert_equal 1, c.tag
185
+ c.call(V::NOOP)
186
+ assert_equal 2, c.tag
187
+ c.call(V::NOOP)
188
+ assert_equal 3, c.tag
189
+ c.call(V::NOOP)
190
+ assert_equal 4, c.tag
191
+ c.call(V::NOOP)
192
+ assert_equal 5, c.tag
193
+ c.call(V::NOOP)
194
+ assert_equal 6, c.tag
195
+ c.call(V::NOOP)
196
+ assert_equal 7, c.tag
197
+ c.call(V::NOOP)
198
+ assert_equal 8, c.tag
199
+ c.call(V::NOOP)
200
+ assert_equal 9, c.tag
201
+ end
202
+
203
+ def test_no_overlap_in_tags
204
+ c.cbx[0] = Proc.new {}
205
+ assert_equal 1, c.call(V::NOOP)
206
+ end
207
+
208
+ def test_rollover_tag_when_maxed_out
209
+ c.tag = Fraggle::MaxInt32
210
+ c.call(V::NOOP)
211
+
212
+ assert_equal Fraggle::MinInt32, c.tag
213
+ end
214
+
215
+ end
data/test/live_test.rb ADDED
@@ -0,0 +1,197 @@
1
+ require 'fraggle'
2
+
3
+ class LiveTest < Test::Unit::TestCase
4
+ def start(timeout=1, &blk)
5
+ EM.run do
6
+ if timeout > 0
7
+ EM.add_timer(timeout) { fail "Test timeout!" }
8
+ end
9
+
10
+ c = Fraggle.connect(
11
+ "127.0.0.1:8046",
12
+ :assemble => false
13
+ )
14
+
15
+ blk.call(c)
16
+ end
17
+ end
18
+
19
+ def stop
20
+ EM.stop
21
+ end
22
+
23
+ def test_get
24
+ start do |c|
25
+ c.get "/ping" do |e|
26
+ assert e.ok?, e.err_detail
27
+ assert e.cas > 0
28
+ assert_equal "pong", e.value
29
+ stop
30
+ end
31
+ end
32
+ end
33
+
34
+ def test_set
35
+ start do |c|
36
+ c.set "/test-set", "a", :clobber do |ea|
37
+ assert ea.ok?, ea.err_detail
38
+ assert ea.cas > 0
39
+ assert_nil ea.value
40
+
41
+ c.get "/test-set" do |eb|
42
+ assert eb.ok?, eb.err_detail
43
+ assert_equal "a", eb.value
44
+ stop
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ def test_del
51
+ start do |c|
52
+ c.set "/test-del", "a", :clobber do |e|
53
+ assert e.ok?, e.err_detail
54
+
55
+ c.del("/test-del", e.cas) do |de|
56
+ assert de.ok?, de.err_detail
57
+ stop
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ def test_error
64
+ start do |c|
65
+ c.set "/test-error", "a", :clobber do |ea|
66
+ assert ! ea.mismatch?
67
+ assert ea.ok?, ea.err_detail
68
+ c.set "/test-error", "b", :missing do |eb|
69
+ assert eb.mismatch?, eb.err_detail
70
+ stop
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ def test_watch
77
+ start do |c|
78
+ count = 0
79
+ c.watch("/**") do |e|
80
+ assert e.ok?, e.err_detail
81
+
82
+ count += 1
83
+ if count == 9
84
+ stop
85
+ end
86
+ end
87
+
88
+ 10.times do
89
+ EM.next_tick { c.set("/test-watch", "something", :clobber) }
90
+ end
91
+ end
92
+ end
93
+
94
+ def test_snap
95
+ start do |c|
96
+ c.set "/test-snap", "a", :clobber do |e|
97
+ assert e.ok?, e.err_detail
98
+
99
+ c.snap do |se|
100
+ assert se.ok?, se.err_detail
101
+ assert_not_equal 0, se.id
102
+
103
+ c.set "/test-snap", "b", :clobber do |e|
104
+ assert e.ok?, e.err_detail
105
+
106
+ c.get "/test-snap", se.id do |ge|
107
+ assert ge.ok?, ge.err_detail
108
+ assert_equal "a", ge.value
109
+ stop
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ # TODO: ??? Shouldn't a deleted snapid produce an error on read?
118
+ def test_delsnap
119
+ start do |c|
120
+ c.snap do |se|
121
+ assert se.ok?, se.err_detail
122
+ assert_not_equal 0, se.id
123
+
124
+
125
+ c.delsnap se.id do |de|
126
+ assert de.ok?, de.err_detail
127
+
128
+ c.get "/ping", se.id do |ge|
129
+ assert ! ge.ok?, ge.err_detail
130
+ stop
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+
137
+ def test_noop
138
+ start do |c|
139
+ c.noop do |e|
140
+ assert e.ok?, e.err_detail
141
+ stop
142
+ end
143
+ end
144
+ end
145
+
146
+ def test_cancel
147
+ start do |c|
148
+ tag = c.watch("/test-cancel") do |e, done|
149
+ if ! done
150
+ assert e.ok?, e.err_detail
151
+ end
152
+
153
+ if done
154
+ stop
155
+ end
156
+
157
+ c.cancel(tag)
158
+ end
159
+
160
+ c.set("/test-cancel", "a", :clobber)
161
+ end
162
+ end
163
+
164
+ def test_walk
165
+ start do |c|
166
+
167
+ exp = [
168
+ ["/test-walk/1", "a"],
169
+ ["/test-walk/2", "b"],
170
+ ["/test-walk/3", "c"]
171
+ ]
172
+
173
+ n = exp.length
174
+
175
+ exp.each do |path, val|
176
+ c.set path, val, :clobber do |e|
177
+ assert e.ok?, e.err_detail
178
+ n -= 1
179
+
180
+ if n == 0
181
+ items = []
182
+ c.walk "/test-walk/*" do |e, done|
183
+ if done
184
+ assert_equal exp, items
185
+ stop
186
+ else
187
+ items << [e.path, e.value]
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
193
+
194
+ end
195
+ end
196
+
197
+ end
@@ -0,0 +1,175 @@
1
+ require 'fraggle'
2
+
3
+ class RecConn
4
+ include Fraggle
5
+
6
+ attr_reader :store, :recs
7
+
8
+ def initialize
9
+ super("1:1", :assemble => true)
10
+ post_init
11
+ @store = {}
12
+ @recs = []
13
+ end
14
+
15
+ def get(path, sid=0, &blk)
16
+ res = store.fetch(path) { fail("testing: no slot for #{path}") }
17
+ blk.call(res)
18
+ end
19
+
20
+ def reconnect(host, port)
21
+ @recs << [host, port]
22
+ end
23
+
24
+ def send_data(data)
25
+ # do nothing
26
+ end
27
+ end
28
+
29
+ class ReconnectTest < Test::Unit::TestCase
30
+
31
+ Walk = 0
32
+ Watch = 1
33
+
34
+ attr_reader :c
35
+
36
+ def setup
37
+ @c = RecConn.new
38
+ end
39
+
40
+ def reply(to, path, value)
41
+ res = Fraggle::Response.new
42
+ res.tag = to
43
+ res.flags = Fraggle::Response::Flag::VALID
44
+ res.path = path
45
+ res.value = value
46
+ res.validate!
47
+
48
+ c.receive_response(res)
49
+ end
50
+
51
+ def set(path, value)
52
+ res = Fraggle::Response.new
53
+ res.tag = 123
54
+ res.flags = Fraggle::Response::Flag::VALID
55
+ res.value = value
56
+ res.validate!
57
+
58
+ c.store[path] = res
59
+ end
60
+
61
+ def test_ignore_current
62
+ assert_equal Hash.new, c.doozers
63
+
64
+ set "/doozer/info/ABC/public-addr", "1:1"
65
+ reply(Walk, "/doozer/slot/1", "ABC")
66
+
67
+ assert_equal Hash.new, c.doozers
68
+ end
69
+
70
+ def test_add_other_slots_at_start
71
+ set "/doozer/info/DEF/public-addr", "2:2"
72
+ set "/doozer/info/GHI/public-addr", "3:3"
73
+ reply(Walk, "/doozer/slot/2", "DEF")
74
+ reply(Walk, "/doozer/slot/3", "GHI")
75
+
76
+ exp = {
77
+ "/doozer/slot/2" => "2:2",
78
+ "/doozer/slot/3" => "3:3"
79
+ }
80
+
81
+ assert_equal exp, c.doozers
82
+ end
83
+
84
+ def test_add_new_slots_as_they_come
85
+ set "/doozer/info/DEF/public-addr", "2:2"
86
+ set "/doozer/info/GHI/public-addr", "3:3"
87
+ reply(Watch, "/doozer/slot/2", "DEF")
88
+ reply(Watch, "/doozer/slot/3", "GHI")
89
+
90
+ exp = {
91
+ "/doozer/slot/2" => "2:2",
92
+ "/doozer/slot/3" => "3:3"
93
+ }
94
+
95
+ assert_equal exp, c.doozers
96
+ end
97
+
98
+ def test_del_slots_if_they_emptied
99
+ set "/doozer/info/DEF/public-addr", "2:2"
100
+ set "/doozer/info/GHI/public-addr", "3:3"
101
+ reply(Walk, "/doozer/slot/2", "DEF")
102
+ reply(Walk, "/doozer/slot/3", "GHI")
103
+
104
+ # Del
105
+ reply(Watch, "/doozer/slot/3", "")
106
+
107
+ exp = {
108
+ "/doozer/slot/2" => "2:2"
109
+ }
110
+
111
+ assert_equal exp, c.doozers
112
+ end
113
+
114
+ def test_raise_error_if_given_by_server
115
+ res = Fraggle::Response.new
116
+ res.tag = Walk
117
+ res.flags = Fraggle::Response::Flag::VALID
118
+ res.err_code = Fraggle::Response::Err::OTHER
119
+ res.err_detail = "invalid glob"
120
+
121
+ assert_raises Fraggle::AssemblyError do
122
+ c.receive_response(res)
123
+ end
124
+ end
125
+
126
+ def test_out_of_doozers
127
+ assert_raises Fraggle::AssemblyError do
128
+ c.unbind
129
+ end
130
+ end
131
+
132
+ def test_first_reconnect_success
133
+ set "/doozer/info/DEF/public-addr", "2:2"
134
+ set "/doozer/info/GHI/public-addr", "3:3"
135
+ reply(Walk, "/doozer/slot/2", "DEF")
136
+ reply(Walk, "/doozer/slot/3", "GHI")
137
+
138
+ c.unbind
139
+ assert_equal 1, c.recs.length
140
+
141
+ # The order in which the client try is non-detrministic because we're
142
+ # shifting off a Hash.
143
+ assert ["2:2", "3:3"].include?(c.addr)
144
+ end
145
+
146
+ def test_second_reconnect_success
147
+ set "/doozer/info/DEF/public-addr", "2:2"
148
+ set "/doozer/info/GHI/public-addr", "3:3"
149
+ reply(Walk, "/doozer/slot/2", "DEF")
150
+ reply(Walk, "/doozer/slot/3", "GHI")
151
+
152
+ c.unbind
153
+ c.unbind
154
+ assert_equal 2, c.recs.length
155
+
156
+ # The order in which the client try is non-detrministic because we're
157
+ # shifting off a Hash.
158
+ assert ["2:2", "3:3"].include?(c.addr)
159
+ end
160
+
161
+ def test_all_recconcts_fail
162
+ set "/doozer/info/DEF/public-addr", "2:2"
163
+ set "/doozer/info/GHI/public-addr", "3:3"
164
+ reply(Walk, "/doozer/slot/2", "DEF")
165
+ reply(Walk, "/doozer/slot/3", "GHI")
166
+
167
+ c.unbind
168
+ c.unbind
169
+
170
+ assert_raises Fraggle::AssemblyError do
171
+ c.unbind
172
+ end
173
+ end
174
+
175
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fraggle
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Blake Mizerany
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2011-01-25 00:00:00 -08:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: beefcake
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ - 1
30
+ - 1
31
+ version: 0.1.1
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ description: A Ruby/EventMachine Client for Doozer
35
+ email:
36
+ executables: []
37
+
38
+ extensions: []
39
+
40
+ extra_rdoc_files:
41
+ - README.md
42
+ - LICENSE
43
+ files:
44
+ - LICENSE
45
+ - README.md
46
+ - lib/fraggle/proto.rb
47
+ - lib/fraggle.rb
48
+ - test/core_test.rb
49
+ - test/live_test.rb
50
+ - test/reconnect_test.rb
51
+ has_rdoc: true
52
+ homepage: http://github.com/bmizerany/fraggle
53
+ licenses: []
54
+
55
+ post_install_message:
56
+ rdoc_options:
57
+ - --line-numbers
58
+ - --inline-source
59
+ - --title
60
+ - Sinatra
61
+ - --main
62
+ - README.rdoc
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ segments:
70
+ - 0
71
+ version: "0"
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ segments:
77
+ - 0
78
+ version: "0"
79
+ requirements: []
80
+
81
+ rubyforge_project: fraggle
82
+ rubygems_version: 1.3.6
83
+ signing_key:
84
+ specification_version: 2
85
+ summary: A Ruby/EventMachine Client for Doozer
86
+ test_files:
87
+ - test/core_test.rb
88
+ - test/live_test.rb
89
+ - test/reconnect_test.rb