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
@@ -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
|
+
|