riak-client 1.2.0 → 1.4.0

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 (47) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/Gemfile +1 -7
  4. data/README.markdown +66 -0
  5. data/RELEASE_NOTES.md +27 -0
  6. data/lib/riak/bucket.rb +24 -5
  7. data/lib/riak/client.rb +42 -7
  8. data/lib/riak/client/beefcake/message_codes.rb +56 -0
  9. data/lib/riak/client/beefcake/messages.rb +190 -18
  10. data/lib/riak/client/beefcake_protobuffs_backend.rb +143 -10
  11. data/lib/riak/client/feature_detection.rb +26 -1
  12. data/lib/riak/client/http_backend.rb +58 -9
  13. data/lib/riak/client/http_backend/bucket_streamer.rb +15 -0
  14. data/lib/riak/client/http_backend/chunked_json_streamer.rb +42 -0
  15. data/lib/riak/client/http_backend/configuration.rb +17 -1
  16. data/lib/riak/client/http_backend/key_streamer.rb +4 -32
  17. data/lib/riak/client/protobuffs_backend.rb +12 -34
  18. data/lib/riak/counter.rb +101 -0
  19. data/lib/riak/index_collection.rb +71 -0
  20. data/lib/riak/list_buckets.rb +28 -0
  21. data/lib/riak/locale/en.yml +14 -0
  22. data/lib/riak/multiget.rb +123 -0
  23. data/lib/riak/node.rb +2 -0
  24. data/lib/riak/node/configuration.rb +32 -21
  25. data/lib/riak/node/defaults.rb +2 -0
  26. data/lib/riak/node/generation.rb +19 -7
  27. data/lib/riak/node/version.rb +2 -16
  28. data/lib/riak/robject.rb +1 -0
  29. data/lib/riak/secondary_index.rb +67 -0
  30. data/lib/riak/version.rb +1 -1
  31. data/riak-client.gemspec +3 -2
  32. data/spec/integration/riak/counters_spec.rb +51 -0
  33. data/spec/integration/riak/http_backends_spec.rb +24 -14
  34. data/spec/integration/riak/node_spec.rb +6 -28
  35. data/spec/riak/beefcake_protobuffs_backend_spec.rb +84 -0
  36. data/spec/riak/bucket_spec.rb +55 -5
  37. data/spec/riak/client_spec.rb +34 -0
  38. data/spec/riak/counter_spec.rb +122 -0
  39. data/spec/riak/index_collection_spec.rb +50 -0
  40. data/spec/riak/list_buckets_spec.rb +41 -0
  41. data/spec/riak/multiget_spec.rb +76 -0
  42. data/spec/riak/robject_spec.rb +4 -1
  43. data/spec/riak/secondary_index_spec.rb +225 -0
  44. data/spec/spec_helper.rb +1 -0
  45. data/spec/support/sometimes.rb +2 -2
  46. data/spec/support/unified_backend_examples.rb +4 -0
  47. metadata +41 -47
@@ -1,40 +1,12 @@
1
- require 'riak/util/escape'
2
- require 'riak/json'
1
+ require 'riak/client/http_backend/chunked_json_streamer'
3
2
 
4
3
  module Riak
5
4
  class Client
6
5
  class HTTPBackend
7
6
  # @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
7
+ class KeyStreamer < ChunkedJsonStreamer
8
+ def get_values(obj)
9
+ obj['keys']
38
10
  end
39
11
  end
40
12
  end
@@ -4,6 +4,7 @@ require 'base64'
4
4
  require 'digest/sha1'
5
5
  require 'riak/util/translation'
6
6
  require 'riak/client/feature_detection'
7
+ require 'riak/client/beefcake/message_codes'
7
8
 
8
9
  module Riak
9
10
  class Client
@@ -12,38 +13,7 @@ module Riak
12
13
  include Util::Escape
13
14
  include FeatureDetection
14
15
 
