mattmatt-cap-ext-parallelize 0.1.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.
@@ -0,0 +1,58 @@
1
+ cap-ext-parallelize - A Capistrano extension for parallel task execution
2
+ =============
3
+
4
+ Imagine you want to restart several processes, either on one or on different
5
+ servers, but that flingin flangin Capistrano just doesn't want to run all the
6
+ restart tasks in parallel.
7
+
8
+ I know what you're saying, Capistrano already has a command called `parallel`.
9
+ That should do right? Not exactly, we wanted to be able to run complete tasks,
10
+ no arbitrary blocks in parallel. The command `parallel` is only able to run
11
+ specific shell commands, and it looks weird when you want to run several of
12
+ them on only one host.
13
+
14
+ We were inspired by the syntax though, so when you want to run arbitrary blocks
15
+ in your Capistrano tasks, you can do it like this:
16
+
17
+ parallelize do |session|
18
+ session.run {deploy.restart}
19
+ session.run {queue.restart}
20
+ session.run {daemon.restart}
21
+ end
22
+
23
+ Every task will be run in its own thread, opening a new connection to the server.
24
+ Because of this you should be aware of potential resource exhaustion. You can
25
+ limit the number of threads in two ways, either set a variable (it defaults
26
+ to 10):
27
+
28
+ set :parallelize_thread_count, 10
29
+
30
+ Or specify it with a parameter:
31
+
32
+ parallelize(5) do
33
+ ...
34
+ end
35
+
36
+ If one of your tasks ran in a transaction block and issued a rollback,
37
+ parallelize will rollback all other threads, if they have rollback statements
38
+ defined.
39
+
40
+ Installation
41
+ ============
42
+
43
+ 1. Install the gem
44
+
45
+ gem install -s http://gems.github.com mattmatt-cap-ext-parallelize
46
+
47
+ 2. Add the following line to your Capfile
48
+
49
+ require 'cap\_ext\_parallelize'
50
+
51
+ 3. There is no step 3
52
+
53
+ License
54
+ =======
55
+
56
+ (c) 2009 Mathias Meyer, Jonathan Weiss
57
+
58
+ MIT-License
@@ -0,0 +1,33 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+
5
+ begin
6
+ require 'jeweler'
7
+ Jeweler::Tasks.new do |s|
8
+ s.name = 'cap-ext-parallelize'
9
+ s.summary = 'A drop-in replacement for Capistrano to fire off Webistrano deployments transparently without losing the joy of using the cap command.'
10
+ s.email = 'meyer@paperplanes.de'
11
+ s.homepage = 'http://github.com/mattmatt/cap-ext-parallelize'
12
+ s.authors = ["Mathias Meyer"]
13
+ s.files = FileList["[A-Z]*", "{lib,test}/**/*"]
14
+ s.add_dependency 'capistrano'
15
+ end
16
+ rescue LoadError
17
+ puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
18
+ end
19
+
20
+ desc "Default Task"
21
+ task :default => ["test"]
22
+
23
+ desc "Runs the unit tests"
24
+ task :test => "test:unit"
25
+
26
+ namespace :test do
27
+ desc "Unit tests"
28
+ Rake::TestTask.new(:unit) do |t|
29
+ t.libs << 'test/unit'
30
+ t.pattern = "test/*_shoulda.rb"
31
+ t.verbose = true
32
+ end
33
+ end
@@ -0,0 +1,4 @@
1
+ ---
2
+ :minor: 1
3
+ :patch: 1
4
+ :major: 0
@@ -0,0 +1,9 @@
1
+ require "#{File.dirname(__FILE__)}/capistrano/configuration/extensions/actions/invocation"
2
+ require "#{File.dirname(__FILE__)}/capistrano/configuration/extensions/connections"
3
+ require "#{File.dirname(__FILE__)}/capistrano/configuration/extensions/execution"
4
+
5
+ class Capistrano::Configuration
6
+ include Capistrano::Configuration::Extensions::Actions::Invocation
7
+ include Capistrano::Configuration::Extensions::Connections
8
+ include Capistrano::Configuration::Extensions::Execution
9
+ end
@@ -0,0 +1,82 @@
1
+ module Capistrano
2
+ class Configuration
3
+ module Extensions
4
+ module Actions
5
+ module Invocation
6
+ class BlockProxy
7
+ attr_accessor :blocks
8
+
9
+ def initialize
10
+ @blocks = []
11
+ end
12
+
13
+ def run(&block)
14
+ blocks << block
15
+ end
16
+ end
17
+
18
+ def parallelize(thread_count = nil)
19
+ set :parallelize_thread_count, 10 unless respond_to?(:parallelize_thread_count)
20
+
21
+ proxy = BlockProxy.new
22
+ yield proxy
23
+
24
+ logger.info "Running #{proxy.blocks.size} threads in chunks of #{thread_count || parallelize_thread_count}"
25
+
26
+ run_parallelize_loop(proxy, thread_count || parallelize_thread_count)
27
+ end
28
+
29
+ def run_parallelize_loop(proxy, thread_count)
30
+ batch = 1
31
+ all_threads = []
32
+ proxy.blocks.each_slice(thread_count) do |chunk|
33
+ logger.info "Running batch number #{batch}"
34
+ threads = run_in_threads(chunk)
35
+ all_threads << threads
36
+ wait_for(threads)
37
+ rollback_all_threads(all_threads.flatten) and return if threads.any? {|t| t[:rolled_back] || t[:exception_raised]}
38
+ batch += 1
39
+ end
40
+ end
41
+
42
+ def run_in_threads(blocks)
43
+ blocks.collect do |blk|
44
+ thread = Thread.new do
45
+ logger.info "Running block in background thread"
46
+ blk.call
47
+ end
48
+ begin
49
+ thread.run
50
+ rescue ThreadError
51
+ thread[:exception_raised] = $!
52
+ end
53
+ thread
54
+ end
55
+ end
56
+
57
+ def wait_for(threads)
58
+ threads.each do |thread|
59
+ begin
60
+ thread.join
61
+ rescue
62
+ logger.important "Subthread failed: #{$!.message}"
63
+ end
64
+ end
65
+ end
66
+
67
+ def rollback_all_threads(threads)
68
+ Thread.new do
69
+ threads.select {|t| !t[:rolled_back]}.each do |thread|
70
+ Thread.current[:rollback_requests] = thread[:rollback_requests]
71
+ rollback!
72
+ end
73
+ end.join
74
+ rollback! # Rolling back main thread too
75
+ true
76
+ end
77
+
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,55 @@
1
+ module Capistrano
2
+ class Configuration
3
+ module Extensions
4
+ # Thread-safe(r) version of the Capistrano default
5
+ # connection handling.
6
+ module Connections
7
+ def initialize_with_connections(*args) #:nodoc:
8
+ initialize_without_connections(*args)
9
+ Thread.current[:sessions] = {}
10
+ Thread.current[:failed_sessions] = []
11
+ end
12
+
13
+ # Indicate that the given server could not be connected to.
14
+ def failed!(server)
15
+ Thread.current[:failed_sessions] << server
16
+ end
17
+
18
+ # A hash of the SSH sessions that are currently open and available.
19
+ # Because sessions are constructed lazily, this will only contain
20
+ # connections to those servers that have been the targets of one or more
21
+ # executed tasks.
22
+ def sessions
23
+ Thread.current[:sessions] ||= {}
24
+ end
25
+
26
+ # Query whether previous connection attempts to the given server have
27
+ # failed.
28
+ def has_failed?(server)
29
+ Thread.current[:failed_sessions].include?(server)
30
+ end
31
+
32
+ def teardown_connections_to(servers)
33
+ servers.each do |server|
34
+ sessions[server].close
35
+ sessions.delete(server)
36
+ end
37
+ end
38
+
39
+ private
40
+ def establish_connection_to(server, failures=nil)
41
+ current_thread = Thread.current
42
+ Thread.new { safely_establish_connection_to(server, current_thread, failures) }
43
+ end
44
+
45
+ def safely_establish_connection_to(server, thread, failures=nil)
46
+ thread[:sessions] ||= {}
47
+ thread[:sessions][server] ||= connection_factory.connect_to(server)
48
+ rescue Exception => err
49
+ raise unless failures
50
+ failures << { :server => server, :error => err }
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,38 @@
1
+ module Capistrano
2
+ class Configuration
3
+ module Extensions
4
+ module Execution
5
+ def task_call_frames
6
+ Thread.current[:task_call_frames] ||= []
7
+ end
8
+
9
+ def rollback_requests=(rollback_requests)
10
+ Thread.current[:rollback_requests] = rollback_requests
11
+ end
12
+
13
+ def rollback_requests
14
+ Thread.current[:rollback_requests]
15
+ end
16
+
17
+ def current_task
18
+ all_task_call_frames = Thread.main[:task_call_frames] + task_call_frames
19
+ return nil if all_task_call_frames.empty?
20
+ all_task_call_frames.last.task
21
+ end
22
+
23
+ def transaction
24
+ super do
25
+ self.rollback_requests = [] unless transaction?
26
+ yield
27
+ end
28
+ end
29
+
30
+ def rollback!
31
+ return if Thread.current[:rollback_requests].nil?
32
+ Thread.current[:rolled_back] = true
33
+ super
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,226 @@
1
+ require "utils"
2
+ require "capistrano/task_definition"
3
+ require "capistrano/configuration"
4
+ require "#{File.join(File.dirname(__FILE__), '..', 'lib')}/cap_ext_parallelize"
5
+
6
+ class ConfigurationActionsParallelInvocationTest < Test::Unit::TestCase
7
+ class MockConfig
8
+ attr_reader :options
9
+ attr_accessor :debug
10
+ attr_accessor :dry_run
11
+ attr_reader :tasks, :namespaces, :fully_qualified_name, :parent
12
+ attr_reader :state, :original_initialize_called
13
+ attr_accessor :logger, :default_task
14
+ attr_accessor :parallelize_thread_count
15
+
16
+ def initialize(options)
17
+ @original_initialize_called = true
18
+ @tasks = {}
19
+ @namespaces = {}
20
+ @state = {}
21
+ @fully_qualified_name = options[:fqn]
22
+ @parent = options[:parent]
23
+ @logger = options.delete(:logger)
24
+ @options = {}
25
+ @parallelize_thread_count = 10
26
+ end
27
+
28
+ def [](*args)
29
+ @options[*args]
30
+ end
31
+
32
+ def set(name, value)
33
+ @options[name] = value
34
+ end
35
+
36
+ def fetch(*args)
37
+ @options.fetch(*args)
38
+ end
39
+
40
+ include Capistrano::Configuration::Execution
41
+ include Capistrano::Configuration::Actions::Invocation
42
+ include Capistrano::Configuration::Extensions::Execution
43
+ include Capistrano::Configuration::Extensions::Actions::Invocation
44
+ end
45
+
46
+ def setup
47
+ @config = MockConfig.new(:logger => stub(:debug => nil, :info => nil, :important => nil))
48
+ @original_io_proc = MockConfig.default_io_proc
49
+ end
50
+
51
+ def test_parallelize_should_run_all_collected_tasks
52
+ aaa = new_task(@config, :aaa) do
53
+ parallelize do |session|
54
+ session.run {(state[:has_been_run] ||= []) << :first}
55
+ session.run {(state[:has_been_run] ||= []) << :second}
56
+ end
57
+ end
58
+ @config.execute_task(aaa)
59
+ assert @config.state[:has_been_run].include?(:first)
60
+ assert @config.state[:has_been_run].include?(:second)
61
+ end
62
+
63
+ def test_parallelize_should_rollback_all_threads_when_one_thread_raises_error
64
+ ccc = new_task(@config, :ccc) do
65
+ on_rollback {(state[:rollback] ||= []) << :first}
66
+ raise "boom"
67
+ end
68
+
69
+ eee = new_task(@config, :eee) do
70
+ on_rollback {(state[:rollback] ||= []) << :second}
71
+ end
72
+
73
+ ddd = new_task(@config, :ddd) do
74
+ transaction {execute_task(eee)}
75
+ end
76
+
77
+ bbb = new_task(@config, :bbb) {transaction {execute_task(ccc)}}
78
+
79
+ aaa = new_task(@config, :aaa) do
80
+ on_rollback {puts 'rolled back'}
81
+ parallelize do |session|
82
+ session.run {execute_task(bbb)}
83
+ session.run {execute_task(ddd)}
84
+ end
85
+ end
86
+
87
+ @config.execute_task(aaa)
88
+ assert @config.state[:rollback].include?(:first)
89
+ assert @config.state[:rollback].include?(:second)
90
+ end
91
+
92
+ def test_parallelize_should_rollback_only_run_threads_when_one_thread_raises_error
93
+ ccc = new_task(@config, :ccc) do
94
+ on_rollback {(state[:rollback] ||= []) << :first}
95
+ raise "boom"
96
+ end
97
+
98
+ eee = new_task(@config, :eee) do
99
+ on_rollback {(state[:rollback] ||= []) << :second}
100
+ end
101
+
102
+ ddd = new_task(@config, :ddd) do
103
+ transaction {execute_task(eee)}
104
+ end
105
+
106
+ bbb = new_task(@config, :bbb) {transaction {execute_task(ccc)}}
107
+
108
+ aaa = new_task(@config, :aaa) do
109
+ on_rollback {puts 'rolled back'}
110
+ parallelize do |session|
111
+ session.run {execute_task(bbb)}
112
+ session.run {execute_task(ddd)}
113
+ end
114
+ end
115
+ @config.parallelize_thread_count = 1
116
+ @config.execute_task(aaa)
117
+ assert @config.state[:rollback].include?(:first)
118
+ assert !@config.state[:rollback].include?(:second)
119
+ end
120
+
121
+ def test_parallelize_should_rollback_all_threads_when_one_thread_raises_error
122
+ ccc = new_task(@config, :ccc) do
123
+ on_rollback {(state[:rollback] ||= []) << :first}
124
+ sleep 0.1
125
+ raise "boom"
126
+ end
127
+
128
+ eee = new_task(@config, :eee) do
129
+ on_rollback {(state[:rollback] ||= []) << :second}
130
+ end
131
+
132
+ ddd = new_task(@config, :ddd) do
133
+ transaction {execute_task(eee)}
134
+ end
135
+
136
+ bbb = new_task(@config, :bbb) {transaction {execute_task(ccc)}}
137
+
138
+ aaa = new_task(@config, :aaa) do
139
+ on_rollback {puts 'rolled back'}
140
+ parallelize do |session|
141
+ session.run {execute_task(bbb)}
142
+ session.run {execute_task(ddd)}
143
+ end
144
+ end
145
+
146
+ @config.execute_task(aaa)
147
+ assert @config.state[:rollback].include?(:first)
148
+ assert @config.state[:rollback].include?(:second)
149
+ end
150
+
151
+ def test_should_not_rollback_threads_twice
152
+ ccc = new_task(@config, :ccc) do
153
+ on_rollback {(state[:rollback] ||= []) << :first}
154
+ raise "boom"
155
+ end
156
+
157
+ eee = new_task(@config, :eee) do
158
+ on_rollback {(state[:rollback] ||= []) << :second}
159
+ end
160
+
161
+ ddd = new_task(@config, :ddd) do
162
+ transaction {execute_task(eee)}
163
+ end
164
+
165
+ bbb = new_task(@config, :bbb) {transaction {execute_task(ccc)}}
166
+
167
+ aaa = new_task(@config, :aaa) do
168
+ on_rollback {puts 'rolled back'}
169
+ parallelize do |session|
170
+ session.run {execute_task(bbb)}
171
+ session.run {execute_task(ddd)}
172
+ end
173
+ end
174
+
175
+ @config.execute_task(aaa)
176
+ assert_equal 2, @config.state[:rollback].size
177
+ assert @config.state[:rollback].include?(:first)
178
+ assert @config.state[:rollback].include?(:second)
179
+ end
180
+
181
+ def test_should_not_rollback_threads_twice
182
+
183
+ eee = new_task(@config, :eee) do
184
+ on_rollback {(state[:rollback] ||= []) << :second}
185
+ end
186
+
187
+ ddd = new_task(@config, :ddd) do
188
+ transaction {execute_task(eee)}
189
+ end
190
+
191
+ aaa = new_task(@config, :aaa) do
192
+ on_rollback {(state[:rollback] ||= []) << :main}
193
+ parallelize do |session|
194
+ session.run {execute_task(bbb)}
195
+ session.run {execute_task(ddd)}
196
+ end
197
+ end
198
+
199
+ bbb = new_task(@config, :bbb) do
200
+ transaction do
201
+ execute_task(aaa)
202
+ end
203
+ end
204
+
205
+ @config.execute_task(bbb)
206
+ assert_equal 2, @config.state[:rollback].size
207
+ assert @config.state[:rollback].include?(:main)
208
+ assert @config.state[:rollback].include?(:second)
209
+ end
210
+
211
+ def test_should_run_each_run_block_in_separate_thread
212
+ aaa = new_task(@config, :aaa) do
213
+ parallelize do |session|
214
+ session.run {execute_task(bbb)}
215
+ session.run {execute_task(ddd)}
216
+ end
217
+ end
218
+ end
219
+
220
+ private
221
+ def new_task(namespace, name, options={}, &block)
222
+ block ||= stack_inspector
223
+ namespace.tasks[name] = Capistrano::TaskDefinition.new(name, namespace, &block)
224
+ end
225
+
226
+ end
@@ -0,0 +1,38 @@
1
+ begin
2
+ require 'rubygems'
3
+ gem 'mocha'
4
+ rescue LoadError
5
+ end
6
+
7
+ require 'test/unit'
8
+ require 'mocha'
9
+ require 'capistrano/server_definition'
10
+
11
+ module TestExtensions
12
+ def server(host, options={})
13
+ Capistrano::ServerDefinition.new(host, options)
14
+ end
15
+
16
+ def namespace(fqn=nil)
17
+ space = stub(:roles => {}, :fully_qualified_name => fqn, :default_task => nil)
18
+ yield(space) if block_given?
19
+ space
20
+ end
21
+
22
+ def role(space, name, *args)
23
+ opts = args.last.is_a?(Hash) ? args.pop : {}
24
+ space.roles[name] ||= []
25
+ space.roles[name].concat(args.map { |h| Capistrano::ServerDefinition.new(h, opts) })
26
+ end
27
+
28
+ def new_task(name, namespace=@namespace, options={}, &block)
29
+ block ||= Proc.new {}
30
+ task = Capistrano::TaskDefinition.new(name, namespace, options, &block)
31
+ assert_equal block, task.body
32
+ return task
33
+ end
34
+ end
35
+
36
+ class Test::Unit::TestCase
37
+ include TestExtensions
38
+ end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mattmatt-cap-ext-parallelize
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Mathias Meyer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-03-19 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: capistrano
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ description:
26
+ email: meyer@paperplanes.de
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files: []
32
+
33
+ files:
34
+ - Rakefile
35
+ - README.md
36
+ - VERSION.yml
37
+ - lib/cap_ext_parallelize.rb
38
+ - lib/capistrano
39
+ - lib/capistrano/configuration
40
+ - lib/capistrano/configuration/extensions
41
+ - lib/capistrano/configuration/extensions/actions
42
+ - lib/capistrano/configuration/extensions/actions/extensions
43
+ - lib/capistrano/configuration/extensions/actions/invocation.rb
44
+ - lib/capistrano/configuration/extensions/connections.rb
45
+ - lib/capistrano/configuration/extensions/execution.rb
46
+ - test/parallel_invocation_test.rb
47
+ - test/utils.rb
48
+ has_rdoc: true
49
+ homepage: http://github.com/mattmatt/cap-ext-parallelize
50
+ post_install_message:
51
+ rdoc_options:
52
+ - --inline-source
53
+ - --charset=UTF-8
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: "0"
61
+ version:
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: "0"
67
+ version:
68
+ requirements: []
69
+
70
+ rubyforge_project:
71
+ rubygems_version: 1.2.0
72
+ signing_key:
73
+ specification_version: 2
74
+ summary: A drop-in replacement for Capistrano to fire off Webistrano deployments transparently without losing the joy of using the cap command.
75
+ test_files: []
76
+