fraggle 0.1.1 → 0.2.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/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