better-riak-client 1.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. data/LICENSE +16 -0
  2. data/README.markdown +198 -0
  3. data/RELEASE_NOTES.md +211 -0
  4. data/better-riak-client.gemspec +61 -0
  5. data/erl_src/riak_kv_test014_backend.beam +0 -0
  6. data/erl_src/riak_kv_test014_backend.erl +189 -0
  7. data/erl_src/riak_kv_test_backend.beam +0 -0
  8. data/erl_src/riak_kv_test_backend.erl +697 -0
  9. data/erl_src/riak_search_test_backend.beam +0 -0
  10. data/erl_src/riak_search_test_backend.erl +175 -0
  11. data/lib/riak/bucket.rb +221 -0
  12. data/lib/riak/client/beefcake/messages.rb +213 -0
  13. data/lib/riak/client/beefcake/object_methods.rb +111 -0
  14. data/lib/riak/client/beefcake_protobuffs_backend.rb +226 -0
  15. data/lib/riak/client/decaying.rb +36 -0
  16. data/lib/riak/client/excon_backend.rb +162 -0
  17. data/lib/riak/client/feature_detection.rb +88 -0
  18. data/lib/riak/client/http_backend/configuration.rb +211 -0
  19. data/lib/riak/client/http_backend/key_streamer.rb +43 -0
  20. data/lib/riak/client/http_backend/object_methods.rb +106 -0
  21. data/lib/riak/client/http_backend/request_headers.rb +34 -0
  22. data/lib/riak/client/http_backend/transport_methods.rb +201 -0
  23. data/lib/riak/client/http_backend.rb +340 -0
  24. data/lib/riak/client/net_http_backend.rb +82 -0
  25. data/lib/riak/client/node.rb +115 -0
  26. data/lib/riak/client/protobuffs_backend.rb +173 -0
  27. data/lib/riak/client/search.rb +91 -0
  28. data/lib/riak/client.rb +540 -0
  29. data/lib/riak/cluster.rb +151 -0
  30. data/lib/riak/core_ext/blank.rb +53 -0
  31. data/lib/riak/core_ext/deep_dup.rb +13 -0
  32. data/lib/riak/core_ext/extract_options.rb +7 -0
  33. data/lib/riak/core_ext/json.rb +15 -0
  34. data/lib/riak/core_ext/slice.rb +18 -0
  35. data/lib/riak/core_ext/stringify_keys.rb +10 -0
  36. data/lib/riak/core_ext/symbolize_keys.rb +10 -0
  37. data/lib/riak/core_ext/to_param.rb +31 -0
  38. data/lib/riak/core_ext.rb +7 -0
  39. data/lib/riak/encoding.rb +6 -0
  40. data/lib/riak/failed_request.rb +81 -0
  41. data/lib/riak/i18n.rb +5 -0
  42. data/lib/riak/json.rb +52 -0
  43. data/lib/riak/link.rb +94 -0
  44. data/lib/riak/locale/en.yml +53 -0
  45. data/lib/riak/locale/fr.yml +52 -0
  46. data/lib/riak/map_reduce/filter_builder.rb +103 -0
  47. data/lib/riak/map_reduce/phase.rb +98 -0
  48. data/lib/riak/map_reduce.rb +225 -0
  49. data/lib/riak/map_reduce_error.rb +7 -0
  50. data/lib/riak/node/configuration.rb +293 -0
  51. data/lib/riak/node/console.rb +133 -0
  52. data/lib/riak/node/control.rb +207 -0
  53. data/lib/riak/node/defaults.rb +83 -0
  54. data/lib/riak/node/generation.rb +106 -0
  55. data/lib/riak/node/log.rb +34 -0
  56. data/lib/riak/node/version.rb +43 -0
  57. data/lib/riak/node.rb +38 -0
  58. data/lib/riak/robject.rb +318 -0
  59. data/lib/riak/search.rb +3 -0
  60. data/lib/riak/serializers.rb +74 -0
  61. data/lib/riak/stamp.rb +77 -0
  62. data/lib/riak/test_server.rb +89 -0
  63. data/lib/riak/util/escape.rb +76 -0
  64. data/lib/riak/util/headers.rb +53 -0
  65. data/lib/riak/util/multipart/stream_parser.rb +62 -0
  66. data/lib/riak/util/multipart.rb +52 -0
  67. data/lib/riak/util/tcp_socket_extensions.rb +58 -0
  68. data/lib/riak/util/translation.rb +19 -0
  69. data/lib/riak/version.rb +3 -0
  70. data/lib/riak/walk_spec.rb +105 -0
  71. data/lib/riak.rb +21 -0
  72. metadata +348 -0
