corosync-commander 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,11 @@
1
+ # A sample Gemfile
2
+ source "https://rubygems.org"
3
+
4
+ gem 'corosync', '~>0.0.3'
5
+
6
+ group :development do
7
+ gem 'rake'
8
+ gem 'yard'
9
+ gem 'rdoc'
10
+ gem 'rspec'
11
+ end
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,7 @@
1
+ ########################################
2
+ desc 'Run tests'
3
+ require 'rspec/core/rake_task'
4
+ RSpec::Core::RakeTask.new(:test) do |t|
5
+ t.pattern = 'spec/**/*.rb'
6
+ t.rspec_opts = '-c -f d --fail-fast'
7
+ end
@@ -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,3 @@
1
+ class CorosyncCommander
2
+ GEM_VERSION = '0.0.1'
3
+ end
@@ -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
@@ -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: