blodsband 0.0.2
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/lib/blodsband.rb +27 -0
- data/lib/blodsband/error.rb +18 -0
- data/lib/blodsband/future.rb +31 -0
- data/lib/blodsband/multi.rb +37 -0
- data/lib/blodsband/riak.rb +48 -0
- data/lib/blodsband/riak/bucket.rb +1169 -0
- data/lib/blodsband/riak/counter.rb +77 -0
- data/lib/blodsband/riak/list.rb +880 -0
- data/lib/blodsband/riak/lock.rb +68 -0
- data/lib/blodsband/riak/map.rb +143 -0
- data/lib/blodsband/riak/mr.rb +103 -0
- data/lib/blodsband/riak/response.rb +208 -0
- data/lib/blodsband/riak/search.rb +233 -0
- data/lib/blodsband/riak/sset.rb +104 -0
- metadata +107 -0
data/lib/blodsband.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
|
2
|
+
require 'uri'
|
3
|
+
require 'em-synchrony'
|
4
|
+
require 'em-synchrony/em-http'
|
5
|
+
require 'em-http/middleware/json_response'
|
6
|
+
require 'yajl'
|
7
|
+
require 'mail'
|
8
|
+
require 'date'
|
9
|
+
require 'cgi'
|
10
|
+
require 'rexml/document'
|
11
|
+
require 'digest/sha1'
|
12
|
+
|
13
|
+
$LOAD_PATH.unshift(File.expand_path('lib'))
|
14
|
+
|
15
|
+
require 'blodsband/future'
|
16
|
+
require 'blodsband/multi'
|
17
|
+
require 'blodsband/error'
|
18
|
+
require 'blodsband/riak'
|
19
|
+
require 'blodsband/riak/mr'
|
20
|
+
require 'blodsband/riak/response'
|
21
|
+
require 'blodsband/riak/lock'
|
22
|
+
require 'blodsband/riak/list'
|
23
|
+
require 'blodsband/riak/map'
|
24
|
+
require 'blodsband/riak/sset'
|
25
|
+
require 'blodsband/riak/bucket'
|
26
|
+
require 'blodsband/riak/counter'
|
27
|
+
require 'blodsband/riak/search'
|
@@ -0,0 +1,18 @@
|
|
1
|
+
|
2
|
+
module Blodsband
|
3
|
+
|
4
|
+
#
|
5
|
+
# A class that collects a single HTTP based error.
|
6
|
+
#
|
7
|
+
class Error < RuntimeError
|
8
|
+
attr_reader :response, :status
|
9
|
+
def initialize(http)
|
10
|
+
@status = http.response_header.status
|
11
|
+
@response = http.response
|
12
|
+
end
|
13
|
+
def to_s
|
14
|
+
"#{@response}: #{@status}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
|
2
|
+
module Blodsband
|
3
|
+
|
4
|
+
#
|
5
|
+
# A class that encapsulates an asynchronous operation.
|
6
|
+
#
|
7
|
+
# Used to avoid the callback carbonara usually produced when relying heavily on asynchronous code.
|
8
|
+
#
|
9
|
+
class Future
|
10
|
+
|
11
|
+
#
|
12
|
+
# Create a future that will run a block upon request.
|
13
|
+
#
|
14
|
+
# @param [block] block the block to run when {#get} is called.
|
15
|
+
#
|
16
|
+
def initialize(&block)
|
17
|
+
@block = block
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
# Get the result from the asynchronous operation.
|
22
|
+
#
|
23
|
+
# @return [Object] whatever the block given in {#initialize} produced.
|
24
|
+
#
|
25
|
+
def get
|
26
|
+
@value ||= @block.call
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
|
2
|
+
module Blodsband
|
3
|
+
|
4
|
+
class Multi < EM::Synchrony::Multi
|
5
|
+
|
6
|
+
#
|
7
|
+
# An error that collects all errors in a single {Blodsband::Multi} and renders them in a readable way.
|
8
|
+
#
|
9
|
+
class Error < RuntimeError
|
10
|
+
def initialize(multi)
|
11
|
+
@bad = {}
|
12
|
+
@bad.merge!(multi.responses[:errback])
|
13
|
+
multi.responses[:callback].each do |key, value|
|
14
|
+
@bad[key] = value unless [200,204].include?(value.response_header.status)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
def to_s
|
18
|
+
@bad.values.inject([]) do |sum, http|
|
19
|
+
if http.response_header.status == 0
|
20
|
+
sum + ["Connection timed out: #{http.req.uri}"]
|
21
|
+
else
|
22
|
+
sum + ["#{http.response_header.status}: #{http.response}"]
|
23
|
+
end
|
24
|
+
end.join("\n")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# Does exactly what {::EventMachine::Synchrony::Multi} does, but keeps waiting until it is really REALLY finished (and not just until it just happens to get scheduled again).
|
30
|
+
#
|
31
|
+
def really_perform
|
32
|
+
perform while !finished?
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Blodsband
|
2
|
+
|
3
|
+
class Riak
|
4
|
+
|
5
|
+
#
|
6
|
+
# Initialize a Riak client.
|
7
|
+
#
|
8
|
+
# @param [URI] url a url pointing to the HTML port of a Riak node.
|
9
|
+
#
|
10
|
+
def initialize(url)
|
11
|
+
@url = url
|
12
|
+
end
|
13
|
+
|
14
|
+
#
|
15
|
+
# Get a {Blodsband::Riak::Bucket}.
|
16
|
+
#
|
17
|
+
# @param [String] name see {Blodsband::Riak::Bucket#initialize}
|
18
|
+
# @param [Hash] options see {Blodsband::Riak::Bucket#initialize}
|
19
|
+
#
|
20
|
+
# @return [Blodsband::Riak::Bucket] a Bucket with the provided configuration and the {::URI} of this {Blodsband::Riak}.
|
21
|
+
#
|
22
|
+
def bucket(name, options = {})
|
23
|
+
Bucket.new(@url, name, options)
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# Get a {Blodsband::Riak::Search}
|
28
|
+
#
|
29
|
+
# @param [URI] url a url pointing to the HTML port of a Riak node.
|
30
|
+
#
|
31
|
+
# @return [Blodsband::Riak::Search] a search instance initialized with the {::URI} of this {Blodsband::Riak}.
|
32
|
+
#
|
33
|
+
def search
|
34
|
+
Search.new(@url)
|
35
|
+
end
|
36
|
+
|
37
|
+
#
|
38
|
+
# Get a map/reduce helper instance.
|
39
|
+
#
|
40
|
+
# @return [Blodsband::Riak::Mr] a {Blodsband::Riak::Mr} with the {::URI} of this {Blodsband::Riak}.
|
41
|
+
#
|
42
|
+
def mr
|
43
|
+
Mr.new(@url)
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,1169 @@
|
|
1
|
+
|
2
|
+
module Blodsband
|
3
|
+
|
4
|
+
class Riak
|
5
|
+
|
6
|
+
#
|
7
|
+
# Encapsulates functionality of Riak buckets.
|
8
|
+
#
|
9
|
+
class Bucket
|
10
|
+
|
11
|
+
#
|
12
|
+
# [String] The name of the Riak bucket this {Blodsband::Riak::Bucket} refers to.
|
13
|
+
#
|
14
|
+
attr_reader :name
|
15
|
+
#
|
16
|
+
# [Hash<Symbol, Object>] The default options for this {Blodsband::Riak::Bucket}
|
17
|
+
#
|
18
|
+
attr_reader :defaults
|
19
|
+
|
20
|
+
#
|
21
|
+
# Create a {Blodsband::Riak::Bucket}.
|
22
|
+
#
|
23
|
+
# @param [::URI] url a url pointing to the HTML port of a Riak node.
|
24
|
+
# @param [String] name the name of the bucket.
|
25
|
+
# @param [Hash<Symbol, Object>] options
|
26
|
+
# :client_id:: [String] <code>client_id</code> to use when communicating with Riak. Will default to a random 256 bit string.
|
27
|
+
# :unique:: [true, false] if this bucket should always default to <code>:unique => true</code> when doing {Blodsband::Riak::Bucket#get}. Will default to <code>false</code>.
|
28
|
+
#
|
29
|
+
def initialize(url, name, options = {})
|
30
|
+
@url = url
|
31
|
+
@name = name
|
32
|
+
@defaults = options.merge(:client_id => rand(1 << 256).to_s(36))
|
33
|
+
raise "Unknown options to #{self.class}#initialize: #{options}" unless options.empty?
|
34
|
+
end
|
35
|
+
|
36
|
+
#
|
37
|
+
# Sets the {Blodsband::Riak::Bucket} property <code>:allow_mult</code> to <code>true</code>
|
38
|
+
# and the default option <code>:unique</code> to <code>true</code>.
|
39
|
+
#
|
40
|
+
# @return [Blodsband::Riak::Bucket] this same bucket with the new options set.
|
41
|
+
#
|
42
|
+
def unique
|
43
|
+
@defaults[:unique] = true
|
44
|
+
self.props = {:allow_mult => true}
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
#
|
49
|
+
# Sets the {Blodsband::Riak::Bucket} property <code>:search</code> to <code>true</code>.
|
50
|
+
#
|
51
|
+
# @return [Blodsband::Riak::Bucket] this same bucket.
|
52
|
+
#
|
53
|
+
def indexed
|
54
|
+
self.props = {:search => true}
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
#
|
59
|
+
# @return [Hash<Symbol, Object>] the properties of this Riak bucket.
|
60
|
+
#
|
61
|
+
def props
|
62
|
+
Yajl::Parser.parse(EM::HttpRequest.new(uri).get.response)['props']
|
63
|
+
end
|
64
|
+
|
65
|
+
#
|
66
|
+
# Sets the properties of this Riak bucket.
|
67
|
+
# Will merge in the given properties with the existing ones, so a complete property {::Hash} is not necessary.
|
68
|
+
#
|
69
|
+
# @param [Hash<Symbol, Object>] p the new properties.
|
70
|
+
#
|
71
|
+
def props=(p)
|
72
|
+
EM::HttpRequest.new(uri).put(:head => {"Content-Type" => "application/json"},
|
73
|
+
:body => Yajl::Encoder.encode(:props => p))
|
74
|
+
end
|
75
|
+
|
76
|
+
#
|
77
|
+
# {include:Bucket#delete}
|
78
|
+
#
|
79
|
+
# @param (see #delete)
|
80
|
+
#
|
81
|
+
# @return [Blodsband::Future<Blodsband::Riak::Response>] the eventual response from Riak for the resulting HTTP request.
|
82
|
+
#
|
83
|
+
def adelete(key, options = {})
|
84
|
+
riak_params = options.delete(:riak_params)
|
85
|
+
m = Multi.new
|
86
|
+
url = uri(key)
|
87
|
+
url += "?#{riak_params.collect do |k,v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" end.join("&")}" if riak_params
|
88
|
+
m.add(:resp, EM::HttpRequest.new(url).adelete)
|
89
|
+
return(Future.new do
|
90
|
+
m.really_perform
|
91
|
+
Response.parse(m)
|
92
|
+
end)
|
93
|
+
end
|
94
|
+
|
95
|
+
#
|
96
|
+
# Delete a key in Riak.
|
97
|
+
#
|
98
|
+
# @param [String] key the key to delete.
|
99
|
+
# @param [Hash<Symbol, Object>] options
|
100
|
+
# :riak_params:: [Hash<String, String>] <code>param_name => param_value</code> of extra parameters to send to Riak when doing the HTTP request.
|
101
|
+
#
|
102
|
+
# @return [Blodsband::Riak::Response] the response from Riak for the resulting HTTP request.
|
103
|
+
#
|
104
|
+
def delete(key, options = {})
|
105
|
+
adelete(key, options).get
|
106
|
+
end
|
107
|
+
|
108
|
+
#
|
109
|
+
# {include:Bucket#put}
|
110
|
+
#
|
111
|
+
# @param [String] key the key to put the value under.
|
112
|
+
# @param [Object] value the value to put under the key.
|
113
|
+
#
|
114
|
+
# @return [Blodsband::Riak::Response] the response from Riak for the resulting HTTP request.
|
115
|
+
#
|
116
|
+
def []=(key, value)
|
117
|
+
self.put(key, value)
|
118
|
+
end
|
119
|
+
|
120
|
+
#
|
121
|
+
# {include:Bucket#put}
|
122
|
+
#
|
123
|
+
# @param (see #put)
|
124
|
+
#
|
125
|
+
# @return [Blodsband::Future<Blodsband::Riak::Response>] the eventual response from Riak for the resulting HTTP request.
|
126
|
+
#
|
127
|
+
def aput(key, value, options = {})
|
128
|
+
links = value.links if value.respond_to?(:links)
|
129
|
+
links = (links || {}).merge(options.delete(:links)) if options.include?(:links)
|
130
|
+
|
131
|
+
index_rels = value.index_rels if value.respond_to?(:index_rels)
|
132
|
+
index_rels = (index_rels || {}).merge(options.delete(:index_rels)) if options.include?(:index_rels)
|
133
|
+
|
134
|
+
riak_params = options.delete(:riak_params)
|
135
|
+
|
136
|
+
vclock = value.vclock if value.respond_to?(:vclock)
|
137
|
+
vclock = options.delete(:vclock) if options.include?(:vclock)
|
138
|
+
|
139
|
+
client_id = options.delete(:client_id) || @defaults[:client_id]
|
140
|
+
|
141
|
+
meta = value.meta if value.respond_to?(:meta)
|
142
|
+
meta = (meta || {}).merge(options.delete(:meta)) if options.include?(:meta)
|
143
|
+
meta ||= {}
|
144
|
+
|
145
|
+
raise "Unknown options to #{self.class}#put(#{key}, #{value}, ...): #{options.inspect}" unless options.empty?
|
146
|
+
head = {
|
147
|
+
"Content-Type" => "application/json",
|
148
|
+
"Accept" => "multipart/mixed, application/json"
|
149
|
+
}
|
150
|
+
meta.each do |k, v|
|
151
|
+
head["X-Riak-Meta-#{k}"] = v.to_s
|
152
|
+
end
|
153
|
+
head["X-Riak-ClientId"] = client_id if client_id
|
154
|
+
head["Link"] = create_link_header(links) if links
|
155
|
+
head["X-Riak-Vclock"] = vclock if vclock
|
156
|
+
index_rel_futures = []
|
157
|
+
if index_rels
|
158
|
+
index_rels.each do |names, relations|
|
159
|
+
relations.each do |rel|
|
160
|
+
index_rel_futures << aadd_index_rel(key, names, rel)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
url = uri(key)
|
165
|
+
url += "?#{riak_params.collect do |k,v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" end.join("&")}" if riak_params
|
166
|
+
m = Multi.new
|
167
|
+
m.add(:resp,
|
168
|
+
EM::HttpRequest.new(url).apost(:body => Yajl::Encoder.encode(value),
|
169
|
+
:head => head))
|
170
|
+
return(Future.new do
|
171
|
+
m.really_perform
|
172
|
+
index_rel_futures.each do |f|
|
173
|
+
f.get
|
174
|
+
end
|
175
|
+
Response.parse(m, :bucket => @name, :key => key)
|
176
|
+
end)
|
177
|
+
end
|
178
|
+
|
179
|
+
#
|
180
|
+
# Put a value in Riak.
|
181
|
+
#
|
182
|
+
# @param [String] key the key to put the value under.
|
183
|
+
# @param [Object] value the value to insert under the key. If this {::Object} responds to <code>:vclock</code>, <code>:links</code>, <code>:index_rels</code> or <code>:meta</code> the return values from those methods will be used as options in the resulting {#put}.
|
184
|
+
# @param [Hash<Symbol, Object>] options
|
185
|
+
# :links:: [Hash<Symbol, Array<Array<String, String>>>] <code>tagname => [[bucket, key], [bucket, key], ...]</code>. Will default to the <code>#links</code> of the <code>value</code> if available.
|
186
|
+
# :index_rels:: [Hash<Array<Symbol, Symbol>, Array<Array<String, String>>>] <code>[i_am_to_relation, relation_is_to_me] => [[bucket, key], [bucket, key], ...]</code>. Example: <code>bucket.put(artist.key, artist, :links_rels => {[:artists, :tracks] => artist.track_refs})</code>. Will default to the <code>#index_rels</code> of the <code>value</code> if available.
|
187
|
+
# :riak_params:: [Hash<String, String>] <code>param_name => param_value</code> of extra parameters to send to Riak when doing the HTTP request.
|
188
|
+
# :vclock:: [String] the <code>vclock</code> returned when the object was originally fetched. Will default to the <code>#vclock</code> of the <code>value</code> if available.
|
189
|
+
# :client_id:: [String] the <code>client_id</code> to use when communicating with Riak. Will default to the <code>client_id</code> of the {Blodsband::Riak::Bucket}.
|
190
|
+
# :meta:: [Hash<String, String>] <code>meta_name => meta_value</code> of Riak meta data to save with the object, that will be returned when fetching the <code>value</code> next time. Will default to the <code>#meta</code> of the <code>value</code> if available.
|
191
|
+
#
|
192
|
+
# @return [Blodsband::Riak::Response] the response from riak for the resulting HTTP request.
|
193
|
+
#
|
194
|
+
def put(key, value, options = {})
|
195
|
+
aput(key, value, options).get
|
196
|
+
end
|
197
|
+
|
198
|
+
#
|
199
|
+
# Fetch a value from Riak.
|
200
|
+
#
|
201
|
+
# @param [String] key the key for the value.
|
202
|
+
#
|
203
|
+
# @return [Blodsband::Riak::Response] the response from Riak for the resulting HTTP request (which will also be ie a {::String} or a {::Hash}.
|
204
|
+
#
|
205
|
+
def [](key)
|
206
|
+
self.get(key)
|
207
|
+
end
|
208
|
+
|
209
|
+
#
|
210
|
+
# Efficiently check what keys correspond to existing values in Riak.
|
211
|
+
#
|
212
|
+
# @param [Array<String>] keys an {::Array} of {::String} containing the keys for which to check if values exist.
|
213
|
+
#
|
214
|
+
# @return [Set<String>] the keys having values.
|
215
|
+
#
|
216
|
+
def has_many?(keys)
|
217
|
+
ahas_many?(keys).get
|
218
|
+
end
|
219
|
+
|
220
|
+
#
|
221
|
+
# {include:Bucket#has_many?}
|
222
|
+
#
|
223
|
+
# @param (see #has_many?)
|
224
|
+
#
|
225
|
+
# @return [Blodsband::Future<Set<String>>] a Set eventually containing the keys for which values exist.
|
226
|
+
#
|
227
|
+
def ahas_many?(keys)
|
228
|
+
future = Mr.new(@url).
|
229
|
+
inputs(keys.collect do |key| [@name, key] end).
|
230
|
+
map({
|
231
|
+
:keep => false,
|
232
|
+
:language => "javascript",
|
233
|
+
:source => <<EOF
|
234
|
+
function(v,k,a) {
|
235
|
+
if (v.values != null) {
|
236
|
+
return [v];
|
237
|
+
} else {
|
238
|
+
return [];
|
239
|
+
}
|
240
|
+
}
|
241
|
+
EOF
|
242
|
+
}).
|
243
|
+
reduce({
|
244
|
+
:keep => true,
|
245
|
+
:language => "javascript",
|
246
|
+
:source => <<EOF
|
247
|
+
function(v) {
|
248
|
+
rval = [];
|
249
|
+
for (var i = 0; i < v.length; i++) {
|
250
|
+
if (v[i].values != null) {
|
251
|
+
rval.push(v[i].key);
|
252
|
+
}
|
253
|
+
}
|
254
|
+
return rval;
|
255
|
+
}
|
256
|
+
EOF
|
257
|
+
}).arun
|
258
|
+
return(Future.new do
|
259
|
+
Set.new(future.get)
|
260
|
+
end)
|
261
|
+
end
|
262
|
+
|
263
|
+
#
|
264
|
+
# Efficiently fetch values for many keys in Riak.
|
265
|
+
#
|
266
|
+
# @param [Array] keys an {::Array} of {::String} containing the keys for which to fetch the values.
|
267
|
+
#
|
268
|
+
# @return [Array<Blodsband::Riak::Response>] the response from Riak for the resulting HTTP request (which will also be ie a {::String}s or a {::Hash}es.
|
269
|
+
#
|
270
|
+
def get_many(keys)
|
271
|
+
aget_many(keys).get
|
272
|
+
end
|
273
|
+
|
274
|
+
#
|
275
|
+
# {include:Bucket#get_many}
|
276
|
+
#
|
277
|
+
# @param (see #get_many)
|
278
|
+
#
|
279
|
+
# @return [Blodsband::Future<Array<Blodsband::Riak::Response>>] the eventual response from Riak for the resulting HTTP request (which will also be ie a {::String}s or a {::Hash}es.
|
280
|
+
#
|
281
|
+
def aget_many(keys)
|
282
|
+
Mr.new(@url).
|
283
|
+
inputs(keys.collect do |key| [@name, key] end).
|
284
|
+
map({
|
285
|
+
:keep => false,
|
286
|
+
:language => "javascript",
|
287
|
+
:source => <<EOF
|
288
|
+
function(v,k,a) {
|
289
|
+
if (v.values != null) {
|
290
|
+
return [v];
|
291
|
+
} else {
|
292
|
+
return [];
|
293
|
+
}
|
294
|
+
}
|
295
|
+
EOF
|
296
|
+
}).
|
297
|
+
reduce({
|
298
|
+
:keep => true,
|
299
|
+
:language => "javascript",
|
300
|
+
:source => <<EOF
|
301
|
+
function(v) {
|
302
|
+
rval = [];
|
303
|
+
for (var i = 0; i < v.length; i++) {
|
304
|
+
if (v[i].values != null) {
|
305
|
+
rval.push(JSON.parse(v[i].values[0].data));
|
306
|
+
}
|
307
|
+
}
|
308
|
+
return rval;
|
309
|
+
}
|
310
|
+
EOF
|
311
|
+
}).arun
|
312
|
+
end
|
313
|
+
|
314
|
+
#
|
315
|
+
# {include:Bucket#get}
|
316
|
+
#
|
317
|
+
# @param (see #get)
|
318
|
+
#
|
319
|
+
# @return [Blodsband::Future<Blodsband::Riak::Response>] the eventual response from Riak for the resulting HTTP request (which will also be ie a {::String} or a {::Hash}.
|
320
|
+
#
|
321
|
+
def aget(key, options = {})
|
322
|
+
links = options.delete(:links)
|
323
|
+
index_rels = options.delete(:index_rels)
|
324
|
+
riak_params = options.delete(:riak_params)
|
325
|
+
unique = options.include?(:unique) ? options.delete(:unique) : @defaults[:unique]
|
326
|
+
ary = options.delete(:ary) || @defaults[:ary]
|
327
|
+
raise "Unknown options to #{self.class}#get(#{key}, ...): #{options.inspect}" unless options.empty?
|
328
|
+
u = uri(key)
|
329
|
+
m = Multi.new
|
330
|
+
if links
|
331
|
+
links.each do |linkchain|
|
332
|
+
m.add(linkchain, EM::HttpRequest.new("#{u}#{create_link_walker(linkchain)}").aget)
|
333
|
+
end
|
334
|
+
end
|
335
|
+
mrs = []
|
336
|
+
if index_rels
|
337
|
+
index_rels.each do |names|
|
338
|
+
mrs << aindex_rels(key, names)
|
339
|
+
end
|
340
|
+
end
|
341
|
+
u += "?#{riak_params.collect do |k,v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" end.join("&")}" if riak_params
|
342
|
+
m.add(:resp, EM::HttpRequest.new(u).aget(:head => {"Accept" => "multipart/mixed, application/json"}))
|
343
|
+
return(Future.new do
|
344
|
+
m.really_perform
|
345
|
+
resp = Response.parse(m, :links => links, :bucket => @name, :key => key)
|
346
|
+
resp = find_winner(key, resp) if unique && resp && resp.status == 300
|
347
|
+
resp = arify(resp) if ary
|
348
|
+
rval = resp
|
349
|
+
if mrs.size > 0
|
350
|
+
unless resp.is_a?(Hash)
|
351
|
+
rval = [resp]
|
352
|
+
end
|
353
|
+
class << resp
|
354
|
+
attr_reader :index_rels
|
355
|
+
end
|
356
|
+
mrs.each_with_index do |mr, index|
|
357
|
+
resp.instance_eval do
|
358
|
+
@index_rels ||= {}
|
359
|
+
@index_rels[index_rels[index]] = mr.get.collect do |hash| [hash["bucket"], hash["key"]] end
|
360
|
+
end
|
361
|
+
if resp.is_a?(Hash)
|
362
|
+
resp[index_rels[index][1].to_s] = mr.get.collect do |hash| extended_index_rel(hash) end
|
363
|
+
else
|
364
|
+
rval << mr.get.collect do |hash| extended_index_rel(hash) end
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
368
|
+
rval
|
369
|
+
end)
|
370
|
+
end
|
371
|
+
|
372
|
+
#
|
373
|
+
# {include:Bucket#[]}
|
374
|
+
#
|
375
|
+
# @param [String] key the key from which to fetch the value.
|
376
|
+
# @param [Hash<Symbol, Object>] options
|
377
|
+
# :links:: [Array<Array<String>>] an {::Array} of Riak link chains to fetch for the key. Example: <code>artist.get(key, :links => [["aliases"], ["friends", "friends"]])</code>
|
378
|
+
# :index_rels:: [Array<Array<Symbol>>] an {::Array} of relationships stored as index relations to fetch for the key. Example: <code>artist.get(key, :index_rels => [[:artists, :tracks], [:artists, :followers]])</code>
|
379
|
+
# :unique:: [true, false] if <code>true</code>, any siblings returned from Riak will be reduced to one using a method that will return the currently likely winner in an ongoing {#put_if_missing} scenario.
|
380
|
+
# :ary:: [true, false] if <code>true</code>, will be guaranteed to return an {::Array} containing zero or more values depending on the number of siblings and deleted values.
|
381
|
+
#
|
382
|
+
# @return (see #[])
|
383
|
+
#
|
384
|
+
def get(key, options = {})
|
385
|
+
aget(key, options).get
|
386
|
+
end
|
387
|
+
|
388
|
+
#
|
389
|
+
# Get a concurrent {Blodsband::Riak::Tree} in this {Blodsband::Riak::Bucket}.
|
390
|
+
#
|
391
|
+
# @param [String] key the key for this lock.
|
392
|
+
#
|
393
|
+
# @return [Blodsband::Riak::Lock] a lock supporting concurrent synchronization over a Riak cluster.
|
394
|
+
#
|
395
|
+
def lock(key)
|
396
|
+
return Lock.new(self, key)
|
397
|
+
end
|
398
|
+
|
399
|
+
#
|
400
|
+
# Get a concurrent {Blodsband::Riak::List} in this {Blodsband::Riak::Bucket}.
|
401
|
+
#
|
402
|
+
# @param [String] key the key for this list.
|
403
|
+
#
|
404
|
+
# @return [Blodsband::Riak::List] a list supporting concurrent update and reads.
|
405
|
+
#
|
406
|
+
def list(key)
|
407
|
+
return List.new(self, key)
|
408
|
+
end
|
409
|
+
|
410
|
+
#
|
411
|
+
# Get a concurrent {Blodsband::Riak::Map} in this {Blodsband::Riak::Bucket}.
|
412
|
+
#
|
413
|
+
# @param [String] key the key for this map.
|
414
|
+
#
|
415
|
+
# @return [Blodsband::Riak::Map] a map supporting concurrent updates and reads.
|
416
|
+
#
|
417
|
+
def map(key)
|
418
|
+
return Map.new(self, key)
|
419
|
+
end
|
420
|
+
|
421
|
+
#
|
422
|
+
# Get a concurrent {Blodsband::Riak::Sset} in this {Blodsband::Riak::Bucket}.
|
423
|
+
#
|
424
|
+
# @param [String] key the key for this set.
|
425
|
+
#
|
426
|
+
# @return [Blodsband::Riak::Sset] a set supporting concurrent updates and reads.
|
427
|
+
#
|
428
|
+
def sset(key)
|
429
|
+
return Sset.new(self, key)
|
430
|
+
end
|
431
|
+
|
432
|
+
#
|
433
|
+
# Get a concurrent {Blodsband::Riak::Counter} in this {Blodsband::Riak::Bucket}.
|
434
|
+
#
|
435
|
+
# @param [String] key the key for this counter.
|
436
|
+
#
|
437
|
+
# @return [Blodsband::Riak::Counter] a counter supporting concurrent update and reads.
|
438
|
+
#
|
439
|
+
def counter(key)
|
440
|
+
return Counter.new(self, key)
|
441
|
+
end
|
442
|
+
|
443
|
+
#
|
444
|
+
# Put a value in the bucket if the key is missing.
|
445
|
+
#
|
446
|
+
# @param [String] key the key to put the value under (if it doesn't already exist).
|
447
|
+
# @param [Object] value the value to put under the key (if the key doesn't already exist).
|
448
|
+
# @param [Hash<Symbol, Object>] options
|
449
|
+
# :client_id:: [String] <code>client_id</code> to use when communicating with Riak for this operation. Will default to the <code>client_id</code> of this {Blodsband::Riak::Bucket}.
|
450
|
+
#
|
451
|
+
# @return [Object] whatever this call succeeded in inserting under <code>key</code>, or <code>nil</code>.
|
452
|
+
#
|
453
|
+
def put_if_missing(key, value, options = {})
|
454
|
+
aput_if_missing(key, value, options).get
|
455
|
+
end
|
456
|
+
|
457
|
+
#
|
458
|
+
# {include:Bucket#put_if_missing}
|
459
|
+
#
|
460
|
+
# @param (see #put_if_missing)
|
461
|
+
#
|
462
|
+
# @return [Blodsband::Future<Object>] whatever this call eventually succeeded in inserting under <code>key</code>, or <code>nil</code>.
|
463
|
+
#
|
464
|
+
def aput_if_missing(key, value, options = {})
|
465
|
+
acas(key, value, nil, options)
|
466
|
+
end
|
467
|
+
|
468
|
+
#
|
469
|
+
# Compare the value of a key to an expected vclock (or nil) and uniquely set a new value if the
|
470
|
+
# current value matches the expectations.
|
471
|
+
#
|
472
|
+
# @param [String] key the key to look at.
|
473
|
+
# @param [Object] value the value to put under the key.
|
474
|
+
# @param [String] expected_vclock the vclock to replace, or <code>nil</code> if only set if the value is missing.
|
475
|
+
# @param [Hash<Symbol, Object>] options
|
476
|
+
# :client_id:: [String] <code>client_id</code> to use when communicating with Riak for this operation. Will default to the <code>client_id</code> of this {Blodsband::Riak::Bucket}.
|
477
|
+
#
|
478
|
+
# @return [Object] whatever this call succeeded in inserting under <code>key</code>, or <code>nil</code>.
|
479
|
+
#
|
480
|
+
def cas(key, value, expected_vclock, options = {})
|
481
|
+
acas(key, value, expected_vclock, options).get
|
482
|
+
end
|
483
|
+
|
484
|
+
#
|
485
|
+
# {include:Bucket#cas}
|
486
|
+
#
|
487
|
+
# @param (see #cas)
|
488
|
+
#
|
489
|
+
# @return [Blodsband::Future<Object>] whatever this call eventually succeeded in inserting under <code>key</code>, or <code>nil</code>.
|
490
|
+
#
|
491
|
+
def acas(key, value, expected_vclock, options = {})
|
492
|
+
#
|
493
|
+
# While result is :nothing we will continue working.
|
494
|
+
#
|
495
|
+
result = :nothing
|
496
|
+
#
|
497
|
+
# Remember the fiber that started it all
|
498
|
+
#
|
499
|
+
parent = Fiber.current
|
500
|
+
#
|
501
|
+
# Create a fiber that does our dirty work for us
|
502
|
+
#
|
503
|
+
Fiber.new do
|
504
|
+
post_check_sleep = options.delete(:post_check_sleep)
|
505
|
+
post_write_sleep = options.delete(:post_write_sleep)
|
506
|
+
client_id = @defaults[:client_id] || rand(1 << 256).to_s(36)
|
507
|
+
cas_id = rand(1 << 256).to_s(36)
|
508
|
+
|
509
|
+
current = get(key)
|
510
|
+
sleep(post_check_sleep) if post_check_sleep
|
511
|
+
|
512
|
+
#
|
513
|
+
# While we lack a result
|
514
|
+
#
|
515
|
+
while result == :nothing do
|
516
|
+
if (expected_vclock.nil? && current.nil?) || (current.respond_to?(:vclock) && current.vclock == expected_vclock)
|
517
|
+
#
|
518
|
+
# The current value is what we expected, write our value and repeat with the response
|
519
|
+
#
|
520
|
+
current = put(key,
|
521
|
+
value,
|
522
|
+
:riak_params => {:w => :all, :returnbody => true},
|
523
|
+
:client_id => client_id,
|
524
|
+
:vclock => current.respond_to?(:vclock) ? current.vclock : nil,
|
525
|
+
:meta => {
|
526
|
+
"cas-state" => "prospect",
|
527
|
+
"cas-voter" => cas_id,
|
528
|
+
"cas-id" => cas_id
|
529
|
+
})
|
530
|
+
sleep(post_write_sleep) if post_write_sleep
|
531
|
+
else
|
532
|
+
#
|
533
|
+
# The current value was not what we expected, check what is the winner.
|
534
|
+
#
|
535
|
+
winner = find_winner(key, current, options.merge(:client_id => client_id,
|
536
|
+
:cas_voter => cas_id))
|
537
|
+
if winner.nil?
|
538
|
+
#
|
539
|
+
# No winner, we tried to overwrite an existing value that didn't exist.
|
540
|
+
#
|
541
|
+
result = nil
|
542
|
+
elsif winner.meta["cas-id"] == cas_id
|
543
|
+
#
|
544
|
+
# We are winners!
|
545
|
+
#
|
546
|
+
result = winner
|
547
|
+
else
|
548
|
+
#
|
549
|
+
# We lost :/
|
550
|
+
#
|
551
|
+
result = nil
|
552
|
+
end
|
553
|
+
end
|
554
|
+
end
|
555
|
+
#
|
556
|
+
# Resume the parent if necessary.
|
557
|
+
#
|
558
|
+
parent.resume if parent.alive? && parent != Fiber.current
|
559
|
+
end.resume
|
560
|
+
return(Future.new do
|
561
|
+
rval = nil
|
562
|
+
Fiber.yield while result == :nothing
|
563
|
+
result
|
564
|
+
end)
|
565
|
+
end
|
566
|
+
|
567
|
+
#
|
568
|
+
# {include:Bucket#delete_index_rel}
|
569
|
+
#
|
570
|
+
# @param (see #delete_index_rel)
|
571
|
+
#
|
572
|
+
# @return [Blodsband::Future<Blodsband::Riak::Response>] the eventual response from Riak for the resulting HTTP request (which will also be ie a {::String} or a {::Hash}.
|
573
|
+
#
|
574
|
+
def adelete_index_rel(key, names, rel)
|
575
|
+
Bucket.new(@url, names.sort.join("-")).adelete(index_rel_key(key, names, rel))
|
576
|
+
end
|
577
|
+
|
578
|
+
#
|
579
|
+
# Remove an index stored relation from Riak.
|
580
|
+
#
|
581
|
+
# @param [String] key the key from which to remove the relation.
|
582
|
+
# @param [Array<Symbol, Symbol>] names the specification of the relationship type from which to remove a relation.
|
583
|
+
# @param [Array<String, String>] rel the bucket and key to remove from the relationship type.
|
584
|
+
#
|
585
|
+
# @return [Blodsband::Riak::Response] the eventual response from Riak for the resulting HTTP request (which will also be ie a {::String} or a {::Hash}.
|
586
|
+
#
|
587
|
+
def delete_index_rel(key, names, rel)
|
588
|
+
adelete_index_rel(key, names, rel).get
|
589
|
+
end
|
590
|
+
|
591
|
+
#
|
592
|
+
# {include:Bucket#add_index_rel}
|
593
|
+
#
|
594
|
+
# @note (see #add_index_rel)
|
595
|
+
#
|
596
|
+
# @param (see #add_index_rel)
|
597
|
+
#
|
598
|
+
# @return [Blodsband::Riak::Future<Blodsband::Riak::Response>] the eventual response from Riak for the resulting HTTP request.
|
599
|
+
#
|
600
|
+
def aadd_index_rel(key, names, rel)
|
601
|
+
Bucket.new(@url, names.sort.join("-")).aput(index_rel_key(key, names, rel), index_rel_document(names, key, rel))
|
602
|
+
end
|
603
|
+
|
604
|
+
#
|
605
|
+
# Add an index stored relation to Riak.
|
606
|
+
#
|
607
|
+
# Right now this has proven meaningful up to relation sizes of about 10000 due to Riak Search limitations
|
608
|
+
# when it comes to how it counts and fetches documents. After that a clear linear scaling is observable.
|
609
|
+
#
|
610
|
+
# @note The <code>rel</code> parameter can have a {::Hash} as a third element containing extra
|
611
|
+
# attributes for the relationship. These attributes will be stored in the relationship document
|
612
|
+
# and can be used to filter queries on this relationship type. This is also usable from the {#put}
|
613
|
+
# method and will have the same effect there.
|
614
|
+
#
|
615
|
+
# @param [String] key the key for which to add the relation.
|
616
|
+
# @param [Array<Symbol, Symbol>] names the specification of the relationship type for which to add a relation.
|
617
|
+
# @param [Array<String, String>] rel the bucket and key to add to the relationship type.
|
618
|
+
#
|
619
|
+
# @return [Blodsband::Riak::Response] the response from Riak for the resulting HTTP request.
|
620
|
+
#
|
621
|
+
def add_index_rel(key, names, rel)
|
622
|
+
aadd_index_rel(key, names, rel).get
|
623
|
+
end
|
624
|
+
|
625
|
+
#
|
626
|
+
# {include:Bucket#count_index_rels}
|
627
|
+
#
|
628
|
+
# @note (see #add_index_rel)
|
629
|
+
#
|
630
|
+
# @param (see #count_index_rels)
|
631
|
+
#
|
632
|
+
# @return [Blodsband::Future<Integer>] the number of matching relations eventually returned.
|
633
|
+
#
|
634
|
+
def acount_index_rels(key, names)
|
635
|
+
future = Search.new(@url).asearch(create_search_expression(key, names), :index => names.sort.join("-"), :page => 1, :per_page => 1)
|
636
|
+
return(Future.new do
|
637
|
+
future.get[:total]
|
638
|
+
end)
|
639
|
+
end
|
640
|
+
|
641
|
+
#
|
642
|
+
# Count the number of relations of a given type.
|
643
|
+
#
|
644
|
+
# @note (see #add_index_rel)
|
645
|
+
#
|
646
|
+
# @param [String] key the key for which to count relations.
|
647
|
+
# @param [Array<Symbol, Symbol>] names the specification of the relationship type to count.
|
648
|
+
#
|
649
|
+
# @return [Integer] the number of matching relations.
|
650
|
+
#
|
651
|
+
def count_index_rels(key, names)
|
652
|
+
acount_index_rels(key, names).get
|
653
|
+
end
|
654
|
+
|
655
|
+
#
|
656
|
+
# {include:Bucket#has_index_rel?}
|
657
|
+
#
|
658
|
+
# @param (see #has_index_rel?)
|
659
|
+
#
|
660
|
+
# @return [Blodsband::Future<true,false>] the eventual response to whether the relation exists.
|
661
|
+
#
|
662
|
+
def ahas_index_rel?(key, names, rel)
|
663
|
+
future = Bucket.new(@url, names.sort.join("-")).aget(index_rel_key(key, names, rel))
|
664
|
+
return(Future.new do
|
665
|
+
!future.get.nil?
|
666
|
+
end)
|
667
|
+
end
|
668
|
+
|
669
|
+
#
|
670
|
+
# Check whether a given relation exists.
|
671
|
+
#
|
672
|
+
# @param [String] key the key for which to check the relations.
|
673
|
+
# @param [Array<Symbol, Symbol>] names the specification of the relationship type in which to search.
|
674
|
+
# @param [Array<String, String>] rel the bucket and key to check if it exists in the relation type.
|
675
|
+
#
|
676
|
+
# @return [true, false] whether the relation exists.
|
677
|
+
#
|
678
|
+
def has_index_rel?(key, names, rel)
|
679
|
+
ahas_index_rel?(key, names, rel).get
|
680
|
+
end
|
681
|
+
|
682
|
+
#
|
683
|
+
# {include:Bucket#index_rels}
|
684
|
+
#
|
685
|
+
# @note (see #index_rels)
|
686
|
+
#
|
687
|
+
# @param (see #index_rels)
|
688
|
+
#
|
689
|
+
# @return [Blodsband::Riak::Future<Array<Blodsband::Riak::Response>>] a future containing an {::Array} containing the eventually retrieved {Blodsband::Riak::Response}s stored in the index as these relations.
|
690
|
+
#
|
691
|
+
def aindex_rels(key, names)
|
692
|
+
index_rel_mr(key, names).arun
|
693
|
+
end
|
694
|
+
|
695
|
+
#
|
696
|
+
# {include:Bucket#recip_index_rels}
|
697
|
+
#
|
698
|
+
# @note (see #index_rels)
|
699
|
+
#
|
700
|
+
# @param (see #index_rels)
|
701
|
+
#
|
702
|
+
# @return [Blodsband::Riak::Future<Array<Blodsband::Riak::Response>>] a future containing an {::Array} containing the retrieved {Blodsband::Riak:Response}s stored in the index as these relations.
|
703
|
+
#
|
704
|
+
def arecip_index_rels(key, names)
|
705
|
+
recip_index_rel_mr(key, names).arun
|
706
|
+
end
|
707
|
+
|
708
|
+
#
|
709
|
+
# Retrieve a set of relations having the same relation to us as we to them.
|
710
|
+
#
|
711
|
+
# @note (see #index_rels)
|
712
|
+
#
|
713
|
+
# @param [String] key the key to which the relations are connected.
|
714
|
+
# @param [Array<Symbol, Symbol>] names the specification of the relationship type to fetch.
|
715
|
+
#
|
716
|
+
# @return [Array<Blodsband::Riak::Response>] an {::Array} containing the retrieved {Blodsband::Riak:Response}s stored in the index as these relations.
|
717
|
+
#
|
718
|
+
def recip_index_rels(key, names)
|
719
|
+
arecip_index_rels(key, names).get
|
720
|
+
end
|
721
|
+
|
722
|
+
#
|
723
|
+
# Retrieve a set of relations stored in the Riak index.
|
724
|
+
#
|
725
|
+
# @note The <code>names</code> parameter can have a {::Hash} as third element containing a filter definition
|
726
|
+
# <code>:field => :required_value</code> that will be used to filter the relations returned. This
|
727
|
+
# is also usable from the {#get} method and will have the same effect there.
|
728
|
+
#
|
729
|
+
# @param [String] key the key to which the relations are connected.
|
730
|
+
# @param [Array<Symbol, Symbol>] names the specification of the relationship type to fetch.
|
731
|
+
#
|
732
|
+
# @return [Array<Blodsband::Riak::Response>] a future containing an {::Array} containing the retrieved {Blodsband::Riak::Response}s stored in the index as these relations.
|
733
|
+
#
|
734
|
+
def index_rels(key, names)
|
735
|
+
aindex_rels(key, names).get
|
736
|
+
end
|
737
|
+
|
738
|
+
private
|
739
|
+
|
740
|
+
def arify(resp)
|
741
|
+
if resp.nil?
|
742
|
+
[]
|
743
|
+
elsif resp.status == 300
|
744
|
+
resp.compact!
|
745
|
+
resp
|
746
|
+
else
|
747
|
+
resp.copy_to([resp])
|
748
|
+
end
|
749
|
+
end
|
750
|
+
|
751
|
+
def vote_winner(current)
|
752
|
+
if current.status == 300
|
753
|
+
#
|
754
|
+
# Multiple alternatives
|
755
|
+
#
|
756
|
+
winner = nil
|
757
|
+
current.compact.each do |alternative|
|
758
|
+
#
|
759
|
+
# Find the one already considered a winner, OR the one with the highest cas-id and cas-voter
|
760
|
+
#
|
761
|
+
if winner.nil?
|
762
|
+
winner = alternative
|
763
|
+
elsif winner.meta["cas-state"] != "winner"
|
764
|
+
if (alternative.meta["cas-state"] == "winner" ||
|
765
|
+
alternative.meta["cas-id"].to_s > winner.meta["cas-id"].to_s ||
|
766
|
+
(alternative.meta["cas-id"].to_s == winner.meta["cas-id"].to_s &&
|
767
|
+
alternative.meta["cas-voter"].to_s > winner.meta["cas-voter"].to_s))
|
768
|
+
winner = alternative
|
769
|
+
end
|
770
|
+
end
|
771
|
+
end
|
772
|
+
#
|
773
|
+
# Make the winner look like it came directly from a #get
|
774
|
+
#
|
775
|
+
current.copy_to(winner, :except => [:status])
|
776
|
+
else
|
777
|
+
current
|
778
|
+
end
|
779
|
+
end
|
780
|
+
|
781
|
+
def find_winner(key, current, options = {})
|
782
|
+
client_id = options.delete(:client_id) || @defaults[:client_id]
|
783
|
+
cas_voter = options.delete(:cas_voter)
|
784
|
+
post_check_sleep = options.delete(:post_check_sleep)
|
785
|
+
post_write_sleep = options.delete(:post_write_sleep)
|
786
|
+
unique_wait_threshold = options.delete(:unique_wait_threshold) || @defaults[:unique_wait_threshold] || 3
|
787
|
+
raise "Unknown options to #{self}.find_winner(...): #{options.inspect}" unless options.empty?
|
788
|
+
#STDERR.puts("#{$$} setting read without to 0 from #{caller.join("\n")}")
|
789
|
+
reads_without_winner = 0
|
790
|
+
|
791
|
+
cas_id = nil
|
792
|
+
result = :nothing
|
793
|
+
while result == :nothing
|
794
|
+
if current.nil?
|
795
|
+
#
|
796
|
+
# If there is no document, then the empty document is the winner
|
797
|
+
#
|
798
|
+
result = nil
|
799
|
+
else
|
800
|
+
#STDERR.puts("#{$$} trying to find a winner among #{current.inspect}")
|
801
|
+
status = current.status
|
802
|
+
winner = vote_winner(current)
|
803
|
+
if winner.meta["cas-state"] == "winner"
|
804
|
+
#
|
805
|
+
# If the winner is already marked as winner.
|
806
|
+
#
|
807
|
+
if cas_voter == winner.meta["cas-voter"]
|
808
|
+
#
|
809
|
+
# If the winner has our cas_voter.
|
810
|
+
#
|
811
|
+
if status == 300
|
812
|
+
#
|
813
|
+
# If this was a winner in a sibling horde, overwrite the sibling horde with the winner.
|
814
|
+
#
|
815
|
+
#STDERR.puts("#{$$} #{winner.inspect} was our winner, but it's not alone so overwriting the sibling horde")
|
816
|
+
current = put(key,
|
817
|
+
winner,
|
818
|
+
:riak_params => {:w => :all, :returnbody => true},
|
819
|
+
:client_id => client_id)
|
820
|
+
sleep(post_write_sleep) if post_write_sleep
|
821
|
+
else
|
822
|
+
#
|
823
|
+
# Else just set result as current winner.
|
824
|
+
#
|
825
|
+
result = winner
|
826
|
+
end
|
827
|
+
else
|
828
|
+
#
|
829
|
+
# If the winner doesn't have our cas_voter, just set it as the result.
|
830
|
+
#
|
831
|
+
result = winner
|
832
|
+
end
|
833
|
+
else
|
834
|
+
if cas_voter.nil?
|
835
|
+
#
|
836
|
+
# If we have not yet voted.
|
837
|
+
#
|
838
|
+
if reads_without_winner < unique_wait_threshold
|
839
|
+
#
|
840
|
+
# If we still have patience.
|
841
|
+
#
|
842
|
+
#STDERR.puts("#{$$} has only tried to re-read #{reads_without_winner} times, hanging on")
|
843
|
+
EM::Synchrony.sleep(0.1)
|
844
|
+
current = get(key, :unique => false)
|
845
|
+
reads_without_winner += 1
|
846
|
+
else
|
847
|
+
#
|
848
|
+
# We need to DO something! Lets select this winner as our own and write it,
|
849
|
+
# and see what happens.
|
850
|
+
#
|
851
|
+
cas_voter = rand(1 << 256).to_s(36)
|
852
|
+
winner.meta["cas-voter"] = cas_voter
|
853
|
+
#STDERR.puts("#{$$} we have no horse, selecting #{winner.inspect}")
|
854
|
+
current = put(key,
|
855
|
+
winner,
|
856
|
+
:riak_params => {:w => :all, :returnbody => true},
|
857
|
+
:client_id => client_id)
|
858
|
+
end
|
859
|
+
else
|
860
|
+
if status == 300
|
861
|
+
#
|
862
|
+
# We got multiple results
|
863
|
+
#
|
864
|
+
if cas_voter == winner.meta["cas-voter"]
|
865
|
+
#
|
866
|
+
# The winner has our cas_voter, try to replace the horde with the winner
|
867
|
+
#
|
868
|
+
#STDERR.puts("#{$$} #{winner} was our winner, but unmarked and in a horde. overwriting siblings")
|
869
|
+
current = put(key,
|
870
|
+
winner,
|
871
|
+
:riak_params => {:w => :all, :returnbody => true},
|
872
|
+
:client_id => client_id)
|
873
|
+
else
|
874
|
+
#
|
875
|
+
# The winner doesn't have our cas_voter, but it is still there after we made at least one write,
|
876
|
+
# so just give up and accept it as a winner.
|
877
|
+
#
|
878
|
+
result = winner
|
879
|
+
end
|
880
|
+
else
|
881
|
+
#
|
882
|
+
# We only got one result after at least one write.
|
883
|
+
#
|
884
|
+
if cas_voter == winner.meta["cas-voter"]
|
885
|
+
#
|
886
|
+
# It has our cas_voter, so lets mark it as a winner.
|
887
|
+
#
|
888
|
+
winner.meta["cas-state"] = "winner"
|
889
|
+
#STDERR.puts("#{$$} #{winner} was our winner, but unmarked. marking it")
|
890
|
+
current = put(key,
|
891
|
+
winner,
|
892
|
+
:riak_params => {:w => :all, :returnbody => true},
|
893
|
+
:client_id => client_id)
|
894
|
+
else
|
895
|
+
#
|
896
|
+
# It doesn't have our id. Lets just set it as the result.
|
897
|
+
#
|
898
|
+
result = winner
|
899
|
+
end
|
900
|
+
end
|
901
|
+
end
|
902
|
+
end
|
903
|
+
end
|
904
|
+
end
|
905
|
+
#STDERR.puts("#{$$} returning #{result.inspect}")
|
906
|
+
result
|
907
|
+
end
|
908
|
+
|
909
|
+
def create_link_walker(ary)
|
910
|
+
if ary.nil?
|
911
|
+
""
|
912
|
+
else
|
913
|
+
rval = []
|
914
|
+
ary.each do |link|
|
915
|
+
rval << "_,#{link},1"
|
916
|
+
end
|
917
|
+
"/#{rval.join("/")}"
|
918
|
+
end
|
919
|
+
end
|
920
|
+
|
921
|
+
def create_link_header(hash)
|
922
|
+
rval = []
|
923
|
+
hash.each do |type, links|
|
924
|
+
links.each do |link|
|
925
|
+
rval << "</#{key_uri(link[0], link[1])}>; riaktag=\"#{type}\""
|
926
|
+
end
|
927
|
+
end
|
928
|
+
return rval.join(", ")
|
929
|
+
end
|
930
|
+
|
931
|
+
def mergehash(master, stuff)
|
932
|
+
stuff.each do |k, v|
|
933
|
+
if master.include?(k)
|
934
|
+
current = master[k]
|
935
|
+
if current.respond_to?(:<<)
|
936
|
+
v.each do |e|
|
937
|
+
current << e
|
938
|
+
end
|
939
|
+
elsif current.is_a?(Hash)
|
940
|
+
mergehash(current, v)
|
941
|
+
else
|
942
|
+
master[k] = v
|
943
|
+
end
|
944
|
+
else
|
945
|
+
master[k] = v
|
946
|
+
end
|
947
|
+
end
|
948
|
+
end
|
949
|
+
|
950
|
+
def index_rel_subkey(bucket, key)
|
951
|
+
"<#{key}@#{bucket}>"
|
952
|
+
end
|
953
|
+
|
954
|
+
def index_rel_key(key, names, rel)
|
955
|
+
n = names[0...2]
|
956
|
+
if n.sort == n
|
957
|
+
"#{index_rel_subkey(name, key)}-#{index_rel_subkey(rel[0], rel[1])}"
|
958
|
+
else
|
959
|
+
"#{index_rel_subkey(rel[0], rel[1])}-#{index_rel_subkey(name, key)}"
|
960
|
+
end
|
961
|
+
end
|
962
|
+
|
963
|
+
def extended_index_rel(hash)
|
964
|
+
rval = hash["document"]
|
965
|
+
class << rval
|
966
|
+
attr_reader :key
|
967
|
+
attr_reader :bucket
|
968
|
+
end
|
969
|
+
rval.instance_eval do
|
970
|
+
@key = hash["key"]
|
971
|
+
@bucket = hash["bucket"]
|
972
|
+
end
|
973
|
+
rval
|
974
|
+
end
|
975
|
+
|
976
|
+
def index_rel_document(names, key, rel)
|
977
|
+
rval = if rel.size == 3
|
978
|
+
rel[2]
|
979
|
+
elsif rel.size == 2
|
980
|
+
{}
|
981
|
+
else
|
982
|
+
raise "Illegal argument to #index_rel_document(#{names}, #{key}, #{rel}): Third argument must be Array of size 2 or 3"
|
983
|
+
end
|
984
|
+
rval.merge("#{names[0]}_key" => index_rel_subkey(name, key),
|
985
|
+
"#{names[1]}_key" => index_rel_subkey(rel[0], rel[1]))
|
986
|
+
end
|
987
|
+
|
988
|
+
def index_rel_extra_search(hash)
|
989
|
+
hash.collect do |k, v|
|
990
|
+
"#{k}:\"#{v}\""
|
991
|
+
end.join(" AND ")
|
992
|
+
end
|
993
|
+
|
994
|
+
def create_search_expression(key, names)
|
995
|
+
#
|
996
|
+
# Create a search expression that finds all relations where we are on one side of the relation.
|
997
|
+
# Optionally with extra search criteria.
|
998
|
+
#
|
999
|
+
search = if names.size == 3
|
1000
|
+
extra = names.delete_at(2)
|
1001
|
+
"#{names[0]}_key:\"#{index_rel_subkey(name, key)}\" AND (#{index_rel_extra_search(extra)})"
|
1002
|
+
elsif names.size == 2
|
1003
|
+
"#{names[0]}_key:\"#{index_rel_subkey(name, key)}\""
|
1004
|
+
else
|
1005
|
+
raise "Illegal argument to #index_rel_mr(#{key}, #{names}): Second argument must be Array of size 2 or 3"
|
1006
|
+
end
|
1007
|
+
end
|
1008
|
+
|
1009
|
+
def recip_index_rel_mr(key, names)
|
1010
|
+
search = create_search_expression(key, names)
|
1011
|
+
Mr.new(@url).
|
1012
|
+
#
|
1013
|
+
# Create a new Mr where the input is the result of above search.
|
1014
|
+
#
|
1015
|
+
inputs(:module => "riak_search",
|
1016
|
+
:function => "mapred_search",
|
1017
|
+
:arg => [names.sort.join("-"),
|
1018
|
+
search]).
|
1019
|
+
#
|
1020
|
+
# Redirect the Mr to the reverse relation (where left and right side switch places).
|
1021
|
+
#
|
1022
|
+
map({
|
1023
|
+
:keep => false,
|
1024
|
+
:language => "javascript",
|
1025
|
+
:source => <<EOF
|
1026
|
+
function(v,k,a) {
|
1027
|
+
var key_match = /^(<.*@.*>)-(<.*@.*>)$/.exec(v["key"]);
|
1028
|
+
return [[v["bucket"], key_match[2] + "-" + key_match[1]]];
|
1029
|
+
}
|
1030
|
+
EOF
|
1031
|
+
}).
|
1032
|
+
#
|
1033
|
+
# Redirect the Mr to the endpoint of the reverse relation (that is not us, ie that is what we started looking
|
1034
|
+
# at before we reversed it), filtering out the deleted ones (where .values == null)
|
1035
|
+
#
|
1036
|
+
map({
|
1037
|
+
:keep => false,
|
1038
|
+
:language => "javascript",
|
1039
|
+
:source => <<EOF
|
1040
|
+
function(v,k,a) {
|
1041
|
+
var rel_key = JSON.parse(v.values[0].data).#{names[0]}_key;
|
1042
|
+
if ((match = /^<(.*)@(.*)>$/.exec(rel_key)) != null) {
|
1043
|
+
return [[match[2], match[1]]];
|
1044
|
+
} else {
|
1045
|
+
return [];
|
1046
|
+
}
|
1047
|
+
}
|
1048
|
+
EOF
|
1049
|
+
}).
|
1050
|
+
#
|
1051
|
+
# Collect the documents on the other side of those bucket/key combos,
|
1052
|
+
# filtering out those that are deleted (have .values == null)
|
1053
|
+
#
|
1054
|
+
map({
|
1055
|
+
:keep => false,
|
1056
|
+
:language => "javascript",
|
1057
|
+
:source => <<EOF
|
1058
|
+
function(v,k,a) {
|
1059
|
+
if (v.values != null) {
|
1060
|
+
return [v];
|
1061
|
+
} else {
|
1062
|
+
return [];
|
1063
|
+
}
|
1064
|
+
}
|
1065
|
+
EOF
|
1066
|
+
}).
|
1067
|
+
#
|
1068
|
+
# Filter out the documents that actually exist (have .values != null)
|
1069
|
+
#
|
1070
|
+
reduce({
|
1071
|
+
:keep => true,
|
1072
|
+
:language => "javascript",
|
1073
|
+
:source => <<EOF
|
1074
|
+
function(v) {
|
1075
|
+
rval = [];
|
1076
|
+
for (var i = 0; i < v.length; i++) {
|
1077
|
+
if (v[i].values != null) {
|
1078
|
+
rval.push({"document": JSON.parse(v[i].values[0].data), "bucket": v[i].bucket, "key": v[i].key});
|
1079
|
+
}
|
1080
|
+
}
|
1081
|
+
return rval;
|
1082
|
+
}
|
1083
|
+
EOF
|
1084
|
+
})
|
1085
|
+
end
|
1086
|
+
|
1087
|
+
def index_rel_mr(key, names)
|
1088
|
+
search = create_search_expression(key, names)
|
1089
|
+
Mr.new(@url).
|
1090
|
+
#
|
1091
|
+
# Create a new Mr where the input is the result of above search.
|
1092
|
+
#
|
1093
|
+
inputs(:module => "riak_search",
|
1094
|
+
:function => "mapred_search",
|
1095
|
+
:arg => [names.sort.join("-"),
|
1096
|
+
search]).
|
1097
|
+
#
|
1098
|
+
# Redirect the Mr to the bucket/key combo on the other side of the relation.
|
1099
|
+
#
|
1100
|
+
map({
|
1101
|
+
:keep => false,
|
1102
|
+
:language => "javascript",
|
1103
|
+
:source => <<EOF
|
1104
|
+
function(v,k,a) {
|
1105
|
+
var rel_key = JSON.parse(v.values[0].data).#{names[1]}_key;
|
1106
|
+
if ((match = /^<(.*)@(.*)>$/.exec(rel_key)) != null) {
|
1107
|
+
return [[match[2], match[1]]];
|
1108
|
+
} else {
|
1109
|
+
return [];
|
1110
|
+
}
|
1111
|
+
}
|
1112
|
+
EOF
|
1113
|
+
}).
|
1114
|
+
#
|
1115
|
+
# Collect the documents on the other side of those bucket/key combos,
|
1116
|
+
# filtering out those that are deleted (have .values == null)
|
1117
|
+
#
|
1118
|
+
map({
|
1119
|
+
:keep => false,
|
1120
|
+
:language => "javascript",
|
1121
|
+
:source => <<EOF
|
1122
|
+
function(v,k,a) {
|
1123
|
+
if (v.values != null) {
|
1124
|
+
return [v];
|
1125
|
+
} else {
|
1126
|
+
return [];
|
1127
|
+
}
|
1128
|
+
}
|
1129
|
+
EOF
|
1130
|
+
}).
|
1131
|
+
#
|
1132
|
+
# Filter out the documents that actually exist (have .values != null)
|
1133
|
+
#
|
1134
|
+
reduce({
|
1135
|
+
:keep => true,
|
1136
|
+
:language => "javascript",
|
1137
|
+
:source => <<EOF
|
1138
|
+
function(v) {
|
1139
|
+
rval = [];
|
1140
|
+
for (var i = 0; i < v.length; i++) {
|
1141
|
+
if (v[i].values != null) {
|
1142
|
+
rval.push({"document": JSON.parse(v[i].values[0].data), "bucket": v[i].bucket, "key": v[i].key});
|
1143
|
+
}
|
1144
|
+
}
|
1145
|
+
return rval;
|
1146
|
+
}
|
1147
|
+
EOF
|
1148
|
+
})
|
1149
|
+
end
|
1150
|
+
|
1151
|
+
def key_uri(bucket, key)
|
1152
|
+
"buckets/#{CGI.escape(bucket)}/keys/#{CGI.escape(key)}"
|
1153
|
+
end
|
1154
|
+
|
1155
|
+
def uri(*args)
|
1156
|
+
if args.size == 0
|
1157
|
+
URI.join(@url.to_s, "riak/#{CGI.escape(name)}")
|
1158
|
+
elsif args.size == 1
|
1159
|
+
URI.join(@url.to_s, key_uri(name, args[0]))
|
1160
|
+
elsif args.size == 2
|
1161
|
+
URI.join(@url.to_s, key_uri(args[0], args[1]))
|
1162
|
+
end
|
1163
|
+
end
|
1164
|
+
|
1165
|
+
end
|
1166
|
+
|
1167
|
+
end
|
1168
|
+
|
1169
|
+
end
|