resque-forker 1.0.beta

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
@@ -0,0 +1,2 @@
1
+ 2010-07-30 v1.0
2
+ Extracted from Flowtown with permission
data/MIT-LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2010 Flowtown, Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
data/README.rdoc ADDED
@@ -0,0 +1,163 @@
1
+ = Resque::Forker
2
+
3
+ Super awesome forking action for Resque workers.
4
+
5
+ == Forking Workers
6
+
7
+ If you're like us, you have a sizeable application with many models, libraries
8
+ and dependencies that are shared between the front-facing UI and the back-end
9
+ processing. And like us, you're Resque worker are loading the entire application
10
+ each time the fire up.
11
+
12
+ If you're running 8 workers that can be quite the CPU-churning delay loading
13
+ them all up. Exactly the problem we're going to solve by starting the
14
+ application once and then forking it. Forking all these workers takes
15
+ milliseconds. Faster restart means faster deploy and less downtime. Yay!
16
+
17
+
18
+ == Creating the script
19
+
20
+ We're going to create a Ruby script that loads the applications, handles
21
+ connections, and decides what kind of workload (how many workers on which
22
+ queues) to process.
23
+
24
+ Edit this to your needs and place it in script/workers:
25
+
26
+ #!/usr/bin/env ruby
27
+ require "resque/forker"
28
+
29
+ # Load the application.
30
+ Resque.setup do |forker|
31
+ require File.dirname(__FILE__) + "/../config/environment"
32
+ ActiveRecord::Base.connection.disconnect!
33
+ if Rails.env.production?
34
+ forker.logger = Rails.logger
35
+ forker.workload = ["*"] * 4 # 4 workers on all queues
36
+ forker.user "www-data", "www-data" # don't run as root
37
+ end
38
+ end
39
+ # Stuff to do after forking a worker.
40
+ Resque.after_fork do
41
+ ActiveRecord::Base.establish_connection
42
+ end
43
+ Resque.fork!
44
+
45
+ You can now run workers from the command line:
46
+
47
+ $ ruby script/workers
48
+
49
+ In development mode you will get one worker that outputs to the console. In
50
+ production you get four workers that log messages to the Rails logger and run
51
+ under the www-data account (never run as root).
52
+
53
+ Worker processes can't share connections with each other, so we're closing the
54
+ database connection from the master process and then establishing new connection
55
+ for each individual worker. You'll have to do the same with other libraries that
56
+ maintain open connections (MongoMapper, Vanity, etc)
57
+
58
+ You tell Resque::Forker what workload to process using an array of queue lists.
59
+ Each array element represents one worker, so 4 elements would start up four
60
+ workers. The element's value tell the worker which queues to process. For
61
+ example, if you want four workers processing the import queue, and two of these
62
+ workers also processing the export queue:
63
+
64
+ forker.workload = ["import", "import,export"] * 2
65
+
66
+
67
+ == Controlling the Workers
68
+
69
+ You can use these signals to control individual workers, or send them to the
70
+ master process, which will propagate them to all workers:
71
+
72
+ kill -QUIT -- Quit gracefully
73
+ kill -TERM -- Terminate immediately
74
+ kill -USR1 -- Stop any ongoing job
75
+ kill -USR2 -- Suspend worker
76
+ kill -CONT -- Resume suspended worker
77
+
78
+ After deploying you want to stop all workers, reload the master process (and
79
+ the application and its configuration) and have all workers restarted. Simply
80
+ send it the HUP signal. That easy.
81
+
82
+ You probably want to suspend/resume (USR2/CONT signals) if you're doing any
83
+ maintenance work that may disrupt the workers, like rake db:migrate. Of course
84
+ you can stop/start the master process, but what would be the fun of that.
85
+
86
+ Of course, you want the workers to start after reboot and each way to control
87
+ them. Read on how to use Resque::Forker with Upstart.
88
+
89
+
90
+ == Using Upstart and Capistrano
91
+
92
+ If you're running a recent release of Ubuntu, you can get Upstart to manage your
93
+ workers.
94
+
95
+ Edit this to your needs and place it in /etc/init/workers:
96
+
97
+ start on runlevel [2345]
98
+ stop on runlevel [06]
99
+ chdir /var/www/myapp/current
100
+ env RAILS_ENV=production
101
+ exec script/workers
102
+ respawn
103
+
104
+ After reading this, Upstart to make sure your workers are always up and running.
105
+ It's awesome like that.
106
+
107
+ To start, stop, check status and reload:
108
+
109
+ $ start workers
110
+ $ stop workers
111
+ $ status workers
112
+ $ reload workers
113
+
114
+ You need to be root to start/stop the workers. However, if you change ownership
115
+ of the workers (see fork.user above) you can reload them as that user. You can
116
+ do something like this in your Capfile:
117
+
118
+ namespace :workers do
119
+ task :pause do
120
+ run "status workers | cut -d ' ' -f 4 | xargs kill -USR2"
121
+ end
122
+ task :resume do
123
+ run "status workers | cut -d ' ' -f 4 | xargs kill -CONT"
124
+ end
125
+ task :reload do
126
+ run "reload workers"
127
+ end
128
+ end
129
+ after "deploy:update_code", "workers:reload"
130
+
131
+ Because of the way Upstart works, there is no need for PID file or running as
132
+ daemon. Yay for sane process supervisors! When you reload workers,
133
+ Resque::Forker reloads itself (and the application) while keeping the same PID.
134
+
135
+
136
+ == Troubleshooting
137
+
138
+ If you're using Bundler, you might need to run the script using:
139
+
140
+ exec bundle exec script/workers
141
+
142
+ If you're using RVM and Bundler, you might need to create a wrapper and use it:
143
+
144
+ exec run_bundle script/workers
145
+
146
+ The point is, when the script starts it will expect both resque and
147
+ resque-forker must be available for loading (that typically means GEMPATH).
148
+ Depending on your setup, they may be loaded by Bundler, available in the RVM
149
+ gemset, installed as system gems, etc.
150
+
151
+ If you're hitting a wall, remember that any settings and aliases that you have
152
+ in .bashrc (RVM, for example, or the path to bundle) are not sourced by Upstart,
153
+ so commands that "just work" when you run from the console will fail.
154
+
155
+ What you can do to troubleshoot this situation is run as root in a new shell
156
+ that doesn't have your regular account settings:
157
+
158
+ $ env -i sudo /bin/bash --norc --noprofile
159
+
160
+
161
+ == Credits
162
+
163
+ Copyright (c) 2010 Flowtown, Inc.
data/Rakefile ADDED
@@ -0,0 +1,22 @@
1
+ spec = Gem::Specification.load(File.expand_path("resque-forker.gemspec", File.dirname(__FILE__)))
2
+
3
+ desc "Build the Gem"
4
+ task :build do
5
+ sh "gem build #{spec.name}.gemspec"
6
+ end
7
+
8
+ desc "Install #{spec.name} locally"
9
+ task :install=>:build do
10
+ sudo = "sudo" unless File.writable?( Gem::ConfigMap[:bindir])
11
+ sh "#{sudo} gem install #{spec.name}-#{spec.version}.gem"
12
+ end
13
+
14
+ desc "Push new release to gemcutter and git tag"
15
+ task :push=>["build"] do
16
+ sh "git push"
17
+ puts "Tagging version #{spec.version} .."
18
+ sh "git tag v#{spec.version}"
19
+ sh "git push --tag"
20
+ puts "Building and pushing gem .."
21
+ sh "gem push #{spec.name}-#{spec.version}.gem"
22
+ end
@@ -0,0 +1,232 @@
1
+ require "logger"
2
+ require "resque"
3
+
4
+ module Resque
5
+ # Loading Rails, the application and all its dependencies takes significant time
6
+ # and eats up memory. We keep startup time manageable by loading the application
7
+ # once and forking the worker processes. When using REE, this will also keep
8
+ # memory usage low. Note that we can't reuse connections and file handles in
9
+ # child processes, so no saving on opening database connections, etc.
10
+ #
11
+ # To use this library, wrap your setup and teardown blocks:
12
+ # Resque.setup do |forker|
13
+ # require File.dirname(__FILE__) + "/../config/environment"
14
+ # ActiveRecord::Base.connection.disconnect!
15
+ # forker.logger = Rails.logger
16
+ # forker.user "nobody", "nobody"
17
+ # end
18
+ # Resque.after_fork do
19
+ # ActiveRecord::Base.establish_connection
20
+ # end
21
+ #
22
+ # Most libraries cannot share connections between child processes, you want to
23
+ # close these in the parent process (during setup) and reopen connections for
24
+ # each worker when it needs it to process a job (during after_work). This
25
+ # example shows how to do that for ActiveRecord, you will need to do the same
26
+ # for other libraries, e.g. MongoMapper, Vanity.
27
+ #
28
+ # All the forking action is handled by a single call:
29
+ # # Three workers, processing all queues
30
+ # Resque.fork! ["*"] * 3
31
+ #
32
+ # The workload is specified as an array of lists of queues, that way you can
33
+ # decide how many workers to fork (length of the array) and give each worker
34
+ # different set of queues to work with. For example, to have four workers
35
+ # processing import queue, and only two of these also processing export queue:
36
+ #
37
+ # Resque.fork! ["import,export", "import"] * 2
38
+ #
39
+ # Once the process is up and running, you control it by sending signals:
40
+ # - kill -QUIT -- Quit gracefully
41
+ # - kill -TERM -- Terminate immediately
42
+ # - kill -USR2 -- Suspend all workers (e.g when running rake db:migrate)
43
+ # - kill -CONT -- Resume suspended workers
44
+ # - kill -HUP -- Shutdown and restart
45
+ #
46
+ # The HUP signal will wait for all existing jobs to complete, run the teardown
47
+ # block, and reload the script with the same environment and arguments. It will
48
+ # reload the application, the libraries, and of course any configuration changes
49
+ # in this script (e.g. changes to the workload).
50
+ #
51
+ # The reloaded process keeps the same PID so you can use it with upstart:
52
+ # reload workers
53
+ # The upstart script could look something like this:
54
+ # start on runlevel [2345]
55
+ # stop on runlevel [06]
56
+ # chdir /var/app/current
57
+ # env RAILS_ENV=production
58
+ # exec script/workers
59
+ # respawn
60
+ class Forker
61
+ def initialize(options = nil)
62
+ @options = options || {}
63
+ @logger = @options[:logger] || Logger.new($stderr)
64
+ @workload = ["*"]
65
+ @children = []
66
+ begin
67
+ require "system_timer"
68
+ @timeout = SystemTimer.method(:timeout_after)
69
+ rescue NameError, LoadError
70
+ require "timeout"
71
+ @timeout = method(:timeout)
72
+ end
73
+ end
74
+
75
+ # Workload is an array of queue sets, one entry per workers (so four entries
76
+ # if you want four workers). Each entry is comma-separated queue names.
77
+ attr_accessor :workload
78
+
79
+ # Defaults to stderr, but you may want to point this at Rails logger.
80
+ attr_accessor :logger
81
+
82
+ # Run and never return.
83
+ def run
84
+ @logger.info "** Running as #{Process.pid}"
85
+ setup_signals
86
+ if setup = Resque.setup
87
+ @logger.info "** Loading application ..."
88
+ setup.call self
89
+ end
90
+ reap_children
91
+ @logger.info "** Forking workers"
92
+ enable_gc_optimizations
93
+ # Serious forking action.
94
+ @workload.each do |queues|
95
+ @children << fork { run_worker queues }
96
+ end
97
+ rescue
98
+ @logger.error "** Failed to load application: #{$!.message}"
99
+ @logger.error $!.backtrace.join("\n")
100
+ ensure
101
+ # Sleep forever.
102
+ sleep 5 while true
103
+ end
104
+
105
+ # Change ownership of this process.
106
+ def user(user, group)
107
+ uid = Etc.getpwnam(user).uid
108
+ gid = Etc.getgrnam(group).gid
109
+ if Process.euid != uid || Process.egid != gid
110
+ Process.initgroups user, gid
111
+ Process::GID.change_privilege gid
112
+ Process::UID.change_privilege uid
113
+ end
114
+ end
115
+
116
+ protected
117
+
118
+ # Setup signal handlers.
119
+ def setup_signals
120
+ # Stop gracefully
121
+ trap :QUIT do
122
+ stop
123
+ exit
124
+ end
125
+ # Pause/continue processing
126
+ trap(:USR1) { Process.kill :USR1, *@children }
127
+ trap(:USR2) { Process.kill :USR2, *@children }
128
+ trap(:CONT) { Process.kill :CONT, *@children }
129
+ # Reincarnate. Stop children, and reload binary (application and all)
130
+ # while keeping same PID.
131
+ trap :HUP do
132
+ @logger.info "** Reincarnating ..."
133
+ stop
134
+ exec $0, *ARGV
135
+ end
136
+ # Terminate quickly
137
+ trap(:TERM) { shutdown! }
138
+ trap(:INT) { shutdown! }
139
+ end
140
+
141
+ # Enables GC Optimizations if you're running REE.
142
+ # http://www.rubyenterpriseedition.com/faq.html#adapt_apps_for_cow
143
+ def enable_gc_optimizations
144
+ if GC.respond_to?(:copy_on_write_friendly=)
145
+ GC.copy_on_write_friendly = true
146
+ end
147
+ end
148
+
149
+ # Run and never return.
150
+ def run_worker(queues)
151
+ worker = Resque::Worker.new(*queues.split(","))
152
+ worker.verbose = $VERBOSE
153
+ worker.very_verbose = $DEBUG
154
+ worker.work(@options[:interval] || 5) # interval, will block
155
+ end
156
+
157
+ # Stop child processes and run any teardown action.
158
+ def stop(gracefully = true)
159
+ @logger.info "** Quitting ..."
160
+ @children.each do |pid|
161
+ begin
162
+ Process.kill gracefully ? :QUIT : :TERM, pid
163
+ sleep 0.1
164
+ rescue Errno::ESRCH
165
+ end
166
+ end
167
+ reap_children
168
+ if teardown = Resque.teardown
169
+ @timeout.call @options[:terminate] || 5 do
170
+ begin
171
+ teardown.call self
172
+ rescue Exception
173
+ end
174
+ end
175
+ end
176
+ @logger.info "** Good night"
177
+ end
178
+
179
+ # Eat up zombies.
180
+ def reap_children
181
+ loop do
182
+ wpid, status = Process.waitpid2(0, Process::WNOHANG)
183
+ wpid or break
184
+ end
185
+ @children.clear
186
+ rescue Errno::ECHILD
187
+ end
188
+
189
+ # When stop is not good enough.
190
+ def shutdown!
191
+ @logger.info "** Terminating"
192
+ stop false
193
+ exit!
194
+ end
195
+
196
+ end
197
+
198
+
199
+ # Specify what needs to be done to setup the application. This is the place
200
+ # to load Rails or do anything expensive. For example:
201
+ # Resque.setup do
202
+ # require File.dirname(__FILE__) + "/../config/environment"
203
+ # ActiveRecord.disconnect
204
+ # end
205
+ def setup(&block)
206
+ block ? (@setup = block) : @setup
207
+ end
208
+
209
+ # Do this before exiting or before reloading.
210
+ def teardown(&block)
211
+ block ? (@teardown = block) : @teardown
212
+ end
213
+
214
+ # Forks workers to take care of the workload.
215
+ #
216
+ # The workload is an array of queue names, one entry for each worker. For
217
+ # example, if you want to run four workers processing all queues.
218
+ # Resque.fork! ["*"] * 4
219
+ # If you want four workers, all of which will be processing imports, but
220
+ # only two processing exports:
221
+ # Resque.fork! ["import", "import,export"] * 2
222
+ #
223
+ # Options are:
224
+ # - :logger -- Which Logger to use (default logger to stdout)
225
+ # - :interval -- Processing interval (default to 5 seconds)
226
+ # - :terminate -- Timeout running teardown block (default to 5 seconds)
227
+ def fork!(workload = nil, options = nil)
228
+ forker = Resque::Forker.new(options)
229
+ forker.workload = workload if workload
230
+ forker.run
231
+ end
232
+ end
@@ -0,0 +1,19 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = "resque-forker"
3
+ spec.version = "1.0.beta"
4
+ spec.author = "Assaf Arkin"
5
+ spec.email = "assaf@labnotes.org"
6
+ spec.homepage = "http://github.com/assaf/resque-forker"
7
+ spec.summary = "Super awesome forking action for Resque workers"
8
+ spec.post_install_message = ""
9
+
10
+ spec.files = Dir["{lib,script}/**/*", "CHANGELOG", "MIT-LICENSE", "README.rdoc", "Rakefile", "resque-forker.gemspec"]
11
+
12
+ spec.has_rdoc = true
13
+ spec.extra_rdoc_files = "README.rdoc", "CHANGELOG"
14
+ spec.rdoc_options = "--title", "Resque Forker #{spec.version}", "--main", "README.rdoc",
15
+ "--webcvs", "http://github.com/assaf/#{spec.name}"
16
+
17
+ spec.required_ruby_version = '>= 1.8.7'
18
+ spec.add_dependency "resque"
19
+ end
data/script/workers ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+ require "resque/forker"
3
+
4
+ # Load the application.
5
+ Resque.setup do |forker|
6
+ require File.dirname(__FILE__) + "/../config/environment"
7
+ ActiveRecord::Base.connection.disconnect!
8
+ if Rails.env.production?
9
+ forker.logger = Rails.logger
10
+ forker.workload = ["*"] * 4 # 4 workers on all queues
11
+ forker.user "www-data", "www-data" # don't run as root
12
+ end
13
+ end
14
+ # Stuff to do after forking a worker.
15
+ Resque.after_fork do
16
+ ActiveRecord::Base.establish_connection
17
+ end
18
+ Resque.fork!
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: resque-forker
3
+ version: !ruby/object:Gem::Version
4
+ hash: 31098137
5
+ prerelease: true
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - beta
10
+ version: 1.0.beta
11
+ platform: ruby
12
+ authors:
13
+ - Assaf Arkin
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-07-30 00:00:00 -07:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: resque
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ description:
36
+ email: assaf@labnotes.org
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - README.rdoc
43
+ - CHANGELOG
44
+ files:
45
+ - lib/resque/forker.rb
46
+ - script/workers
47
+ - CHANGELOG
48
+ - MIT-LICENSE
49
+ - README.rdoc
50
+ - Rakefile
51
+ - resque-forker.gemspec
52
+ has_rdoc: true
53
+ homepage: http://github.com/assaf/resque-forker
54
+ licenses: []
55
+
56
+ post_install_message: ""
57
+ rdoc_options:
58
+ - --title
59
+ - Resque Forker 1.0.beta
60
+ - --main
61
+ - README.rdoc
62
+ - --webcvs
63
+ - http://github.com/assaf/resque-forker
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ hash: 57
72
+ segments:
73
+ - 1
74
+ - 8
75
+ - 7
76
+ version: 1.8.7
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
+ none: false
79
+ requirements:
80
+ - - ">"
81
+ - !ruby/object:Gem::Version
82
+ hash: 25
83
+ segments:
84
+ - 1
85
+ - 3
86
+ - 1
87
+ version: 1.3.1
88
+ requirements: []
89
+
90
+ rubyforge_project:
91
+ rubygems_version: 1.3.7
92
+ signing_key:
93
+ specification_version: 3
94
+ summary: Super awesome forking action for Resque workers
95
+ test_files: []
96
+