capistrano_multiconfig_parallel 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.
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