ripple 0.5.1 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CONTRIBUTORS.textile +1 -0
- data/README.textile +6 -4
- data/RELEASE_NOTES.textile +20 -0
- data/VERSION +1 -1
- data/lib/riak/bucket.rb +52 -2
- data/lib/riak/client.rb +2 -2
- data/lib/riak/client/curb_backend.rb +42 -23
- data/lib/riak/link.rb +4 -4
- data/lib/riak/map_reduce.rb +9 -1
- data/lib/riak/robject.rb +30 -7
- data/lib/riak/util/fiber1.8.rb +48 -0
- data/lib/riak/walk_spec.rb +1 -1
- data/lib/ripple.rb +3 -1
- data/lib/ripple/document.rb +4 -3
- data/lib/ripple/document/attribute_methods.rb +1 -1
- data/lib/ripple/document/finders.rb +5 -4
- data/lib/ripple/document/timestamps.rb +22 -0
- data/lib/ripple/embedded_document.rb +1 -0
- data/lib/ripple/locale/en.yml +2 -1
- data/ripple.gemspec +6 -2
- data/spec/fixtures/multipart-with-body.txt +2 -2
- data/spec/riak/bucket_spec.rb +79 -8
- data/spec/riak/client_spec.rb +4 -4
- data/spec/riak/http_backend_spec.rb +8 -8
- data/spec/riak/link_spec.rb +12 -12
- data/spec/riak/map_reduce_spec.rb +5 -0
- data/spec/riak/object_spec.rb +54 -16
- data/spec/riak/walk_spec_spec.rb +1 -1
- data/spec/ripple/finders_spec.rb +19 -19
- data/spec/ripple/persistence_spec.rb +4 -4
- data/spec/ripple/ripple_spec.rb +1 -0
- data/spec/ripple/timestamps_spec.rb +49 -0
- data/spec/support/http_backend_implementation_examples.rb +36 -36
- metadata +6 -2
data/CONTRIBUTORS.textile
CHANGED
data/README.textile
CHANGED
@@ -76,15 +76,15 @@ class Email
|
|
76
76
|
property :body, String
|
77
77
|
end
|
78
78
|
|
79
|
-
email = Email.find("37458abc752f8413e") # GET /
|
79
|
+
email = Email.find("37458abc752f8413e") # GET /riak/emails/37458abc752f8413e
|
80
80
|
email.from = "someone@nowhere.net"
|
81
|
-
email.save # PUT /
|
81
|
+
email.save # PUT /riak/emails/37458abc752f8413e
|
82
82
|
|
83
83
|
reply = Email.new
|
84
84
|
reply.from = "justin@bashoooo.com"
|
85
85
|
reply.to = "sean@geeemail.com"
|
86
86
|
reply.body = "Riak is a good fit for scalable Ruby apps."
|
87
|
-
reply.save # POST /
|
87
|
+
reply.save # POST /riak/emails (Riak-assigned key)
|
88
88
|
</pre></notextile>
|
89
89
|
|
90
90
|
h2. How to Contribute
|
@@ -120,7 +120,9 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
120
120
|
See the License for the specific language governing permissions and
|
121
121
|
limitations under the License.
|
122
122
|
|
123
|
-
h2. Auxillary
|
123
|
+
h2. Auxillary Licenses
|
124
124
|
|
125
125
|
The included photo (spec/fixtures/cat.jpg) is Copyright ©2009 "Sean Cribbs":http://seancribbs.com/, and is
|
126
126
|
licensed under the "Creative Commons Attribution Non-Commercial 3.0":http://creativecommons.org/licenses/by-nc/3.0 license. !http://i.creativecommons.org/l/by-nc/3.0/88x31.png!
|
127
|
+
|
128
|
+
The "Poor Man's Fibers" implementation (lib/riak/util/fiber1.8.rb) is Copyright ©2008 Aman Gupta.
|
data/RELEASE_NOTES.textile
CHANGED
@@ -1,5 +1,25 @@
|
|
1
1
|
h1. Ripple Release Notes
|
2
2
|
|
3
|
+
h2. 0.6.0 Feature Release - 2010-03-05
|
4
|
+
|
5
|
+
This release contains enhancements and bugfixes in preparation for the
|
6
|
+
Riak 0.9 release.
|
7
|
+
|
8
|
+
* The CurbBackend now uses fibers to prevent curl-handle corruption when
|
9
|
+
a block is given to streaming operations.
|
10
|
+
* The default prefix is now "/riak/" to match the latest version of Riak.
|
11
|
+
* The client configuration for Ripple is now used.
|
12
|
+
* Added Bucket#new and Bucket#get_or_new for easily creating new objects.
|
13
|
+
* Added Bucket#allow_mult and Bucket#n_value accessors for more easily setting
|
14
|
+
bucket properties.
|
15
|
+
* Added timestamps! method for easily adding created_at/updated_at to documents.
|
16
|
+
[Adam Hunter]
|
17
|
+
* The 'links' collection on RObject is now a Set instead of an Array.
|
18
|
+
* All literal messages are now stored in YAML localization files.
|
19
|
+
* Object siblings (caused by concurrent updates when allow_mult is true) can now
|
20
|
+
be accessed directly.
|
21
|
+
* Map-reduce jobs now have timeouts (in parity with Riak).
|
22
|
+
|
3
23
|
h2. 0.5.1 Patch Release - 2010-02-22
|
4
24
|
|
5
25
|
This is a minor release with fixes for Ruby 1.9, bundler/edge Rails,
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.6.0
|
data/lib/riak/bucket.rb
CHANGED
@@ -34,7 +34,7 @@ module Riak
|
|
34
34
|
def initialize(client, name)
|
35
35
|
raise ArgumentError, t("client_type", :client => client.inspect) unless Client === client
|
36
36
|
raise ArgumentError, t("string_type", :string => name.inspect) unless String === name
|
37
|
-
@client, @name = client, name
|
37
|
+
@client, @name, @props = client, name, {}
|
38
38
|
end
|
39
39
|
|
40
40
|
# Load information for the bucket from a response given by the {Riak::Client::HTTPBackend}.
|
@@ -92,11 +92,61 @@ module Riak
|
|
92
92
|
# @return [Riak::RObject] the object
|
93
93
|
# @raise [FailedRequest] if the object is not found or some other error occurs
|
94
94
|
def get(key, options={})
|
95
|
-
|
95
|
+
code = allow_mult ? [200,300] : 200
|
96
|
+
response = @client.http.get(code, @client.prefix, name, key, options, {})
|
96
97
|
RObject.new(self, key).load(response)
|
97
98
|
end
|
98
99
|
alias :[] :get
|
99
100
|
|
101
|
+
# Create a new blank object
|
102
|
+
# @param [String] key the key of the new object
|
103
|
+
# @return [RObject] the new, unsaved object
|
104
|
+
def new(key=nil)
|
105
|
+
RObject.new(self, key).tap do |obj|
|
106
|
+
obj.content_type = "application/json"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Fetches an object if it exists, otherwise creates a new one with the given key
|
111
|
+
# @param [String] key the key to fetch or create
|
112
|
+
# @return [RObject] the new or existing object
|
113
|
+
def get_or_new(key, options={})
|
114
|
+
begin
|
115
|
+
get(key, options)
|
116
|
+
rescue Riak::FailedRequest => fr
|
117
|
+
if fr.code.to_i == 404
|
118
|
+
new(key)
|
119
|
+
else
|
120
|
+
raise fr
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# @return [true, false] whether the bucket allows divergent siblings
|
126
|
+
def allow_mult
|
127
|
+
props['allow_mult']
|
128
|
+
end
|
129
|
+
|
130
|
+
# Set the allow_mult property. *NOTE* This will result in a PUT request to Riak.
|
131
|
+
# @param [true, false] value whether the bucket should allow siblings
|
132
|
+
def allow_mult=(value)
|
133
|
+
self.props = props.merge('allow_mult' => value)
|
134
|
+
value
|
135
|
+
end
|
136
|
+
|
137
|
+
# @return [Fixnum] the N value, or number of replicas for this bucket
|
138
|
+
def n_value
|
139
|
+
props['n_val']
|
140
|
+
end
|
141
|
+
|
142
|
+
# Set the N value (number of replicas). *NOTE* This will result in a PUT request to Riak.
|
143
|
+
# Setting this value after the bucket has objects stored in it may have unpredictable results.
|
144
|
+
# @param [Fixnum] value the number of replicas the bucket should keep of each object
|
145
|
+
def n_value=(value)
|
146
|
+
self.props = props.merge('n_val' => value)
|
147
|
+
value
|
148
|
+
end
|
149
|
+
|
100
150
|
# @return [String] a representation suitable for IRB and debugging output
|
101
151
|
def inspect
|
102
152
|
"#<Riak::Bucket #{client.http.path(client.prefix, name).to_s}#{" keys=[#{keys.join(',')}]" if defined?(@keys)}>"
|
data/lib/riak/client.rb
CHANGED
@@ -47,7 +47,7 @@ module Riak
|
|
47
47
|
# @param [Hash] options configuration options for the client
|
48
48
|
# @option options [String] :host ('127.0.0.1') The host or IP address for the Riak endpoint
|
49
49
|
# @option options [Fixnum] :port (8098) The port of the Riak HTTP endpoint
|
50
|
-
# @option options [String] :prefix ('/
|
50
|
+
# @option options [String] :prefix ('/riak/') The URL path prefix to the main HTTP endpoint
|
51
51
|
# @option options [String] :mapred ('/mapred') The path to the map-reduce HTTP endpoint
|
52
52
|
# @option options [Fixnum, String] :client_id (rand(MAX_CLIENT_ID)) The internal client ID used by Riak to route responses
|
53
53
|
# @raise [ArgumentError] raised if any options are invalid
|
@@ -56,7 +56,7 @@ module Riak
|
|
56
56
|
self.host = options[:host] || "127.0.0.1"
|
57
57
|
self.port = options[:port] || 8098
|
58
58
|
self.client_id = options[:client_id] || make_client_id
|
59
|
-
self.prefix = options[:prefix] || "/
|
59
|
+
self.prefix = options[:prefix] || "/riak/"
|
60
60
|
self.mapred = options[:mapred] || "/mapred"
|
61
61
|
raise ArgumentError, t("missing_host_and_port") unless @host && @port
|
62
62
|
end
|
@@ -13,6 +13,12 @@
|
|
13
13
|
# limitations under the License.
|
14
14
|
require 'riak'
|
15
15
|
|
16
|
+
begin
|
17
|
+
require 'fiber'
|
18
|
+
rescue LoadError
|
19
|
+
require 'riak/util/fiber1.8'
|
20
|
+
end
|
21
|
+
|
16
22
|
module Riak
|
17
23
|
class Client
|
18
24
|
# An HTTP backend for Riak::Client that uses the 'curb' library/gem.
|
@@ -20,44 +26,57 @@ module Riak
|
|
20
26
|
# the backend based on Net::HTTP.
|
21
27
|
# Conforms to the Riak::Client::HTTPBackend interface.
|
22
28
|
class CurbBackend < HTTPBackend
|
23
|
-
# @private
|
24
|
-
def initialize(client)
|
25
|
-
super
|
26
|
-
@curl = Curl::Easy.new
|
27
|
-
@curl.follow_location = false
|
28
|
-
@curl.on_header do |header_line|
|
29
|
-
@response_headers.parse(header_line)
|
30
|
-
header_line.size
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
29
|
private
|
35
30
|
def perform(method, uri, headers, expect, data=nil)
|
36
31
|
# Setup
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
32
|
+
curl.headers = headers
|
33
|
+
curl.url = uri.to_s
|
34
|
+
response_headers.initialize_http_header(nil)
|
35
|
+
if block_given?
|
36
|
+
_curl = curl
|
37
|
+
Fiber.new {
|
38
|
+
f = Fiber.current
|
39
|
+
_curl.on_body {|chunk| f.resume(chunk); chunk.size }
|
40
|
+
loop do
|
41
|
+
yield Fiber.yield
|
42
|
+
end
|
43
|
+
}.resume
|
44
|
+
else
|
45
|
+
curl.on_body
|
46
|
+
end
|
42
47
|
# Perform
|
43
48
|
case method
|
44
49
|
when :put, :post
|
45
|
-
|
50
|
+
curl.send("http_#{method}", data)
|
46
51
|
else
|
47
|
-
|
52
|
+
curl.send("http_#{method}")
|
48
53
|
end
|
49
54
|
|
50
55
|
# Verify
|
51
|
-
if valid_response?(expect,
|
52
|
-
result = { :headers =>
|
53
|
-
if return_body?(method,
|
54
|
-
result[:body] =
|
56
|
+
if valid_response?(expect, curl.response_code)
|
57
|
+
result = { :headers => response_headers.to_hash, :code => curl.response_code.to_i }
|
58
|
+
if return_body?(method, curl.response_code, block_given?)
|
59
|
+
result[:body] = curl.body_str
|
55
60
|
end
|
56
61
|
result
|
57
62
|
else
|
58
|
-
raise FailedRequest.new(method, expect,
|
63
|
+
raise FailedRequest.new(method, expect, curl.response_code, response_headers.to_hash, curl.body_str)
|
59
64
|
end
|
60
65
|
end
|
66
|
+
|
67
|
+
def curl
|
68
|
+
Thread.current[:curl_easy_handle] ||= Curl::Easy.new.tap do |c|
|
69
|
+
c.follow_location = false
|
70
|
+
c.on_header do |header_line|
|
71
|
+
response_headers.parse(header_line)
|
72
|
+
header_line.size
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def response_headers
|
78
|
+
Thread.current[:response_headers] ||= Riak::Util::Headers.new
|
79
|
+
end
|
61
80
|
end
|
62
81
|
end
|
63
82
|
end
|
data/lib/riak/link.rb
CHANGED
@@ -35,14 +35,14 @@ module Riak
|
|
35
35
|
@url, @rel = url, rel
|
36
36
|
end
|
37
37
|
|
38
|
-
# @return [String] bucket_name, if the Link url is a known Riak link ("/
|
38
|
+
# @return [String] bucket_name, if the Link url is a known Riak link ("/riak/<bucket>/<key>")
|
39
39
|
def bucket
|
40
|
-
$1 if url =~ %r{/
|
40
|
+
$1 if url =~ %r{/riak/([^/]+)/?}
|
41
41
|
end
|
42
42
|
|
43
|
-
# @return [String] key, if the Link url is a known Riak link ("/
|
43
|
+
# @return [String] key, if the Link url is a known Riak link ("/riak/<bucket>/<key>")
|
44
44
|
def key
|
45
|
-
$1 if url =~ %r{/
|
45
|
+
$1 if url =~ %r{/riak/[^/]+/([^/]+)/?}
|
46
46
|
end
|
47
47
|
|
48
48
|
def inspect; to_s; end
|
data/lib/riak/map_reduce.rb
CHANGED
@@ -124,10 +124,18 @@ module Riak
|
|
124
124
|
self
|
125
125
|
end
|
126
126
|
|
127
|
+
# Sets the timeout for the map-reduce job.
|
128
|
+
# @param [Fixnum] value the job timeout, in milliseconds
|
129
|
+
def timeout(value)
|
130
|
+
@timeout = value
|
131
|
+
end
|
132
|
+
|
127
133
|
# Convert the job to JSON for submission over the HTTP interface.
|
128
134
|
# @return [String] the JSON representation
|
129
135
|
def to_json(options={})
|
130
|
-
|
136
|
+
hash = {"inputs" => inputs, "query" => query.map(&:as_json)}
|
137
|
+
hash['timeout'] = @timeout.to_i if @timeout
|
138
|
+
ActiveSupport::JSON.encode(hash, options)
|
131
139
|
end
|
132
140
|
|
133
141
|
# Executes this map-reduce job.
|
data/lib/riak/robject.rb
CHANGED
@@ -12,6 +12,7 @@
|
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
14
|
require 'riak'
|
15
|
+
require 'set'
|
15
16
|
|
16
17
|
module Riak
|
17
18
|
# Parent class of all object types supported by ripple. {Riak::RObject} represents
|
@@ -36,7 +37,7 @@ module Riak
|
|
36
37
|
# @return [Object] the data stored in Riak at this object's key. Varies in format by content-type, defaulting to String from the response body.
|
37
38
|
attr_accessor :data
|
38
39
|
|
39
|
-
# @return [
|
40
|
+
# @return [Set<Link>] an Set of {Riak::Link} objects for relationships between this object and other resources
|
40
41
|
attr_accessor :links
|
41
42
|
|
42
43
|
# @return [String] the ETag header from the most recent HTTP response, useful for caching and reloading
|
@@ -54,7 +55,8 @@ module Riak
|
|
54
55
|
# @see Bucket#get
|
55
56
|
def initialize(bucket, key=nil)
|
56
57
|
@bucket, @key = bucket, key
|
57
|
-
@links, @meta =
|
58
|
+
@links, @meta = Set.new, {}
|
59
|
+
yield self if block_given?
|
58
60
|
end
|
59
61
|
|
60
62
|
# Load object data from an HTTP response
|
@@ -63,7 +65,7 @@ module Riak
|
|
63
65
|
extract_header(response, "location", :key) {|v| URI.unescape(v.split("/").last) }
|
64
66
|
extract_header(response, "content-type", :content_type)
|
65
67
|
extract_header(response, "x-riak-vclock", :vclock)
|
66
|
-
extract_header(response, "link", :links) {|v| Link.parse(v) }
|
68
|
+
extract_header(response, "link", :links) {|v| Set.new(Link.parse(v)) }
|
67
69
|
extract_header(response, "etag", :etag)
|
68
70
|
extract_header(response, "last-modified", :last_modified) {|v| Time.httpdate(v) }
|
69
71
|
@meta = response[:headers].inject({}) do |h,(k,v)|
|
@@ -72,6 +74,8 @@ module Riak
|
|
72
74
|
end
|
73
75
|
h
|
74
76
|
end
|
77
|
+
@conflict = response[:code].try(:to_i) == 300 && content_type =~ /multipart\/mixed/
|
78
|
+
@siblings = nil
|
75
79
|
@data = deserialize(response[:body]) if response[:body].present?
|
76
80
|
self
|
77
81
|
end
|
@@ -126,8 +130,9 @@ module Riak
|
|
126
130
|
def reload(options={})
|
127
131
|
force = options.delete(:force)
|
128
132
|
return self unless @key && (@vclock || force)
|
129
|
-
|
130
|
-
|
133
|
+
codes = @bucket.allow_mult ? [200,300,304] : [200,304]
|
134
|
+
response = @bucket.client.http.get(codes, @bucket.client.prefix, @bucket.name, @key, options, reload_headers)
|
135
|
+
load(response) unless response[:code] == 304
|
131
136
|
self
|
132
137
|
end
|
133
138
|
|
@@ -141,6 +146,24 @@ module Riak
|
|
141
146
|
freeze
|
142
147
|
end
|
143
148
|
|
149
|
+
# Returns sibling objects when in conflict.
|
150
|
+
# @return [Array<RObject>] an array of conflicting sibling objects for this key
|
151
|
+
# @return [self] this object when not in conflict
|
152
|
+
def siblings
|
153
|
+
return self unless conflict?
|
154
|
+
@siblings ||= Multipart.parse(data, Multipart.extract_boundary(content_type)).map do |part|
|
155
|
+
RObject.new(self.bucket, self.key) do |sibling|
|
156
|
+
sibling.load(part)
|
157
|
+
sibling.vclock = vclock
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# @return [true,false] Whether this object has conflicting sibling objects (divergent vclocks)
|
163
|
+
def conflict?
|
164
|
+
@conflict.present?
|
165
|
+
end
|
166
|
+
|
144
167
|
# Serializes the internal object data for sending to Riak. Differs based on the content-type.
|
145
168
|
# This method is called internally when storing the object.
|
146
169
|
# Automatically serialized formats:
|
@@ -207,12 +230,12 @@ module Riak
|
|
207
230
|
[]
|
208
231
|
end
|
209
232
|
end
|
210
|
-
|
233
|
+
|
211
234
|
# Converts the object to a link suitable for linking other objects to it
|
212
235
|
def to_link(tag=nil)
|
213
236
|
Link.new(@bucket.client.http.path(@bucket.client.prefix, @bucket.name, @key).path, tag)
|
214
237
|
end
|
215
|
-
|
238
|
+
|
216
239
|
private
|
217
240
|
def extract_header(response, name, attribute=nil)
|
218
241
|
if response[:headers][name].present?
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# Poor Man's Fiber (API compatible Thread based Fiber implementation for Ruby 1.8)
|
2
|
+
# (c) 2008 Aman Gupta (tmm1)
|
3
|
+
|
4
|
+
unless defined? Fiber
|
5
|
+
require 'thread'
|
6
|
+
|
7
|
+
class FiberError < StandardError; end
|
8
|
+
|
9
|
+
class Fiber
|
10
|
+
def initialize
|
11
|
+
raise ArgumentError, 'new Fiber requires a block' unless block_given?
|
12
|
+
|
13
|
+
@yield = Queue.new
|
14
|
+
@resume = Queue.new
|
15
|
+
|
16
|
+
@thread = Thread.new{ @yield.push [ *yield(*@resume.pop) ] }
|
17
|
+
@thread.abort_on_exception = true
|
18
|
+
@thread[:fiber] = self
|
19
|
+
end
|
20
|
+
attr_reader :thread
|
21
|
+
|
22
|
+
def resume *args
|
23
|
+
raise FiberError, 'dead fiber called' unless @thread.alive?
|
24
|
+
@resume.push(args)
|
25
|
+
result = @yield.pop
|
26
|
+
result.size > 1 ? result : result.first
|
27
|
+
end
|
28
|
+
|
29
|
+
def yield *args
|
30
|
+
@yield.push(args)
|
31
|
+
result = @resume.pop
|
32
|
+
result.size > 1 ? result : result.first
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.yield *args
|
36
|
+
raise FiberError, "can't yield from root fiber" unless fiber = Thread.current[:fiber]
|
37
|
+
fiber.yield(*args)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.current
|
41
|
+
Thread.current[:fiber] or raise FiberError, 'not inside a fiber'
|
42
|
+
end
|
43
|
+
|
44
|
+
def inspect
|
45
|
+
"#<#{self.class}:0x#{self.object_id.to_s(16)}>"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/riak/walk_spec.rb
CHANGED
@@ -18,7 +18,7 @@ module Riak
|
|
18
18
|
# The specification of how to follow links from one object to another in Riak,
|
19
19
|
# when using the link-walker resource.
|
20
20
|
# Example link-walking operation:
|
21
|
-
# GET /
|
21
|
+
# GET /riak/artists/REM/albums,_,_/tracks,_,1
|
22
22
|
# This operation would have two WalkSpecs:
|
23
23
|
# Riak::WalkSpec.new({:bucket => 'albums'})
|
24
24
|
# Riak::WalkSpec.new({:bucket => 'tracks', :result => true})
|
data/lib/ripple.rb
CHANGED
@@ -28,10 +28,12 @@ module Ripple
|
|
28
28
|
autoload :PropertyTypeMismatch
|
29
29
|
autoload :Translation
|
30
30
|
|
31
|
+
DEFAULT_CONFIG = {}
|
32
|
+
|
31
33
|
class << self
|
32
34
|
# @return [Riak::Client] The client for the current thread.
|
33
35
|
def client
|
34
|
-
Thread.current[:ripple_client] ||= Riak::Client.new
|
36
|
+
Thread.current[:ripple_client] ||= Riak::Client.new(config)
|
35
37
|
end
|
36
38
|
|
37
39
|
# Sets the client for the current thread.
|