corosync-commander 0.0.2 → 0.1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +2 -1
- data/Gemfile.lock +2 -2
- data/README.md +30 -0
- data/Rakefile +6 -16
- data/VERSION +1 -0
- data/corosync-commander.gemspec +2 -4
- data/examples/ccsh.rb +91 -0
- data/examples/remote_commands.rb +38 -0
- data/lib/corosync_commander/execution.rb +8 -4
- data/lib/corosync_commander.rb +99 -10
- data/spec/corosync_commander.rb +27 -2
- metadata +8 -5
- data/lib/version.rb +0 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9ffd9a76d120685a5cc4ca5b46d63f416c25f2ac
|
4
|
+
data.tar.gz: 8c9f81c68f0ac85a2bc053f634e0f61a8f86257a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5e6ca5cd16bca2f7e87392357cc65de13e6e9f53608d8b3f87f1aef52332f1df86a027bc56abed06a42263ac52627612730bb371664837f0c42d82e263a2375b
|
7
|
+
data.tar.gz: e001b910753074d6633c0704f7c10099119d6d6dcd006e20851e5c4b46a443a1edfe5f19aac1cd37aad39cd59c86ff856db545a7ea8195f9cdff48ef7c61f81a
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
GEM
|
2
2
|
remote: https://rubygems.org/
|
3
3
|
specs:
|
4
|
-
corosync (0.0.
|
4
|
+
corosync (0.1.0.0)
|
5
5
|
ffi (~> 1.9)
|
6
6
|
diff-lcs (1.2.4)
|
7
7
|
ffi (1.9.3)
|
@@ -23,7 +23,7 @@ PLATFORMS
|
|
23
23
|
ruby
|
24
24
|
|
25
25
|
DEPENDENCIES
|
26
|
-
corosync (~> 0.0
|
26
|
+
corosync (~> 0.1.0)
|
27
27
|
rake
|
28
28
|
rdoc
|
29
29
|
rspec
|
data/README.md
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# corosync-commander
|
2
|
+
|
3
|
+
## Description
|
4
|
+
corosync-commander is a simplified interface into cluster communication via the Corosync library, based on the [corosync gem](http://github.com/phemmer/ruby-corosync/).
|
5
|
+
|
6
|
+
It allows you to build apps which communicate with each other in a reliable fashion. You can send messages to the apps and ensure that every app receives the message in the exact same order. This lets you synchronize data between the apps by only sending deltas.
|
7
|
+
The key to this is that when you send a message to the cluster, you receive your own message. Thus corosync-commander becomes a communication bus between the frontend and backend of your application.
|
8
|
+
|
9
|
+
## Examples
|
10
|
+
|
11
|
+
There is a fully working example in the `examples` directory, but here's a brief condensed version:
|
12
|
+
|
13
|
+
require 'corosync_commander'
|
14
|
+
cc = CorosyncCommander.new
|
15
|
+
cc.commands.register('shell command') do |sender, shellcmd|
|
16
|
+
%x{#{shellcmd}}
|
17
|
+
end
|
18
|
+
cc.join('remote commands')
|
19
|
+
|
20
|
+
exe = cc.execute([], 'shell command', 'hostname')
|
21
|
+
enum = exe.to_enum
|
22
|
+
begin
|
23
|
+
enum.each do |sender, response|
|
24
|
+
$stdout.write "#{sender.nodeid}:#{sender.pid}: #{response.chomp}\n"
|
25
|
+
end
|
26
|
+
rescue CorosyncCommander::RemoteException => e
|
27
|
+
puts "Caught remote exception: #{e}"
|
28
|
+
retry
|
29
|
+
end
|
30
|
+
end
|
data/Rakefile
CHANGED
@@ -17,10 +17,7 @@ end
|
|
17
17
|
|
18
18
|
desc 'Bump version'
|
19
19
|
task 'version' do
|
20
|
-
|
21
|
-
const = 'CorosyncCommander::GEM_VERSION'
|
22
|
-
|
23
|
-
current_version = %x{ruby -e "require './#{file}'; puts #{const}"}.chomp
|
20
|
+
current_version = File.read('VERSION').chomp
|
24
21
|
current_version_commit = %x{git rev-parse --verify #{current_version} 2>/dev/null}.chomp
|
25
22
|
current_head_commit = %x{git rev-parse HEAD}.chomp
|
26
23
|
if current_version_commit != '' and current_version_commit != current_head_commit then
|
@@ -32,23 +29,17 @@ task 'version' do
|
|
32
29
|
print "Next version? (#{next_version}): "
|
33
30
|
response = STDIN.gets.chomp
|
34
31
|
if response != '' then
|
35
|
-
raise StandardError, "Not a valid version" unless response.match(/^[0-9\.]
|
32
|
+
raise StandardError, "Not a valid version" unless response.match(/^[0-9\.]+$/)
|
36
33
|
next_version = response
|
37
34
|
end
|
38
35
|
|
39
|
-
|
40
|
-
|
41
|
-
File.open(file, 'r') do |file|
|
42
|
-
file.each_line do |line|
|
43
|
-
new_file_content += line.sub(/(#{const_name}\s*=\s*['"])#{current_version}(['"])/, "\\1#{next_version}\\2")
|
44
|
-
end
|
45
|
-
end
|
46
|
-
File.open(file, 'w') do |file|
|
47
|
-
file.write new_file_content
|
36
|
+
File.open('VERSION', 'w') do |file|
|
37
|
+
file.puts next_version
|
48
38
|
end
|
49
39
|
message = %x{git log #{current_version_commit}..HEAD --pretty=format:'* %s%n %an (%ai) - @%h%n'}.gsub(/'/, "'\\\\''")
|
50
40
|
|
51
|
-
sh "git commit -m 'Version: #{next_version}\n\n#{message}'
|
41
|
+
sh "git commit -m 'Version: #{next_version}\n\n#{message}' VERSION"
|
42
|
+
sh "git tag #{next_version}"
|
52
43
|
|
53
44
|
@spec = nil
|
54
45
|
end
|
@@ -62,7 +53,6 @@ end
|
|
62
53
|
desc 'Publish gem file'
|
63
54
|
task 'publish' do
|
64
55
|
gem_file = "#{spec.name}-#{spec.version}.gem"
|
65
|
-
sh "git tag #{spec.version}"
|
66
56
|
sh "git push"
|
67
57
|
sh "gem push #{gem_file}"
|
68
58
|
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0.0
|
data/corosync-commander.gemspec
CHANGED
@@ -1,6 +1,4 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
Gem::Specification.new 'corosync-commander', CorosyncCommander::GEM_VERSION do |s|
|
1
|
+
Gem::Specification.new 'corosync-commander', File.read('VERSION').chomp do |s|
|
4
2
|
s.description = 'Provides a simplified interface for issuing commands to nodes in a Corosync closed process group.'
|
5
3
|
s.summary = 'Sends/receives Corosync CPG commands'
|
6
4
|
s.homepage = 'http://github.com/phemmer/ruby-corosync-commander/'
|
@@ -9,5 +7,5 @@ Gem::Specification.new 'corosync-commander', CorosyncCommander::GEM_VERSION do |
|
|
9
7
|
s.license = 'MIT'
|
10
8
|
s.files = %x{git ls-files}.split("\n")
|
11
9
|
|
12
|
-
s.add_runtime_dependency 'corosync', '~> 0.0
|
10
|
+
s.add_runtime_dependency 'corosync', '~> 0.1.0'
|
13
11
|
end
|
data/examples/ccsh.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# This is a more heavyweight example of the remote_commands example.
|
3
|
+
# It utilizes additional features such as quorum. It follows the same principles though, and you can launch it as many times as you want to play with it.
|
4
|
+
|
5
|
+
if RUBY_ENGINE == 'ruby' and ENV['RUBY_THREAD_MACHINE_STACK_SIZE'].to_i < 1572864 then
|
6
|
+
ENV['RUBY_THREAD_MACHINE_STACK_SIZE'] = '1572864'
|
7
|
+
exec('ruby', $0, *ARGV)
|
8
|
+
end
|
9
|
+
|
10
|
+
require 'rubygems'
|
11
|
+
require 'bundler/setup'
|
12
|
+
|
13
|
+
$:.unshift(File.expand_path('../../lib', File.realpath(__FILE__)))
|
14
|
+
|
15
|
+
require 'corosync_commander'
|
16
|
+
require 'timeout'
|
17
|
+
require 'readline'
|
18
|
+
|
19
|
+
class CCSH
|
20
|
+
def initialize
|
21
|
+
@cc = CorosyncCommander.new
|
22
|
+
@cc.commands.register 'sh', &self.method(:cc_sh)
|
23
|
+
@cc.on_confchg &self.method(:cc_confchg)
|
24
|
+
@cc.on_quorumchg &self.method(:cc_quorumchg)
|
25
|
+
|
26
|
+
@cc.join('ccsh')
|
27
|
+
end
|
28
|
+
|
29
|
+
def cc_sh(sender, command)
|
30
|
+
output = nil
|
31
|
+
status = nil
|
32
|
+
begin
|
33
|
+
Timeout::timeout(10) do
|
34
|
+
output = %x{#{command}}
|
35
|
+
status = $?.exitstatus
|
36
|
+
end
|
37
|
+
rescue Timeout::Error => e
|
38
|
+
output = 'Command timed out'
|
39
|
+
status = 255
|
40
|
+
end
|
41
|
+
|
42
|
+
[status, output]
|
43
|
+
end
|
44
|
+
|
45
|
+
def cc_confchg(members, left, joined)
|
46
|
+
$stderr.puts "Group membership changed"
|
47
|
+
$stderr.puts "Members: #{members.map{|m| m.to_s}.join(' ')}" if members.size > 0
|
48
|
+
$stderr.puts "Lost: #{left.map{|m| m.to_s}.join(' ')}" if left.size > 0
|
49
|
+
$stderr.puts "Joined: #{joined.map{|m| m.to_s}.join(' ')}" if joined.size > 0
|
50
|
+
$stderr.puts ""
|
51
|
+
end
|
52
|
+
|
53
|
+
def cc_quorumchg(quorate, members)
|
54
|
+
msg = quorate ? "gained" : "lost"
|
55
|
+
$stderr.puts "Quorum #{msg}"
|
56
|
+
$stderr.puts ""
|
57
|
+
end
|
58
|
+
|
59
|
+
def run
|
60
|
+
while line = Readline.readline('> ', true) do
|
61
|
+
line.chomp!
|
62
|
+
|
63
|
+
if line.match(/^!\s*(.*)/) then
|
64
|
+
# internal command
|
65
|
+
command = $1
|
66
|
+
if command == 'leaders' then
|
67
|
+
leader_pool = @cc.instance_variable_get(:@leader_pool)
|
68
|
+
$stdout.write(leader_pool.map{|m| m.to_s}.join("\n") + "\n")
|
69
|
+
elsif command == 'whoami' then
|
70
|
+
$stdout.write(@cc.cpg.member.to_s + "\n")
|
71
|
+
elsif command == 'exit' then
|
72
|
+
exit
|
73
|
+
end
|
74
|
+
else
|
75
|
+
# remote command
|
76
|
+
exe = @cc.execute([], 'sh', line)
|
77
|
+
exe.to_enum.each do |sender, result|
|
78
|
+
status, output = result
|
79
|
+
output.split("\n").each do |line|
|
80
|
+
$stdout.write("#{sender}: #{line}\n")
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
$stdout.write("\n")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
ccsh = CCSH.new
|
91
|
+
ccsh.run
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# This example will send shell commands to all the running instances in the cluster.
|
2
|
+
# It connects to the 'remote commands' CPG group, executes any commands received, and sends the result back to the app that sent the command.
|
3
|
+
# The local app will read commands from STDIN and display the response from all the nodes.
|
4
|
+
#
|
5
|
+
# Launch this as many times as you want and play around with it.
|
6
|
+
#
|
7
|
+
|
8
|
+
|
9
|
+
if RUBY_ENGINE == 'ruby' and !ENV['RUBY_THREAD_MACHINE_STACK_SIZE'] then
|
10
|
+
ENV['RUBY_THREAD_MACHINE_STACK_SIZE'] = '1572864'
|
11
|
+
exec('ruby', $0, *ARGV)
|
12
|
+
end
|
13
|
+
|
14
|
+
$:.unshift(File.expand_path('../../lib', __FILE__))
|
15
|
+
require 'corosync_commander'
|
16
|
+
cc = CorosyncCommander.new
|
17
|
+
cc.commands.register('shell command') do |sender, shellcmd|
|
18
|
+
%x{#{shellcmd}}
|
19
|
+
end
|
20
|
+
cc.join('remote commands')
|
21
|
+
|
22
|
+
while line = STDIN.gets do
|
23
|
+
exe = cc.execute([], 'shell command', line.chomp)
|
24
|
+
enum = exe.to_enum
|
25
|
+
begin
|
26
|
+
enum.each do |sender, response|
|
27
|
+
$stdout.write "
|
28
|
+
#{sender.nodeid}:#{sender.pid}:
|
29
|
+
#{response.chomp}
|
30
|
+
========================================
|
31
|
+
|
32
|
+
"
|
33
|
+
end
|
34
|
+
rescue CorosyncCommander::RemoteException => e
|
35
|
+
puts "Caught remote exception: #{e}"
|
36
|
+
retry
|
37
|
+
end
|
38
|
+
end
|
@@ -47,18 +47,21 @@ class CorosyncCommander::Execution
|
|
47
47
|
# Provides an enumerator that can be looped through.
|
48
48
|
# Will raise an exception if the remote node generated an exception. Can be verified by calling `is_a?(CorosyncCommander::RemoteException)`.
|
49
49
|
# Restarting the enumerator will not restart from the first response, it will continue on to the next.
|
50
|
+
# @param ignore [Exception, Boolean] List of exception classes to suppress. `true` for all
|
50
51
|
# @yieldparam response [Object] The response generated by the remote command.
|
51
52
|
# @yieldparam sender [Corosync::CPG::Member] The node which generated the response.
|
52
53
|
# @return [Enumerator]
|
53
|
-
def to_enum(
|
54
|
+
def to_enum(*ignore)
|
55
|
+
ignore = [Exception] if ignore.size == 1 and ignore[0] == true
|
56
|
+
|
54
57
|
Enumerator.new do |block|
|
55
58
|
begin
|
56
59
|
while response = self.response do
|
57
|
-
block.yield response.
|
60
|
+
block.yield response.sender, response.content
|
58
61
|
end
|
59
62
|
rescue CorosyncCommander::RemoteException => e
|
60
|
-
|
61
|
-
|
63
|
+
retry if ignore.find {|extype| e.is_a?(extype)}
|
64
|
+
raise e
|
62
65
|
end
|
63
66
|
end
|
64
67
|
end
|
@@ -104,6 +107,7 @@ class CorosyncCommander::Execution
|
|
104
107
|
|
105
108
|
raise RuntimeError, "Received unexpected response while waiting for echo" if message.type != 'echo'
|
106
109
|
|
110
|
+
# CorosyncCommander#cpg_message sets the content to the list of cpg members at the time the echo was received
|
107
111
|
@pending_members = @recipients.size == 0 ? message.content.dup : message.content & @recipients
|
108
112
|
end
|
109
113
|
|
data/lib/corosync_commander.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'corosync/cpg'
|
2
|
+
require 'corosync/quorum'
|
2
3
|
require File.expand_path('../corosync_commander/execution', __FILE__)
|
3
4
|
require File.expand_path('../corosync_commander/execution/message', __FILE__)
|
4
5
|
require File.expand_path('../corosync_commander/callback_list', __FILE__)
|
@@ -16,7 +17,7 @@ require File.expand_path('../corosync_commander/callback_list', __FILE__)
|
|
16
17
|
#
|
17
18
|
# @example
|
18
19
|
# cc = CorosyncCommander.new
|
19
|
-
# cc.commands.register('shell command') do |shellcmd|
|
20
|
+
# cc.commands.register('shell command') do |sender, shellcmd|
|
20
21
|
# %x{#{shellcmd}}
|
21
22
|
# end
|
22
23
|
# cc.join('my group')
|
@@ -26,7 +27,7 @@ require File.expand_path('../corosync_commander/callback_list', __FILE__)
|
|
26
27
|
# enum = exe.to_enum
|
27
28
|
# hostnames = []
|
28
29
|
# begin
|
29
|
-
# enum.each do |
|
30
|
+
# enum.each do |sender, response|
|
30
31
|
# hostnames << response
|
31
32
|
# end
|
32
33
|
# rescue CorosyncCommander::RemoteException => e
|
@@ -64,6 +65,11 @@ class CorosyncCommander
|
|
64
65
|
@cpg.connect
|
65
66
|
@cpg.fd.close_on_exec = true
|
66
67
|
|
68
|
+
@quorum = Corosync::Quorum.new
|
69
|
+
@quorum.on_notify {|*args| quorum_notify(*args)}
|
70
|
+
@quorum.connect
|
71
|
+
@quorum.fd.close_on_exec = true
|
72
|
+
|
67
73
|
@cpg_members = nil
|
68
74
|
|
69
75
|
@leader_pool = []
|
@@ -78,16 +84,30 @@ class CorosyncCommander
|
|
78
84
|
|
79
85
|
@command_callbacks = CorosyncCommander::CallbackList.new
|
80
86
|
|
87
|
+
if RUBY_ENGINE == 'ruby' and (Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.0.0') or ENV['RUBY_THREAD_MACHINE_STACK_SIZE'].to_i < 1572864) then
|
88
|
+
abort "MRI Ruby must be >= 2.0 and RUBY_THREAD_MACHINE_STACK_SIZE must be > 1572864"
|
89
|
+
end
|
90
|
+
|
91
|
+
join(group_name) if group_name
|
92
|
+
end
|
93
|
+
|
94
|
+
# Starts watching for notifications
|
95
|
+
# @return [void]
|
96
|
+
def start
|
97
|
+
@quorum.start(true)
|
98
|
+
|
81
99
|
@dispatch_thread = Thread.new do
|
82
100
|
Thread.current.abort_on_exception = true
|
83
101
|
loop do
|
84
|
-
@cpg.
|
102
|
+
select_ready = select([@cpg.fd, @quorum.fd], [], [])
|
103
|
+
if select_ready[0].include?(@quorum.fd) then
|
104
|
+
@quorum.dispatch
|
105
|
+
end
|
106
|
+
if select_ready[0].include?(@cpg.fd) then
|
107
|
+
@cpg.dispatch
|
108
|
+
end
|
85
109
|
end
|
86
110
|
end
|
87
|
-
|
88
|
-
if group_name then
|
89
|
-
join(group_name)
|
90
|
-
end
|
91
111
|
end
|
92
112
|
|
93
113
|
# Joins the specified group.
|
@@ -95,17 +115,30 @@ class CorosyncCommander
|
|
95
115
|
# @param group_name [String] Name of group to join
|
96
116
|
# @return [void]
|
97
117
|
def join(group_name)
|
118
|
+
start unless @dispatch_thread
|
119
|
+
|
98
120
|
@cpg.join(group_name)
|
99
121
|
end
|
100
122
|
|
123
|
+
# Leave the active CPG group.
|
124
|
+
# Will not stop quorum notifications. If you wish to stop quorum as well you should use {#stop} instead.
|
125
|
+
# @return [void]
|
126
|
+
def leave
|
127
|
+
@cpg.leave
|
128
|
+
end
|
129
|
+
|
101
130
|
# Shuts down the dispatch thread and disconnects CPG
|
102
131
|
# @return [void]
|
103
132
|
def stop
|
104
|
-
@dispatch_thread.kill
|
133
|
+
@dispatch_thread.kill if !@dispatch_thread.nil?
|
105
134
|
@dispatch_thread = nil
|
106
|
-
|
135
|
+
|
136
|
+
@cpg.close if !@cpg.nil?
|
107
137
|
@cpg = nil
|
108
138
|
@cpg_members = nil
|
139
|
+
|
140
|
+
@quorum.finalize if !@quorum.nil?
|
141
|
+
@quorum = nil
|
109
142
|
end
|
110
143
|
|
111
144
|
def next_execution_id()
|
@@ -154,6 +187,19 @@ class CorosyncCommander
|
|
154
187
|
message_echo.content = @cpg_members
|
155
188
|
execution_queue << message_echo
|
156
189
|
end
|
190
|
+
elsif message.type == 'leader reset' then
|
191
|
+
# The sender is requesting we reset their leader position
|
192
|
+
# For remote node, act as if the node left.
|
193
|
+
# For the local node, act as if we just joined.
|
194
|
+
if sender != @cpg.member then
|
195
|
+
@leader_pool.sync_synchronize(:EX) do
|
196
|
+
@leader_pool.delete(sender)
|
197
|
+
end
|
198
|
+
else
|
199
|
+
@leader_pool.sync_synchronize(:EX) do
|
200
|
+
@leader_pool.replace(@cpg_members.to_a)
|
201
|
+
end
|
202
|
+
end
|
157
203
|
elsif message.type != 'command' and message.recipients.include?(@cpg.member) then
|
158
204
|
# It's a response to us
|
159
205
|
execution_queue = nil
|
@@ -183,6 +229,7 @@ class CorosyncCommander
|
|
183
229
|
message_reply = message.reply(reply_value)
|
184
230
|
@cpg.send(message_reply)
|
185
231
|
rescue => e
|
232
|
+
$stderr.puts "Exception: #{e} (#{e.class})\n#{e.backtrace.join("\n")}"
|
186
233
|
message_reply = message.reply([e.class, e.to_s, e.backtrace])
|
187
234
|
message_reply.type = 'exception'
|
188
235
|
@cpg.send(message_reply)
|
@@ -214,7 +261,7 @@ class CorosyncCommander
|
|
214
261
|
end
|
215
262
|
|
216
263
|
@execution_queues.sync_synchronize(:SH) do
|
217
|
-
@execution_queues.each do |queue|
|
264
|
+
@execution_queues.values.each do |queue|
|
218
265
|
messages.each do |message|
|
219
266
|
queue << message
|
220
267
|
end
|
@@ -222,10 +269,34 @@ class CorosyncCommander
|
|
222
269
|
end
|
223
270
|
end
|
224
271
|
|
272
|
+
# Callback to execute when the CPG configuration changes
|
273
|
+
# @yieldparam member_list [Array<Corosync::CPG::Member>] List of members in group after change
|
274
|
+
# @yieldparam left_list [Array<Corosync::CPG::Member>] List of members which left the group
|
275
|
+
# @yieldparam join_list [Array<Corosync::CPG::Member>] List of members which joined the group
|
225
276
|
def on_confchg(&block)
|
226
277
|
@confchg_callback = block
|
227
278
|
end
|
228
279
|
|
280
|
+
# @!visibility private
|
281
|
+
def quorum_notify(quorate, node_list)
|
282
|
+
return if @quorate == quorate # didn't change
|
283
|
+
@quorate = quorate
|
284
|
+
|
285
|
+
if quorate then
|
286
|
+
# we just became quorate
|
287
|
+
@cpg.send(CorosyncCommander::Execution::Message.new(:recipients => [], :type => 'leader reset')) # 'leader reset' simulates a leave and then a join of this node
|
288
|
+
end
|
289
|
+
|
290
|
+
@quorumchg_callback.call(quorate, node_list) if @quorumchg_callback
|
291
|
+
end
|
292
|
+
|
293
|
+
# Callback to execute when the quorum state changes
|
294
|
+
# @yieldparam quorate [Boolean] Whether cluster is quorate
|
295
|
+
# @yieldparam member_list [Array] List of node IDs in the cluster after change
|
296
|
+
def on_quorumchg(&block)
|
297
|
+
@quorumchg_callback = block
|
298
|
+
end
|
299
|
+
|
229
300
|
# @!attribute [r] commands
|
230
301
|
# @return [CorosyncCommander::CallbackList] List of command callbacks
|
231
302
|
def commands
|
@@ -277,6 +348,12 @@ class CorosyncCommander
|
|
277
348
|
# This is slightly different than just calling `leader_position == 0` in that if it is -1 (meaning we havent received the CPG confchg callback yet), we wait for the CPG join to complete.
|
278
349
|
# @return [Boolean]
|
279
350
|
def leader?
|
351
|
+
|
352
|
+
# The way leadership works is that we record the members that were present when we joined the group in @leader_pool. Each time a node leaves the group, we remove them from @leader_pool. Once we become the only member in @leader_pool, we are the leader.
|
353
|
+
# Now in the event that the cluster splits, this becomes complicated. Each side will see the members of the other side leaving. So each side will end up with their own leader. Since they can't talk to eachother, having a leader in each group is perfectly fine. However when the 2 sides re-join, each side will see the members of the other side joining as new nodes, and both leaders will remain as leaders.
|
354
|
+
# We solve this by using the quorum status. When we go from inquorate to quorate, we give up our position. We send a 'leader reset' command to the cluster which tells everyone to remove us from their @leader_pool. When we receive the message ourself, we set @leader_pool to the group members at that moment.
|
355
|
+
# It doesn't matter if multiple members end up doing a 'leader reset' at the same time. It basically simulates the node leaving and then joining. Whoever performs the action first will move to the front. It will capture @leader_pool as the current members when it receives it's own message, and as the other resets come in, it will remove those members. Leaving itself in front of the ones that just joined (and reset after). But it will still remain after all the members that didn't do a reset.
|
356
|
+
|
280
357
|
position = nil
|
281
358
|
loop do
|
282
359
|
position = leader_position
|
@@ -287,4 +364,16 @@ class CorosyncCommander
|
|
287
364
|
end
|
288
365
|
position == 0
|
289
366
|
end
|
367
|
+
|
368
|
+
# Indicates whether cluster is quorate.
|
369
|
+
# @return [Boolean]
|
370
|
+
def quorate?
|
371
|
+
@quorate
|
372
|
+
end
|
373
|
+
|
374
|
+
# List of current members
|
375
|
+
# @return [Array<Corosync::CPG::Member>] List of members currently in the group
|
376
|
+
def members
|
377
|
+
@cpg_members
|
378
|
+
end
|
290
379
|
end
|
data/spec/corosync_commander.rb
CHANGED
@@ -44,7 +44,7 @@ describe CorosyncCommander do
|
|
44
44
|
|
45
45
|
it 'calls the callback' do
|
46
46
|
exe = @cc.execute([], 'summation', 123, 456)
|
47
|
-
results = exe.to_enum.collect do |response
|
47
|
+
results = exe.to_enum.collect do |node,response|
|
48
48
|
response
|
49
49
|
end
|
50
50
|
|
@@ -101,6 +101,31 @@ describe CorosyncCommander do
|
|
101
101
|
end
|
102
102
|
end
|
103
103
|
|
104
|
+
it 'handles pending responses when a node leaves' do
|
105
|
+
complete = false
|
106
|
+
Timeout.timeout(5) do
|
107
|
+
forkpid = fork do
|
108
|
+
cc = CorosyncCommander.new(@cc.cpg.group)
|
109
|
+
cc.commands.register('exit') do |sender|
|
110
|
+
sleep 2
|
111
|
+
exit!
|
112
|
+
end
|
113
|
+
cc.dispatch_thread.join
|
114
|
+
exit 1
|
115
|
+
end
|
116
|
+
|
117
|
+
sleep 1 # wait for the fork to connect
|
118
|
+
|
119
|
+
@cc.commands.register('exit') {} # ignore it
|
120
|
+
exe = @cc.execute([], 'exit')
|
121
|
+
exe.wait
|
122
|
+
complete = true
|
123
|
+
Process.kill('TERM', forkpid)
|
124
|
+
end
|
125
|
+
|
126
|
+
expect(complete).to eq(true)
|
127
|
+
end
|
128
|
+
|
104
129
|
it 'captures remote exceptions' do
|
105
130
|
Timeout.timeout(5) do
|
106
131
|
@cc.commands.register('make exception') do
|
@@ -141,7 +166,7 @@ describe CorosyncCommander do
|
|
141
166
|
responses = []
|
142
167
|
exceptions = 0
|
143
168
|
begin
|
144
|
-
enum.each do |response
|
169
|
+
enum.each do |node,response|
|
145
170
|
responses << response
|
146
171
|
end
|
147
172
|
rescue CorosyncCommander::RemoteException
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: corosync-commander
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Patrick Hemmer
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2014-03-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: corosync
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ~>
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.0
|
19
|
+
version: 0.1.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ~>
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 0.0
|
26
|
+
version: 0.1.0
|
27
27
|
description: Provides a simplified interface for issuing commands to nodes in a Corosync
|
28
28
|
closed process group.
|
29
29
|
email: patrick.hemmer@gmail.com
|
@@ -35,13 +35,16 @@ files:
|
|
35
35
|
- Gemfile
|
36
36
|
- Gemfile.lock
|
37
37
|
- LICENSE
|
38
|
+
- README.md
|
38
39
|
- Rakefile
|
40
|
+
- VERSION
|
39
41
|
- corosync-commander.gemspec
|
42
|
+
- examples/ccsh.rb
|
43
|
+
- examples/remote_commands.rb
|
40
44
|
- lib/corosync_commander.rb
|
41
45
|
- lib/corosync_commander/callback_list.rb
|
42
46
|
- lib/corosync_commander/execution.rb
|
43
47
|
- lib/corosync_commander/execution/message.rb
|
44
|
-
- lib/version.rb
|
45
48
|
- spec/corosync_commander.rb
|
46
49
|
- spec/spec_helper.rb
|
47
50
|
homepage: http://github.com/phemmer/ruby-corosync-commander/
|
data/lib/version.rb
DELETED