riak-client 1.2.0 → 1.4.0

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