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