corosync-commander 0.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +30 -0
- data/LICENSE +21 -0
- data/Rakefile +7 -0
- data/corosync-commander.gemspec +13 -0
- data/lib/corosync_commander/callback_list.rb +65 -0
- data/lib/corosync_commander/execution/message.rb +45 -0
- data/lib/corosync_commander/execution.rb +124 -0
- data/lib/corosync_commander.rb +233 -0
- data/lib/version.rb +3 -0
- data/spec/corosync_commander.rb +181 -0
- data/spec/spec_helper.rb +1 -0
- metadata +72 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 90dec889b8284d9eacfcf3ad9fc2e30156790428
|
4
|
+
data.tar.gz: 8beb44d2adfb7f708b48e0f986f8e1e8c68300c5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a65aa251126d8f329c7f9d125b9c4d7508270a5fedcf49e2b147612073ceadaddae4afc13683a4355ef6dd0a86830861f346a70c5f5df78f78c3505cec9a5671
|
7
|
+
data.tar.gz: 5ff4ac01de166776a97c30fa7a8d7f5338383bb927e89c2ea68964b06609a9969f1942f01ec044f2597473a67a0195154df7e618d783ffca5be34e70acaf4595
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
*.gem
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
GEM
|
2
|
+
remote: https://rubygems.org/
|
3
|
+
specs:
|
4
|
+
corosync (0.0.3)
|
5
|
+
ffi (~> 1.9)
|
6
|
+
diff-lcs (1.2.4)
|
7
|
+
ffi (1.9.3)
|
8
|
+
json (1.8.1)
|
9
|
+
rake (10.1.0)
|
10
|
+
rdoc (4.0.1)
|
11
|
+
json (~> 1.4)
|
12
|
+
rspec (2.14.1)
|
13
|
+
rspec-core (~> 2.14.0)
|
14
|
+
rspec-expectations (~> 2.14.0)
|
15
|
+
rspec-mocks (~> 2.14.0)
|
16
|
+
rspec-core (2.14.6)
|
17
|
+
rspec-expectations (2.14.3)
|
18
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
19
|
+
rspec-mocks (2.14.4)
|
20
|
+
yard (0.8.7.2)
|
21
|
+
|
22
|
+
PLATFORMS
|
23
|
+
ruby
|
24
|
+
|
25
|
+
DEPENDENCIES
|
26
|
+
corosync (~> 0.0.3)
|
27
|
+
rake
|
28
|
+
rdoc
|
29
|
+
rspec
|
30
|
+
yard
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2013 Patrick Hemmer <patrick.hemmer@gmail.com>
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require File.expand_path('../lib/version.rb', __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new 'corosync-commander', CorosyncCommander::GEM_VERSION do |s|
|
4
|
+
s.description = 'Provides a simplified interface for issuing commands to nodes in a Corosync closed process group.'
|
5
|
+
s.summary = 'Sends/receives Corosync CPG commands'
|
6
|
+
s.homepage = 'http://github.com/phemmer/ruby-corosync-commander/'
|
7
|
+
s.author = 'Patrick Hemmer'
|
8
|
+
s.email = 'patrick.hemmer@gmail.com'
|
9
|
+
s.license = 'MIT'
|
10
|
+
s.files = %x{git ls-files}.split("\n")
|
11
|
+
|
12
|
+
s.add_runtime_dependency 'corosync', '~> 0.0.3'
|
13
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
class CorosyncCommander::CallbackList
|
2
|
+
include Enumerable
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
@callbacks = {}
|
6
|
+
@callbacks.extend(Sync_m)
|
7
|
+
end
|
8
|
+
|
9
|
+
# Iterate through each command/callback
|
10
|
+
# @yieldparam command [String] Name of command
|
11
|
+
# @yieldparam callback [Proc] Proc to be executed upon receiving command
|
12
|
+
def each(&block)
|
13
|
+
callbacks = nil
|
14
|
+
@callbacks.synchronize(:SH) do
|
15
|
+
callbacks = @callbacks.dup
|
16
|
+
end
|
17
|
+
callbacks.each(&block)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Assign a callback
|
21
|
+
# @example
|
22
|
+
# cc.commands['my command'] = Proc.new do
|
23
|
+
# puts "Hello world!"
|
24
|
+
# end
|
25
|
+
# @param command [String] Name of command
|
26
|
+
# @param block [Proc] Proc to call when command is executed
|
27
|
+
# @return [Proc]
|
28
|
+
def []=(command, block)
|
29
|
+
@callbacks.synchronize(:EX) do
|
30
|
+
@callbacks[command] = block
|
31
|
+
end
|
32
|
+
block
|
33
|
+
end
|
34
|
+
|
35
|
+
# Assign a callback
|
36
|
+
# This is another method of assigning a callback
|
37
|
+
# @example
|
38
|
+
# cc.commands.register('my command') do
|
39
|
+
# puts "Hellow world!"
|
40
|
+
# end
|
41
|
+
# @param command [String] Name of command
|
42
|
+
# @param block [Proc] Proc to call when command is executed
|
43
|
+
# @return [Proc]
|
44
|
+
def register(command, &block)
|
45
|
+
self[command] = block
|
46
|
+
end
|
47
|
+
|
48
|
+
# Retrieve a registered command callback
|
49
|
+
# @param command [String] Name of command
|
50
|
+
# @return [Proc]
|
51
|
+
def [](command)
|
52
|
+
@callbacks.synchronize(:SH) do
|
53
|
+
@callbacks[command]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Delete a command callback
|
58
|
+
# @param command [String] Name of command
|
59
|
+
# @return [Proc] The deleted command callback
|
60
|
+
def delete(command)
|
61
|
+
@callbacks.synchronize(:EX) do
|
62
|
+
@callbacks.delete(command)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
class CorosyncCommander
|
2
|
+
class Execution
|
3
|
+
end
|
4
|
+
end
|
5
|
+
class CorosyncCommander::Execution::Message
|
6
|
+
attr_reader :sender
|
7
|
+
attr_reader :recipients
|
8
|
+
attr_reader :execution_id
|
9
|
+
attr_accessor :type
|
10
|
+
attr_accessor :content
|
11
|
+
|
12
|
+
def self.from_cpg_message(data, sender)
|
13
|
+
data = JSON.parse(data)
|
14
|
+
|
15
|
+
recipients = Corosync::CPG::MemberList.new
|
16
|
+
data[0].each do |m|
|
17
|
+
nodeid,pid = m.split(':').map{|i| i.to_i}
|
18
|
+
recipients << Corosync::CPG::Member.new(nodeid,pid)
|
19
|
+
end
|
20
|
+
|
21
|
+
execution_id = data[1]
|
22
|
+
|
23
|
+
type = data[2]
|
24
|
+
|
25
|
+
content = data[3]
|
26
|
+
|
27
|
+
self.new(:sender => sender, :recipients => recipients, :execution_id => execution_id, :type => type, :content => content)
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(params = {})
|
31
|
+
@sender = params[:sender]
|
32
|
+
@recipients = Corosync::CPG::MemberList.new(params[:recipients])
|
33
|
+
@execution_id = params[:execution_id]
|
34
|
+
@type = params[:type]
|
35
|
+
@content = params[:content]
|
36
|
+
end
|
37
|
+
|
38
|
+
def reply(content)
|
39
|
+
self.class.new(:recipients => [@sender], :execution_id => @execution_id, :type => 'response', :content => content)
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_s
|
43
|
+
[@recipients.to_a, @execution_id, @type, @content].to_json
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
class CorosyncCommander
|
2
|
+
end
|
3
|
+
class CorosyncCommander::Execution
|
4
|
+
attr_reader :queue
|
5
|
+
attr_reader :id
|
6
|
+
attr_reader :recipients
|
7
|
+
attr_reader :command
|
8
|
+
attr_reader :args
|
9
|
+
attr_reader :pending_members
|
10
|
+
|
11
|
+
def initialize(cc, id, recipients, command, args)
|
12
|
+
@cc = cc
|
13
|
+
@id = id
|
14
|
+
@recipients = Corosync::CPG::MemberList.new(recipients)
|
15
|
+
@command = command
|
16
|
+
@args = args
|
17
|
+
|
18
|
+
@queue = Queue.new
|
19
|
+
|
20
|
+
@pending_members = nil
|
21
|
+
|
22
|
+
@responses = []
|
23
|
+
end
|
24
|
+
|
25
|
+
# Gets the next response, blocking if none has been returned yet.
|
26
|
+
# This will also raise an exception if the remote process raised an exception. This can be tested by calling `exception.is_a?(CorosyncCommander::RemoteException)`
|
27
|
+
# @return [CorosyncCommander::Execution::Message] The response from the remote host. Returns `nil` when there are no more responses.
|
28
|
+
def response
|
29
|
+
response = get_response
|
30
|
+
|
31
|
+
return nil if response.nil?
|
32
|
+
|
33
|
+
if response.type == 'exception'
|
34
|
+
e_class = Kernel.const_get(response.content[0])
|
35
|
+
e_class = StandardError if e_class.nil? or !(e_class <= Exception) # The remote node might have types we don't have. So if we don't have them use StandardError
|
36
|
+
e = e_class.new(response.content[1] + " (CorosyncCommander::RemoteException@#{response.sender})")
|
37
|
+
e.set_backtrace(response.content[2])
|
38
|
+
e.extend(CorosyncCommander::RemoteException)
|
39
|
+
e.sender = response.sender
|
40
|
+
raise e
|
41
|
+
end
|
42
|
+
|
43
|
+
response
|
44
|
+
end
|
45
|
+
alias_method :next, :response
|
46
|
+
|
47
|
+
# Provides an enumerator that can be looped through.
|
48
|
+
# Will raise an exception if the remote node generated an exception. Can be verified by calling `is_a?(CorosyncCommander::RemoteException)`.
|
49
|
+
# Restarting the enumerator will not restart from the first response, it will continue on to the next.
|
50
|
+
# @yieldparam response [Object] The response generated by the remote command.
|
51
|
+
# @yieldparam sender [Corosync::CPG::Member] The node which generated the response.
|
52
|
+
# @return [Enumerator]
|
53
|
+
def to_enum(ignore_exception = false)
|
54
|
+
Enumerator.new do |block|
|
55
|
+
begin
|
56
|
+
while response = self.response do
|
57
|
+
block.yield response.content, response.sender
|
58
|
+
end
|
59
|
+
rescue CorosyncCommander::RemoteException => e
|
60
|
+
raise e unless ignore_exception
|
61
|
+
retry
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Wait for all responses to come in, but discard them.
|
67
|
+
# Useful to block waiting for the remote commands to finish when you dont care about the result.
|
68
|
+
# @param ignore_exception [Boolean] Whether to ignore remote exceptions, or raise them. If `true`, remote exceptions will not raise an exception here.
|
69
|
+
# @return [Boolean] Returns `true` if no exceptions were raised, `false` otherwise.
|
70
|
+
def wait(ignore_exception = false)
|
71
|
+
success = true
|
72
|
+
begin
|
73
|
+
while response do end
|
74
|
+
rescue CorosyncCommander::RemoteException => e
|
75
|
+
success = false
|
76
|
+
retry if ignore_exception
|
77
|
+
raise e
|
78
|
+
end
|
79
|
+
success
|
80
|
+
end
|
81
|
+
|
82
|
+
# This is just so that we can remove the queue from execution_queues and avoid running unnecessary code on receipt of message/confchg
|
83
|
+
def discard
|
84
|
+
@cc.execution_queues.sync_synchronize(:EX) do
|
85
|
+
@cc.execution_queues.delete(@id)
|
86
|
+
end
|
87
|
+
@queue.clear
|
88
|
+
@queue = []
|
89
|
+
end
|
90
|
+
|
91
|
+
# Gets the next response message from the queue.
|
92
|
+
# This is used internally and is probably not what you want. See {#response}
|
93
|
+
# @return [CorosyncCommander::Execution::Message]
|
94
|
+
def get_response
|
95
|
+
return if !@queue.is_a?(Queue) # we've called `clear`
|
96
|
+
|
97
|
+
while @pending_members.nil?
|
98
|
+
message = @queue.shift
|
99
|
+
|
100
|
+
next if message.type == 'leave' # we havent received the echo, so we dont care yet
|
101
|
+
|
102
|
+
raise RuntimeError, "Received unexpected response while waiting for echo" if message.type != 'echo'
|
103
|
+
|
104
|
+
@pending_members = @recipients.size == 0 ? message.content.dup : message.content & @recipients
|
105
|
+
end
|
106
|
+
|
107
|
+
return if @pending_members.size == 0
|
108
|
+
|
109
|
+
message = @queue.shift
|
110
|
+
|
111
|
+
@pending_members.delete message.sender
|
112
|
+
if @pending_members.size == 0 then
|
113
|
+
self.discard
|
114
|
+
end
|
115
|
+
|
116
|
+
return if message.type == 'leave' # we already did @pending_members.delete above
|
117
|
+
|
118
|
+
message
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
module CorosyncCommander::RemoteException
|
123
|
+
attr_accessor :sender
|
124
|
+
end
|
@@ -0,0 +1,233 @@
|
|
1
|
+
require 'corosync/cpg'
|
2
|
+
require File.expand_path('../corosync_commander/execution', __FILE__)
|
3
|
+
require File.expand_path('../corosync_commander/execution/message', __FILE__)
|
4
|
+
require File.expand_path('../corosync_commander/callback_list', __FILE__)
|
5
|
+
|
6
|
+
# This provides a simplified interface into Corosync::CPG.
|
7
|
+
# The main use case is for sending commands to a remote server, and waiting for the responses.
|
8
|
+
#
|
9
|
+
# This library takes care of:
|
10
|
+
# * Ensuring a consistent message format.
|
11
|
+
# * Sending messages to all, or just specific nodes.
|
12
|
+
# * Invoking the appropriate callback (and passing parameters) based on the command sent.
|
13
|
+
# * Resonding with the return value of the callback.
|
14
|
+
# * Handling exceptions and sending them back to the sender.
|
15
|
+
# * Knowing exactly how many responses should be coming back.
|
16
|
+
#
|
17
|
+
# @example
|
18
|
+
# cc = CorosyncCommander.new
|
19
|
+
# cc.commands.register('shell command') do |shellcmd|
|
20
|
+
# %x{#{shellcmd}}
|
21
|
+
# end
|
22
|
+
# cc.join('my group')
|
23
|
+
#
|
24
|
+
# exe = cc.execute([], 'shell command', 'hostname')
|
25
|
+
#
|
26
|
+
# enum = exe.to_enum
|
27
|
+
# hostnames = []
|
28
|
+
# begin
|
29
|
+
# enum.each do |response, node|
|
30
|
+
# hostname << response
|
31
|
+
# end
|
32
|
+
# rescue CorosyncCommander::RemoteException => e
|
33
|
+
# puts "Caught remote exception: #{e}"
|
34
|
+
# retry
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# puts "Hostnames: #{hostnames.join(' ')}"
|
38
|
+
#
|
39
|
+
#
|
40
|
+
# == IMPORTANT: Will not work without tuning ruby.
|
41
|
+
# You cannot use this with MRI Ruby older than 2.0. Even with 2.0 you must tune ruby. This is because Corosync CPG (as of 1.4.3) allocates a 1mb buffer on the stack. Ruby 2.0 only allocates a 512kb stack for threads. This gem uses a thread for handling incoming messages. Thus if you try to use older ruby you will get segfaults.
|
42
|
+
#
|
43
|
+
# Ruby 2.0 allows increasing the thread stack size. You can do this with the RUBY_THREAD_MACHINE_STACK_SIZE environment variable. The advised value to set is 1.5mb.
|
44
|
+
# RUBY_THREAD_MACHINE_STACK_SIZE=1572864 ruby yourscript.rb
|
45
|
+
class CorosyncCommander
|
46
|
+
require 'thread'
|
47
|
+
require 'sync'
|
48
|
+
require 'json'
|
49
|
+
|
50
|
+
attr_reader :cpg
|
51
|
+
|
52
|
+
attr_reader :execution_queues
|
53
|
+
|
54
|
+
# Creates a new instance and connects to CPG.
|
55
|
+
# If a group name is provided, it will join that group. Otherwise it will only connect. This is so that you can establish the command callbacks and avoid NotImplementedError exceptions
|
56
|
+
# @param group_name [String] Name of the group to join
|
57
|
+
def initialize(group_name = nil)
|
58
|
+
@cpg = Corosync::CPG.new
|
59
|
+
@cpg.on_message {|*args| cpg_message(*args)}
|
60
|
+
@cpg.on_confchg {|*args| cpg_confchg(*args)}
|
61
|
+
@cpg.connect
|
62
|
+
@cpg.fd.close_on_exec = true
|
63
|
+
|
64
|
+
@cpg_members = nil
|
65
|
+
|
66
|
+
# we can either share the msgid counter across all threads, or have a msgid counter on each thread and send the thread ID with each message. I prefer the former
|
67
|
+
@next_execution_id = 0
|
68
|
+
@next_execution_id_mutex = Mutex.new
|
69
|
+
|
70
|
+
@execution_queues = {}
|
71
|
+
@execution_queues.extend(Sync_m)
|
72
|
+
|
73
|
+
@command_callbacks = CorosyncCommander::CallbackList.new
|
74
|
+
|
75
|
+
@dispatch_thread = Thread.new do
|
76
|
+
Thread.current.abort_on_exception = true
|
77
|
+
loop do
|
78
|
+
@cpg.dispatch
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
if group_name then
|
83
|
+
join(group_name)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Joins the specified group.
|
88
|
+
# This is provided separate from initialization so that callbacks can be registered before joining the group so that you wont get NotImplementedError exceptions
|
89
|
+
# @param group_name [String] Name of group to join
|
90
|
+
# @return [void]
|
91
|
+
def join(group_name)
|
92
|
+
@cpg.join(group_name)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Shuts down the dispatch thread and disconnects CPG
|
96
|
+
# @return [void]
|
97
|
+
def stop
|
98
|
+
@dispatch_thread.kill
|
99
|
+
@dispatch_thread = nil
|
100
|
+
@cpg.disconnect
|
101
|
+
@cpg = nil
|
102
|
+
@cpg_members = nil
|
103
|
+
end
|
104
|
+
|
105
|
+
def next_execution_id()
|
106
|
+
id = nil
|
107
|
+
@next_execution_id_mutex.synchronize do
|
108
|
+
id = @next_execution_id += 1
|
109
|
+
end
|
110
|
+
id
|
111
|
+
end
|
112
|
+
private :next_execution_id
|
113
|
+
|
114
|
+
# Used as a callback on receipt of a CPG message
|
115
|
+
# @param message [String] data structure passed to @cpg.send
|
116
|
+
# * message[0] == [Array<String>] Each string is "nodeid:pid" of the intended message recipients
|
117
|
+
# * msgid == [Integer]
|
118
|
+
# * In the event of a new message, this, combined with `member` will uniquely identify this message
|
119
|
+
# * In the event of a reply, this is the message ID sent in the original message
|
120
|
+
# * type == [String] command/response/exception
|
121
|
+
# * args == [Array]
|
122
|
+
# * In the event of a command, this will be the arguments passed to CorosyncCommander.send
|
123
|
+
# * In the event of a response, this will be the return value of the command handler
|
124
|
+
# * In the event of an exception, this will be the exception string and backtrace
|
125
|
+
# @param sender [Corosync::CPG::Member] Sender of the message
|
126
|
+
# @!visibility private
|
127
|
+
def cpg_message(message, sender)
|
128
|
+
message = CorosyncCommander::Execution::Message.from_cpg_message(message, sender)
|
129
|
+
|
130
|
+
if sender == @cpg.member || message.recipients.include?(@cpg.member)
|
131
|
+
execution_queue = nil
|
132
|
+
@execution_queues.sync_synchronize(:SH) do
|
133
|
+
execution_queue = @execution_queues[message.execution_id]
|
134
|
+
end
|
135
|
+
if !execution_queue.nil? then
|
136
|
+
# someone is listening
|
137
|
+
if sender == @cpg.member and message.type == 'command' then
|
138
|
+
# the Execution object needs a list of the members at the time it's message was received
|
139
|
+
message_echo = message.dup
|
140
|
+
message_echo.type = 'echo'
|
141
|
+
message_echo.content = @cpg_members
|
142
|
+
execution_queue << message_echo
|
143
|
+
else
|
144
|
+
execution_queue << message
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
if message.recipients.size > 0 and !message.recipients.include?(@cpg.member) then
|
150
|
+
return
|
151
|
+
end
|
152
|
+
|
153
|
+
if message.type == 'command' then
|
154
|
+
# we received a command from another node
|
155
|
+
begin
|
156
|
+
# see if we've got a registered callback
|
157
|
+
command_callback = nil
|
158
|
+
|
159
|
+
command_name = message.content[0]
|
160
|
+
command_callback = @command_callbacks[command_name]
|
161
|
+
if command_callback.nil? then
|
162
|
+
raise NotImplementedError, "No callback registered for command '#{command_name}'"
|
163
|
+
end
|
164
|
+
|
165
|
+
command_args = message.content[1]
|
166
|
+
reply_value = command_callback.call(*command_args)
|
167
|
+
message_reply = message.reply(reply_value)
|
168
|
+
@cpg.send(message_reply)
|
169
|
+
rescue => e
|
170
|
+
message_reply = message.reply([e.class, e.to_s, e.backtrace])
|
171
|
+
message_reply.type = 'exception'
|
172
|
+
@cpg.send(message_reply)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# @!visibility private
|
178
|
+
def cpg_confchg(member_list, left_list, join_list)
|
179
|
+
@cpg_members = member_list
|
180
|
+
|
181
|
+
# we look for any members leaving the cluster, and if so we notify all threads that are waiting for a response that they may have just lost a node
|
182
|
+
return if left_list.size == 0
|
183
|
+
|
184
|
+
messages = left_list.map do |member|
|
185
|
+
CorosyncCommander::Execution::Message.new(:sender => member, :type => 'leave')
|
186
|
+
end
|
187
|
+
|
188
|
+
@execution_queues.sync_synchronize(:SH) do
|
189
|
+
@execution_queues.each do |queue|
|
190
|
+
messages.each do |message|
|
191
|
+
queue << message
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# @!attribute [r] commands
|
198
|
+
# @return [CorosyncCommander::CallbackList] List of command callbacks
|
199
|
+
def commands
|
200
|
+
@command_callbacks
|
201
|
+
end
|
202
|
+
|
203
|
+
# Execute a remote command.
|
204
|
+
# @param recipients [Array<Corosync::CPG::Member>] List of recipients to send to, or an empty array to broadcast to all members of the group.
|
205
|
+
# @param command [String] The name of the remote command to execute. If no such command exists on the remote node a NotImplementedError exception will be raised when enumerating the results.
|
206
|
+
# @param args Any further arguments will be passed to the command callback on the remote host.
|
207
|
+
# @return [CorosyncCommander::Execution]
|
208
|
+
def execute(recipients, command, *args)
|
209
|
+
execution = CorosyncCommander::Execution.new(self, next_execution_id, recipients, command, args)
|
210
|
+
|
211
|
+
message = CorosyncCommander::Execution::Message.new(:recipients => recipients, :execution_id => execution.id, :type => 'command', :content => [command, args])
|
212
|
+
|
213
|
+
@execution_queues.synchronize(:EX) do
|
214
|
+
@execution_queues[execution.id] = execution.queue
|
215
|
+
end
|
216
|
+
# Technique stolen from http://www.mikeperham.com/2010/02/24/the-trouble-with-ruby-finalizers/
|
217
|
+
#TODO We definitately need a spec test to validate the execution object gets garbage collected
|
218
|
+
ObjectSpace.define_finalizer(execution, execution_queue_finalizer(execution.id))
|
219
|
+
|
220
|
+
@cpg.send(message)
|
221
|
+
|
222
|
+
execution
|
223
|
+
end
|
224
|
+
# This is so that we remove our queue from the execution queue list when we get garbage collected.
|
225
|
+
def execution_queue_finalizer(execution_id)
|
226
|
+
proc do
|
227
|
+
@execution_queues.synchronize(:EX) do
|
228
|
+
@execution_queues.delete(execution_id)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
private :execution_queue_finalizer
|
233
|
+
end
|
data/lib/version.rb
ADDED
@@ -0,0 +1,181 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'timeout'
|
3
|
+
|
4
|
+
describe CorosyncCommander do
|
5
|
+
def fork_execute(wait, cc_recip, command, *args)
|
6
|
+
recip = cc_recip.cpg.member
|
7
|
+
pid = fork do
|
8
|
+
cc = CorosyncCommander.new(cc_recip.cpg.group)
|
9
|
+
exe = cc.execute([recip], command, *args)
|
10
|
+
exe.wait
|
11
|
+
exit!(0)
|
12
|
+
end
|
13
|
+
if wait then
|
14
|
+
status = Process.wait2(pid)
|
15
|
+
status.exitstatus
|
16
|
+
else
|
17
|
+
pid
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
before(:all) do
|
22
|
+
Timeout.timeout(1) do
|
23
|
+
@cc = CorosyncCommander.new("CorosyncCommander RSPEC #{Random.rand(2 ** 32)}")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'can call cpg_dispatch' do
|
28
|
+
# this is just so that if the thread test fails, we verify that it's not `dispatch` that's the issue
|
29
|
+
@cc.cpg.dispatch(0)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'can call cpg_dispatch on a thread' do
|
33
|
+
Timeout.timeout(2) do
|
34
|
+
pid = fork do
|
35
|
+
t = Thread.new do
|
36
|
+
@cc.cpg.dispatch(0)
|
37
|
+
end
|
38
|
+
t.join
|
39
|
+
exit!(0)
|
40
|
+
end
|
41
|
+
status = Process.wait2(pid)
|
42
|
+
expect(status[1]).to eq(0)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'registers a callback (block style)' do
|
47
|
+
@cc.commands.register 'summation' do |arg1,arg2|
|
48
|
+
arg1 + arg2
|
49
|
+
end
|
50
|
+
|
51
|
+
expect(@cc.commands['summation']).to be_a(Proc)
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'registers a callback (assignment style)' do
|
55
|
+
@cc.commands['summation'] = Proc.new do |arg1,arg2|
|
56
|
+
arg1 + arg2
|
57
|
+
end
|
58
|
+
expect(@cc.commands['summation']).to be_a(Proc)
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'calls the callback' do
|
62
|
+
exe = @cc.execute([], 'summation', 123, 456)
|
63
|
+
results = exe.to_enum.collect do |response,node|
|
64
|
+
response
|
65
|
+
end
|
66
|
+
|
67
|
+
expect(results.size).to eq(1)
|
68
|
+
expect(results.first).to eq(123 + 456)
|
69
|
+
end
|
70
|
+
|
71
|
+
=begin doesn't work for some reason. will investigate later as it's not critical
|
72
|
+
it 'removes queues on garbage collection' do
|
73
|
+
GC.start
|
74
|
+
GC.disable
|
75
|
+
|
76
|
+
@cc.commands.register('nothing') do end
|
77
|
+
|
78
|
+
queue_size_before = @cc.execution_queues.size
|
79
|
+
|
80
|
+
@cc.execute([], 'nothing')
|
81
|
+
queue_size_during = @cc.execution_queues.size
|
82
|
+
|
83
|
+
GC.enable
|
84
|
+
GC.start
|
85
|
+
queue_size_after = @cc.execution_queues.size
|
86
|
+
|
87
|
+
expect(queue_size_during).to eq(queue_size_before + 1)
|
88
|
+
expect(queue_size_after).to eq(queue_size_before)
|
89
|
+
end
|
90
|
+
=end
|
91
|
+
|
92
|
+
it 'works with multiple processes' do
|
93
|
+
Timeout.timeout(1) do
|
94
|
+
sum = 0
|
95
|
+
num1 = Random.rand(2 ** 32)
|
96
|
+
num2 = Random.rand(2 ** 32)
|
97
|
+
|
98
|
+
@cc.commands.register('summation2') do |number|
|
99
|
+
sum += number
|
100
|
+
end
|
101
|
+
|
102
|
+
recipient = @cc.cpg.member
|
103
|
+
forkpid = fork do
|
104
|
+
cc = CorosyncCommander.new(@cc.cpg.group)
|
105
|
+
exe = cc.execute([recipient], 'summation2', num1)
|
106
|
+
exe.wait
|
107
|
+
end
|
108
|
+
|
109
|
+
exe = @cc.execute([recipient], 'summation2', num2)
|
110
|
+
exe.wait
|
111
|
+
|
112
|
+
result = Process.wait2(forkpid)
|
113
|
+
|
114
|
+
expect(result[1].exitstatus).to eq(0)
|
115
|
+
|
116
|
+
expect(sum).to eq(num1 + num2)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'captures remote exceptions' do
|
121
|
+
Timeout.timeout(5) do
|
122
|
+
@cc.commands.register('make exception') do
|
123
|
+
0/0
|
124
|
+
end
|
125
|
+
exe = @cc.execute(@cc.cpg.member, 'make exception')
|
126
|
+
expect{exe.wait}.to raise_error(ZeroDivisionError)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'resumes after exception' do
|
131
|
+
Timeout.timeout(5) do
|
132
|
+
@cc.commands.register('resumes after exception') do
|
133
|
+
'OK'
|
134
|
+
end
|
135
|
+
|
136
|
+
forkpid1 = fork do
|
137
|
+
cc = CorosyncCommander.new(@cc.cpg.group)
|
138
|
+
cc.commands.register('resumes after exception') do
|
139
|
+
0/0
|
140
|
+
end
|
141
|
+
sleep 5
|
142
|
+
end
|
143
|
+
|
144
|
+
forkpid2 = fork do
|
145
|
+
cc = CorosyncCommander.new(@cc.cpg.group)
|
146
|
+
cc.commands.register('resumes after exception') do
|
147
|
+
sleep 0.5 # make sure this response is not before the exception one
|
148
|
+
'OK'
|
149
|
+
end
|
150
|
+
sleep 5
|
151
|
+
end
|
152
|
+
|
153
|
+
sleep 1 # we have to wait for the forks to connect to the group
|
154
|
+
|
155
|
+
exe = @cc.execute([], 'resumes after exception')
|
156
|
+
enum = exe.to_enum
|
157
|
+
responses = []
|
158
|
+
exceptions = 0
|
159
|
+
begin
|
160
|
+
enum.each do |response,node|
|
161
|
+
responses << response
|
162
|
+
end
|
163
|
+
rescue CorosyncCommander::RemoteException
|
164
|
+
exceptions += 1
|
165
|
+
retry
|
166
|
+
end
|
167
|
+
|
168
|
+
[forkpid1,forkpid2].each do |forkpid|
|
169
|
+
begin
|
170
|
+
Process.kill('TERM', forkpid)
|
171
|
+
rescue Errno::ESRCH => e
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
|
176
|
+
expect(responses.size).to eq(2)
|
177
|
+
expect(exceptions).to eq(1)
|
178
|
+
expect(responses.find_all{|r| r == 'OK'}.size).to eq(2)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.expand_path('../../lib/corosync_commander', __FILE__)
|
metadata
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: corosync-commander
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Patrick Hemmer
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-11-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: corosync
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.0.3
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.0.3
|
27
|
+
description: Provides a simplified interface for issuing commands to nodes in a Corosync
|
28
|
+
closed process group.
|
29
|
+
email: patrick.hemmer@gmail.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- .gitignore
|
35
|
+
- Gemfile
|
36
|
+
- Gemfile.lock
|
37
|
+
- LICENSE
|
38
|
+
- Rakefile
|
39
|
+
- corosync-commander.gemspec
|
40
|
+
- lib/corosync_commander.rb
|
41
|
+
- lib/corosync_commander/callback_list.rb
|
42
|
+
- lib/corosync_commander/execution.rb
|
43
|
+
- lib/corosync_commander/execution/message.rb
|
44
|
+
- lib/version.rb
|
45
|
+
- spec/corosync_commander.rb
|
46
|
+
- spec/spec_helper.rb
|
47
|
+
homepage: http://github.com/phemmer/ruby-corosync-commander/
|
48
|
+
licenses:
|
49
|
+
- MIT
|
50
|
+
metadata: {}
|
51
|
+
post_install_message:
|
52
|
+
rdoc_options: []
|
53
|
+
require_paths:
|
54
|
+
- lib
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - '>='
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - '>='
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: '0'
|
65
|
+
requirements: []
|
66
|
+
rubyforge_project:
|
67
|
+
rubygems_version: 2.0.3
|
68
|
+
signing_key:
|
69
|
+
specification_version: 4
|
70
|
+
summary: Sends/receives Corosync CPG commands
|
71
|
+
test_files: []
|
72
|
+
has_rdoc:
|