15
- # Message Codes Enum
16
- MESSAGE_CODES = %W[
17
- ErrorResp
18
- PingReq
19
- PingResp
20
- GetClientIdReq
21
- GetClientIdResp
22
- SetClientIdReq
23
- SetClientIdResp
24
- GetServerInfoReq
25
- GetServerInfoResp
26
- GetReq
27
- GetResp
28
- PutReq
29
- PutResp
30
- DelReq
31
- DelResp
32
- ListBucketsReq
33
- ListBucketsResp
34
- ListKeysReq
35
- ListKeysResp
36
- GetBucketReq
37
- GetBucketResp
38
- SetBucketReq
39
- SetBucketResp
40
- MapRedReq
41
- MapRedResp
42
- IndexReq
43
- IndexResp
44
- SearchQueryReq
45
- SearchQueryResp
46
- ].map {|s| s.intern }.freeze
16
+ MESSAGE_CODES = BeefcakeMessageCodes
47
17
 
48
18
  def self.simple(method, code)
49
19
  define_method method do
@@ -62,7 +32,6 @@ module Riak
62
32
  simple :ping, :PingReq
63
33
  simple :get_client_id, :GetClientIdReq
64
34
  simple :server_info, :GetServerInfoReq
65
- simple :list_buckets, :ListBucketsReq
66
35
 
67
36
  # Performs a secondary-index query via emulation through MapReduce.
68
37
  # @param [String, Bucket] bucket the bucket to query
@@ -144,6 +113,9 @@ module Riak
144
113
  unless quorum_controls?
145
114
  [:notfound_ok, :basic_quorum, :pr, :pw].each {|k| options.delete k }
146
115
  end
116
+ unless key_object_bucket_timeouts?
117
+ options.delete :timeout
118
+ end
147
119
  unless pb_head?
148
120
  [:head, :return_head].each {|k| options.delete k }
149
121
  end
@@ -160,7 +132,9 @@ module Riak
160
132
  def normalize_quorums(options={})
161
133
  options.dup.tap do |o|
162
134
  [:r, :pr, :w, :pw, :dw, :rw].each do |k|
163
- o[k] = normalize_quorum_value(o[k]) if o[k]
135
+ next o[k] = normalize_quorum_value(o[k]) if o[k]
136
+ s = k.to_s
137
+ o[k] = o[s] = denormalize_quorum_value(o[s]) if o[s]
164
138
  end
165
139
  end
166
140
  end
@@ -168,6 +142,10 @@ module Riak
168
142
  def normalize_quorum_value(q)
169
143
  QUORUMS[q.to_s] || q.to_i
170
144
  end
145
+
146
+ def denormalize_quorum_value(q)
147
+ QUORUMS.invert[q] || q.to_i
148
+ end
171
149
  end
172
150
  end
173
151
  end
