better-riak-client 1.0.5

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