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,7 @@
1
+ require 'riak/util/translation'
2
+
3
+ module Riak
4
+ # Raised when an error occurred in the Javascript map-reduce chain.
5
+ # The message will be the body of the JSON error response.
6
+ class MapReduceError < StandardError; end
7
+ end
@@ -0,0 +1,293 @@
1
+ require 'pathname'
2
+ require 'yaml'
3
+
4
+ module Riak
5
+ class Node
6
+ # The directories (and accessor methods) that will be created
7
+ # under the generated node.
8
+ NODE_DIRECTORIES = [:bin, :etc, :log, :data, :ring, :pipe]
9
+
10
+ NODE_DIRECTORIES.each do |dir|
11
+ # Makes accessor methods for all the node directories that
12
+ # return Pathname objects.
13
+ class_eval %Q{
14
+ def #{dir}
15
+ root + '#{dir}'
16
+ end
17
+ }
18
+ end
19
+
20
+ # @return [Hash] the contents of the Erlang environment, which will
21
+ # be created into the app.config file.
22
+ attr_reader :env
23
+
24
+ # @return [Hash] the command-line switches for the Erlang virtual
25
+ # machine, which will be created into the vm.args file
26
+ attr_reader :vm
27
+
28
+ # @return [Hash] the configuration that was passed to the Node
29
+ # when initialized
30
+ attr_reader :configuration
31
+
32
+ # @return [Array<Pathname>] where user Erlang code will be loaded from
33
+ def erlang_sources
34
+ env[:riak_kv][:add_paths].map {|p| Pathname.new(p) }
35
+ end
36
+
37
+ # @return [Pathname] where user Javascript code will be loaded from
38
+ def javascript_source
39
+ Pathname.new(env[:riak_kv][:js_source_dir])
40
+ end
41
+
42
+ # @return [Fixnum] the size of the ring, i.e. number of partitions
43
+ def ring_size
44
+ env[:riak_core][:ring_creation_size]
45
+ end
46
+
47
+ # @return [Fixnum] The port used for handing off data to other nodes.
48
+ def handoff_port
49
+ env[:riak_core][:handoff_port]
50
+ end
51
+
52
+ # @return [Fixnum] The port to which the HTTP API is connected.
53
+ def http_port
54
+ # We'll only support 0.14 and later, which uses http rather than web_ip/web_port
55
+ env[:riak_core][:http][0][1]
56
+ end
57
+
58
+ # @return [Fixnum] the port to which the Protocol Buffers API is connected.
59
+ def pb_port
60
+ env[:riak_kv][:pb_port]
61
+ end
62
+
63
+ # @return [String] the interface to which the HTTP API is connected
64
+ def http_ip
65
+ env[:riak_core][:http][0][0]
66
+ end
67
+
68
+ # @return [String] the interface to which the Protocol Buffers API is connected
69
+ def pb_ip
70
+ env[:riak_kv][:pb_ip]
71
+ end
72
+
73
+ # @return [Symbol] the storage backend for Riak Search.
74
+ def search_backend
75
+ env[:riak_search][:search_backend]
76
+ end
77
+
78
+ # @return [Symbol] the storage backend for Riak KV.
79
+ def kv_backend
80
+ env[:riak_kv][:storage_backend]
81
+ end
82
+
83
+ # @return [String] the name of the Riak node as seen by distributed Erlang
84
+ # communication. AKA "-name" in vm.args.
85
+ def name
86
+ vm['-name']
87
+ end
88
+
89
+ # @return [String] the cookie/shared secret used for connecting
90
+ # a cluster
91
+ def cookie
92
+ vm['-setcookie']
93
+ end
94
+
95
+ # The source of the Riak installation from where the {Node} will
96
+ # be generated. This should point to the directory that contains
97
+ # the 'riak[search]' and 'riak[search]-admin' scripts.
98
+ # @return [Pathname] the source Riak installation
99
+ attr_reader :source
100
+
101
+ # The root directory of the {Node}, where all files are placed
102
+ # after generation.
103
+ # @return [Pathname] the root directory of the node
104
+ attr_reader :root
105
+
106
+ # The script for starting, stopping and pinging the Node.
107
+ # @return [Pathname] the path to the control script
108
+ def control_script
109
+ @control_script ||= root + 'bin' + control_script_name
110
+ end
111
+
112
+ # The name of the 'riak' or 'riaksearch' control script.
113
+ # @return [String] 'riak' or 'riaksearch'
114
+ def control_script_name
115
+ @control_script_name ||= (source + 'riaksearch').exist? ? 'riaksearch' : 'riak'
116
+ end
117
+
118
+ # The script for controlling non-lifecycle features of Riak like
119
+ # joining, leaving, status, ringready, etc.
120
+ # @return [Pathname] the path to the administrative script
121
+ def admin_script
122
+ @admin_script ||= root + 'bin' + "#{control_script_name}-admin"
123
+ end
124
+
125
+ # The "manifest" file where the node configuration will be
126
+ # written.
127
+ # @return [Pathname] the path to the manifest
128
+ def manifest
129
+ root + '.node.yml'
130
+ end
131
+
132
+ protected
133
+ # Populates the proper node configuration from the input config.
134
+ def configure(hash)
135
+ raise ArgumentError, t('source_and_root_required') unless hash[:source] && hash[:root]
136
+ @configuration = hash
137
+ configure_paths
138
+ configure_manifest
139
+ configure_settings
140
+ configure_logging
141
+ configure_data
142
+ configure_ports(hash[:interface], hash[:min_port])
143
+ configure_name(hash[:interface])
144
+ end
145
+
146
+ # Reads the manifest if it exists, overrides the passed configuration.
147
+ def configure_manifest
148
+ @configuration = YAML.load_file(manifest.to_s) if exist?
149
+ end
150
+
151
+ # Sets the data directories for the various on-disk backends and
152
+ # the ring state.
153
+ def configure_data
154
+ [:bitcask, :eleveldb, :merge_index].each {|k| env[k] ||= {} }
155
+ env[:bitcask][:data_root] ||= (data + 'bitcask').expand_path.to_s
156
+ env[:eleveldb][:data_root] ||= (data + 'leveldb').expand_path.to_s
157
+ env[:merge_index][:data_root] ||= (data + 'merge_index').expand_path.to_s
158
+ env[:riak_core][:ring_state_dir] ||= ring.expand_path.to_s
159
+ env[:riak_core][:slide_private_dir] ||= (data + 'slide-data').expand_path.to_s
160
+ NODE_DIRECTORIES.each do |dir|
161
+ next if [:ring, :pipe].include?(dir)
162
+ env[:riak_core][:"platform_#{dir}_dir"] ||= send(dir).to_s
163
+ end
164
+ end
165
+
166
+ # Sets directories and handlers for logging.
167
+ def configure_logging
168
+ if env[:lager]
169
+ env[:lager][:handlers] = {
170
+ :lager_console_backend => :info,
171
+ :lager_file_backend => [
172
+ Tuple[(log+"error.log").expand_path.to_s, :error],
173
+ Tuple[(log+"console.log").expand_path.to_s, :info]
174
+ ]
175
+ }
176
+ env[:lager][:crash_log] = (log+"crash.log").to_s
177
+ else
178
+ # TODO: Need a better way to detect this, the defaults point
179
+ # to 1.0-style configs. Maybe there should be some kind of
180
+ # detection routine.
181
+ # Use sasl error logger for 0.14.
182
+ env[:riak_err] ||= {
183
+ :term_max_size => 65536,
184
+ :fmt_max_bytes => 65536
185
+ }
186
+ env[:sasl] = {
187
+ :sasl_error_logger => Tuple[:file, (log+"sasl-error.log").expand_path.to_s],
188
+ :errlog_type => :error,
189
+ :error_logger_mf_dir => (log+"sasl").expand_path.to_s,
190
+ :error_logger_mf_maxbytes => 10485760,
191
+ :error_logger_mf_maxfiles => 5
192
+ }
193
+ end
194
+ vm['-env ERL_CRASH_DUMP'] = (log + 'erl_crash.dump').to_s
195
+ end
196
+
197
+ # Sets the node name and cookie for distributed Erlang.
198
+ def configure_name(interface)
199
+ interface ||= "127.0.0.1"
200
+ vm["-name"] ||= configuration[:name] || "riak#{rand(1000000).to_s}@#{interface}"
201
+ vm["-setcookie"] ||= configuration[:cookie] || "#{rand(100000).to_s}_#{rand(1000000).to_s}"
202
+ end
203
+
204
+ # Merges input configuration with the defaults.
205
+ def configure_settings
206
+ @env = deep_merge(env.dup, configuration[:env]) if configuration[:env]
207
+ @vm = vm.merge(configuration[:vm]) if configuration[:vm]
208
+ end
209
+
210
+ # Sets the source directory and root directory of the generated node.
211
+ def configure_paths
212
+ @source = Pathname.new(configuration[:source]).expand_path
213
+ @root = Pathname.new(configuration[:root]).expand_path
214
+ # Systems like Homebrew and Stow symlink the scripts into $PATH,
215
+ # but RUNNER_BASE_DIR is not relative to the symlink.
216
+ if (@source + control_script_name).symlink?
217
+ @source = (@source + control_script_name).realpath.parent
218
+ end
219
+ end
220
+
221
+ # Sets ports and interfaces for http, protocol buffers, and handoff.
222
+ def configure_ports(interface, min_port)
223
+ interface ||= "127.0.0.1"
224
+ min_port ||= 8080
225
+ unless env[:riak_core][:http]
226
+ env[:riak_core][:http] = [Tuple[interface, min_port]]
227
+ min_port += 1
228
+ end
229
+ env[:riak_core][:http] = env[:riak_core][:http].map {|pair| Tuple[*pair] }
230
+ env[:riak_kv][:pb_ip] = interface unless env[:riak_kv][:pb_ip]
231
+ unless env[:riak_kv][:pb_port]
232
+ env[:riak_kv][:pb_port] = min_port
233
+ min_port += 1
234
+ end
235
+ unless env[:riak_core][:handoff_port]
236
+ env[:riak_core][:handoff_port] = min_port
237
+ min_port += 1
238
+ end
239
+ end
240
+
241
+ # Implements a deep-merge of two {Hash} instances.
242
+ # @param [Hash] source the original hash
243
+ # @param [Hash] target the new hash
244
+ # @return [Hash] a {Hash} whose {Hash} values have also been merged
245
+ def deep_merge(source, target)
246
+ source.merge(target) do |key, old_val, new_val|
247
+ if Hash === old_val && Hash === new_val
248
+ deep_merge(old_val, new_val)
249
+ else
250
+ new_val
251
+ end
252
+ end
253
+ end
254
+
255
+ # This class lets us specify that some settings should be emitted
256
+ # as Erlang tuples, even though the first element is not
257
+ # necessarily a Symbol.
258
+ class Tuple < Array; end
259
+
260
+ # Recursively converts a {Hash} into an Erlang configuration
261
+ # string that is appropriate for the app.config file.
262
+ # @param [Hash] hash a collection of configuration values
263
+ # @param [Fixnum] depth the current nesting level of
264
+ # generation/indentation
265
+ # @return [String] Erlang proplists in a String for use in
266
+ # app.config
267
+ def to_erlang_config(hash, depth = 1)
268
+ padding = ' ' * depth
269
+ parent_padding = ' ' * (depth-1)
270
+ values = hash.map do |k,v|
271
+ "{#{k}, #{value_to_erlang(v, depth)}}"
272
+ end.join(",\n#{padding}")
273
+ "[\n#{padding}#{values}\n#{parent_padding}]"
274
+ end
275
+
276
+ # Converts a value to an Erlang term. Mutually recursive with
277
+ # {#to_erlang_config}.
278
+ def value_to_erlang(v, depth=1)
279
+ case v
280
+ when Hash
281
+ to_erlang_config(v, depth+1)
282
+ when String
283
+ "\"#{v}\""
284
+ when Tuple
285
+ "{" << v.map {|i| value_to_erlang(i, depth+1) }.join(", ") << "}"
286
+ when Array
287
+ "[" << v.map {|i| value_to_erlang(i, depth+1) }.join(", ") << "]"
288
+ else
289
+ v.to_s
290
+ end
291
+ end
292
+ end
293
+ end
@@ -0,0 +1,133 @@
1
+ require 'expect'
2
+ require 'pathname'
3
+ require 'riak/util/translation'
4
+
5
+ if ENV['DEBUG_RIAK_CONSOLE']
6
+ $expect_verbose = true
7
+ end
8
+
9
+ module Riak
10
+ class Node
11
+ # Eases working with the Erlang console when attached to the Riak
12
+ # node.
13
+ class Console
14
+ include Util::Translation
15
+
16
+ # @return [String] the name of the connected node
17
+ attr_accessor :nodename
18
+
19
+ # Opens a {Console} by connecting to the node.
20
+ # @return [Console] the opened console
21
+ def self.open(node)
22
+ new node.pipe, node.name
23
+ end
24
+
25
+ # Creates a {Console} from the IO pipes connected to the node.
26
+ # @param [String,Pathname] pipedir path to the pipes opened by
27
+ # run_erl
28
+ # @param [String] nodename the name of the node the Console will
29
+ # be attached to
30
+ def initialize(pipedir, nodename)
31
+ @nodename = nodename
32
+ @mutex = Mutex.new
33
+ @prompt = /\(#{Regexp.escape(nodename)}\)\d+>\s*/
34
+ pipedir = Pathname(pipedir)
35
+ pipedir.children.each do |path|
36
+ if path.pipe?
37
+ if path.fnmatch("*.r") # Read pipe
38
+ # debug "Found read pipe: #{path}"
39
+ @rfile ||= path
40
+ elsif path.fnmatch("*.w") # Write pipe
41
+ # debug "Found write pipe: #{path}"
42
+ @wfile ||= path
43
+ end
44
+ else
45
+ debug "Non-pipe found! #{path}"
46
+ end
47
+ end
48
+ raise ArgumentError, t('no_pipes', :path => pipedir.to_s) if [@rfile, @wfile].any? {|p| p.nil? }
49
+
50
+ begin
51
+ debug "Opening write pipe."
52
+ @w = open_write_pipe
53
+ @w.sync = true
54
+ read_ready = false
55
+ # Using threads, we get around JRuby's blocking-open
56
+ # behavior. The main thread pumps the write pipe full of
57
+ # data so that run_erl will start echoing it into the read
58
+ # pipe once we have started the open. On non-JVM rubies,
59
+ # O_NONBLOCK works and we proceed as expected.
60
+ Thread.new do
61
+ debug "Opening read pipe."
62
+ @r = open_read_pipe
63
+ read_ready = true
64
+ end
65
+ Thread.pass
66
+ @w.print "\n" until read_ready
67
+ command "ok."
68
+ debug "Initialized console: #{@r.inspect} #{@w.inspect}"
69
+ rescue => e
70
+ debug e.message
71
+ close
72
+ raise t('no_pipes', :path => pipedir.to_s) + "[ #{e.message} ]"
73
+ end
74
+ end
75
+
76
+ # Sends an Erlang command to the console
77
+ # @param [String] cmd an Erlang expression to send to the node
78
+ def command(cmd)
79
+ @mutex.synchronize do
80
+ begin
81
+ debug "Sending command #{cmd.inspect}"
82
+ @w.print "#{cmd}\n"
83
+ wait_for_erlang_prompt
84
+ rescue SystemCallError
85
+ close
86
+ end
87
+ end
88
+ end
89
+
90
+ # Detects whether the console connection is still open, that is,
91
+ # if the node hasn't disconnected from the other side of the
92
+ # pipe.
93
+ def open?
94
+ (@r && !@r.closed?) && (@w && !@w.closed?)
95
+ end
96
+
97
+ # Scans the output of the console until an Erlang shell prompt
98
+ # is found. Called by {#command} to ensure that the submitted
99
+ # command succeeds.
100
+ def wait_for_erlang_prompt
101
+ wait_for @prompt
102
+ end
103
+
104
+ # Scans the output of the console for the given pattern.
105
+ # @param [String, Regexp] pattern the pattern to scan for
106
+ def wait_for(pattern)
107
+ debug "Scanning for #{pattern.inspect}"
108
+ @r.expect(pattern)
109
+ end
110
+
111
+ # Closes the console by detaching from the pipes.
112
+ def close
113
+ @r.close if @r && !@r.closed?
114
+ @w.close if @w && !@w.closed?
115
+ freeze
116
+ end
117
+
118
+ protected
119
+
120
+ def debug(msg)
121
+ $stderr.puts msg if ENV["DEBUG_RIAK_CONSOLE"]
122
+ end
123
+
124
+ def open_write_pipe
125
+ @wfile.open(File::WRONLY|File::NONBLOCK)
126
+ end
127
+
128
+ def open_read_pipe
129
+ @rfile.open(File::RDONLY|File::NONBLOCK)
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,207 @@
1
+ require 'riak/node/console'
2
+ require 'riak/util/tcp_socket_extensions'
3
+
4
+ module Riak
5
+ class Node
6
+ # Regexp for parsing riak-admin status output. Takes into account
7
+ # the minor bug fixed by {https://github.com/basho/riak_kv/pull/134}
8
+ # and multiline output used when lists of things grow long.
9
+ STATS_REGEXP = /^([^:\n]+)\s:\s((?:.*)(?:\n\s+[^\n]+)*)/
10
+
11
+ # Is the node running?
12
+ # @return [true, false] If the node is running
13
+ def started?
14
+ pinged = ping
15
+ pinged.strip =~ /pong/ || pinged.strip !~ /Node '[^']+' not responding to pings/
16
+ end
17
+
18
+ # Is the node stopped? (opposite of {#started?}).
19
+ # @return [true, false] If the node is stopped
20
+ # @see #started?
21
+ def stopped?
22
+ !started?
23
+ end
24
+
25
+ # Starts the node.
26
+ # @return [String] the output of the 'riak start' command
27
+ def start
28
+ res = riak 'start'
29
+ wait_for_startup
30
+ res
31
+ end
32
+
33
+ # Stops the node
34
+ # @return [String] the output of the 'riak stop' command
35
+ def stop
36
+ res = riak 'stop'
37
+ wait_for_shutdown
38
+ res
39
+ end
40
+
41
+ # Restarts the node
42
+ # @return [String] the output of the 'riak restart' command
43
+ def restart
44
+ riak 'restart'
45
+ end
46
+
47
+ # Reboots the node
48
+ # @return [String] the output of the 'riak reboot' command
49
+ def restart
50
+ riak 'reboot'
51
+ end
52
+
53
+ # Pings the node
54
+ # @return [String] the output of the 'riak ping' command
55
+ def ping
56
+ begin
57
+ riak 'ping'
58
+ rescue SystemCallError
59
+ # If the control script doesn't exist or has the wrong
60
+ # permissions, we should still return something sane so we can
61
+ # do the right thing.
62
+ "Node '#{name}' not responding to pings."
63
+ end
64
+ end
65
+
66
+ # Attach to the node's console via the pipe.
67
+ # @return [Riak::Node::Console] A console manager for sending
68
+ # commands to the Riak node
69
+ # @see #with_console
70
+ def attach
71
+ Console.open self
72
+ end
73
+
74
+ # Execute the block against the Riak node's console.
75
+ # @yield [console] A block of commands to be run against the console
76
+ # @yieldparam [Riak::Node::Console] console A console manager for
77
+ # sending commands to the Riak node
78
+ def with_console
79
+ begin
80
+ console = attach
81
+ yield console
82
+ ensure
83
+ console.close if console
84
+ end
85
+ end
86
+
87
+ # Joins the node to another node to create a cluster.
88
+ # @return [String] the output of the 'riak-admin join' command
89
+ def join(node)
90
+ node = node.name if Node === node
91
+ riak_admin 'join', node
92
+ end
93
+
94
+ # Removes this node from its current cluster, handing off all data.
95
+ # @return [String] the output of the 'riak-admin leave' command
96
+ def leave
97
+ riak_admin 'leave'
98
+ end
99
+
100
+ # Forcibly removes a node from the current cluster without
101
+ # invoking handoff.
102
+ # @return [String] the output of the 'riak-admin remove <node>'
103
+ # command
104
+ def remove(node)
105
+ node = node.name if Node === node
106
+ riak_admin 'remove', node
107
+ end
108
+
109
+ # Captures the status of the node.
110
+ # @return [Hash] a collection of information about the node
111
+ def status
112
+ output = riak_admin 'status'
113
+ if $?.success?
114
+ result = {}
115
+ Hash[output.scan(STATS_REGEXP)]
116
+ end
117
+ end
118
+
119
+ # Detects whether the node's cluster has converged on the ring.
120
+ # @return [true,false] whether the ring is stable
121
+ def ringready?
122
+ output = riak_admin 'ringready'
123
+ output =~ /^TRUE/ || $?.success?
124
+ end
125
+
126
+ # Lists riak_core applications that have registered as available,
127
+ # e.g. ["riak_kv", "riak_search", "riak_pipe"]
128
+ # @return [Array<String>] a list of running services
129
+ def services
130
+ output = riak_admin 'services'
131
+ if $?.success?
132
+ output.strip.match(/^\[(.*)\]$/)[1].split(/,/)
133
+ else
134
+ []
135
+ end
136
+ end
137
+
138
+ # Forces the node to restart/reload its JavaScript VMs,
139
+ # effectively reloading any user-provided code.
140
+ def js_reload
141
+ riak_admin 'js_reload'
142
+ end
143
+
144
+ # Provides the status of members of the ring.
145
+ # @return [Hash] a collection of stats about ring members
146
+ def member_status
147
+ output = riak_admin 'member_status'
148
+ result = {}
149
+ if $?.success?
150
+ output.each_line do |line|
151
+ next if line =~ /^(?:[=-]|Status)+/ # Skip the pretty headers
152
+ if line =~ %r{^Valid:(\d+) / Leaving:(\d+) / Exiting:(\d+) / Joining:(\d+) / Down:(\d+)}
153
+ result.merge!(:valid => $1.to_i,
154
+ :leaving => $2.to_i,
155
+ :exiting => $3.to_i,
156
+ :joining => $4.to_i,
157
+ :down => $5.to_i)
158
+ else
159
+ result[:members] ||= {}
160
+ status, ring, pending, node = line.split(/\s+/)
161
+ node = $1 if node =~ /^'(.*)'$/
162
+ ring = $1.to_f if ring =~ /(\d+\.\d+)%/
163
+ result[:members][node] = {
164
+ :status => status,
165
+ :ring => ring,
166
+ :pending => (pending == '--') ? 0 : pending.to_i
167
+ }
168
+ end
169
+ end
170
+ end
171
+ result
172
+ end
173
+
174
+ # @return [Array<String>] a list of node names that are also
175
+ # members of this node's cluster, and empty list if the
176
+ # {#member_status} fails or no other nodes are present
177
+ def peers
178
+ all_nodes = member_status[:members] && member_status[:members].keys.reject {|n| n == name }
179
+ all_nodes || []
180
+ end
181
+
182
+ protected
183
+ # Runs a command using the 'riak' control script.
184
+ def riak(*commands)
185
+ `#{control_script} #{commands.join(' ')} 2>&1`
186
+ end
187
+
188
+ # Runs a command using the 'riak-admin' script.
189
+ def riak_admin(*commands)
190
+ `#{admin_script} #{commands.join(' ')} 2>&1`
191
+ end
192
+
193
+ # Waits for the HTTP port to become available, which is a better
194
+ # indication of readiness than the start script finishing.
195
+ def wait_for_startup
196
+ TCPSocket.wait_for_service_with_timeout(:host => http_ip,
197
+ :port => http_port,
198
+ :timeout => 10)
199
+ end
200
+
201
+ def wait_for_shutdown
202
+ TCPSocket.wait_for_service_termination_with_timeout(:host => http_ip,
203
+ :port => http_port,
204
+ :timeout => 10)
205
+ end
206
+ end
207
+ end