capistrano_multiconfig_parallel 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|