resque-forker 1.0.beta

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/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
+