fraggle 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -16,15 +16,18 @@
16
16
  # connected. In the event of a lost connection, fraggle will attempt
17
17
  # other doozers until one accepts or it runs out of options; An
18
18
  # AssemlyError will be raised if that later happens.
19
- c = Fraggle.connect "127.0.0.1:8046"
20
-
21
- c.get "/foo" do |e|
22
- if e.ok?
23
- e.value # => "bar"
24
- e.cas # => "123"
25
- e.dir? # => false
26
- e.notdir? # => true
27
- end
19
+ c = Fraggle.connect "doozerd://127.0.0.1:8046"
20
+
21
+ req = c.get "/foo" do |e|
22
+ e.value # => "bar"
23
+ e.cas # => "123"
24
+ e.dir? # => false
25
+ e.notdir? # => true
26
+ end
27
+
28
+ req.error do |e|
29
+ e.err_code # => nil
30
+ e.err_detail # => nil
28
31
  end
29
32
 
30
33
  watch = c.watch "/foo" do |e|
@@ -44,20 +47,15 @@
44
47
 
45
48
  # Phoney check for example
46
49
  if can_stop_watching?(path)
47
- c.cancel(watch)
50
+ watch.cancel
48
51
  end
49
52
  end
50
53
 
51
54
  ## Setting a key (this will trigger the watch above)
52
- c.set "/foo", "zomg!", :missing do |e|
55
+ req = c.set "/foo", "zomg!", :missing do |e|
53
56
  case true
54
57
  when e.mismatch? # CAS mis-match
55
58
  # retry if we must
56
- c.set "/foo", "zomg!", e.cas do |e|
57
- if ! e.ok?
58
- # we give up
59
- end
60
- end
61
59
  when e.ok?
62
60
  e.cas # => "123"
63
61
  else
@@ -65,6 +63,24 @@
65
63
  end
66
64
  end
67
65
 
66
+ req.error do |e|
67
+ # This is the default behavior for fraggle.
68
+ # I'm showing this to bring attention to the use of the
69
+ # error callback.
70
+ raise e.err_detail
71
+ end
72
+
73
+ # Knowning when a command is done is useful in some cases.
74
+ # Use the `done` callback for those situations.
75
+ ents = []
76
+ req = c.getdir("/test") do |e|
77
+ ents << e
78
+ end
79
+
80
+ req.done do
81
+ p ents
82
+ end
83
+
68
84
  end
69
85
 
70
86
 
data/lib/fraggle.rb CHANGED
@@ -1,322 +1,12 @@
1
- require 'beefcake'
2
- require 'eventmachine'
3
- require 'fraggle/proto'
1
+ require 'fraggle/snap'
2
+ require 'uri'
4
3
 
5
4
  module Fraggle
6
5
 
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
6
+ def self.connect(uri, *args)
7
+ uri = URI(uri)
8
+ c = EM.connect(uri.host, uri.port, Client, uri, *args)
9
+ Snap.new(0, c)
320
10
  end
321
11
 
322
12
  end
