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 +22 -0
- data/README.md +68 -0
- data/lib/fraggle.rb +322 -0
- data/lib/fraggle/proto.rb +75 -0
- data/test/core_test.rb +215 -0
- data/test/live_test.rb +197 -0
- data/test/reconnect_test.rb +175 -0
- metadata +89 -0
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
|