@@ -0,0 +1,101 @@
1
+ require 'riak/failed_request'
2
+
3
+ module Riak
4
+
5
+ # A distributed counter that supports incrementing by positive and negative
6
+ # integers.
7
+ class Counter
8
+ include Util::Translation
9
+ attr_accessor :bucket
10
+ attr_accessor :key
11
+ attr_accessor :client
12
+
13
+ # Create a Riak counter.
14
+ # @param [Bucket] bucket the {Riak::Bucket} for this counter
15
+ # @param [String] key the name of the counter
16
+ def initialize(bucket, key)
17
+ raise ArgumentError, t("bucket_type", bucket: bucket.inspect) unless bucket.is_a? Bucket
18
+ raise ArgumentError, t("string_type", string: key.inspect) unless key.is_a? String
19
+ @bucket, @key = bucket, key
20
+ @client = bucket.client
21
+
22
+ validate_bucket
23
+ end
24
+
25
+ # Retrieve the current value of the counter.
26
+ # @param [Hash] options
27
+ # @option options [Fixnum,String] :r ("quorum") read quorum (numeric or
28
+ # symbolic)
29
+ def value(options={})
30
+ backend do |backend|
31
+ backend.get_counter bucket, key, options
32
+ end
33
+ end
34
+ alias :to_i :value
35
+
36
+ # Increment the counter and return its new value.
37
+ # @param amount [Integer] the amount to increment the counter by.
38
+ def increment_and_return(amount=1)
39
+ increment amount, return_value: true
40
+ end
41
+
42
+ # Decrement the counter and return its new value.
43
+ # @param amount [Integer] the amount to decrement the counter by. Negative
44
+ # values increment the counter.
45
+ def decrement_and_return(amount=1)
46
+ increment_and_return -amount
47
+ end
48
+
49
+ # Increment the counter.
50
+ # @param amount [Integer] the amount to increment the counter by
51
+ # @param [Hash] options
52
+ # @option options [Boolean] :return_value whether to return the new counter
53
+ # value. Default false.
54
+ # @option options [Fixnum,String] :r ("quorum") read quorum (numeric or
55
+ # symbolic)
56
+ # @option options [Fixnum] :w the "w" parameter (Write quorum)
57
+ # @option options [Fixnum] :dw the "dw" parameter (Durable-write quorum)
58
+ def increment(amount=1, options={})
59
+ validate_amount amount
60
+
61
+ backend do |backend|
62
+ backend.post_counter bucket, key, amount, options
63
+ end
64
+ end
65
+
66
+ # Decrement the counter.
67
+ # @param amount [Integer] the amount to decrement the counter by. Negative
68
+ # values increment the counter.
69
+ # @param [Hash] options
70
+ # @option options [Boolean] :return_value whether to return the new counter
71
+ # value. Default false.
72
+ # @option options [Fixnum,String] :r ("quorum") read quorum (numeric or
73
+ # symbolic)
74
+ # @option options [Fixnum] :w the "w" parameter (Write quorum)
75
+ # @option options [Fixnum] :dw the "dw" parameter (Durable-write quorum)
76
+ def decrement(amount=1, options={})
77
+ increment(-amount, options)
78
+ end
79
+
80
+ private
81
+ def validate_bucket
82
+ raise ArgumentError, t("counter.bucket_needs_allow_mult") unless bucket.allow_mult
83
+ end
84
+
85
+ def validate_amount(amount)
86
+ raise ArgumentError, t("counter.increment_by_integer") unless amount.is_a? Integer
87
+ end
88
+
89
+ def backend(&blk)
90
+ begin
91
+ return client.backend &blk
92
+ rescue Riak::FailedRequest => e
93
+ raise QuorumError.new e if e.message =~ /unsatisfied/
94
+ raise e
95
+ end
96
+ end
97
+
98
+ class QuorumError < Riak::FailedRequest
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,71 @@
1
+ module Riak
2
+
3
+ # IndexCollection provides extra tools for managing index matches returned by
4
+ # a Secondary Index query. In Riak 1.4, these queries can be paginaged, and
5
+ # match keys up with the index values they matched against.
6
+ class IndexCollection < Array
7
+
8
+ # @return [String] The continuation used to retrieve the next page of a
9
+ # paginated query.
10
+ attr_accessor :continuation
11
+
12
+ # @return [Hash<Integer/String, String>] A hash of index keys (String or
13
+ # Integer, depending on whether the query was a binary or integer) to
14
+ # arrays of keys.
15
+ attr_accessor :with_terms
16
+
17
+ def self.new_from_json(json)
18
+ parsed = JSON.parse json
19
+ fresh = nil
20
+ if parsed['keys']
21
+ fresh = new parsed['keys']
22
+ elsif parsed['results']
23
+ fresh_terms = load_json_terms(parsed)
24
+ fresh = new fresh_terms.values.flatten
25
+ fresh.with_terms = fresh_terms
26
+ else
27
+ fresh = new []
28
+ end
29
+ fresh.continuation = parsed['continuation']
30
+
31
+ fresh
32
+ end
33
+
34
+ def self.new_from_protobuf(message)
35
+ fresh = nil
36
+ if message.keys
37
+ fresh = new message.keys
38
+ elsif message.results
39
+ fresh_terms = load_pb_terms(message)
40
+ fresh = new fresh_terms.values.flatten
41
+ fresh.with_terms = fresh_terms
42
+ else
43
+ fresh = new
44
+ end
45
+ fresh.continuation = message.continuation
46
+
47
+ fresh
48
+ end
49
+
50
+ private
51
+ def self.load_json_terms(parsed)
52
+ fresh_terms = Hash.new{Array.new}
53
+ parsed['results'].each do |r|
54
+ k = r.keys.first
55
+ v = r[k]
56
+ fresh_terms[k] += [v]
57
+ end
58
+
59
+ fresh_terms
60
+ end
61
+
62
+ def self.load_pb_terms(message)
63
+ fresh_terms = Hash.new{Array.new}
64
+ message.results.each do |r|
65
+ fresh_terms[r.key] += [r.value]
66
+ end
67
+
68
+ fresh_terms
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,28 @@
1
+ module Riak
2
+ class ListBuckets
3
+ def initialize(client, options, block)
4
+ @client = client
5
+ @block = block
6
+ @options = options
7
+ perform_request
8
+ end
9
+
10
+ def perform_request
11
+ @client.backend do |be|
12
+ be.list_buckets @options, &wrapped_block
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def wrapped_block
19
+ proc do |bucket_names|
20
+ next if bucket_names.nil?
21
+ bucket_names.each do |bucket_name|
22
+ bucket = @client.bucket bucket_name
23
+ @block.call bucket
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -2,9 +2,13 @@ en:
2
2
  riak:
