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