tlconnor-spawn 1.0.0
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.
- data/.gitignore +2 -0
- data/CHANGELOG +55 -0
- data/LICENSE +22 -0
- data/README.markdown +193 -0
- data/lib/spawn.rb +6 -0
- data/lib/spawn/patches.rb +109 -0
- data/lib/spawn/spawn.rb +211 -0
- data/tlconnor-spawn.gemspec +15 -0
- metadata +74 -0
data/.gitignore
ADDED
data/CHANGELOG
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
v0.1 - 2007/09/13
|
2
|
+
|
3
|
+
initial version
|
4
|
+
|
5
|
+
--------------------------------------------------
|
6
|
+
v0.2 - 2007/09/28
|
7
|
+
|
8
|
+
* return PID of the child process
|
9
|
+
* added ":detach => false" option
|
10
|
+
|
11
|
+
--------------------------------------------------
|
12
|
+
v0.3 - 2007/10/15
|
13
|
+
|
14
|
+
* added ':method => :thread' for threaded spawns
|
15
|
+
* removed ':detach => false' option in favor of more generic implementation
|
16
|
+
* added ability to set configuration of the form 'Spawn::method :thread'
|
17
|
+
* added patch to ActiveRecord::Base to allow for more efficient reconnect in child processes
|
18
|
+
* added monkey patch for http://dev.rubyonrails.org/ticket/7579
|
19
|
+
* added wait() method to wait for spawned code blocks
|
20
|
+
* don't allow threading if allow_concurrency=false
|
21
|
+
|
22
|
+
--------------------------------------------------
|
23
|
+
v0.4 - 2008/1/26
|
24
|
+
|
25
|
+
* default to :thread on windows, still :fork on all other platforms
|
26
|
+
* raise exception when used with :method=>:true and allow_concurrency != true
|
27
|
+
|
28
|
+
--------------------------------------------------
|
29
|
+
v0.5 - 2008/3/1
|
30
|
+
* also default to :thread on JRuby (java)
|
31
|
+
* added new :method => :yield which doesn't fork or thread, this is useful for testing
|
32
|
+
* fixed problem with connections piling up on PostgreSQL
|
33
|
+
|
34
|
+
--------------------------------------------------
|
35
|
+
v0.6 - 2008/04/21
|
36
|
+
* only apply clear_reloadable_connections patch on Rails 1.x (7579 fixed in Rails 2.x)
|
37
|
+
* made it more responsive in more environments by disconnecting from the listener socket in the forked process
|
38
|
+
|
39
|
+
--------------------------------------------------
|
40
|
+
v0.7 - 2008/04/24
|
41
|
+
* more generic mechanism for closing resources after fork
|
42
|
+
* check for existence of Mongrel before patching it
|
43
|
+
|
44
|
+
--------------------------------------------------
|
45
|
+
v0.8 - 2008/05/02
|
46
|
+
* call exit! within the ensure block so that at_exit handlers aren't called on exceptions
|
47
|
+
* set logger from RAILS_DEFAULT_LOGGER if available, else STDERR
|
48
|
+
|
49
|
+
--------------------------------------------------
|
50
|
+
v0.9 - 2008/05/11
|
51
|
+
* added ability to set nice level for child process
|
52
|
+
|
53
|
+
--------------------------------------------------
|
54
|
+
v1.0 - 2010/10/09
|
55
|
+
* merged edged to master, let's call this version 1.0
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2007 Tom Anderson (tom@squeat.com)
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without
|
6
|
+
restriction, including without limitation the rights to use,
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the
|
9
|
+
Software is furnished to do so, subject to the following
|
10
|
+
conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,193 @@
|
|
1
|
+
# Spawn
|
2
|
+
|
3
|
+
This plugin provides a 'spawn' method to easily fork OR thread long-running sections of
|
4
|
+
code so that your application can return results to your users more quickly.
|
5
|
+
This plugin works by creating new database connections in ActiveRecord::Base for the
|
6
|
+
spawned block.
|
7
|
+
|
8
|
+
The plugin also patches ActiveRecord::Base to handle some known bugs when using
|
9
|
+
threads (see lib/patches.rb).
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
To install the plugin from the master branch (recommended).
|
14
|
+
|
15
|
+
script/plugin install git://github.com/tra/spawn.git
|
16
|
+
|
17
|
+
If you want to install the plugin from the 'edge' branch (latest development):
|
18
|
+
|
19
|
+
script/plugin install git://github.com/tra/spawn.git -r edge
|
20
|
+
|
21
|
+
If you are unfortunate enough to be stuck on Rails 1.x, then it is recommended you
|
22
|
+
stick with v1.0 of this plugin (Rails 1.x won't be supported in future versions but
|
23
|
+
it might still work if you're lucky). To install this version:
|
24
|
+
|
25
|
+
script/plugin install git://github.com/tra/spawn.git -r master:v1.0
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
Here's a simple example of how to demonstrate the spawn plugin.
|
30
|
+
In one of your controllers, insert this code (after installing the plugin of course):
|
31
|
+
|
32
|
+
spawn do
|
33
|
+
logger.info("I feel sleepy...")
|
34
|
+
sleep 11
|
35
|
+
logger.info("Time to wake up!")
|
36
|
+
end
|
37
|
+
|
38
|
+
If everything is working correctly, your controller should finish quickly then you'll see
|
39
|
+
the last log message several seconds later.
|
40
|
+
|
41
|
+
If you need to wait for the spawned processes/threads, then pass the objects returned by
|
42
|
+
spawn to Spawn::wait(), like this:
|
43
|
+
|
44
|
+
N.times do |i|
|
45
|
+
# spawn N blocks of code
|
46
|
+
spawn_ids[i] = spawn do
|
47
|
+
something(i)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
# wait for all N blocks of code to finish running
|
51
|
+
wait(spawn_ids)
|
52
|
+
|
53
|
+
## Options
|
54
|
+
|
55
|
+
The options you can pass to spawn are:
|
56
|
+
|
57
|
+
<table>
|
58
|
+
<tr><th>Option</th><th>Values</th></tr>
|
59
|
+
<tr><td>:method</td><td>:fork, :thread, :yield</td></tr>
|
60
|
+
<tr><td>:nice</td><td>integer value 0-19, 19 = really nice</td></tr>
|
61
|
+
<tr><td>:kill</td><td>boolean value indicating whether the parent should kill the spawned process
|
62
|
+
when it exits (only valid when :method => :fork)</td></tr>
|
63
|
+
<tr><td>:argv</td><td>string to override the process name</td></tr>
|
64
|
+
</table>
|
65
|
+
|
66
|
+
Any option to spawn can be set as a default so that you don't have to pass them in
|
67
|
+
to every call of spawn. To configure the spawn default options, add a line to
|
68
|
+
your configuration file(s) like this:
|
69
|
+
|
70
|
+
Spawn::default_options {:method => :thread}
|
71
|
+
|
72
|
+
If you don't set any default options, the :method will default to :fork. To
|
73
|
+
specify different values for different environments, add the default_options call to
|
74
|
+
he appropriate environment file (development.rb, test.rb). For testing you can set
|
75
|
+
the default :method to :yield so that the code is run inline.
|
76
|
+
|
77
|
+
# in environment.rb
|
78
|
+
Spawn::method :method => :fork, :nice => 7
|
79
|
+
# in test.rb, will override the environment.rb setting
|
80
|
+
Spawn::method :method => :yield
|
81
|
+
|
82
|
+
This allows you to set your production and development environments to use different
|
83
|
+
methods according to your needs.
|
84
|
+
|
85
|
+
### be nice
|
86
|
+
|
87
|
+
If you want your forked child to run at a lower priority than the parent process, pass in
|
88
|
+
the :nice option like this:
|
89
|
+
|
90
|
+
spawn(:nice => 7) do
|
91
|
+
do_something_nicely
|
92
|
+
end
|
93
|
+
|
94
|
+
### fork me
|
95
|
+
|
96
|
+
By default, spawn will use the fork to spawn child processes. You can configure it to
|
97
|
+
do threading either by telling the spawn method when you call it or by configuring your
|
98
|
+
environment.
|
99
|
+
For example, this is how you can tell spawn to use threading on the call,
|
100
|
+
|
101
|
+
spawn(:method => :thread) do
|
102
|
+
something
|
103
|
+
end
|
104
|
+
|
105
|
+
For older versions of Rails (1.x), when using the :thread setting, spawn will check to
|
106
|
+
make sure that you have set allow_concurrency=true in your configuration. If you
|
107
|
+
want this setting then put this line in one of your environment config files:
|
108
|
+
|
109
|
+
config.active_record.allow_concurrency = true
|
110
|
+
|
111
|
+
If it is not set, then spawn will raise an exception.
|
112
|
+
|
113
|
+
### kill or be killed
|
114
|
+
|
115
|
+
Depending on your application, you may want the children processes to go away when
|
116
|
+
the parent process exits. By default spawn lets the children live after the
|
117
|
+
parent dies. But you can tell it to kill the children by setting the :kill option
|
118
|
+
to true.
|
119
|
+
|
120
|
+
### a process by any other name
|
121
|
+
|
122
|
+
If you'd like to be able to identify which processes are spawned by looking at the
|
123
|
+
output of ps then set the :argv option with a string of your choice.
|
124
|
+
You should then be able to see this string as the process name when
|
125
|
+
listing the running processes (ps).
|
126
|
+
|
127
|
+
For example, if you do something like this,
|
128
|
+
|
129
|
+
3.times do |i|
|
130
|
+
spawn(:argv => "spawn -#{i}-") do
|
131
|
+
something(i)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
then in the shell,
|
136
|
+
|
137
|
+
$ ps -ef | grep spawn
|
138
|
+
502 2645 2642 0 0:00.01 ttys002 0:00.02 spawn -0-
|
139
|
+
502 2646 2642 0 0:00.02 ttys002 0:00.02 spawn -1-
|
140
|
+
502 2647 2642 0 0:00.02 ttys002 0:00.03 spawn -2-
|
141
|
+
|
142
|
+
The length of the process name may be limited by your OS so you might want to experiment
|
143
|
+
to see how long it can be (it may be limited by the length of the original process name).
|
144
|
+
|
145
|
+
## Forking vs. Threading
|
146
|
+
|
147
|
+
There are several tradeoffs for using threading vs. forking. Forking was chosen as the
|
148
|
+
default primarily because it requires no configuration to get it working out of the box.
|
149
|
+
|
150
|
+
Forking advantages:
|
151
|
+
|
152
|
+
- more reliable? - the ActiveRecord code is generally not deemed to be thread-safe.
|
153
|
+
Even though spawn attempts to patch known problems with the threaded implementation,
|
154
|
+
there are no guarantees. Forking is heavier but should be fairly reliable.
|
155
|
+
- keep running - this could also be a disadvantage, but you may find you want to fork
|
156
|
+
off a process that could have a life longer than its parent. For example, maybe you
|
157
|
+
want to restart your server without killing the spawned processes.
|
158
|
+
We don't necessarily condone this (i.e. haven't tried it) but it's technically possible.
|
159
|
+
- easier - forking works out of the box with spawn, threading requires you set
|
160
|
+
allow_concurrency=true (for older versions of Rails).
|
161
|
+
Also, beware of automatic reloading of classes in development
|
162
|
+
mode (config.cache_classes = false).
|
163
|
+
|
164
|
+
Threading advantages:
|
165
|
+
- less filling - threads take less resources... how much less? it depends. Some
|
166
|
+
flavors of Unix are pretty efficient at forking so the threading advantage may not
|
167
|
+
be as big as you think... but then again, maybe it's more than you think. ;-)
|
168
|
+
- debugging - you can set breakpoints in your threads
|
169
|
+
|
170
|
+
## Acknowledgements
|
171
|
+
|
172
|
+
This plugin was initially inspired by Scott Persinger's blog post on how to use fork
|
173
|
+
in rails for background processing.
|
174
|
+
http://geekblog.vodpod.com/?p=26
|
175
|
+
|
176
|
+
Further inspiration for the threading implementation came from Jonathon Rochkind's
|
177
|
+
blog post on threading in rails.
|
178
|
+
http://bibwild.wordpress.com/2007/08/28/threading-in-rails/
|
179
|
+
|
180
|
+
Also thanks to all who have helped debug problems and suggest improvements
|
181
|
+
including:
|
182
|
+
|
183
|
+
- Ahmed Adam, Tristan Schneiter, Scott Haug, Andrew Garfield, Eugene Otto, Dan Sharp,
|
184
|
+
Olivier Ruffin, Adrian Duyzer, Cyrille Labesse
|
185
|
+
|
186
|
+
- Garry Tan, Matt Jankowski (Rails 2.2.x fixes), Mina Naguib (Rails 2.3.6 fix)
|
187
|
+
|
188
|
+
- Tim Kadom, Mauricio Marcon Zaffari, Danial Pearce, Hongli Lai, Scott Wadden
|
189
|
+
(passenger fixes)
|
190
|
+
|
191
|
+
- <your name here>
|
192
|
+
|
193
|
+
Copyright (c) 2007-present Tom Anderson (tom@squeat.com), see LICENSE
|
data/lib/spawn.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
# see activerecord/lib/active_record/connection_adaptors/abstract/connection_specification.rb
|
2
|
+
class ActiveRecord::Base
|
3
|
+
# reconnect without disconnecting
|
4
|
+
if Spawn::RAILS_2_2
|
5
|
+
def self.spawn_reconnect(klass=self)
|
6
|
+
# keep ancestors' connection_handlers around to avoid them being garbage collected in the forked child
|
7
|
+
@@ancestor_connection_handlers ||= []
|
8
|
+
@@ancestor_connection_handlers << self.connection_handler
|
9
|
+
self.connection_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
|
10
|
+
|
11
|
+
establish_connection
|
12
|
+
end
|
13
|
+
else
|
14
|
+
def self.spawn_reconnect(klass=self)
|
15
|
+
spec = @@defined_connections[klass.name]
|
16
|
+
konn = active_connections[klass.name]
|
17
|
+
# remove from internal arrays before calling establish_connection so that
|
18
|
+
# the connection isn't disconnected when it calls AR::Base.remove_connection
|
19
|
+
@@defined_connections.delete_if { |key, value| value == spec }
|
20
|
+
active_connections.delete_if { |key, value| value == konn }
|
21
|
+
establish_connection(spec ? spec.config : nil)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# this patch not needed on Rails 2.x and later
|
26
|
+
if Spawn::RAILS_1_x
|
27
|
+
# monkey patch to fix threading problems,
|
28
|
+
# see: http://dev.rubyonrails.org/ticket/7579
|
29
|
+
def self.clear_reloadable_connections!
|
30
|
+
if @@allow_concurrency
|
31
|
+
# Hash keyed by thread_id in @@active_connections. Hash of hashes.
|
32
|
+
@@active_connections.each do |thread_id, conns|
|
33
|
+
conns.each do |name, conn|
|
34
|
+
if conn.requires_reloading?
|
35
|
+
conn.disconnect!
|
36
|
+
@@active_connections[thread_id].delete(name)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
else
|
41
|
+
# Just one level hash, no concurrency.
|
42
|
+
@@active_connections.each do |name, conn|
|
43
|
+
if conn.requires_reloading?
|
44
|
+
conn.disconnect!
|
45
|
+
@@active_connections.delete(name)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# see mongrel/lib/mongrel.rb
|
54
|
+
# it's possible that this is not defined if you're running outside of mongrel
|
55
|
+
# examples: ./script/runner or ./script/console
|
56
|
+
if defined? Mongrel::HttpServer
|
57
|
+
class Mongrel::HttpServer
|
58
|
+
# redefine Montrel::HttpServer::process_client so that we can intercept
|
59
|
+
# the socket that is being used so Spawn can close it upon forking
|
60
|
+
alias_method :orig_process_client, :process_client
|
61
|
+
def process_client(client)
|
62
|
+
Spawn.resources_to_close(client, @socket)
|
63
|
+
orig_process_client(client)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
need_passenger_patch = true
|
69
|
+
if defined? PhusionPassenger::VERSION_STRING
|
70
|
+
# The VERSION_STRING variable was defined sometime after 2.1.0.
|
71
|
+
# We don't need passenger patch for 2.2.2 or later.
|
72
|
+
pv = PhusionPassenger::VERSION_STRING.split('.').collect{|s| s.to_i}
|
73
|
+
need_passenger_patch = pv[0] < 2 || (pv[0] == 2 && (pv[1] < 2 || (pv[1] == 2 && pv[2] < 2)))
|
74
|
+
end
|
75
|
+
|
76
|
+
if need_passenger_patch
|
77
|
+
# Patch for work with passenger < 2.1.0
|
78
|
+
if defined? Passenger::Railz::RequestHandler
|
79
|
+
class Passenger::Railz::RequestHandler
|
80
|
+
alias_method :orig_process_request, :process_request
|
81
|
+
def process_request(headers, input, output)
|
82
|
+
Spawn.resources_to_close(input, output)
|
83
|
+
orig_process_request(headers, input, output)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Patch for work with passenger >= 2.1.0
|
89
|
+
if defined? PhusionPassenger::Railz::RequestHandler
|
90
|
+
class PhusionPassenger::Railz::RequestHandler
|
91
|
+
alias_method :orig_process_request, :process_request
|
92
|
+
def process_request(headers, input, output)
|
93
|
+
Spawn.resources_to_close(input, output)
|
94
|
+
orig_process_request(headers, input, output)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Patch for passenger with Rails >= 2.3.0 (uses rack)
|
100
|
+
if defined? PhusionPassenger::Rack::RequestHandler
|
101
|
+
class PhusionPassenger::Rack::RequestHandler
|
102
|
+
alias_method :orig_process_request, :process_request
|
103
|
+
def process_request(headers, input, output)
|
104
|
+
Spawn.resources_to_close(input, output)
|
105
|
+
orig_process_request(headers, input, output)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
data/lib/spawn/spawn.rb
ADDED
@@ -0,0 +1,211 @@
|
|
1
|
+
module Spawn
|
2
|
+
RAILS_1_x = (::Rails::VERSION::MAJOR == 1) unless defined?(RAILS_1_x)
|
3
|
+
RAILS_2_2 = (::Rails::VERSION::MAJOR > 2 || (::Rails::VERSION::MAJOR == 2 && ::Rails::VERSION::MINOR >= 2)) unless defined?(RAILS_2_2)
|
4
|
+
|
5
|
+
@@default_options = {
|
6
|
+
# default to forking (unless windows or jruby)
|
7
|
+
:method => ((RUBY_PLATFORM =~ /(win32|java)/) ? :thread : :fork),
|
8
|
+
:nice => nil,
|
9
|
+
:kill => false,
|
10
|
+
:argv => nil
|
11
|
+
}
|
12
|
+
|
13
|
+
# things to close in child process
|
14
|
+
@@resources = []
|
15
|
+
# in some environments, logger isn't defined
|
16
|
+
@@logger = defined?(RAILS_DEFAULT_LOGGER) ? RAILS_DEFAULT_LOGGER : Logger.new(STDERR)
|
17
|
+
# forked children to kill on exit
|
18
|
+
@@punks = []
|
19
|
+
|
20
|
+
# Set the options to use every time spawn is called unless specified
|
21
|
+
# otherwise. For example, in your environment, do something like
|
22
|
+
# this:
|
23
|
+
# Spawn::default_options = {:nice => 5}
|
24
|
+
# to default to using the :nice option with a value of 5 on every call.
|
25
|
+
# Valid options are:
|
26
|
+
# :method => (:thread | :fork | :yield)
|
27
|
+
# :nice => nice value of the forked process
|
28
|
+
# :kill => whether or not the parent process will kill the
|
29
|
+
# spawned child process when the parent exits
|
30
|
+
# :argv => changes name of the spawned process as seen in ps
|
31
|
+
def self.default_options(options = {})
|
32
|
+
@@default_options.merge!(options)
|
33
|
+
@@logger.info "spawn> default options = #{options.inspect}"
|
34
|
+
end
|
35
|
+
|
36
|
+
# @deprecated - please use Spawn::default_options(:method => ) instead
|
37
|
+
# add calls to this in your environment.rb to set your configuration, for example,
|
38
|
+
# to use forking everywhere except your 'development' environment:
|
39
|
+
# Spawn::method :fork
|
40
|
+
# Spawn::method :thread, 'development'
|
41
|
+
def self.method(method, env = nil)
|
42
|
+
@@logger.warn "spawn> please use Spawn::default_options(:method => #{method}) instead of Spawn::method"
|
43
|
+
if !env || env == RAILS_ENV
|
44
|
+
default_options :method => method
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# set the resources to disconnect from in the child process (when forking)
|
49
|
+
def self.resources_to_close(*resources)
|
50
|
+
@@resources = resources
|
51
|
+
end
|
52
|
+
|
53
|
+
# close all the resources added by calls to resource_to_close
|
54
|
+
def self.close_resources
|
55
|
+
@@resources.each do |resource|
|
56
|
+
resource.close if resource && resource.respond_to?(:close) && !resource.closed?
|
57
|
+
end
|
58
|
+
# in case somebody spawns recursively
|
59
|
+
@@resources.clear
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.alive?(pid)
|
63
|
+
begin
|
64
|
+
Process::kill 0, pid
|
65
|
+
# if the process is alive then kill won't throw an exception
|
66
|
+
true
|
67
|
+
rescue Errno::ESRCH
|
68
|
+
false
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.kill_punks
|
73
|
+
@@punks.each do |punk|
|
74
|
+
if alive?(punk)
|
75
|
+
@@logger.info "spawn> parent(#{Process.pid}) killing child(#{punk})"
|
76
|
+
begin
|
77
|
+
Process.kill("TERM", punk)
|
78
|
+
rescue
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
@@punks = []
|
83
|
+
end
|
84
|
+
# register to kill marked children when parent exits
|
85
|
+
at_exit {kill_punks}
|
86
|
+
|
87
|
+
# Spawns a long-running section of code and returns the ID of the spawned process.
|
88
|
+
# By default the process will be a forked process. To use threading, pass
|
89
|
+
# :method => :thread or override the default behavior in the environment by setting
|
90
|
+
# 'Spawn::method :thread'.
|
91
|
+
def spawn(opts = {})
|
92
|
+
options = @@default_options.merge(opts.symbolize_keys)
|
93
|
+
# setting options[:method] will override configured value in default_options[:method]
|
94
|
+
if options[:method] == :yield
|
95
|
+
yield
|
96
|
+
elsif options[:method] == :thread
|
97
|
+
# for versions before 2.2, check for allow_concurrency
|
98
|
+
if RAILS_2_2 || ActiveRecord::Base.allow_concurrency
|
99
|
+
thread_it(options) { yield }
|
100
|
+
else
|
101
|
+
@@logger.error("spawn(:method=>:thread) only allowed when allow_concurrency=true")
|
102
|
+
raise "spawn requires config.active_record.allow_concurrency=true when used with :method=>:thread"
|
103
|
+
end
|
104
|
+
else
|
105
|
+
fork_it(options) { yield }
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def wait(sids = [])
|
110
|
+
# wait for all threads and/or forks (if a single sid passed in, convert to array first)
|
111
|
+
Array(sids).each do |sid|
|
112
|
+
if sid.type == :thread
|
113
|
+
sid.handle.join()
|
114
|
+
else
|
115
|
+
begin
|
116
|
+
Process.wait(sid.handle)
|
117
|
+
rescue
|
118
|
+
# if the process is already done, ignore the error
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
# clean up connections from expired threads
|
123
|
+
ActiveRecord::Base.verify_active_connections!()
|
124
|
+
end
|
125
|
+
|
126
|
+
class SpawnId
|
127
|
+
attr_accessor :type
|
128
|
+
attr_accessor :handle
|
129
|
+
def initialize(t, h)
|
130
|
+
self.type = t
|
131
|
+
self.handle = h
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
protected
|
136
|
+
def fork_it(options)
|
137
|
+
# The problem with rails is that it only has one connection (per class),
|
138
|
+
# so when we fork a new process, we need to reconnect.
|
139
|
+
@@logger.debug "spawn> parent PID = #{Process.pid}"
|
140
|
+
child = fork do
|
141
|
+
begin
|
142
|
+
start = Time.now
|
143
|
+
@@logger.debug "spawn> child PID = #{Process.pid}"
|
144
|
+
|
145
|
+
# this child has no children of it's own to kill (yet)
|
146
|
+
@@punks = []
|
147
|
+
|
148
|
+
# set the nice priority if needed
|
149
|
+
Process.setpriority(Process::PRIO_PROCESS, 0, options[:nice]) if options[:nice]
|
150
|
+
|
151
|
+
# disconnect from the listening socket, et al
|
152
|
+
Spawn.close_resources
|
153
|
+
# get a new connection so the parent can keep the original one
|
154
|
+
ActiveRecord::Base.spawn_reconnect
|
155
|
+
|
156
|
+
# set the process name
|
157
|
+
$0 = options[:argv] if options[:argv]
|
158
|
+
|
159
|
+
# run the block of code that takes so long
|
160
|
+
yield
|
161
|
+
|
162
|
+
rescue => ex
|
163
|
+
@@logger.error "spawn> Exception in child[#{Process.pid}] - #{ex.class}: #{ex.message}"
|
164
|
+
ensure
|
165
|
+
begin
|
166
|
+
# to be safe, catch errors on closing the connnections too
|
167
|
+
if RAILS_2_2
|
168
|
+
ActiveRecord::Base.connection_handler.clear_all_connections!
|
169
|
+
else
|
170
|
+
ActiveRecord::Base.connection.disconnect!
|
171
|
+
ActiveRecord::Base.remove_connection
|
172
|
+
end
|
173
|
+
ensure
|
174
|
+
@@logger.info "spawn> child[#{Process.pid}] took #{Time.now - start} sec"
|
175
|
+
# ensure log is flushed since we are using exit!
|
176
|
+
@@logger.flush if @@logger.respond_to?(:flush)
|
177
|
+
# this child might also have children to kill if it called spawn
|
178
|
+
Spawn::kill_punks
|
179
|
+
# this form of exit doesn't call at_exit handlers
|
180
|
+
exit!(0)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# detach from child process (parent may still wait for detached process if they wish)
|
186
|
+
Process.detach(child)
|
187
|
+
|
188
|
+
# remove dead children from the target list to avoid memory leaks
|
189
|
+
@@punks.delete_if {|punk| !Spawn::alive?(punk)}
|
190
|
+
|
191
|
+
# mark this child for death when this process dies
|
192
|
+
if options[:kill]
|
193
|
+
@@punks << child
|
194
|
+
@@logger.debug "spawn> death row = #{@@punks.inspect}"
|
195
|
+
end
|
196
|
+
|
197
|
+
return SpawnId.new(:fork, child)
|
198
|
+
end
|
199
|
+
|
200
|
+
def thread_it(options)
|
201
|
+
# clean up stale connections from previous threads
|
202
|
+
ActiveRecord::Base.verify_active_connections!()
|
203
|
+
thr = Thread.new do
|
204
|
+
# run the long-running code block
|
205
|
+
yield
|
206
|
+
end
|
207
|
+
thr.priority = -options[:nice] if options[:nice]
|
208
|
+
return SpawnId.new(:thread, thr)
|
209
|
+
end
|
210
|
+
|
211
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
$:.push File.expand_path("../lib", __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |gem|
|
4
|
+
gem.name = 'tlconnor-spawn'
|
5
|
+
gem.version = '1.0.0'
|
6
|
+
gem.platform = Gem::Platform::RUBY
|
7
|
+
gem.authors = ['Tim Connor']
|
8
|
+
gem.email = 'tlconnor@gmail.com'
|
9
|
+
gem.summary = 'Spawn'
|
10
|
+
gem.description = gem.summary
|
11
|
+
|
12
|
+
gem.files = `git ls-files`.split("\n")
|
13
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
14
|
+
gem.require_paths = ["lib"]
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tlconnor-spawn
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 23
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
- 0
|
10
|
+
version: 1.0.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Tim Connor
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-03-09 00:00:00 +13:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: Spawn
|
23
|
+
email: tlconnor@gmail.com
|
24
|
+
executables: []
|
25
|
+
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files: []
|
29
|
+
|
30
|
+
files:
|
31
|
+
- .gitignore
|
32
|
+
- CHANGELOG
|
33
|
+
- LICENSE
|
34
|
+
- README.markdown
|
35
|
+
- lib/spawn.rb
|
36
|
+
- lib/spawn/patches.rb
|
37
|
+
- lib/spawn/spawn.rb
|
38
|
+
- tlconnor-spawn.gemspec
|
39
|
+
has_rdoc: true
|
40
|
+
homepage:
|
41
|
+
licenses: []
|
42
|
+
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options: []
|
45
|
+
|
46
|
+
require_paths:
|
47
|
+
- lib
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
hash: 3
|
54
|
+
segments:
|
55
|
+
- 0
|
56
|
+
version: "0"
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
hash: 3
|
63
|
+
segments:
|
64
|
+
- 0
|
65
|
+
version: "0"
|
66
|
+
requirements: []
|
67
|
+
|
68
|
+
rubyforge_project:
|
69
|
+
rubygems_version: 1.6.2
|
70
|
+
signing_key:
|
71
|
+
specification_version: 3
|
72
|
+
summary: Spawn
|
73
|
+
test_files: []
|
74
|
+
|