fraggle 0.1.0

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/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