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