blodsband 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+