@@ -0,0 +1,88 @@
1
+ module Riak
2
+ class Client
3
+ # Methods that can be used to determine whether certain features
4
+ # are supported by the Riak node to which the client backend is
5
+ # connected.
6
+ #
7
+ # Backends must implement the "get_server_version" method,
8
+ # returning a string representing the Riak node's version. This is
9
+ # implemented on HTTP using the stats resource, and on Protocol
10
+ # Buffers using the RpbGetServerInfoReq message.
11
+ module FeatureDetection
12
+ # Constants representing Riak versions
13
+ VERSION = {
14
+ 1 => Gem::Version.new("1.0.0"),
15
+ 1.1 => Gem::Version.new("1.1.0"),
16
+ 1.2 => Gem::Version.new("1.2.0")
17
+ }.freeze
18
+
19
+ # @return [String] the version of the Riak node
20
+ # @abstract
21
+ def get_server_version
22
+ raise NotImplementedError
23
+ end
24
+
25
+ # @return [Gem::Version] the version of the Riak node to which
26
+ # this backend is connected
27
+ def server_version
28
+ @server_version ||= Gem::Version.new(get_server_version)
29
+ end
30
+
31
+ # @return [true,false] whether MapReduce requests can be submitted without
32
+ # phases.
33
+ def mapred_phaseless?
34
+ at_least? VERSION[1.1]
35
+ end
36
+
37
+ # @return [true,false] whether secondary index queries are
38
+ # supported over Protocol Buffers
39
+ def pb_indexes?
40
+ at_least? VERSION[1.2]
41
+ end
42
+
43
+ # @return [true,false] whether search queries are supported over
44
+ # Protocol Buffers
45
+ def pb_search?
46
+ at_least? VERSION[1.2]
47
+ end
48
+
49
+ # @return [true,false] whether conditional fetch/store semantics
50
+ # are supported over Protocol Buffers
51
+ def pb_conditionals?
52
+ at_least? VERSION[1]
53
+ end
54
+
55
+ # @return [true,false] whether additional quorums and FSM
56
+ # controls are available, e.g. primary quorums, basic_quorum,
57
+ # notfound_ok
58
+ def quorum_controls?
59
+ at_least? VERSION[1]
60
+ end
61
+
62
+ # @return [true,false] whether "not found" responses might
63
+ # include vclocks
64
+ def tombstone_vclocks?
65
+ at_least? VERSION[1]
66
+ end
67
+
68
+ # @return [true,false] whether partial-fetches (vclock and
69
+ # metadata only) are supported over Protocol Buffers
70
+ def pb_head?
71
+ at_least? VERSION[1]
72
+ end
73
+
74
+ protected
75
+ # @return [true,false] whether the server version is the same or
76
+ # newer than the requested version
77
+ def at_least?(version)
78
+ server_version >= version
79
+ end
80
+
81
+ # Backends should call this when their connection is interrupted
82
+ # or reset so as to facilitate rolling upgrades
83
+ def reset_server_version
84
+ @server_version = nil
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,211 @@
1
+
2
+ require 'riak/failed_request'
3
+ require 'riak/client/http_backend'
4
+ require 'riak/link'
5
+
6
+ module Riak
7
+ class Client
8
+ class HTTPBackend
9
+ # Riak 0.14 provides a root URL that enumerates all of the
10
+ # HTTP endpoints and their paths. This module adds methods to
11
+ # auto-discover those endpoints via the root URL. It also adds
12
+ # methods for generating URL paths for specific resources.
13
+ module Configuration
14
+ # @return [URI] a URL path to the "ping" resource
15
+ def ping_path
16
+ path(riak_kv_wm_ping)
17
+ end
18
+
19
+ # @return [URI] a URL path to the "stats" resource
20
+ def stats_path
21
+ path(riak_kv_wm_stats)
22
+ end
23
+
24
+ # @return [URI] a URL path to the "mapred" resource
25
+ # @param options [Hash] query parameters, e.g. chunked=true
26
+ def mapred_path(options={})
27
+ path(riak_kv_wm_mapred, options)
28
+ end
29
+
30
+ # @return [URI] a URL path for the "buckets list" resource
31
+ def bucket_list_path(options={})
32
+ if new_scheme?
33
+ path(riak_kv_wm_buckets, options.merge(:buckets => true))
34
+ else
35
+ path(riak_kv_wm_raw, options.merge(:buckets => true))
36
+ end
37
+ end
38
+
39
+ # @return [URI] a URL path for the "bucket properties"
40
+ # resource
41
+ # @param [String] bucket the bucket name
42
+ def bucket_properties_path(bucket, options={})
43
+ if new_scheme?
44
+ path(riak_kv_wm_buckets, escape(bucket), "props", options)
45
+ else
46
+ path(riak_kv_wm_raw, escape(bucket), options.merge(:props => true, :keys => false))
47
+ end
48
+ end
49
+
50
+ # @return [URI] a URL path for the "list keys" resource
51
+ # @param [String] bucket the bucket whose keys to list
52
+ # @param [Hash] options query parameters, e.g. keys=stream
53
+ def key_list_path(bucket, options={:keys => true})
54
+ if new_scheme?
55
+ path(riak_kv_wm_buckets, escape(bucket), "keys", options)
56
+ else
57
+ path(riak_kv_wm_raw, escape(bucket), options.merge(:props => false))
58
+ end
59
+ end
60
+
61
+ # @return [URI] a URL path for the "object" resource
62
+ # @param [String] bucket the bucket of the object
63
+ # @param [String,nil] key the key of the object, or nil for a
64
+ # server-assigned key when using POST
65
+ def object_path(bucket, key, options={})
66
+ key = escape(key) if key
67
+ if new_scheme?
68
+ path([riak_kv_wm_buckets, escape(bucket), "keys", key, options].compact)
69
+ else
70
+ path([riak_kv_wm_raw, escape(bucket), key, options].compact)
71
+ end
72
+ end
73
+
74
+ # @return [URI] a URL path for the "link-walking" resource
75
+ # @param [String] bucket the bucket of the origin object
76
+ # @param [String] key the key of the origin object
77
+ # @param [Array<WalkSpec>] specs a list of walk specifications
78
+ # to traverse
79
+ def link_walk_path(bucket, key, specs)
80
+ specs = specs.map {|s| s.to_s }
81
+ if new_scheme?
82
+ path(riak_kv_wm_buckets, escape(bucket), "keys", escape(key), *specs)
83
+ else
84
+ path(riak_kv_wm_link_walker, escape(bucket), escape(key), *specs)
85
+ end
86
+ end
87
+
88
+ # @return [URI] a URL path for the "index range query"
89
+ # resource
90
+ # @param [String] bucket the bucket whose index to query
91
+ # @param [String] index the name of the index to query
92
+ # @param [String,Integer] start the start of the range
93
+ # @param [String,Integer] finish the end of the range
94
+ def index_range_path(bucket, index, start, finish, options={})
95
+ raise t('indexes_unsupported') unless new_scheme?
96
+ path(riak_kv_wm_buckets, escape(bucket), "index", escape(index), escape(start.to_s), escape(finish.to_s), options)
97
+ end
98
+
99
+ # @return [URI] a URL path for the "index range query"
100
+ # resource
101
+ # @param [String] bucket the bucket whose index to query
102
+ # @param [String] index the name of the index to query
103
+ # @param [String,Integer] start the start of the range
104
+ # @param [String,Integer] finish the end of the range
105
+ def index_eq_path(bucket, index, value, options={})
106
+ raise t('indexes_unsupported') unless new_scheme?
107
+ path(riak_kv_wm_buckets, escape(bucket), "index", escape(index), escape(value.to_s), options)
108
+ end
109
+
110
+ # @return [URI] a URL path for a Solr query resource
111
+ # @param [String] index the index to query
112
+ # @param [String] query the Lucene-style query string
113
+ # @param [Hash] options additional query options
114
+ def solr_select_path(index, query, options={})
115
+ raise t('search_unsupported') unless riak_solr_searcher_wm
116
+ options = {"q" => query, "wt" => "json"}.merge(options)
117
+ if index
118
+ path(riak_solr_searcher_wm, index, 'select', options)
119
+ else
120
+ path(riak_solr_searcher_wm, 'select', options)
121
+ end
122
+ end
123
+
124
+ # @return [URI] a URL path for a Solr update resource
125
+ # @param [String] index the index to update
126
+ def solr_update_path(index)
127
+ raise t('search_unsupported') unless riak_solr_indexer_wm
128
+ if index
129
+ path(riak_solr_indexer_wm, index, 'update')
130
+ else
131
+ path(riak_solr_indexer_wm, 'update')
132
+ end
133
+ end
134
+
135
+ # @return [URI] a URL path for the Luwak interface
136
+ def luwak_path(key)
137
+ raise t('luwak_unsupported') unless luwak_wm_file
138
+ if key
139
+ path(luwak_wm_file, escape(key))
140
+ else
141
+ path(luwak_wm_file)
142
+ end
143
+ end
144
+
145
+ private
146
+ def server_config
147
+ @server_config ||= {}.tap do |hash|
148
+ begin
149
+ response = get(200, path("/"))
150
+ Link.parse(response[:headers]['link'].first).each {|l| hash[l.tag.intern] ||= l.url }
151
+ rescue Riak::FailedRequest
152
+ end
153
+ end
154
+ end
155
+
156
+ def new_scheme?
157
+ !riak_kv_wm_buckets.nil?
158
+ end
159
+
160
+ def get_server_version
161
+ begin
162
+ # Attempt to grab the server version from the stats resource
163
+ stats = send(:stats)
164
+ stats['riak_kv_version']
165
+ rescue FailedRequest
166
+ # If stats is disabled, we can't assume the Riak version
167
+ # is >= 1.1. However, we can assume the new URL scheme is
168
+ # at least version 1.0.
169
+ new_scheme? ? "1.0.0" : "0.14.0"
170
+ end
171
+ end
172
+
173
+ def riak_kv_wm_buckets
174
+ server_config[:riak_kv_wm_buckets]
175
+ end
176
+
177
+ def riak_kv_wm_raw
178
+ server_config[:riak_kv_wm_raw] || node.http_paths[:prefix]
179
+ end
180
+
181
+ def riak_kv_wm_link_walker
182
+ server_config[:riak_kv_wm_link_walker] || node.http_paths[:prefix]
183
+ end
184
+
185
+ def riak_kv_wm_mapred
186
+ server_config[:riak_kv_wm_mapred] || node.http_paths[:mapred]
187
+ end
188
+
189
+ def riak_kv_wm_ping
190
+ server_config[:riak_kv_wm_ping] || "/ping"
191
+ end
192
+
193
+ def riak_kv_wm_stats
194
+ server_config[:riak_kv_wm_stats] || "/stats"
195
+ end
196
+
197
+ def riak_solr_searcher_wm
198
+ server_config[:riak_solr_searcher_wm]
199
+ end
200
+
201
+ def riak_solr_indexer_wm
202
+ server_config[:riak_solr_indexer_wm]
203
+ end
204
+
205
+ def luwak_wm_file
206
+ server_config[:luwak_wm_file]
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,43 @@
1
+ require 'riak/util/escape'
2
+ require 'riak/json'
3
+
4
+ module Riak
5
+ class Client
6
+ class HTTPBackend
7
+ # @private
8
+ class KeyStreamer
9
+ include Util::Escape
10
+
11
+ def initialize(block)
12
+ @buffer = ""
13
+ @block = block
14
+ end
15
+
16
+ def accept(chunk)
17
+ @buffer << chunk
18
+ consume
19
+ end
20
+
21
+ def to_proc
22
+ method(:accept).to_proc
23
+ end
24
+
25
+ private
26
+ def consume
27
+ while @buffer =~ /\}\{/
28
+ stream($~.pre_match + '}')
29
+ @buffer = '{' + $~.post_match
30
+ end
31
+ end
32
+
33
+ def stream(str)
34
+ obj = JSON.parse(str) rescue nil
35
+ if obj && obj['keys']
36
+ @block.call obj['keys'].map(&method(:maybe_unescape))
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+
@@ -0,0 +1,106 @@
1
+ require 'uri'
2
+ require 'set'
3
+ require 'time'
4
+ require 'riak/client/http_backend'
5
+ require 'riak/robject'
6
+ require 'riak/link'
7
+ require 'riak/util/multipart'
8
+
9
+ module Riak
10
+ class Client
11
+ class HTTPBackend
12
+ # Methods for assisting with the handling of {RObject}s present
13
+ # in HTTP requests and responses.
14
+ module ObjectMethods
15
+ # HTTP header hash that will be sent along when reloading the object
16
+ # @return [Hash] hash of HTTP headers
17
+ def reload_headers(robject)
18
+ {}.tap do |h|
19
+ h['If-None-Match'] = robject.etag if robject.etag.present?
20
+ h['If-Modified-Since'] = robject.last_modified.httpdate if robject.last_modified.present?
21
+ end
22
+ end
23
+
24
+ # HTTP header hash that will be sent along when storing the object
25
+ # @return [Hash] hash of HTTP Headers
26
+ def store_headers(robject)
27
+ {}.tap do |hash|
28
+ hash["Content-Type"] = robject.content_type
29
+ hash["X-Riak-Vclock"] = robject.vclock if robject.vclock
30
+ if robject.prevent_stale_writes && robject.etag.present?
31
+ hash["If-Match"] = robject.etag
32
+ elsif robject.prevent_stale_writes
33
+ hash["If-None-Match"] = "*"
34
+ end
35
+ unless robject.links.blank?
36
+ hash["Link"] = robject.links.reject {|l| l.rel == "up" }.map {|l| l.to_s(new_scheme?) }.join(", ")
37
+ end
38
+ unless robject.meta.blank?
39
+ robject.meta.each do |k,v|
40
+ hash["X-Riak-Meta-#{k}"] = v.to_s
41
+ end
42
+ end
43
+ unless robject.indexes.blank?
44
+ robject.indexes.each do |k,v|
45
+ hash["X-Riak-Index-#{k}"] = v.to_a.sort.map {|i| i.to_s }.join(", ")
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ # Load object data from an HTTP response
52
+ # @param [Hash] response a response from {Riak::Client::HTTPBackend}
53
+ def load_object(robject, response)
54
+ extract_header(robject, response, "location", :key) {|v| URI.unescape(v.match(%r{.*/(.*?)(\?.*)?$})[1]) }
55
+ extract_header(robject, response, "content-type", :content_type)
56
+ extract_header(robject, response, "x-riak-vclock", :vclock)
57
+ extract_header(robject, response, "link", :links) {|v| Set.new(Link.parse(v)) }
58
+ extract_header(robject, response, "etag", :etag)
59
+ extract_header(robject, response, "last-modified", :last_modified) {|v| Time.httpdate(v) }
60
+ robject.meta = response[:headers].inject({}) do |h,(k,v)|
61
+ if k =~ /x-riak-meta-(.*)/i
62
+ h[$1] = v
63
+ end
64
+ h
65
+ end
66
+ robject.indexes = response[:headers].inject(Hash.new {|h,k| h[k] = Set.new }) do |h,(k,v)|
67
+ if k =~ /x-riak-index-((?:.*)_(?:int|bin))$/i
68
+ key = $1
69
+ h[key].merge Array(v).map {|vals| vals.split(/,\s*/).map {|i| key =~ /int$/ ? i.to_i : i } }.flatten
70
+ end
71
+ h
72
+ end
73
+ robject.conflict = (response[:code] && response[:code].to_i == 300 && robject.content_type =~ /multipart\/mixed/)
74
+ robject.siblings = robject.conflict? ? extract_siblings(robject, response[:body]) : nil
75
+ robject.raw_data = response[:body] if response[:body].present? && !robject.conflict?
76
+
77
+ robject.conflict? ? robject.attempt_conflict_resolution : robject
78
+ end
79
+
80
+ private
81
+ def extract_header(robject, response, name, attribute=nil, &block)
82
+ extract_if_present(robject, response[:headers], name, attribute) do |value|
83
+ block ? block.call(value[0]) : value[0]
84
+ end
85
+ end
86
+
87
+ def extract_if_present(robject, hash, key, attribute=nil)
88
+ if hash[key].present?
89
+ attribute ||= key
90
+ value = block_given? ? yield(hash[key]) : hash[key]
91
+ robject.send("#{attribute}=", value)
92
+ end
93
+ end
94
+
95
+ def extract_siblings(robject, data)
96
+ Util::Multipart.parse(data, Util::Multipart.extract_boundary(robject.content_type)).map do |part|
97
+ RObject.new(robject.bucket, robject.key) do |sibling|
98
+ load_object(sibling, part)
99
+ sibling.vclock = robject.vclock
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end