@@ -0,0 +1,325 @@
1
+ require 'fraggle/logger'
2
+ require 'fraggle/meta'
3
+ require 'fraggle/protocol'
4
+ require 'fraggle/request'
5
+ require 'fraggle/response'
6
+ require 'uri'
7
+
8
+ module Fraggle
9
+
10
+ module Client
11
+ include Protocol
12
+ include Logger
13
+
14
+ class Error < StandardError ; end
15
+
16
+
17
+ MinTag = 0
18
+ MaxTag = (1<<32)
19
+
20
+
21
+ def initialize(uri)
22
+ # Simplied for now. Later we'll take a real uri
23
+ # and disect it to init the addrs list
24
+ uri = URI(uri.to_s)
25
+
26
+ @addr = [uri.host, uri.port] * ":"
27
+ @addrs = {}
28
+ @shun = {}
29
+ @cbx = {}
30
+
31
+ # Logging
32
+ @level = ERROR
33
+ @writer = $stderr
34
+ end
35
+
36
+ def receive_response(res)
37
+ debug "received response: #{res.inspect}"
38
+
39
+ if res.err_code
40
+ if req = @cbx.delete(res.tag)
41
+ req.emit(:error, res)
42
+ return
43
+ end
44
+ end
45
+
46
+ if (res.flags & Response::Flag::VALID) > 0
47
+ if req = @cbx[res.tag]
48
+ req.emit(:valid, res)
49
+ end
50
+ end
51
+
52
+ if (res.flags & Response::Flag::DONE) > 0
53
+ if req = @cbx.delete(res.tag)
54
+ req.emit(:done)
55
+ end
56
+ end
57
+ end
58
+
59
+ def checkin(path, cas, &blk)
60
+ req = Request.new
61
+ req.verb = Request::Verb::CHECKIN
62
+ req.path = path
63
+ req.cas = casify(cas)
64
+
65
+ send(req, &blk)
66
+ end
67
+
68
+ def session(prefix=nil, &blk)
69
+ nibbles = "0123456789abcdef"
70
+ postfix = (0...16).map { nibbles[rand(nibbles.length)].chr }.join
71
+ name = prefix ? prefix+"."+postfix : postfix
72
+ estab = false
73
+
74
+ f = Proc.new do |e|
75
+ # If this is the first response from the server, it's go-time.
76
+ if ! estab
77
+ blk.call
78
+ end
79
+
80
+ # We've successfully established a session. Say so.
81
+ estab = true
82
+
83
+ # Get back to the server ASAP
84
+ checkin(name, e.cas, &f)
85
+ end
86
+
87
+ checkin(name, 0, &f)
88
+ end
89
+
90
+ def get(sid, path, &blk)
91
+ req = Request.new
92
+ req.verb = Request::Verb::GET
93
+ req.id = sid if sid != 0 # wire optimization
94
+ req.path = path
95
+
96
+ send(req, &blk)
97
+ end
98
+
99
+ def stat(sid, path, &blk)
100
+ req = Request.new
101
+ req.verb = Request::Verb::STAT
102
+ req.id = sid if sid != 0 # wire optimization
103
+ req.path = path
104
+
105
+ send(req, &blk)
106
+ end
107
+
108
+ def getdir(sid, path, offset, limit, &blk)
109
+ req = Request.new
110
+ req.verb = Request::Verb::GETDIR
111
+ req.id = sid if sid != 0
112
+ req.offset = offset if offset != 0
113
+ req.limit = limit if limit != 0
114
+ req.path = path
115
+
116
+ send(req, &blk)
117
+ end
118
+
119
+ def set(path, value, cas, &blk)
120
+ req = Request.new
121
+ req.verb = Request::Verb::SET
122
+ req.path = path
123
+ req.value = value
124
+ req.cas = casify(cas)
125
+
126
+ send(req, &blk)
127
+ end
128
+
129
+ def del(path, cas, &blk)
130
+ req = Request.new
131
+ req.verb = Request::Verb::DEL
132
+ req.path = path
133
+ req.cas = casify(cas)
134
+
135
+ send(req, &blk)
136
+ end
137
+
138
+ def walk(sid, glob, &blk)
139
+ req = Request.new
140
+ req.verb = Request::Verb::WALK
141
+ req.id = sid if sid != 0 # wire optimization
142
+ req.path = glob
143
+
144
+ cancelable(send(req, &blk))
145
+ end
146
+
147
+ def watch(glob, &blk)
148
+ req = Request.new
149
+ req.verb = Request::Verb::WATCH
150
+ req.path = glob
151
+
152
+ cancelable(send(req, &blk))
153
+ end
154
+
155
+ def snap(&blk)
156
+ req = Request.new
157
+ req.verb = Request::Verb::SNAP
158
+
159
+ send(req, &blk)
160
+ end
161
+
162
+ def delsnap(sid, &blk)
163
+ req = Request.new
164
+ req.verb = Request::Verb::DELSNAP
165
+ req.id = sid
166
+
167
+ send(req, &blk)
168
+ end
169
+
170
+ def noop(&blk)
171
+ req = Request.new
172
+ req.verb = Request::Verb::NOOP
173
+
174
+ send(req, &blk)
175
+ end
176
+
177
+ # Be careful with this. It is recommended you use #cancel on the Request
178
+ # returned to ensure you don't run into a race-condition where you cancel an
179
+ # operation you may have thought was something else.
180
+ def __cancel__(what, &blk)
181
+ req = Request.new
182
+ req.verb = Request::Verb::CANCEL
183
+ req.id = what.tag
184
+
185
+ # Hold on to the tag as unavaiable for reuse until the cancel succeeds.
186
+ @cbx[what.tag] = nil
187
+
188
+ send(req) do |res|
189
+ # Do not send any more responses from the server to this request.
190
+ @cbx.delete(what.tag)
191
+ blk.call(res) if blk
192
+ end
193
+ end
194
+
195
+ def send(req, &blk)
196
+ tag = MinTag
197
+
198
+ while @cbx.has_key?(tag)
199
+ tag += 1
200
+ if tag > MaxTag
201
+ tag = MinTag
202
+ end
203
+ end
204
+
205
+ req.tag = tag
206
+
207
+ if blk
208
+ req.valid(&blk)
209
+ end
210
+
211
+ # Setup a default error handler that gives useful information
212
+ req.error do |e|
213
+ raise Error.new("'error (%d) (%s)' for: %s" % [
214
+ e.err_code,
215
+ e.err_detail.inspect,
216
+ req.inspect
217
+ ])
218
+ end
219
+
220
+ @cbx[req.tag] = req
221
+
222
+ debug "sending request: #{req.inspect}"
223
+ send_request(req)
224
+
225
+ req
226
+ end
227
+
228
+ def cancelable(req)
229
+ c = self
230
+ can = true
231
+
232
+ req.metadef :cancel do
233
+ if can
234
+ can = false
235
+ c.__cancel__(self)
236
+ end
237
+ end
238
+
239
+ req.metadef :canceled? do
240
+ !can
241
+ end
242
+
243
+ req
244
+ end
245
+
246
+ def post_init
247
+ info "successfully connected to #{@addr}"
248
+
249
+ @last_received = Time.now
250
+
251
+ EM.add_periodic_timer(2) do
252
+ if (n = Time.now - last_received) >= 3
253
+ error("timeout talking to #{@addr}")
254
+ close_connection
255
+ else
256
+ debug("ping")
257
+ get(0, "/ping") { debug("pong") }
258
+ end
259
+ end
260
+
261
+ waw = Proc.new do |e|
262
+ if e.value == ""
263
+ addr = @addrs.delete(e.path)
264
+ if addr
265
+ error "noticed #{addr} is gone; removing"
266
+ end
267
+ else
268
+ get 0, "/doozer/info/#{e.value}/public-addr" do |a|
269
+ if @shun.has_key?(a.value)
270
+ if (n = Time.now - @shun[a.value]) > 3
271
+ info "pardoning #{a.value} after #{n} secs"
272
+ @shun.delete(a.value)
273
+ else
274
+ info "ignoring shunned addr #{a.value}"
275
+ next
276
+ end
277
+ end
278
+ # TODO: Be defensive and check the addr value is valid
279
+ @addrs[e.path] = a.value
280
+ info("added #{e.path} addr #{a.value}")
281
+ end
282
+ end
283
+ end
284
+
285
+ watch "/doozer/slot/*", &waw
286
+ walk 0, "/doozer/slot/*", &waw
287
+ end
288
+
289
+ # What happens when a connection is closed for any reason.
290
+ def unbind
291
+ error "disconnected from #{@addr}"
292
+
293
+ # Shun the address we were currently attempting/connected to.
294
+ @shun[@addr] = Time.now
295
+ @addrs.delete_if {|_, v| v == @addr }
296
+
297
+ # We don't want the timer to race us while
298
+ # we're trying to reconnect. Once the reconnect
299
+ # has been complete, we'll start the timer again.
300
+ EM.cancel_timer(@timer)
301
+
302
+ _, @addr = @addrs.shift rescue nil
303
+
304
+ if ! @addr
305
+ # We are all out of addresses to try
306
+ raise "No more doozers!"
307
+ end
308
+
309
+ host, port = @addr.split(":")
310
+ info "attempting reconnect to #{host}:#{port}"
311
+ reconnect(host, port.to_i)
312
+ post_init
313
+ end
314
+
315
+ def casify(cas)
316
+ case cas
317
+ when :missing then Response::Missing
318
+ when :clobber then Response::Clobber
319
+ else cas
320
+ end
321
+ end
322
+
323
+ end
324
+
325
+ end