3
3
  backwards_clock: "System clock moved backwards, ID generation will fail for %{delay} more milliseconds."
4
4
  bucket_link_conversion: "Can't convert a bucket link to a walk spec"
5
+ bucket_type: "invalid argument %{bucket} is not a Riak::Bucket"
5
6
  client_type: "invalid argument %{client} is not a Riak::Client"
6
7
  conflict_resolver_invalid: "The given resolver (%{resolver}) did not respond to :call"
7
8
  content_type_undefined: "content_type is not defined!"
9
+ counter:
10
+ bucket_needs_allow_mult: "Counters require allow_mult to be enabled on their bucket."
11
+ increment_by_integer: "Counters can only be incremented or decremented by integers."
8
12
  deprecated:
9
13
  port: "DEPRECATION: Riak::Client#port has been deprecated, use #http_port or #pb_port for the appropriate protocol.\n%{backtrace}"
10
14
  search: "DEPRECATION: Riak Search features are included in the main client, you no longer need to require 'riak/search'.\n%{backtrace}"
@@ -18,10 +22,17 @@ en:
18
22
  http_failed_request: "Expected %{expected} from Riak but received %{code}. %{body}"
19
23
  hostname_invalid: "host must be a valid hostname"
20
24
  protocol_invalid: "'%{invalid}' is not a valid protocol, valid values are %{valid}"
25
+ index:
26
+ no_next_page: "The returned search did not have a continuation available."
27
+ pagination_not_available: "The Riak server does not support secondary index pagination."
28
+ return_terms_not_available: "The Riak server does not support return_terms."
29
+ streaming_not_available: "The Riak server does not support streaming."
30
+ include_terms_is_wrong: "include_terms isn't a valid option; return_terms is."
21
31
  invalid_basic_auth: "basic auth must be set using 'user:pass'"
22
32
  invalid_client_id: "Invalid client ID, must be a string or between 0 and %{max_id}"
23
33
  invalid_io_object: "Invalid IO-like object assigned to RObject#data. It should be assigned to raw_data instead."
24
34
  invalid_function_value: "invalid value for function: %{value}"
35
+ invalid_multiget_thread_count: "Invalid multiget thread count, must be nil or a positive integer."
25
36
  invalid_options: "Invalid configuration options given."
26
37
  invalid_phase_type: "type must be :map, :reduce, or :link"
27
38
  invalid_ssl_verify_mode: "%{invalid} is not a valid :verify_mode option for SSL. Valid options are 'peer' and 'none'."
@@ -48,7 +59,10 @@ en:
48
59
  source_and_root_required: "Riak::Node configuration must include :source and :root keys."
49
60
  stale_write_prevented: "Stale write prevented by client."
50
61
  stored_function_invalid: "function must have :bucket and :key when a hash"
62
+ streaming_bucket_list_without_block: "Streaming bucket list was requested but no block was given."
51
63
  string_type: "invalid_argument %{string} is not a String"
52
64
  too_few_arguments: "too few arguments: %{params}"
53
65
  walk_spec_invalid_unless_link: "WalkSpec is only valid for a function when the type is :link"
54
66
  wrong_argument_count_walk_spec: "wrong number of arguments (one Hash or bucket,tag,keep required)"
