blodsband 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -0,0 +1,68 @@
|
|
1
|
+
|
2
|
+
module Blodsband
|
3
|
+
|
4
|
+
class Riak
|
5
|
+
|
6
|
+
class Lock
|
7
|
+
|
8
|
+
attr_reader :key
|
9
|
+
|
10
|
+
#
|
11
|
+
# Create a new lock in a bucket.
|
12
|
+
#
|
13
|
+
# @param [Blodsband::Riak::Bucket] bucket the bucket in which the lock shall live.
|
14
|
+
# @param [String] key the key to the lock
|
15
|
+
#
|
16
|
+
def initialize(bucket, key)
|
17
|
+
@bucket = bucket
|
18
|
+
@key = key
|
19
|
+
end
|
20
|
+
|
21
|
+
#
|
22
|
+
# Lock this lock, with an optional timeout
|
23
|
+
#
|
24
|
+
# @param [Number] timeout the maximum time to wait for the lock to be available, in seconds.
|
25
|
+
#
|
26
|
+
# @return [true, false] whether we succeeded in locking before the timeout
|
27
|
+
#
|
28
|
+
def lock(timeout = 1 << 256)
|
29
|
+
deadline = Time.now.to_f + timeout
|
30
|
+
while !@bucket.put_if_missing(key, "locked")
|
31
|
+
EM::Synchrony.sleep(0.1)
|
32
|
+
if Time.now.to_f > deadline
|
33
|
+
return false
|
34
|
+
end
|
35
|
+
end
|
36
|
+
return true
|
37
|
+
end
|
38
|
+
|
39
|
+
#
|
40
|
+
# Unlock this lock
|
41
|
+
#
|
42
|
+
def unlock
|
43
|
+
@bucket.delete(key, :riak_params => {:w => :all})
|
44
|
+
while !@bucket.get(key, :unique => true).nil?
|
45
|
+
EM::Synchrony.sleep(0.1)
|
46
|
+
@bucket.delete(key, :riak_params => {:w => :all})
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
#
|
51
|
+
# Execute a block while exclusively locking this lock.
|
52
|
+
#
|
53
|
+
# @param [Block] block the block to execute.
|
54
|
+
#
|
55
|
+
def synchronize(&block)
|
56
|
+
lock
|
57
|
+
begin
|
58
|
+
yield
|
59
|
+
ensure
|
60
|
+
unlock
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
|
2
|
+
module Blodsband
|
3
|
+
|
4
|
+
class Riak
|
5
|
+
|
6
|
+
#
|
7
|
+
# A concurrent map.
|
8
|
+
#
|
9
|
+
class Map < List
|
10
|
+
|
11
|
+
#
|
12
|
+
# @param [Object] key a key to the map.
|
13
|
+
#
|
14
|
+
# @return [Object] the value in this map for the given key.
|
15
|
+
#
|
16
|
+
def [](key)
|
17
|
+
element_key = bucket.get(key_for(key))
|
18
|
+
if element_key.nil?
|
19
|
+
nil
|
20
|
+
else
|
21
|
+
map_key, value = Element.find(self, element_key).value
|
22
|
+
value
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# @param [Array<Object>] keys some keys to check if they exist in this map.
|
28
|
+
#
|
29
|
+
# @return [Hash<Object, Object>] the argument keys that exist in this map and their values.
|
30
|
+
#
|
31
|
+
def retain(keys)
|
32
|
+
bucket.has_many?(keys.collect do |map_key|
|
33
|
+
key_for(map_key)
|
34
|
+
end).collect do |bucket_key|
|
35
|
+
bucket.aget(bucket_key)
|
36
|
+
end.collect do |f|
|
37
|
+
f.get
|
38
|
+
end.collect do |element_key|
|
39
|
+
bucket.aget(element_key)
|
40
|
+
end.collect do |f|
|
41
|
+
f.get
|
42
|
+
end.inject({}) do |sum, element_value|
|
43
|
+
sum.merge(element_value.first => element_value.last)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
#
|
48
|
+
# @param [Blodsband::Riak::Map] map a {Blodsband::Riak::Map} to intersect this map with.
|
49
|
+
#
|
50
|
+
# @return [Hash<Object, Object>] the intersection of this map with the argument.
|
51
|
+
#
|
52
|
+
def intersect(map)
|
53
|
+
if size > map.size
|
54
|
+
retain(map.to_a.collect do |k,v|
|
55
|
+
k
|
56
|
+
end)
|
57
|
+
else
|
58
|
+
map.retain(to_a.collect do |k,v|
|
59
|
+
k
|
60
|
+
end)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
#
|
65
|
+
# @param [Object] k a key to this map.
|
66
|
+
#
|
67
|
+
# @return [true, false] whether this map contains the argument.
|
68
|
+
#
|
69
|
+
def include?(k)
|
70
|
+
!bucket.get(key_for(k)).nil?
|
71
|
+
end
|
72
|
+
|
73
|
+
#
|
74
|
+
# @param [Object] k a key to this map to delete.
|
75
|
+
#
|
76
|
+
# @return [Object] the value for the key, or nil.
|
77
|
+
#
|
78
|
+
def delete(key)
|
79
|
+
element_key = bucket.get(key_for(key))
|
80
|
+
if element_key.nil?
|
81
|
+
nil
|
82
|
+
else
|
83
|
+
element = Element.find(self, element_key)
|
84
|
+
element.delete
|
85
|
+
element.value.last
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
#
|
90
|
+
# @param [Object] key a key to insert.
|
91
|
+
# @param [Object] value a value to insert.
|
92
|
+
#
|
93
|
+
# @return [Object] the inserted value.
|
94
|
+
#
|
95
|
+
def []=(key, value)
|
96
|
+
element_key = bucket.get(key_for(key))
|
97
|
+
if element_key.nil?
|
98
|
+
append([key, value])
|
99
|
+
else
|
100
|
+
Element.find(self, element_key).value = [key, value]
|
101
|
+
end
|
102
|
+
value
|
103
|
+
end
|
104
|
+
|
105
|
+
protected
|
106
|
+
|
107
|
+
def ok_to_add?(new_element)
|
108
|
+
map_key, value = new_element.value
|
109
|
+
bucket.get(key_for(map_key)).nil?
|
110
|
+
end
|
111
|
+
|
112
|
+
def backlog_delete(new_element)
|
113
|
+
map_key, value = new_element.value
|
114
|
+
backlog << [:delete_element_pointer,
|
115
|
+
map_key]
|
116
|
+
end
|
117
|
+
|
118
|
+
def backlog_add(new_element)
|
119
|
+
map_key, value = new_element.value
|
120
|
+
backlog << [:add_element_pointer,
|
121
|
+
new_element.key,
|
122
|
+
map_key]
|
123
|
+
end
|
124
|
+
|
125
|
+
def delete_element_pointer(map_key)
|
126
|
+
bucket.delete(key_for(map_key))
|
127
|
+
end
|
128
|
+
|
129
|
+
def add_element_pointer(new_element_key, map_key)
|
130
|
+
bucket.put(key_for(map_key), new_element_key, :riak_params => {:w => :all})
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
def key_for(map_key)
|
136
|
+
"#{key}_#{Digest::SHA1.hexdigest(Yajl::Encoder.encode(map_key)).to_i(16).to_s(36)}"
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
|
2
|
+
module Blodsband
|
3
|
+
|
4
|
+
class Riak
|
5
|
+
|
6
|
+
class Mr
|
7
|
+
|
8
|
+
#
|
9
|
+
# Create a new map reduce reference.
|
10
|
+
#
|
11
|
+
# @param [::URI] url a {::URI} pointing to the HTTP port of a Riak node.
|
12
|
+
#
|
13
|
+
def initialize(url)
|
14
|
+
@url = url
|
15
|
+
@inputs = nil
|
16
|
+
@phases = []
|
17
|
+
end
|
18
|
+
|
19
|
+
#
|
20
|
+
# Add inputs to the map reduce.
|
21
|
+
#
|
22
|
+
# @param [Hash<Symbol, Object>] hash the inputs to add. See {http://wiki.basho.com/MapReduce.html#MapReduce-via-the-HTTP-API}.
|
23
|
+
#
|
24
|
+
# @return [Blodsband::Riak::Mr] this {Blodsband::Riak::Mr}.
|
25
|
+
#
|
26
|
+
def inputs(hash)
|
27
|
+
@inputs = hash
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# Add a map phase to the map reduce.
|
33
|
+
#
|
34
|
+
# @param [Hash<Symbol, Object>] hash the map phase to add. See {http://wiki.basho.com/MapReduce.html#MapReduce-via-the-HTTP-API}.
|
35
|
+
#
|
36
|
+
# @return [Blodsband::Riak::Mr] this {Blodsband::Riak::Mr}.
|
37
|
+
#
|
38
|
+
def map(hash)
|
39
|
+
@phases << {"map" => hash}
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
# Add a reduce phase to the map reduce.
|
45
|
+
#
|
46
|
+
# @param [Hash<Symbol, Object>] hash the reduce phase to add. See {http://wiki.basho.com/MapReduce.html#MapReduce-via-the-HTTP-API}.
|
47
|
+
#
|
48
|
+
# @return [Blodsband::Riak::Mr] this {Blodsband::Riak::Mr}.
|
49
|
+
#
|
50
|
+
def reduce(hash)
|
51
|
+
@phases << {"reduce" => hash}
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
#
|
56
|
+
# The body to use in the HTTP request used to execute this map reduce. Mostly for debugging purposes.
|
57
|
+
#
|
58
|
+
# @return [Hash<String, Object>] a {::Hash} to json-encode and send as body when executing the map reduce.
|
59
|
+
#
|
60
|
+
def body
|
61
|
+
{
|
62
|
+
"inputs" => @inputs,
|
63
|
+
"query" => @phases
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
#
|
68
|
+
# Execute this map reduce.
|
69
|
+
#
|
70
|
+
# @return [Blodsband::Riak::Response] the result of running the map reduce in Riak.
|
71
|
+
#
|
72
|
+
def run
|
73
|
+
arun.get
|
74
|
+
end
|
75
|
+
|
76
|
+
#
|
77
|
+
# {include:Mr#run}
|
78
|
+
#
|
79
|
+
# @return [Blodsband::Future<Blodsband::Riak::Response>] the eventual result of running the map reduce in Riak.
|
80
|
+
#
|
81
|
+
def arun
|
82
|
+
m = Multi.new
|
83
|
+
m.add(:mr,
|
84
|
+
EM::HttpRequest.new(URI.join(@url.to_s, "mapred")).
|
85
|
+
apost(:head => {
|
86
|
+
"Content-Type" => "application/json"
|
87
|
+
},
|
88
|
+
:body => Yajl::Encoder.encode(body)))
|
89
|
+
return(Future.new do
|
90
|
+
m.really_perform
|
91
|
+
if err = m.responses[:errback][:mr]
|
92
|
+
Error.new(err)
|
93
|
+
else
|
94
|
+
Yajl::Parser.parse(m.responses[:callback][:mr].response)
|
95
|
+
end
|
96
|
+
end)
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
@@ -0,0 +1,208 @@
|
|
1
|
+
|
2
|
+
module Blodsband
|
3
|
+
|
4
|
+
class Riak
|
5
|
+
|
6
|
+
#
|
7
|
+
# The semi magical class encapsulating responses from Riak. Probably never constructed by API users.
|
8
|
+
#
|
9
|
+
# Will mostly add the following fields to any object including it:
|
10
|
+
# :status an Integer defining the status of the response. 300 means siblings, for example.
|
11
|
+
# :links a Hash<String, Array<String>> with riaktags pointing to bucket/key pairs for the links.
|
12
|
+
# :vclock the vector clock for the fetched value.
|
13
|
+
# :last_modified when the document was last modified in Riak.
|
14
|
+
# :key the key the document was found under (when applicable)
|
15
|
+
# :bucket the name of the bucket the document was found in (when applicable)
|
16
|
+
# :meta a Hash<String, String> with Riak meta keys pointing to Riak meta values for the document.
|
17
|
+
#
|
18
|
+
module Response
|
19
|
+
|
20
|
+
def self.construct(status, headers, body, options = {})
|
21
|
+
rval = Yajl::Parser.parse(body)
|
22
|
+
class << rval
|
23
|
+
include Response
|
24
|
+
end
|
25
|
+
rval.apply(status, headers, options[:bucket], options[:key])
|
26
|
+
rval
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.parse_multipart(part, options = {})
|
30
|
+
if part["Content-Type"] =~ /multipart\/mixed/ || (part.respond_to?(:multipart?) && part.multipart?)
|
31
|
+
part.parts.collect do |inner_part|
|
32
|
+
parse_multipart(inner_part)
|
33
|
+
end
|
34
|
+
else
|
35
|
+
construct(200, part, part.body.to_s, options)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.parse_multipart_response(http, options = {})
|
40
|
+
mailbody = ""
|
41
|
+
http.response_header.each do |k,v|
|
42
|
+
mailbody += "#{k.gsub(/_/, "-")}: #{v}\n"
|
43
|
+
end
|
44
|
+
mailbody += http.response
|
45
|
+
mail = Mail.new(mailbody)
|
46
|
+
mail.parts.collect do |part|
|
47
|
+
parse_multipart(part, options)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.parse_response(http, options = {})
|
52
|
+
return nil if http.nil?
|
53
|
+
return nil if http.response_header.status == 404
|
54
|
+
if (http.response_header.status == 200 ||
|
55
|
+
http.response_header.status == 204 ||
|
56
|
+
(http.response_header.status == 300 && http.response_header["Content-Type"] =~ /multipart\/mixed/))
|
57
|
+
if http.response_header["Content-Type"] =~ /multipart\/mixed/
|
58
|
+
rval = parse_multipart_response(http, options)
|
59
|
+
class << rval
|
60
|
+
include Response
|
61
|
+
end
|
62
|
+
rval.apply(http.response_header.status, http.response_header, options[:bucket], options[:key])
|
63
|
+
rval
|
64
|
+
else
|
65
|
+
construct(http.response_header.status,
|
66
|
+
http.response_header,
|
67
|
+
http.response,
|
68
|
+
options)
|
69
|
+
end
|
70
|
+
else
|
71
|
+
raise Blodsband::Error.new(http)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.inject_links(previous_links, document, links, responses)
|
76
|
+
previous_links << links.shift
|
77
|
+
response = responses.shift
|
78
|
+
if document.is_a?(Hash)
|
79
|
+
document[previous_links.join("->")] = response
|
80
|
+
elsif document.respond_to?(:<<)
|
81
|
+
document << response
|
82
|
+
else
|
83
|
+
document = [document, response]
|
84
|
+
end
|
85
|
+
if links.size > 0
|
86
|
+
document = inject_links(previous_links, document, links, responses)
|
87
|
+
end
|
88
|
+
document
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.parse_callbacks(multi, options = {})
|
92
|
+
resp = parse_response(multi.responses[:callback][:resp], options)
|
93
|
+
link_chains = options.delete(:links) || []
|
94
|
+
link_chains.each do |chain|
|
95
|
+
inject_links([], resp, chain, parse_response(multi.responses[:callback][chain]))
|
96
|
+
end
|
97
|
+
resp
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.parse(item, options = {})
|
101
|
+
if item.is_a?(EM::Synchrony::Multi)
|
102
|
+
if item.responses[:errback].empty?
|
103
|
+
parse_callbacks(item, options)
|
104
|
+
else
|
105
|
+
raise Multi::Error.new(item)
|
106
|
+
end
|
107
|
+
else
|
108
|
+
raise "Unparseable item #{item.inspect}"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.included(base)
|
113
|
+
unless base.include?(InstanceMethods)
|
114
|
+
base.class_eval do
|
115
|
+
attr_reader :status
|
116
|
+
attr_reader :links
|
117
|
+
attr_reader :vclock
|
118
|
+
attr_reader :last_modified
|
119
|
+
attr_reader :key
|
120
|
+
attr_reader :bucket
|
121
|
+
attr_reader :meta
|
122
|
+
include InstanceMethods
|
123
|
+
end
|
124
|
+
base.extend(ClassMethods)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
module InstanceMethods
|
129
|
+
#
|
130
|
+
# Copies the response-specific parts of this {Blodsband::Riak::Response} to <code>other</code> after
|
131
|
+
# having made <code>other</code> include {Blodsband::Riak::Response}.
|
132
|
+
#
|
133
|
+
# Does not copy links or meta since they are value specific.
|
134
|
+
#
|
135
|
+
# @param [Object] other what we want to copy our response data to.
|
136
|
+
# @param [Hash] options
|
137
|
+
# :except:: [Array<:status, :vclock, :last_modified, :key, :bucket>] an array of data not to copy.
|
138
|
+
#
|
139
|
+
# @return [Object] the parameter with {Blodsband::Riak::Response} included and its response data set to that of us.
|
140
|
+
#
|
141
|
+
def copy_to(other, options = {})
|
142
|
+
except = options.delete(:except) || []
|
143
|
+
raise "Unknown option to #copy_to: #{options.inspect}" unless options.empty?
|
144
|
+
class << other
|
145
|
+
include Response
|
146
|
+
end
|
147
|
+
response = self
|
148
|
+
other.instance_eval do
|
149
|
+
@status = response.status unless except.include?(:status)
|
150
|
+
@vclock = response.vclock unless except.include?(:vclock)
|
151
|
+
@last_modified = response.last_modified unless except.include?(:last_modified)
|
152
|
+
@key = response.key unless except.include?(:key)
|
153
|
+
@bucket = response.bucket unless except.include?(:bucket)
|
154
|
+
end
|
155
|
+
other
|
156
|
+
end
|
157
|
+
def apply(status, headers, bucket, key)
|
158
|
+
@bucket = bucket
|
159
|
+
@key = key
|
160
|
+
@status = status
|
161
|
+
@vclock = headers["X-Riak-Vclock"]
|
162
|
+
@last_modified = DateTime.parse(headers["Last-Modified"].to_s) rescue nil
|
163
|
+
@meta = {}
|
164
|
+
if headers.is_a?(Mail::Part)
|
165
|
+
headers.header_fields.each do |field|
|
166
|
+
if match = field.name.match(/^X-Riak-Meta-(.*)$/)
|
167
|
+
@meta[match[1].downcase] = field.value.to_s
|
168
|
+
end
|
169
|
+
end
|
170
|
+
else
|
171
|
+
headers.each do |key, value|
|
172
|
+
if match = key.to_s.match(/^X_RIAK_META_(.*)$/)
|
173
|
+
@meta[match[1].gsub(/_/, "-").downcase] = value.to_s
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
link_header = headers["Link"]
|
178
|
+
if link_header.nil?
|
179
|
+
@links = []
|
180
|
+
else
|
181
|
+
@links = link_header.to_s.split(/,/).inject({}) do |sum, link|
|
182
|
+
parts = link.split(/;/)
|
183
|
+
if parts.size > 0 && keymatch = parts.shift.match(/<\/buckets\/(.*)\/keys\/(.*)>/)
|
184
|
+
key = [keymatch[1], keymatch[2]]
|
185
|
+
type = ""
|
186
|
+
parts.each do |part|
|
187
|
+
if match = part.match(/riaktag="(.*)"/)
|
188
|
+
type = match[1]
|
189
|
+
end
|
190
|
+
end
|
191
|
+
sum[type.strip] ||= []
|
192
|
+
sum[type.strip] << key
|
193
|
+
end
|
194
|
+
sum
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
module ClassMethods
|
201
|
+
end
|
202
|
+
|
203
|
+
end
|
204
|
+
|
205
|
+
end
|
206
|
+
|
207
|
+
end
|
208
|
+
|