capistrano_multiconfig_parallel 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +2 -0
  3. data/.gitignore +21 -0
  4. data/.rspec +1 -0
  5. data/.rubocop.yml +68 -0
  6. data/.travis.yml +12 -0
  7. data/CONTRIBUTING.md +44 -0
  8. data/Gemfile +3 -0
  9. data/Guardfile +12 -0
  10. data/LICENSE +20 -0
  11. data/README.md +220 -0
  12. data/Rakefile +56 -0
  13. data/bin/multi_cap +7 -0
  14. data/capistrano_multiconfig_parallel.gemspec +51 -0
  15. data/img/parallel_demo.png +0 -0
  16. data/init.rb +1 -0
  17. data/lib/capistrano_multiconfig_parallel/application.rb +57 -0
  18. data/lib/capistrano_multiconfig_parallel/base.rb +92 -0
  19. data/lib/capistrano_multiconfig_parallel/celluloid/celluloid_manager.rb +178 -0
  20. data/lib/capistrano_multiconfig_parallel/celluloid/celluloid_worker.rb +238 -0
  21. data/lib/capistrano_multiconfig_parallel/celluloid/child_process.rb +104 -0
  22. data/lib/capistrano_multiconfig_parallel/celluloid/rake_worker.rb +83 -0
  23. data/lib/capistrano_multiconfig_parallel/celluloid/state_machine.rb +49 -0
  24. data/lib/capistrano_multiconfig_parallel/celluloid/terminal_table.rb +122 -0
  25. data/lib/capistrano_multiconfig_parallel/cli.rb +55 -0
  26. data/lib/capistrano_multiconfig_parallel/configuration.rb +70 -0
  27. data/lib/capistrano_multiconfig_parallel/helpers/base_manager.rb +217 -0
  28. data/lib/capistrano_multiconfig_parallel/helpers/multi_app_manager.rb +84 -0
  29. data/lib/capistrano_multiconfig_parallel/helpers/single_app_manager.rb +48 -0
  30. data/lib/capistrano_multiconfig_parallel/helpers/standard_deploy.rb +40 -0
  31. data/lib/capistrano_multiconfig_parallel/initializers/conf.rb +6 -0
  32. data/lib/capistrano_multiconfig_parallel/initializers/confirm_question.rb +25 -0
  33. data/lib/capistrano_multiconfig_parallel/initializers/i18n.rb +10 -0
  34. data/lib/capistrano_multiconfig_parallel/initializers/rake.rb +28 -0
  35. data/lib/capistrano_multiconfig_parallel/multi_app_helpers/dependency_tracker.rb +111 -0
  36. data/lib/capistrano_multiconfig_parallel/multi_app_helpers/interactive_menu.rb +61 -0
  37. data/lib/capistrano_multiconfig_parallel/version.rb +16 -0
  38. data/lib/capistrano_multiconfig_parallel.rb +2 -0
  39. data/spec/spec_helper.rb +48 -0
  40. metadata +648 -0
