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.
- checksums.yaml +7 -0
- data/.coveralls.yml +2 -0
- data/.gitignore +21 -0
- data/.rspec +1 -0
- data/.rubocop.yml +68 -0
- data/.travis.yml +12 -0
- data/CONTRIBUTING.md +44 -0
- data/Gemfile +3 -0
- data/Guardfile +12 -0
- data/LICENSE +20 -0
- data/README.md +220 -0
- data/Rakefile +56 -0
- data/bin/multi_cap +7 -0
- data/capistrano_multiconfig_parallel.gemspec +51 -0
- data/img/parallel_demo.png +0 -0
- data/init.rb +1 -0
- data/lib/capistrano_multiconfig_parallel/application.rb +57 -0
- data/lib/capistrano_multiconfig_parallel/base.rb +92 -0
- data/lib/capistrano_multiconfig_parallel/celluloid/celluloid_manager.rb +178 -0
- data/lib/capistrano_multiconfig_parallel/celluloid/celluloid_worker.rb +238 -0
- data/lib/capistrano_multiconfig_parallel/celluloid/child_process.rb +104 -0
- data/lib/capistrano_multiconfig_parallel/celluloid/rake_worker.rb +83 -0
- data/lib/capistrano_multiconfig_parallel/celluloid/state_machine.rb +49 -0
- data/lib/capistrano_multiconfig_parallel/celluloid/terminal_table.rb +122 -0
- data/lib/capistrano_multiconfig_parallel/cli.rb +55 -0
- data/lib/capistrano_multiconfig_parallel/configuration.rb +70 -0
- data/lib/capistrano_multiconfig_parallel/helpers/base_manager.rb +217 -0
- data/lib/capistrano_multiconfig_parallel/helpers/multi_app_manager.rb +84 -0
- data/lib/capistrano_multiconfig_parallel/helpers/single_app_manager.rb +48 -0
- data/lib/capistrano_multiconfig_parallel/helpers/standard_deploy.rb +40 -0
- data/lib/capistrano_multiconfig_parallel/initializers/conf.rb +6 -0
- data/lib/capistrano_multiconfig_parallel/initializers/confirm_question.rb +25 -0
- data/lib/capistrano_multiconfig_parallel/initializers/i18n.rb +10 -0
- data/lib/capistrano_multiconfig_parallel/initializers/rake.rb +28 -0
- data/lib/capistrano_multiconfig_parallel/multi_app_helpers/dependency_tracker.rb +111 -0
- data/lib/capistrano_multiconfig_parallel/multi_app_helpers/interactive_menu.rb +61 -0
- data/lib/capistrano_multiconfig_parallel/version.rb +16 -0
- data/lib/capistrano_multiconfig_parallel.rb +2 -0
- data/spec/spec_helper.rb +48 -0
- 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
|