fraggle-spanx 4.0.1.1
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/.gitignore +5 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +223 -0
- data/Rakefile +17 -0
- data/bench/gs.rb +87 -0
- data/example/getdir.rb +29 -0
- data/example/readme.rb +82 -0
- data/example/walk.rb +33 -0
- data/example/watch.rb +16 -0
- data/fraggle.gemspec +28 -0
- data/lib/fraggle.rb +59 -0
- data/lib/fraggle/client.rb +211 -0
- data/lib/fraggle/connection.rb +124 -0
- data/lib/fraggle/msg.pb.rb +60 -0
- data/lib/fraggle/response.rb +28 -0
- data/lib/fraggle/version.rb +3 -0
- data/test/fraggle_client_test.rb +238 -0
- data/test/fraggle_protocol_test.rb +88 -0
- data/test/fraggle_test.rb +12 -0
- data/test/fraggle_transaction_test.rb +116 -0
- data/test/helper.rb +78 -0
- metadata +135 -0
data/Gemfile
ADDED
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,223 @@
|
|
1
|
+
# Fraggle
|
2
|
+
## The current gem is v4.0.0.pre.1 is compatible with Doozer 0.6
|
3
|
+
|
4
|
+
Please see the [4.0.0.pre.1 README](https://github.com/ha/fraggle/tree/v4.0.0.pre.1) for instructions on use.
|
5
|
+
|
6
|
+
**An EventMachine based Doozer client**
|
7
|
+
|
8
|
+
## Install
|
9
|
+
|
10
|
+
(For pre-releases, use `--pre`)
|
11
|
+
|
12
|
+
$ gem install fraggle
|
13
|
+
|
14
|
+
## Use
|
15
|
+
|
16
|
+
*Connecting to a cluster*
|
17
|
+
|
18
|
+
Use `Fraggle.connect`. It takes an optional [doozer uri][] (String). If no
|
19
|
+
parameters are given, it will use the DOOZER_URI` environment variable if
|
20
|
+
present, otherwise it will default to the uri containing the default doozer
|
21
|
+
addresses with IP 127.0.0.1 and ports 8046, 8041, 8042, 8043.
|
22
|
+
|
23
|
+
*simple example*
|
24
|
+
|
25
|
+
require 'rubygems'
|
26
|
+
require 'eventmachine'
|
27
|
+
require 'fraggle'
|
28
|
+
|
29
|
+
EM.run do
|
30
|
+
# In the event of a lost connection, fraggle will attempt
|
31
|
+
# other doozers until one accepts or it runs out of options; A NoAddrs
|
32
|
+
# exception will be raised if that later happens.
|
33
|
+
|
34
|
+
Fraggle.connect do |c, err|
|
35
|
+
if err
|
36
|
+
raise err.message
|
37
|
+
end
|
38
|
+
|
39
|
+
c.rev do |v|
|
40
|
+
c.get(v, "/foo") do |e, err|
|
41
|
+
if err
|
42
|
+
err.code # => nil
|
43
|
+
err.detail # => nil
|
44
|
+
else
|
45
|
+
e.value # => nil
|
46
|
+
e.rev # => 0
|
47
|
+
e.missing? # => true
|
48
|
+
end
|
49
|
+
|
50
|
+
p [:get, e, err]
|
51
|
+
end
|
52
|
+
|
53
|
+
## Obtain the current revision the store is at and watch from then on for
|
54
|
+
## any SET or DEL to /foo.
|
55
|
+
c.wait(v, "/foo") do |e, err|
|
56
|
+
# The event has:
|
57
|
+
# ------------------------
|
58
|
+
if err
|
59
|
+
err.code # => nil
|
60
|
+
err.detail # => nil
|
61
|
+
else
|
62
|
+
e.path # => "/foo"
|
63
|
+
e.value # => "zomg!"
|
64
|
+
e.rev # => 123
|
65
|
+
e.set? # => true
|
66
|
+
e.del? # => false
|
67
|
+
end
|
68
|
+
|
69
|
+
p [:wait, e, err]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
## Setting a key (this will trigger the watch above)
|
74
|
+
f = Proc.new do |e, err|
|
75
|
+
p [:e, e, err]
|
76
|
+
|
77
|
+
# This case statement is undesirable. We're working
|
78
|
+
# on better error handling.
|
79
|
+
case (err && err.code rescue err)
|
80
|
+
when Fraggle::Connection::DisconnectedError
|
81
|
+
# Fraggle (for now) does not attempt a non-idempotent request. This means
|
82
|
+
# Fraggle will hand off the error to the user if there is a SET or DEL
|
83
|
+
# with rev 0 (missing) and delete it during the time we may be
|
84
|
+
# disconnected.
|
85
|
+
#
|
86
|
+
# In this scenario, there are no other clients that can exist that will
|
87
|
+
# attempt to set this "lock" if it's missing then delete it. It is safe
|
88
|
+
# for us to resend the request if we were disconnected from the previous
|
89
|
+
# server before a response.
|
90
|
+
#
|
91
|
+
# See High-Availability in the README for more information about this.
|
92
|
+
#
|
93
|
+
c.set(0, "/foo", "zomg!", &f)
|
94
|
+
when Fraggle::REV_MISMATCH
|
95
|
+
p :not_it
|
96
|
+
when nil
|
97
|
+
# Success!
|
98
|
+
p [:it, e]
|
99
|
+
else
|
100
|
+
fail "something bad happened: " + err.inspect
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
c.set(0, "/foo", "zomg!", &f)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
## Consistency
|
109
|
+
|
110
|
+
Fraggle read commands take a `rev`. If no rev is given, Doozer will reply with
|
111
|
+
the most up-to-date data. If you need to do multiple reads at certain
|
112
|
+
point in time for consistency, use the `rev` command.
|
113
|
+
|
114
|
+
c.rev do |v|
|
115
|
+
c.get(v, "/a") { ... }
|
116
|
+
c.get(v, "/b") { ... }
|
117
|
+
c.get(v, "/c") { ... }
|
118
|
+
end
|
119
|
+
|
120
|
+
This also means you can go back in time or into the future!
|
121
|
+
|
122
|
+
# This will not yield until the data store is at revision 100,000
|
123
|
+
c.get(100_000, "/a") { ... }
|
124
|
+
|
125
|
+
NOTE: Doozer's data store is a [persistent data structure][pd]. You can reference the
|
126
|
+
stores history as far back as it is configured to hold it. The default is
|
127
|
+
360,000 revisions. See [data model][] for more information.
|
128
|
+
|
129
|
+
## High Availability
|
130
|
+
|
131
|
+
Fraggle has mechanisms to gracefully deal with connection loss. They are:
|
132
|
+
|
133
|
+
*Resend / Connection loss*
|
134
|
+
|
135
|
+
When a connection is lost and Fraggle successfully reconnects to another
|
136
|
+
Doozer node, Fraggle will resend most pending requests to the new connection.
|
137
|
+
This means you will not miss events; Even events that happened while you were
|
138
|
+
disconnected! All read commands will pick up where they left off. This is
|
139
|
+
valuable to understand because it means you don't need to code for failure on
|
140
|
+
reads; Fraggle gracefully handles it for you.
|
141
|
+
|
142
|
+
Write commands will be resent if their `rev` is greater than 0. These are
|
143
|
+
idempotent requests. A rev of 0 will cause that request's error
|
144
|
+
callback to be invoked with a Fraggle::Connection::Disconnected response.
|
145
|
+
You will have to handle these yourself because Fraggle cannot know whether or
|
146
|
+
not it's safe to retry on your behalf.
|
147
|
+
|
148
|
+
**attempt**
|
149
|
+
|
150
|
+
Before fraggle will attempt a new address after connection loss, it calls the
|
151
|
+
block given to `Fraggle::Client#attempt`. If the block returns `false`,
|
152
|
+
Fraggle will not attempt that address or anymore. The block is called with on
|
153
|
+
parameter `addr`, which is the address being attempted.
|
154
|
+
|
155
|
+
Example:
|
156
|
+
|
157
|
+
c = Fraggle.connect
|
158
|
+
|
159
|
+
c.attempt do |addr|
|
160
|
+
addr =~ /^127\.*$/ # don't connect to localhost doozers
|
161
|
+
end
|
162
|
+
|
163
|
+
The default `attempt` is `Proc.new {|_| true }`
|
164
|
+
|
165
|
+
## Commands
|
166
|
+
|
167
|
+
Each command below behaves according to the [proto spec][], respectively.
|
168
|
+
Their `blk`s are called with two parameters, a `Fraggle::Response` as the first
|
169
|
+
or a `Fraggle::Connection::ResponseError` as the second if a response is
|
170
|
+
returned from the server.
|
171
|
+
|
172
|
+
`set(rev, path, value, &blk)`
|
173
|
+
|
174
|
+
`del(rev, path, &blk)`
|
175
|
+
|
176
|
+
`get(rev, path, &blk)`
|
177
|
+
|
178
|
+
`wait(rev, path, &blk)`
|
179
|
+
|
180
|
+
`rev(&blk)`
|
181
|
+
|
182
|
+
`stat(rev, path, &blk)`
|
183
|
+
|
184
|
+
## Sugar commands
|
185
|
+
|
186
|
+
`watch(rev, path, &blk)`
|
187
|
+
|
188
|
+
Watches `path` (a glob pattern) for changes, from `rev` in history on. Its
|
189
|
+
`blk` is called with a `Fraggle::Response` for each event.
|
190
|
+
|
191
|
+
`getdir(rev, path, off=0, lim=MaxInt64, ents=[], &blk)`
|
192
|
+
|
193
|
+
Behaves like `getdir` but collects `ents`, starting at `off` until all or `lim`
|
194
|
+
entries are read. When done, `blk` is called with the result (an `Array`) as the
|
195
|
+
first parameter or a `Fraggle::Connection::Response` as the second. Depending
|
196
|
+
on the response, one or the other will be set and the other with be `nil`.
|
197
|
+
|
198
|
+
`walk(rev, path, off=0, lim=MaxInt64, ents=[], &blk)`
|
199
|
+
|
200
|
+
Like `getdir`, but but path is a glob pattern and each result contains a `path`,
|
201
|
+
`value`, and `rev`.
|
202
|
+
|
203
|
+
## Dev
|
204
|
+
|
205
|
+
**Clone**
|
206
|
+
|
207
|
+
$ git clone http://github.com/ha/fraggle.git
|
208
|
+
|
209
|
+
**Test**
|
210
|
+
|
211
|
+
$ gem install turn
|
212
|
+
|
213
|
+
$ turn
|
214
|
+
|
215
|
+
**Mailing List**
|
216
|
+
|
217
|
+
Please join the Doozer mailing list for help:
|
218
|
+
http://groups.google.com/forum/#!forum/doozer
|
219
|
+
|
220
|
+
[data model]: https://github.com/ha/doozerd/blob/master/doc/data-model.md
|
221
|
+
[doozer uri]: https://github.com/ha/doozerd/blob/master/doc/uri.md
|
222
|
+
[proto spec]: https://github.com/ha/doozerd/blob/master/doc/proto.md
|
223
|
+
[pd]: http://en.wikipedia.org/wiki/Persistent_data_structure
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler::GemHelper.install_tasks
|
3
|
+
|
4
|
+
HOME = ENV["HOME"]
|
5
|
+
PBDIR = HOME+"/src/doozerd/src/pkg/server"
|
6
|
+
|
7
|
+
namespace :proto do
|
8
|
+
task :update do
|
9
|
+
ENV["BEEFCAKE_NAMESPACE"] = "Fraggle"
|
10
|
+
sh(
|
11
|
+
"protoc",
|
12
|
+
"--beefcake_out", "lib/fraggle",
|
13
|
+
"-I", PBDIR,
|
14
|
+
PBDIR+"/msg.proto"
|
15
|
+
)
|
16
|
+
end
|
17
|
+
end
|
data/bench/gs.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# By Mark McGranaghan
|
4
|
+
|
5
|
+
require "rubygems"
|
6
|
+
require "bundler"
|
7
|
+
Bundler.setup
|
8
|
+
require "fraggle"
|
9
|
+
require "statsample"
|
10
|
+
|
11
|
+
$stdout.sync = true
|
12
|
+
|
13
|
+
abort("gs <[get|set]> <total> <width> [verbose]") if (ARGV.size < 2)
|
14
|
+
op = ARGV.shift.to_sym
|
15
|
+
total = ARGV.shift.to_i
|
16
|
+
width = ARGV.shift.to_i
|
17
|
+
verbose = !!ARGV.shift
|
18
|
+
latencies = []
|
19
|
+
sent_at = nil
|
20
|
+
|
21
|
+
EM.run do
|
22
|
+
Fraggle.connect do |c, err|
|
23
|
+
if err
|
24
|
+
raise err.message
|
25
|
+
end
|
26
|
+
|
27
|
+
sent = 0
|
28
|
+
received = 0
|
29
|
+
start = Time.now
|
30
|
+
|
31
|
+
f = Proc.new do |r, err|
|
32
|
+
if err
|
33
|
+
p [:err, err]
|
34
|
+
next
|
35
|
+
end
|
36
|
+
|
37
|
+
received_at = Time.now
|
38
|
+
received +=1
|
39
|
+
latency = received_at - sent_at
|
40
|
+
|
41
|
+
latencies << latency
|
42
|
+
if verbose
|
43
|
+
$stdout.puts("received=#{received} ok=#{r.ok?} rev=#{r.rev} latency=#{latency}")
|
44
|
+
elsif (received % 10 == 0)
|
45
|
+
$stdout.print(".")
|
46
|
+
end
|
47
|
+
if (received == total)
|
48
|
+
EM.stop
|
49
|
+
elapsed = Time.now - start
|
50
|
+
vector = latencies.to_scale
|
51
|
+
$stdout.puts
|
52
|
+
$stdout.puts("total=#{total}")
|
53
|
+
$stdout.puts("elapsed=#{elapsed}")
|
54
|
+
$stdout.puts("rate=#{total / elapsed}")
|
55
|
+
$stdout.puts("mean=#{vector.mean}")
|
56
|
+
$stdout.puts("sd=#{vector.sd}")
|
57
|
+
$stdout.puts("perc90=#{vector.percentil(90)}")
|
58
|
+
$stdout.puts("perc99=#{vector.percentil(99)}")
|
59
|
+
$stdout.puts("max=#{vector.max}")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
tick = Proc.new do
|
64
|
+
if (sent == total)
|
65
|
+
# done sending
|
66
|
+
elsif ((sent - received) < width)
|
67
|
+
# pipe open
|
68
|
+
sent_at = Time.now
|
69
|
+
sent += 1
|
70
|
+
if verbose
|
71
|
+
$stdout.puts("sent=#{sent}")
|
72
|
+
end
|
73
|
+
|
74
|
+
case op
|
75
|
+
when :get
|
76
|
+
c.get(nil, "/processes/#{sent}", &f)
|
77
|
+
when :set
|
78
|
+
c.set(Fraggle::Clobber, "/processes/#{sent}", "1", &f)
|
79
|
+
end
|
80
|
+
else
|
81
|
+
# pipe closed
|
82
|
+
end
|
83
|
+
EM.next_tick(&tick)
|
84
|
+
end
|
85
|
+
tick.call
|
86
|
+
end
|
87
|
+
end
|
data/example/getdir.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'fraggle'
|
3
|
+
|
4
|
+
EM.run do
|
5
|
+
c = Fraggle.connect do |c, err|
|
6
|
+
c.rev do |v|
|
7
|
+
# Valid
|
8
|
+
req = c.getdir(v, "/ctl/node") do |ents, err|
|
9
|
+
if err
|
10
|
+
p [:err, err]
|
11
|
+
else
|
12
|
+
ents.each do |e|
|
13
|
+
puts File.join(req.path, e.path)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Limit 0 return nothing
|
19
|
+
c.getdir(v, "/ctl/node", 0, 0) do |ents, err|
|
20
|
+
p [:ret, ents, err]
|
21
|
+
end
|
22
|
+
|
23
|
+
# Error
|
24
|
+
c.getdir(v, "/nothere") do |ents, err|
|
25
|
+
p [:ret, ents, err]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/example/readme.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'eventmachine'
|
3
|
+
require 'fraggle'
|
4
|
+
|
5
|
+
EM.run do
|
6
|
+
# In the event of a lost connection, fraggle will attempt
|
7
|
+
# other doozers until one accepts or it runs out of options; A NoAddrs
|
8
|
+
# exception will be raised if that later happens.
|
9
|
+
|
10
|
+
Fraggle.connect do |c, err|
|
11
|
+
if err
|
12
|
+
raise err.message
|
13
|
+
end
|
14
|
+
|
15
|
+
c.rev do |v|
|
16
|
+
c.get(v, "/foo") do |e, err|
|
17
|
+
if err
|
18
|
+
err.code # => nil
|
19
|
+
err.detail # => nil
|
20
|
+
else
|
21
|
+
e.value # => nil
|
22
|
+
e.rev # => 0
|
23
|
+
e.missing? # => true
|
24
|
+
end
|
25
|
+
|
26
|
+
p [:get, e, err]
|
27
|
+
end
|
28
|
+
|
29
|
+
## Obtain the current revision the store is at and watch from then on for
|
30
|
+
## any SET or DEL to /foo.
|
31
|
+
c.wait(v, "/foo") do |e, err|
|
32
|
+
# The event has:
|
33
|
+
# ------------------------
|
34
|
+
if err
|
35
|
+
err.code # => nil
|
36
|
+
err.detail # => nil
|
37
|
+
else
|
38
|
+
e.path # => "/foo"
|
39
|
+
e.value # => "zomg!"
|
40
|
+
e.rev # => 123
|
41
|
+
e.set? # => true
|
42
|
+
e.del? # => false
|
43
|
+
end
|
44
|
+
|
45
|
+
p [:wait, e, err]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
## Setting a key (this will trigger the watch above)
|
50
|
+
f = Proc.new do |e, err|
|
51
|
+
p [:e, e, err]
|
52
|
+
|
53
|
+
# This case statement is undesirable. We're working
|
54
|
+
# on better error handling.
|
55
|
+
case (err && err.code rescue err)
|
56
|
+
when Fraggle::Connection::DisconnectedError
|
57
|
+
# Fraggle (for now) does not attempt a non-idempotent request. This means
|
58
|
+
# Fraggle will hand off the error to the user if there is a SET or DEL
|
59
|
+
# with rev 0 (missing) and delete it during the time we may be
|
60
|
+
# disconnected.
|
61
|
+
#
|
62
|
+
# In this scenario, there are no other clients that can exist that will
|
63
|
+
# attempt to set this "lock" if it's missing then delete it. It is safe
|
64
|
+
# for us to resend the request if we were disconnected from the previous
|
65
|
+
# server before a response.
|
66
|
+
#
|
67
|
+
# See High-Availability in the README for more information about this.
|
68
|
+
#
|
69
|
+
c.set(0, "/foo", "zomg!", &f)
|
70
|
+
when Fraggle::REV_MISMATCH
|
71
|
+
p :not_it
|
72
|
+
when nil
|
73
|
+
# Success!
|
74
|
+
p [:it, e]
|
75
|
+
else
|
76
|
+
fail "something bad happened: " + err.inspect
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
c.set(0, "/foo", "zomg!", &f)
|
81
|
+
end
|
82
|
+
end
|