better-riak-client 1.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +16 -0
- data/README.markdown +198 -0
- data/RELEASE_NOTES.md +211 -0
- data/better-riak-client.gemspec +61 -0
- data/erl_src/riak_kv_test014_backend.beam +0 -0
- data/erl_src/riak_kv_test014_backend.erl +189 -0
- data/erl_src/riak_kv_test_backend.beam +0 -0
- data/erl_src/riak_kv_test_backend.erl +697 -0
- data/erl_src/riak_search_test_backend.beam +0 -0
- data/erl_src/riak_search_test_backend.erl +175 -0
- data/lib/riak/bucket.rb +221 -0
- data/lib/riak/client/beefcake/messages.rb +213 -0
- data/lib/riak/client/beefcake/object_methods.rb +111 -0
- data/lib/riak/client/beefcake_protobuffs_backend.rb +226 -0
- data/lib/riak/client/decaying.rb +36 -0
- data/lib/riak/client/excon_backend.rb +162 -0
- data/lib/riak/client/feature_detection.rb +88 -0
- data/lib/riak/client/http_backend/configuration.rb +211 -0
- data/lib/riak/client/http_backend/key_streamer.rb +43 -0
- data/lib/riak/client/http_backend/object_methods.rb +106 -0
- data/lib/riak/client/http_backend/request_headers.rb +34 -0
- data/lib/riak/client/http_backend/transport_methods.rb +201 -0
- data/lib/riak/client/http_backend.rb +340 -0
- data/lib/riak/client/net_http_backend.rb +82 -0
- data/lib/riak/client/node.rb +115 -0
- data/lib/riak/client/protobuffs_backend.rb +173 -0
- data/lib/riak/client/search.rb +91 -0
- data/lib/riak/client.rb +540 -0
- data/lib/riak/cluster.rb +151 -0
- data/lib/riak/core_ext/blank.rb +53 -0
- data/lib/riak/core_ext/deep_dup.rb +13 -0
- data/lib/riak/core_ext/extract_options.rb +7 -0
- data/lib/riak/core_ext/json.rb +15 -0
- data/lib/riak/core_ext/slice.rb +18 -0
- data/lib/riak/core_ext/stringify_keys.rb +10 -0
- data/lib/riak/core_ext/symbolize_keys.rb +10 -0
- data/lib/riak/core_ext/to_param.rb +31 -0
- data/lib/riak/core_ext.rb +7 -0
- data/lib/riak/encoding.rb +6 -0
- data/lib/riak/failed_request.rb +81 -0
- data/lib/riak/i18n.rb +5 -0
- data/lib/riak/json.rb +52 -0
- data/lib/riak/link.rb +94 -0
- data/lib/riak/locale/en.yml +53 -0
- data/lib/riak/locale/fr.yml +52 -0
- data/lib/riak/map_reduce/filter_builder.rb +103 -0
- data/lib/riak/map_reduce/phase.rb +98 -0
- data/lib/riak/map_reduce.rb +225 -0
- data/lib/riak/map_reduce_error.rb +7 -0
- data/lib/riak/node/configuration.rb +293 -0
- data/lib/riak/node/console.rb +133 -0
- data/lib/riak/node/control.rb +207 -0
- data/lib/riak/node/defaults.rb +83 -0
- data/lib/riak/node/generation.rb +106 -0
- data/lib/riak/node/log.rb +34 -0
- data/lib/riak/node/version.rb +43 -0
- data/lib/riak/node.rb +38 -0
- data/lib/riak/robject.rb +318 -0
- data/lib/riak/search.rb +3 -0
- data/lib/riak/serializers.rb +74 -0
- data/lib/riak/stamp.rb +77 -0
- data/lib/riak/test_server.rb +89 -0
- data/lib/riak/util/escape.rb +76 -0
- data/lib/riak/util/headers.rb +53 -0
- data/lib/riak/util/multipart/stream_parser.rb +62 -0
- data/lib/riak/util/multipart.rb +52 -0
- data/lib/riak/util/tcp_socket_extensions.rb +58 -0
- data/lib/riak/util/translation.rb +19 -0
- data/lib/riak/version.rb +3 -0
- data/lib/riak/walk_spec.rb +105 -0
- data/lib/riak.rb +21 -0
- metadata +348 -0
@@ -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
|