@@ -0,0 +1,92 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ require 'bundler/setup'
4
+ require 'configurations'
5
+ require_relative './configuration'
6
+
7
+ # base module that has the statis methods that this gem is using
8
+ module CapistranoMulticonfigParallel
9
+ include CapistranoMulticonfigParallel::Configuration
10
+
11
+ ENV_KEY_JOB_ID = 'multi_cap_job_id'
12
+ MULTI_KEY = 'multi'
13
+ SINGLE_KEY = 'single'
14
+
15
+ CUSTOM_COMMANDS = {
16
+ CapistranoMulticonfigParallel::MULTI_KEY => {
17
+ menu: 'show_menu',
18
+ stages: 'deploy_multi_stages'
19
+ },
20
+ CapistranoMulticonfigParallel::SINGLE_KEY => {
21
+ stages: 'deploy_multi_stages'
22
+ }
23
+ }
24
+
25
+ class << self
26
+ attr_accessor :show_task_progress, :interactive_menu, :execute_in_sequence, :logger, :show_task_progress_tree
27
+
28
+ def root
29
+ File.expand_path(File.dirname(__dir__))
30
+ end
31
+
32
+ def verify_app_dependencies(stages)
33
+ applications = stages.map { |stage| stage.split(':').reverse[1] }
34
+ wrong = CapistranoMulticonfigParallel.configuration.application_dependencies.find do |hash|
35
+ !applications.include?(hash[:app]) || (hash[:dependencies].present? && hash[:dependencies].find { |val| !applications.include?(val) })
36
+ end
37
+ raise ArgumentError, "invalid configuration for #{wrong.inspect}" if wrong.present?
38
+ end
39
+
40
+ def log_directory
41
+ File.join(CapistranoMulticonfigParallel.detect_root.to_s, 'log')
42
+ end
43
+
44
+ def main_log_file
45
+ File.join(log_directory, 'multi_cap.log')
46
+ end
47
+
48
+ def websokect_log_file
49
+ File.join(log_directory, 'multi_cap_webscoket.log')
50
+ end
51
+
52
+ def enable_logging
53
+ return unless CapistranoMulticonfigParallel::CelluloidManager.debug_enabled
54
+ FileUtils.mkdir_p(log_directory)
55
+ log_file = File.open(main_log_file, 'w')
56
+ log_file.sync = true
57
+ self.logger = ::Logger.new(main_log_file)
58
+ Celluloid.logger = logger
59
+ end
60
+
61
+ def log_message(message)
62
+ return unless logger.present?
63
+ error_message = message.respond_to?(:message) ? message.message : message.inspect
64
+ err_backtrace = message.respond_to?(:backtrace) ? message.backtrace.join("\n\n") : ''
65
+ if err_backtrace.present?
66
+ logger.debug(class: message.class,
67
+ message: error_message,
68
+ backtrace: err_backtrace)
69
+ else
70
+ logger.debug(message)
71
+ end
72
+ end
73
+
74
+ def detect_root
75
+ if ENV['MULTI_CAP_ROOT']
76
+ Pathname.new(ENV['MULTI_CAP_ROOT'])
77
+ elsif defined?(::Rails)
78
+ ::Rails.root
79
+ else
80
+ try_detect_capfile
81
+ end
82
+ end
83
+
84
+ def try_detect_capfile
85
+ root = Pathname.new(FileUtils.pwd)
86
+ root = root.parent unless root.directory?
87
+ root = root.parent until File.exist?(root.join('Capfile')) || root.root?
88
+ raise "Can't detect Rails application root" if root.root?
89
+ root
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,178 @@
1
+ require_relative './celluloid_worker'
2
+ require_relative './terminal_table'
3
+ module CapistranoMulticonfigParallel
4
+ # rubocop:disable ClassLength
5
+ class CelluloidManager
6
+ include Celluloid
7
+ include Celluloid::Notifications
8
+ include Celluloid::Logger
9
+
10
+ cattr_accessor :debug_enabled
11
+
12
+ attr_accessor :jobs, :job_to_worker, :worker_to_job, :actor_system, :job_to_condition, :mutex, :registration_complete
13
+
14
+ attr_reader :worker_supervisor, :workers
15
+ trap_exit :worker_died
16
+
17
+ def initialize(job_manager)
18
+ # start SupervisionGroup
19
+ @worker_supervisor = Celluloid::SupervisionGroup.run!
20
+ @job_manager = job_manager
21
+ @registration_complete = false
22
+ # Get a handle on the SupervisionGroup::Member
23
+ @actor_system = Celluloid.boot
24
+ @mutex = Mutex.new
25
+ # http://rubydoc.info/gems/celluloid/Celluloid/SupervisionGroup/Member
26
+ @workers = @worker_supervisor.pool(CapistranoMulticonfigParallel::CelluloidWorker, as: :workers, size: 10)
27
+ # Get a handle on the PoolManager
28
+ # http://rubydoc.info/gems/celluloid/Celluloid/PoolManager
29
+ # @workers = workers_pool.actor
30
+ @conditions = []
31
+ @jobs = {}
32
+ @job_to_worker = {}
33
+ @worker_to_job = {}
34
+ @job_to_condition = {}
35
+
36
+ @worker_supervisor.supervise_as(:terminal_server, CapistranoMulticonfigParallel::TerminalTable, Actor.current)
37
+ @worker_supervisor.supervise_as(:web_server, CelluloidPubsub::WebServer, self.class.websocket_config.merge(enable_debug: self.class.debug_websocket?))
38
+ end
39
+
40
+ def self.debug_enabled?
41
+ debug_enabled
42
+ end
43
+
44
+ def self.debug_websocket?
45
+ websocket_config['enable_debug'].to_s == 'true'
46
+ end
47
+
48
+ def self.websocket_config
49
+ config = CapistranoMulticonfigParallel.configuration.websocket_server
50
+ config.present? && config.is_a?(Hash) ? config.stringify_keys : {}
51
+ end
52
+
53
+ def generate_job_id(job)
54
+ primary_key = @jobs.size + 1
55
+ job['id'] = primary_key
56
+ @jobs[primary_key] = job
57
+ @jobs[primary_key]
58
+ job['id']
59
+ end
60
+
61
+ # call to send an actor
62
+ # a job
63
+ def delegate(job)
64
+ job = job.stringify_keys
65
+ job['id'] = generate_job_id(job) if job['worker_action'] != 'worker_died'
66
+ @jobs[job['id']] = job
67
+ job['env_options'][CapistranoMulticonfigParallel::ENV_KEY_JOB_ID] = job['id']
68
+ # debug(@jobs)
69
+ # start work and send it to the background
70
+ @workers.async.work(job, Actor.current)
71
+ end
72
+
73
+ # call back from actor once it has received it's job
74
+ # actor should do this asap
75
+ def register_worker_for_job(job, worker)
76
+ job = job.stringify_keys
77
+ if job['id'].blank?
78
+ debug("job id not found. delegating again the job #{job.inspect}") if self.class.debug_enabled?
79
+ delegate(job)
80
+ else
81
+ worker.job_id = job['id'] if worker.job_id.blank?
82
+ @job_to_worker[job['id']] = worker
83
+ @worker_to_job[worker.mailbox.address] = job
84
+ debug("worker #{worker.job_id} registed into manager") if self.class.debug_enabled?
85
+ Actor.current.link worker
86
+ if @job_manager.jobs.size == @job_to_worker.size
87
+ @registration_complete = true
88
+ end
89
+ end
90
+ end
91
+
92
+ def process_jobs(&block)
93
+ @job_to_worker.pmap do |_job_id, worker|
94
+ worker.async.start_task
95
+ end
96
+ if block_given?
97
+ block.call
98
+ else
99
+ wait_task_confirmations
100
+ end
101
+ results2 = []
102
+ @job_to_condition.pmap do |_job_id, hash|
103
+ results2 << hash[:last_condition].wait
104
+ end
105
+ @job_manager.condition.signal(results2) if results2.size == @jobs.size
106
+ end
107
+
108
+ def wait_task_confirmations
109
+ return unless CapistranoMulticonfigParallel.configuration.task_confirmation_active
110
+ CapistranoMulticonfigParallel.configuration.task_confirmations.each_with_index do |task, index|
111
+ results = []
112
+ @jobs.pmap do |job_id, _job|
113
+ results << @job_to_condition[job_id][:first_condition][index].wait
114
+ end
115
+ if results.size == @jobs.size
116
+ confirm_task_approval(results, task)
117
+ end
118
+ end
119
+ end
120
+
121
+ def confirm_task_approval(results, task)
122
+ return unless results.present?
123
+ set :apps_symlink_confirmation, ask_confirm("Do you want to continue the deployment and execute #{task}?", 'Y')
124
+ until fetch(:apps_symlink_confirmation).present?
125
+ sleep(0.1) # keep current thread alive
126
+ end
127
+ return if fetch(:apps_symlink_confirmation).blank? || fetch(:apps_symlink_confirmation).downcase != 'y'
128
+ @jobs.pmap do |job_id, job|
129
+ worker = get_worker_for_job(job_id)
130
+ worker.publish_rake_event('approved' => 'yes',
131
+ 'action' => 'invoke',
132
+ 'job_id' => job['id'],
133
+ 'task' => task
134
+ )
135
+ end
136
+ end
137
+
138
+ def get_worker_for_job(job)
139
+ if job.present?
140
+ if job.is_a?(Hash)
141
+ job = job.stringify_keys
142
+ @job_to_worker[job['id']]
143
+ else
144
+ @job_to_worker[job.to_i]
145
+ end
146
+ else
147
+ return nil
148
+ end
149
+ end
150
+
151
+ # lookup status of job by asking actor running it
152
+ def get_job_status(job)
153
+ status = nil
154
+ if job.present?
155
+ if job.is_a?(Hash)
156
+ job = job.stringify_keys
157
+ actor = @registered_jobs[job['id']]
158
+ status = actor.status
159
+ else
160
+ actor = @registered_jobs[job.to_i]
161
+ status = actor.status
162
+ end
163
+ end
164
+ status
165
+ end
166
+
167
+ def worker_died(worker, reason)
168
+ debug("worker with mailbox #{worker.mailbox.inspect} died for reason: #{reason}") if self.class.debug_enabled?
169
+ job = @worker_to_job[worker.mailbox.address]
170
+ @worker_to_job.delete(worker.mailbox.address)
171
+ debug "restarting #{job} on new worker" if self.class.debug_enabled?
172
+ return if job.blank? || job['worker_action'] == 'worker_died'
173
+ return unless job['worker_action'] == 'deploy'
174
+ job = job.merge(:action => 'deploy:rollback', 'worker_action' => 'worker_died')
175
+ delegate(job)
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,238 @@
1
+ require_relative './child_process'
2
+ require_relative './state_machine'
3
+ module CapistranoMulticonfigParallel
4
+ # rubocop:disable ClassLength
5
+ # worker that will spawn a child process in order to execute a capistrano job and monitor that process
6
+ #
7
+ # @!attribute job
8
+ # @return [Hash] options used for executing capistrano task
9
+ # @option options [String] :id The id of the job ( will ge automatically generated by CapistranoMulticonfigParallel::CelluloidManager when delegating job)
10
+ # @option options [String] :app The application name that will be deployed
11
+ # @option options [String] :env The stage used for that application
12
+ # @option options [String] :action The action that this action will be doing (deploy, or other task)
13
+ # @option options [Hash] :env_options options that are available in the environment variable ENV when this task is going to be executed
14
+ # @option options [Array] :task_arguments arguments to the task
15
+ #
16
+ # @!attribute manager
17
+ # @return [CapistranoMulticonfigParallel::CelluloidManager] the instance of the manager that delegated the job to this worker
18
+ #
19
+ class CelluloidWorker
20
+ include Celluloid
21
+ include Celluloid::Notifications
22
+ include Celluloid::Logger
23
+ class TaskFailed < StandardError; end
24
+
25
+ attr_accessor :job, :manager, :job_id, :app_name, :env_name, :action_name, :env_options, :machine, :client, :task_argv, :execute_deploy, :executed_dry_run,
26
+ :rake_tasks, :current_task_number, # tracking tasks
27
+ :successfull_subscription, :subscription_channel, :publisher_channel, # for subscriptions and publishing events
28
+ :task_confirmations, :manager_condition, :last_manager_condition # for task conifirmations from manager
29
+
30
+ def work(job, manager)
31
+ @job = job
32
+ @manager = manager
33
+
34
+ process_job(job) if job.present?
35
+ debug("worker #{@job_id} received #{job.inspect}") if debug_enabled?
36
+ @subscription_channel = "worker_#{@job_id}"
37
+ @machine = CapistranoMulticonfigParallel::StateMachine.new(job, Actor.current)
38
+ setup_worker_condition
39
+ manager.register_worker_for_job(job, Actor.current)
40
+ end
41
+
42
+ def debug_enabled?
43
+ @manager.class.debug_enabled?
44
+ end
45
+
46
+
47
+ def start_task
48
+ debug("exec worker #{@job_id} starts task with #{@job.inspect}") if debug_enabled?
49
+ @task_confirmations = CapistranoMulticonfigParallel.configuration.task_confirmations
50
+ @client = CelluloidPubsub::Client.connect(actor: Actor.current, enable_debug: @manager.class.debug_websocket?) do |ws|
51
+ ws.subscribe(@subscription_channel)
52
+ end
53
+ end
54
+
55
+ def publish_rake_event(data)
56
+ @client.publish(rake_actor_id(data), data)
57
+ end
58
+
59
+ def rake_actor_id(data)
60
+ data['action'].present? && data['action'] == 'count' ? "rake_worker_#{@job_id}_count" : "rake_worker_#{@job_id}"
61
+ end
62
+
63
+ def on_message(message)
64
+ debug("worker #{@job_id} received: #{message.inspect}") if debug_enabled?
65
+ if @client.succesfull_subscription?(message)
66
+ @successfull_subscription = true
67
+ execute_after_succesfull_subscription
68
+ else
69
+ handle_subscription(message)
70
+ end
71
+ end
72
+
73
+ def execute_after_succesfull_subscription
74
+ setup_task_arguments
75
+ if (@action_name == 'deploy' || @action_name == 'deploy:rollback') && CapistranoMulticonfigParallel.show_task_progress
76
+ @executed_dry_run = true
77
+ @rake_tasks = []
78
+ @task_argv << '--dry-run'
79
+ @task_argv << 'count_rake=true'
80
+ @child_process = CapistranoMulticonfigParallel::ChildProcess.new
81
+ Actor.current.link @child_process
82
+ debug("worker #{@job_id} executes: bundle exec multi_cap #{@task_argv.join(' ')}") if debug_enabled?
83
+ @child_process.async.work("bundle exec multi_cap #{@task_argv.join(' ')}", actor: Actor.current, dry_run: true)
84
+ else
85
+ async.execute_deploy
86
+ end
87
+ end
88
+
89
+ def rake_tasks
90
+ @rake_tasks ||= []
91
+ end
92
+
93
+ def execute_deploy
94
+ @execute_deploy = true
95
+ debug("invocation chain #{@job_id} is : #{@rake_tasks.inspect}") if debug_enabled? && CapistranoMulticonfigParallel.show_task_progress
96
+ if !defined?(@child_process) || @child_process.nil?
97
+ @child_process = CapistranoMulticonfigParallel::ChildProcess.new
98
+ Actor.current.link @child_process
99
+ else
100
+ @client.unsubscribe("rake_worker_#{@job_id}_count")
101
+ @child_process.exit_status = nil
102
+ end
103
+ setup_task_arguments
104
+ debug("worker #{@job_id} executes: bundle exec multi_cap #{@task_argv.join(' ')}") if debug_enabled?
105
+ @child_process.async.work("bundle exec multi_cap #{@task_argv.join(' ')}", actor: Actor.current, silent: true)
106
+ end
107
+
108
+ def on_close(code, reason)
109
+ debug("worker #{@job_id} websocket connection closed: #{code.inspect}, #{reason.inspect}") if debug_enabled?
110
+ end
111
+
112
+ def handle_subscription(message)
113
+ if message_is_about_a_task?(message)
114
+ save_tasks_to_be_executed(message)
115
+ update_machine_state(message['task']) # if message['action'] == 'invoke'
116
+ debug("worker #{@job_id} state is #{@machine.state}") if debug_enabled?
117
+ task_approval(message)
118
+ else
119
+ debug("worker #{@job_id} could not handle #{message}") if debug_enabled?
120
+ end
121
+ end
122
+
123
+ def message_is_about_a_task?(message)
124
+ message.present? && message.is_a?(Hash) && message['action'].present? && message['job_id'].present? && message['task'].present?
125
+ end
126
+
127
+ def task_approval(message)
128
+ if @task_confirmations.include?(message['task']) && message['action'] == 'invoke'
129
+ @manager_condition[message['task']].call(message['task'])
130
+ else
131
+ publish_rake_event(message.merge('approved' => 'yes'))
132
+ end
133
+ end
134
+
135
+ def save_tasks_to_be_executed(message)
136
+ return unless message['action'] == 'count'
137
+ debug("worler #{@job_id} current invocation chain : #{@rake_tasks.inspect}") if debug_enabled?
138
+ @rake_tasks = [] if @rake_tasks.blank?
139
+ @rake_tasks << message['task'] if @rake_tasks.last != message['task']
140
+ end
141
+
142
+ def update_machine_state(name)
143
+ debug("worker #{@job_id} triest to transition from #{@machine.state} to #{name}") if debug_enabled?
144
+ @machine.transitions.on(name.to_s, @machine.state => name.to_s)
145
+ @machine.go_to_transition(name.to_s)
146
+ raise(CapistranoMulticonfigParallel::CelluloidWorker::TaskFailed, "task #{@action} failed ") if name == 'deploy:failed' # force worker to rollback
147
+ end
148
+
149
+ def setup_command_line(*options)
150
+ @task_argv = []
151
+ options.each do |option|
152
+ @task_argv << option
153
+ end
154
+ @task_argv
155
+ end
156
+
157
+ def setup_task_arguments
158
+ # stage = "#{@app_name}:#{@env_name} #{@action_name}"
159
+ stage = @app_name.present? ? "#{@app_name}:#{@env_name}" : "#{@env_name}"
160
+ array_options = ["#{stage}"]
161
+ array_options << "#{@action_name}[#{@task_arguments.join(',')}]"
162
+ @env_options.each do |key, value|
163
+ array_options << "#{key}=#{value}" if value.present?
164
+ end
165
+ array_options << '--trace' if debug_enabled?
166
+ setup_command_line(*array_options)
167
+ end
168
+
169
+ def send_msg(channel, message = nil)
170
+ publish channel, message.present? && message.is_a?(Hash) ? { job_id: @job_id }.merge(message) : { job_id: @job_id, time: Time.now }
171
+ end
172
+
173
+ def process_job(job)
174
+ @job_id = job['id']
175
+ @app_name = job['app']
176
+ @env_name = job['env']
177
+ @action_name = job['action']
178
+ @env_options = {}
179
+ job['env_options'].each do |key, value|
180
+ @env_options[key] = value if value.present?
181
+ end
182
+ @task_arguments = job['task_arguments']
183
+ end
184
+
185
+ def need_confirmation_for_tasks?
186
+ executes_deploy? && CapistranoMulticonfigParallel.configuration.task_confirmation_active
187
+ end
188
+
189
+ def executes_deploy?
190
+ (@action_name == 'deploy' || @action_name == 'deploy:rollback')
191
+ end
192
+
193
+ def setup_worker_condition
194
+ job_termination_condition = Celluloid::Condition.new
195
+ job_confirmation_conditions = []
196
+ CapistranoMulticonfigParallel.configuration.task_confirmations.each do |_task|
197
+ if need_confirmation_for_tasks?
198
+ job_confirmation_conditions << Celluloid::Condition.new
199
+ else
200
+ job_confirmation_conditions << proc { |sum| sum }
201
+ end
202
+ end
203
+ @manager.job_to_condition[@job_id] = { first_condition: job_confirmation_conditions, last_condition: job_termination_condition }
204
+ construct_blocks_for_conditions(job_confirmation_conditions, job_termination_condition)
205
+ end
206
+
207
+ def construct_blocks_for_conditions(job_confirmation_conditions, job_termination_condition)
208
+ hash_conditions = {}
209
+ CapistranoMulticonfigParallel.configuration.task_confirmations.each_with_index do |task, index|
210
+ blk = lambda do |sum|
211
+ need_confirmation_for_tasks? ? job_confirmation_conditions[index].signal(sum) : job_confirmation_conditions[index].call(sum)
212
+ end
213
+ hash_conditions[task] = blk
214
+ end
215
+ blk_termination = lambda do |sum|
216
+ job_termination_condition.signal(sum)
217
+ end
218
+ @manager_condition = hash_conditions
219
+ @last_manager_condition = blk_termination
220
+ end
221
+
222
+ def crashed?
223
+ @action_name == 'deploy:rollback'
224
+ end
225
+
226
+ def notify_finished(exit_status)
227
+ return unless @execute_deploy
228
+ if exit_status.exitstatus != 0
229
+ debug("worker #{job_id} tries to terminate")
230
+ terminate
231
+ else
232
+ update_machine_state('FINISHED')
233
+ debug("worker #{job_id} notifies manager has finished")
234
+ @last_manager_condition.call('yes')
235
+ end
236
+ end
237
+ end
238
+ end
@@ -0,0 +1,104 @@
1
+ module CapistranoMulticonfigParallel
2
+ # class that is used to execute the capistrano tasks and it is invoked by the celluloid worker
3
+ class ChildProcess
4
+ include Celluloid
5
+ include Celluloid::Logger
6
+
7
+ attr_accessor :actor, :pid, :exit_status, :process, :filename
8
+
9
+ def finalize
10
+ EM.stop
11
+ @timer.cancel
12
+ super
13
+ true
14
+ end
15
+
16
+ alias_method :terminate, :finalize
17
+
18
+ def work(cmd, options = {})
19
+ @options = options
20
+ @actor = @options.fetch(:actor, nil)
21
+ EM.run do
22
+ EM.next_tick do
23
+ set_worker_log
24
+ start_async_deploy(cmd, options)
25
+ end
26
+ @timer = EM::PeriodicTimer.new(0.1) do
27
+ check_exit_status
28
+ end
29
+ end
30
+ EM.error_handler do|e|
31
+ puts "Error during event loop for worker #{@actor.job_id}: #{e.inspect}" if @actor.debug_enabled?
32
+ puts e.backtrace if @actor.debug_enabled?
33
+ EM.stop
34
+ end
35
+ end
36
+
37
+ def set_worker_log
38
+ FileUtils.mkdir_p(CapistranoMulticonfigParallel.log_directory)
39
+ @filename = File.join(CapistranoMulticonfigParallel.log_directory, "worker_#{@actor.job_id}.log")
40
+ FileUtils.rm_rf(@filename) if @options[:dry_run] || @actor.executed_dry_run != true
41
+ end
42
+
43
+ def check_exit_status
44
+ return unless @exit_status.present?
45
+ @timer.cancel
46
+ EM.stop
47
+ if @options[:dry_run]
48
+ debug("worker #{@actor.job_id} starts execute deploy") if @actor.debug_enabled?
49
+ @actor.async.execute_deploy
50
+ else
51
+ debug("worker #{@actor.job_id} startsnotify finished") if @actor.debug_enabled?
52
+ @actor.notify_finished(@exit_status)
53
+ end
54
+ end
55
+
56
+ def start_async_deploy(cmd, options)
57
+ RightScale::RightPopen.popen3_async(
58
+ cmd,
59
+ target: self,
60
+ environment: options[:environment].present? ? options[:environment] : nil,
61
+ pid_handler: :on_pid,
62
+ stdout_handler: :on_read_stdout,
63
+ stderr_handler: :on_read_stderr,
64
+ watch_handler: :watch_handler,
65
+ async_exception_handler: :async_exception_handler,
66
+ exit_handler: :on_exit)
67
+ end
68
+
69
+ def on_pid(pid)
70
+ @pid ||= pid
71
+ end
72
+
73
+ def on_input_stdin(data)
74
+ io_callback('stdin', data)
75
+ end
76
+
77
+ def on_read_stdout(data)
78
+ io_callback('stdout', data)
79
+ end
80
+
81
+ def on_read_stderr(data)
82
+ io_callback('stderr', data)
83
+ end
84
+
85
+ def on_exit(status)
86
+ debug "Child process for worker #{@actor.job_id} on_exit disconnected due to error #{status.inspect}" if @actor.debug_enabled?
87
+ @exit_status = status
88
+ end
89
+
90
+ def async_exception_handler(*data)
91
+ debug "Child process for worker #{@actor.job_id} async_exception_handler disconnected due to error #{data.inspect}" if @actor.debug_enabled?
92
+ io_callback('stderr', data)
93
+ @exit_status = 1
94
+ end
95
+
96
+ def watch_handler(process)
97
+ @process ||= process
98
+ end
99
+
100
+ def io_callback(_io, data)
101
+ File.open(@filename, 'a') { |f| f << data }
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,83 @@
1
+ module CapistranoMulticonfigParallel
2
+ # class that handles the rake task and waits for approval from the celluloid worker
3
+ class RakeWorker
4
+ include Celluloid
5
+ include Celluloid::Logger
6
+
7
+ attr_accessor :env, :client, :job_id, :action, :task, :task_approved, :successfull_subscription, :subscription_channel, :publisher_channel
8
+
9
+ def work(env, task, options = {})
10
+ @options = options.stringify_keys
11
+ @env = env
12
+ @job_id = find_job_id
13
+ @subscription_channel = @options['rake_actor_id']
14
+ @publisher_channel = "worker_#{find_job_id}"
15
+ @action = @options['rake_actor_id'].include?('_count') ? 'count' : 'invoke'
16
+ @task = task
17
+ @task_approved = false
18
+ @successfull_subscription = false
19
+ @client = CelluloidPubsub::Client.connect(actor: Actor.current, enable_debug: CapistranoMulticonfigParallel::CelluloidManager.debug_websocket?) do |ws|
20
+ ws.subscribe(@subscription_channel)
21
+ end if !defined?(@client) || @client.nil?
22
+ end
23
+
24
+ def debug_enabled?
25
+ @client.debug_enabled?
26
+ end
27
+
28
+ def task_name
29
+ @task.name
30
+ end
31
+
32
+ def find_job_id
33
+ @env[CapistranoMulticonfigParallel::ENV_KEY_JOB_ID]
34
+ end
35
+
36
+ def task_data
37
+ {
38
+ action: @action,
39
+ task: task_name,
40
+ job_id: find_job_id
41
+ }
42
+ end
43
+
44
+ def publish_new_work(env, task)
45
+ work(env, task, rake_actor_id: @options['rake_actor_id'])
46
+ publish_to_worker(task_data)
47
+ end
48
+
49
+ def publish_to_worker(data)
50
+ @client.publish(@publisher_channel, data)
51
+ end
52
+
53
+ def on_message(message)
54
+ debug("Rake worker #{find_job_id} received after parse #{message}") if debug_enabled?
55
+ if @client.succesfull_subscription?(message)
56
+ publish_subscription_successfull
57
+ elsif message.present? && message['client_action'].blank?
58
+ task_approval(message)
59
+ else
60
+ warn "unknown action: #{message.inspect}" if debug_enabled?
61
+ end
62
+ end
63
+
64
+ def publish_subscription_successfull
65
+ debug("Rake worker #{find_job_id} received parse #{message}") if debug_enabled?
66
+ publish_to_worker(task_data)
67
+ @successfull_subscription = true
68
+ end
69
+
70
+ def task_approval(message)
71
+ if @job_id.to_i == message['job_id'].to_i && message['task'] == task_name && message['approved'] == 'yes'
72
+ @task_approved = true
73
+ else
74
+ warn "unknown invocation #{message.inspect}" if debug_enabled?
75
+ end
76
+ end
77
+
78
+ def on_close(code, reason)
79
+ debug("websocket connection closed: #{code.inspect}, #{reason.inspect}") if debug_enabled?
80
+ terminate
81
+ end
82
+ end
83
+ end