67
+ zero_length_bucket: "bucket name cannot be a String of zero length"
68
+ zero_length_key: "key cannot be a String of zero length"
@@ -0,0 +1,123 @@
1
+ require 'riak/client'
2
+ require 'riak/bucket'
3
+ require 'riak/cluster'
4
+
5
+ module Riak
6
+ # Coordinates a parallel fetch operation for multiple values.
7
+ class Multiget
8
+ include Util::Translation
9
+
10
+ # @return [Riak::Client] the associated client
11
+ attr_reader :client
12
+
13
+ # @return [Array<Bucket, String>] fetch_list an {Array} of {Bucket} and {String} keys to fetch
14
+ attr_reader :fetch_list
15
+
16
+ # @return [Hash<fetch_list_entry, RObject] result_hash a {Hash} of {Bucket} and {String} key pairs to {RObject} instances
17
+ attr_accessor :result_hash
18
+
19
+ # @return [Boolean] finished if the fetch operation has completed
20
+ attr_reader :finished
21
+
22
+ # @return [Integer] The number of threads to use
23
+ attr_accessor :thread_count
24
+
25
+ # Perform a Riak Multiget operation.
26
+ # @param [Client] client the {Riak::Client} that will perform the multiget
27
+ # @param [Array<Bucket, String>] fetch_list an {Array} of {Bucket} and {String} keys to fetch
28
+ # @return [Hash<fetch_list_entry, RObject] result_hash a {Hash} of {Bucket} and {String} key pairs to {Robject} instances
29
+ def self.get_all(client, fetch_list)
30
+ multi = new client, fetch_list
31
+ multi.fetch
32
+ multi.results
33
+ end
34
+
35
+ # Create a Riak Multiget operation.
36
+ # @param [Client] client the {Riak::Client} that will perform the multiget
37
+ # @param [Array<Bucket, String>] fetch_list an {Array} of {Bucket} and {String} keys to fetch
38
+ def initialize(client, fetch_list)
39
+ raise ArgumentError, t('client_type', :client => client.inspect) unless client.is_a? Riak::Client
40
+ raise ArgumentError, t('array_type', :array => fetch_list.inspect) unless fetch_list.is_a? Array
41
+
42
+ validate_fetch_list fetch_list
43
+ @client, @fetch_list = client, fetch_list.uniq
44
+ self.result_hash = Hash.new
45
+ @finished = false
46
+ self.thread_count = client.multiget_threads
47
+ end
48
+
49
+ # Starts the parallelized fetch operation
50
+ # @raise [ArgumentError] when a non-positive-Integer count is given
51
+ def fetch
52
+ queue = fetch_list.dup
53
+ queue_mutex = Mutex.new
54
+ result_mutex = Mutex.new
55
+
56
+ unless thread_count.is_a?(Integer) && thread_count > 0
57
+ raise ArgumentError, t("invalid_multiget_thread_count")
58
+ end
59
+
60
+ @threads = 1.upto(thread_count).map do |_node|
61
+ Thread.new do
62
+ loop do
63
+ pair = queue_mutex.synchronize do
64
+ queue.shift
65
+ end
66
+
67
+ break if pair.nil?
68
+
69
+ found = attempt_fetch(*pair)
70
+ result_mutex.synchronize do
71
+ result_hash[pair] = found
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ def results
79
+ wait_for_finish
80
+ result_hash
81
+ end
82
+
83
+ def finished?
84
+ set_finished_for_thread_liveness
85
+ finished
86
+ end
87
+
88
+ def wait_for_finish
89
+ return if finished?
90
+ @threads.each {|t| t.join }
91
+ @finished = true
92
+ end
93
+
94
+ private
95
+
96
+ def attempt_fetch(bucket, key)
97
+ bucket[key]
98
+ rescue Riak::FailedRequest => e
99
+ raise e unless e.not_found?
100
+ nil
101
+ end
102
+
103
+ def set_finished_for_thread_liveness
104
+ return if @finished # already done
105
+
106
+ all_dead = @threads.none? {|t| t.alive? }
107
+ return unless all_dead # still working
108
+
109
+ @finished = true
110
+ return
111
+ end
112
+
113
+ def validate_fetch_list(fetch_list)
114
+ return unless erroneous = fetch_list.detect do |e|
115
+ bucket, key = e
116
+ next true unless bucket.is_a? Bucket
117
+ next true unless key.is_a? String
118
+ end
119
+
120
+ raise ArgumentError, t('fetch_list_type', :problem => erroneous)
121
+ end
122
+ end
123
+ end