preforker 0.1.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/.document +5 -0
- data/.gitignore +24 -0
- data/LICENSE +20 -0
- data/README.rdoc +77 -0
- data/Rakefile +46 -0
- data/VERSION +1 -0
- data/examples/amqp.rb +24 -0
- data/examples/amqp_client.rb +11 -0
- data/examples/ping_pong.rb +21 -0
- data/lib/preforker/pid_manager.rb +59 -0
- data/lib/preforker/signal_processor.rb +87 -0
- data/lib/preforker/util.rb +42 -0
- data/lib/preforker/worker.rb +86 -0
- data/lib/preforker.rb +189 -0
- data/preforker.gemspec +74 -0
- data/spec/integration/logging_spec.rb +67 -0
- data/spec/integration/timeout_spec.rb +32 -0
- data/spec/integration/worker_control_spec.rb +96 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/support/integration.rb +52 -0
- metadata +117 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Daniel Cadenas
|
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.
|
data/README.rdoc
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
= preforker
|
2
|
+
|
3
|
+
A gem to easily create prefork servers.
|
4
|
+
|
5
|
+
== Example
|
6
|
+
|
7
|
+
Let's see an example using the Mac 'say' command.
|
8
|
+
|
9
|
+
require 'preforker'
|
10
|
+
|
11
|
+
#you can open some socket here or reserve any other resource you want to share with your workers, this is master space
|
12
|
+
Preforker.new(:timeout => 10) do |master|
|
13
|
+
#this block is only run from each worker (10 by default)
|
14
|
+
|
15
|
+
#here you should write the code that is needed to be ran each time a fork is created, initializations, etc.
|
16
|
+
`say hi`
|
17
|
+
|
18
|
+
#here you could IO.select a socket, run an EventMachine service (see example), or just run worker loop
|
19
|
+
#you need to ask master if it wants you alive periodically or else it will kill you after the timeout elapses. Respect your master!
|
20
|
+
while master.wants_me_alive? do
|
21
|
+
sleep 1
|
22
|
+
`say ping pong`
|
23
|
+
end
|
24
|
+
|
25
|
+
#here we can do whatever we want when exiting gracefully
|
26
|
+
`say bye`
|
27
|
+
end.start
|
28
|
+
|
29
|
+
To kill the server just run:
|
30
|
+
|
31
|
+
kill -s QUIT `cat preforker.pid`
|
32
|
+
|
33
|
+
See the examples directory and the specs for more examples.
|
34
|
+
|
35
|
+
== Configuration options
|
36
|
+
|
37
|
+
[:timeout] The timeout in seconds, 5 by default. If a worker takes more than this it will be killed and respawned.
|
38
|
+
[:workers] Number of workers, 10 by default.
|
39
|
+
[:stdout_path] Path to redirect stdout to. By default it's /dev/null
|
40
|
+
[:stderr_path] Path to redirect stderr to. By default it's /dev/null
|
41
|
+
[:app_name] The app name, 'preforker' by default. Used for some ps message, log messages messages and pid file name.
|
42
|
+
[:pid_path] The path to the pid file for this server. By default it's './preforker.pid'.
|
43
|
+
[:logger] This is Logger.new('./preforker.log') by default
|
44
|
+
|
45
|
+
== Signals
|
46
|
+
|
47
|
+
You can send some signals to master to control the way it handles the workers lifetime.
|
48
|
+
|
49
|
+
[WINCH] Gracefully kill all workers but keep master alive
|
50
|
+
[TTIN] Increase number of workers
|
51
|
+
[TTOU] Decrease number of workers
|
52
|
+
[QUIT] Kill workers and master in a graceful way
|
53
|
+
[TERM, INT] Kill workers and master immediately
|
54
|
+
|
55
|
+
== Acknowledgments
|
56
|
+
|
57
|
+
Most of the preforking operating system tricks come from Unicorn. Check out its source code for a hands on tutorial on Unix internals!
|
58
|
+
|
59
|
+
== To do list
|
60
|
+
|
61
|
+
* More tests
|
62
|
+
* Log rotation through the USR1 signal
|
63
|
+
* Have something like min_spare_workers, max_workers
|
64
|
+
|
65
|
+
== Note on Patches/Pull Requests
|
66
|
+
|
67
|
+
* Fork the project.
|
68
|
+
* Make your feature addition or bug fix.
|
69
|
+
* Add tests for it. This is important so I don't break it in a
|
70
|
+
future version unintentionally.
|
71
|
+
* Commit, do not mess with rakefile, version, or history.
|
72
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
73
|
+
* Send me a pull request. Bonus points for topic branches.
|
74
|
+
|
75
|
+
== Copyright
|
76
|
+
|
77
|
+
Copyright (c) 2010 Daniel Cadenas. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "preforker"
|
8
|
+
gem.summary = %Q{A gem to easily create prefork servers.}
|
9
|
+
gem.description = %Q{A gem to easily create prefork servers.}
|
10
|
+
gem.email = "dcadenas@gmail.com"
|
11
|
+
gem.homepage = "http://github.com/dcadenas/preforker"
|
12
|
+
gem.authors = ["Daniel Cadenas"]
|
13
|
+
gem.add_development_dependency "rspec", ">= 1.3.0"
|
14
|
+
gem.add_development_dependency "filetesthelper", ">= 0.10.1"
|
15
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
16
|
+
end
|
17
|
+
Jeweler::GemcutterTasks.new
|
18
|
+
rescue LoadError
|
19
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
20
|
+
end
|
21
|
+
|
22
|
+
require 'spec/rake/spectask'
|
23
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
24
|
+
spec.libs << 'lib' << 'spec'
|
25
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
26
|
+
end
|
27
|
+
|
28
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
29
|
+
spec.libs << 'lib' << 'spec'
|
30
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
31
|
+
spec.rcov = true
|
32
|
+
end
|
33
|
+
|
34
|
+
task :spec => :check_dependencies
|
35
|
+
|
36
|
+
task :default => :spec
|
37
|
+
|
38
|
+
require 'rake/rdoctask'
|
39
|
+
Rake::RDocTask.new do |rdoc|
|
40
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
41
|
+
|
42
|
+
rdoc.rdoc_dir = 'rdoc'
|
43
|
+
rdoc.title = "preforker #{version}"
|
44
|
+
rdoc.rdoc_files.include('README*')
|
45
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
46
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/examples/amqp.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'preforker'
|
3
|
+
require 'mq'
|
4
|
+
|
5
|
+
#be sure to run your AMQP server first
|
6
|
+
|
7
|
+
Preforker.new(:timeout => 5, :workers => 4, :app_name => "Amqp example") do |master|
|
8
|
+
AMQP.start(:host => 'dcadenas-laptop.local') do
|
9
|
+
EM.add_periodic_timer(1) do
|
10
|
+
AMQP.stop{ EM.stop } unless master.wants_me_alive?
|
11
|
+
end
|
12
|
+
|
13
|
+
MQ.prefetch(1)
|
14
|
+
channel = MQ.new
|
15
|
+
test_exchange = channel.fanout('test_exchange')
|
16
|
+
channel.queue('test_queue').bind(test_exchange).subscribe(:ack => true) do |h, msg|
|
17
|
+
$stdout.puts "#{$$} received #{msg.inspect}"
|
18
|
+
h.ack
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end.start
|
22
|
+
|
23
|
+
#run examples/amqp_client.rb to see the output
|
24
|
+
#kill server with: kill -s TERM `cat 'amqp example.pid'`
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bunny'
|
3
|
+
|
4
|
+
Bunny.run(:host => 'dcadenas-laptop.local', :port => 5672, :user => 'guest', :password => 'guest') do |b|
|
5
|
+
e = b.exchange('test_exchange', :type => :fanout)
|
6
|
+
|
7
|
+
25.times do |i|
|
8
|
+
e.publish("number #{i}")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'preforker'
|
2
|
+
|
3
|
+
#you can open some socket here or reserve any other resource you want to share with your workers, this is master space
|
4
|
+
Preforker.new(:timeout => 10) do |master|
|
5
|
+
#this block is only run from each worker (10 by default)
|
6
|
+
|
7
|
+
#here you should write the code that is needed to be ran each time a fork is created, initializations, etc.
|
8
|
+
`say hi`
|
9
|
+
|
10
|
+
#here you could IO.select a socket, run an EventMachine service (see example), or just run worker loop
|
11
|
+
#you need to ask master if it wants you alive periodically or else it will kill you after the timeout elapses. Respect your master!
|
12
|
+
while master.wants_me_alive? do
|
13
|
+
sleep 1
|
14
|
+
`say ping pong`
|
15
|
+
end
|
16
|
+
|
17
|
+
#here we can do whatever we want when exiting gracefully
|
18
|
+
`say bye`
|
19
|
+
end.start
|
20
|
+
|
21
|
+
#to kill the server just run: kill -s QUIT `cat preforker.pid`
|
@@ -0,0 +1,59 @@
|
|
1
|
+
class Preforker
|
2
|
+
class PidManager
|
3
|
+
attr_reader :pid_path, :pid
|
4
|
+
|
5
|
+
def initialize(pid_path)
|
6
|
+
set_pid_path(pid_path, $$)
|
7
|
+
end
|
8
|
+
|
9
|
+
def set_pid_path(new_pid_path, new_pid)
|
10
|
+
if new_pid_path
|
11
|
+
if read_pid = read_path_pid(new_pid_path)
|
12
|
+
return new_pid_path if @pid_path && new_pid_path == @pid_path && read_pid == @pid
|
13
|
+
raise ArgumentError, "#{$$} Already running on PID:#{read_pid} (or #{new_pid_path} is stale)"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
unlink_pid_safe(@pid_path) if @pid_path
|
17
|
+
|
18
|
+
if new_pid_path
|
19
|
+
fp = begin
|
20
|
+
tmp = "#{File.dirname(new_pid_path)}/#{rand}.#{pid}"
|
21
|
+
File.open(tmp, File::RDWR|File::CREAT|File::EXCL, 0644)
|
22
|
+
rescue Errno::EEXIST
|
23
|
+
retry
|
24
|
+
end
|
25
|
+
fp.syswrite("#{new_pid}\n")
|
26
|
+
File.rename(fp.path, new_pid_path)
|
27
|
+
fp.close
|
28
|
+
end
|
29
|
+
|
30
|
+
@pid = new_pid
|
31
|
+
@pid_path = new_pid_path
|
32
|
+
end
|
33
|
+
|
34
|
+
# unlinks a PID file at given +path+ if it contains the current PID
|
35
|
+
# still potentially racy without locking the directory (which is
|
36
|
+
# non-portable and may interact badly with other programs), but the
|
37
|
+
# window for hitting the race condition is small
|
38
|
+
def unlink
|
39
|
+
File.unlink(@pid_path) if @pid_path && File.read(@pid_path).to_i == @pid
|
40
|
+
rescue
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# returns a PID if a given path contains a non-stale PID file,
|
46
|
+
# nil otherwise.
|
47
|
+
def read_path_pid(path)
|
48
|
+
pid = File.read(path).to_i
|
49
|
+
return nil if pid <= 0
|
50
|
+
begin
|
51
|
+
Process.kill(0, pid)
|
52
|
+
pid
|
53
|
+
rescue Errno::ESRCH
|
54
|
+
# don't unlink stale pid files, racy without non-portable locking...
|
55
|
+
end
|
56
|
+
rescue Errno::ENOENT
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
class Preforker
|
2
|
+
class SignalProcessor
|
3
|
+
attr_reader :interesting_signals, :signal_queue
|
4
|
+
def initialize(master)
|
5
|
+
@read_pipe, @write_pipe = IO.pipe
|
6
|
+
@master = master
|
7
|
+
@signal_queue = []
|
8
|
+
@interesting_signals = [:WINCH, :QUIT, :INT, :TERM, :USR1, :USR2, :HUP, :TTIN, :TTOU]
|
9
|
+
@interesting_signals.each { |sig| trap_deferred(sig) }
|
10
|
+
end
|
11
|
+
|
12
|
+
def trap_deferred(signal)
|
13
|
+
trap(signal) do
|
14
|
+
if @signal_queue.size < 5
|
15
|
+
@signal_queue << signal
|
16
|
+
wake_up_master
|
17
|
+
else
|
18
|
+
@master.logger.error "ignoring SIG#{signal}, queue=#{@signal_queue.inspect}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def start_signal_loop
|
24
|
+
last_check = Time.now
|
25
|
+
begin
|
26
|
+
loop do
|
27
|
+
@master.reap_all_workers
|
28
|
+
case @signal_queue.shift
|
29
|
+
when nil
|
30
|
+
# avoid murdering workers after our master process (or the
|
31
|
+
# machine) comes out of suspend/hibernation
|
32
|
+
@master.murder_lazy_workers if (last_check + @master.timeout) >= (last_check = Time.now)
|
33
|
+
@master.maintain_worker_count
|
34
|
+
sleep_master
|
35
|
+
when :WINCH
|
36
|
+
@master.logger.info "Gracefully stopping all workers"
|
37
|
+
@master.number_of_workers = 0
|
38
|
+
when :TTIN
|
39
|
+
@master.number_of_workers += 1
|
40
|
+
when :TTOU
|
41
|
+
@master.number_of_workers -= 1 if @master.number_of_workers > 0
|
42
|
+
when :QUIT # graceful shutdown
|
43
|
+
break
|
44
|
+
when :TERM, :INT # immediate shutdown
|
45
|
+
@master.stop(false)
|
46
|
+
break
|
47
|
+
end
|
48
|
+
end
|
49
|
+
rescue Errno::EINTR
|
50
|
+
retry
|
51
|
+
rescue => e
|
52
|
+
@master.logger.error "Unhandled master loop exception #{e.inspect}.\n#{e.backtrace.join("\n")}"
|
53
|
+
retry
|
54
|
+
ensure
|
55
|
+
@master.logger.info "Master quitting"
|
56
|
+
@master.quit
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# wait for a signal hander to wake us up and then consume the pipe
|
61
|
+
# Wake up every second anyways to run murder_lazy_workers
|
62
|
+
def sleep_master
|
63
|
+
begin
|
64
|
+
maximum_sleep = @master.timeout > 1 ? 1 : @master.timeout / 2
|
65
|
+
ready = IO.select([@read_pipe], nil, nil, maximum_sleep) or return
|
66
|
+
ready.first && ready.first.first or return
|
67
|
+
chunk_size = 16 * 1024
|
68
|
+
loop { @read_pipe.read_nonblock(chunk_size) }
|
69
|
+
rescue Errno::EAGAIN, Errno::EINTR
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def wake_up_master
|
74
|
+
begin
|
75
|
+
@write_pipe.write_nonblock('.') # wakeup master process from select
|
76
|
+
rescue Errno::EAGAIN, Errno::EINTR
|
77
|
+
# pipe is full, master should wake up anyways
|
78
|
+
retry
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def reset
|
83
|
+
@interesting_signals.each { |sig| trap(sig, nil) }
|
84
|
+
@signal_queue.clear
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
|
3
|
+
require 'fcntl'
|
4
|
+
require 'tmpdir'
|
5
|
+
|
6
|
+
class Preforker
|
7
|
+
class TmpIO < ::File
|
8
|
+
|
9
|
+
# for easier env["rack.input"] compatibility
|
10
|
+
def size
|
11
|
+
# flush if sync
|
12
|
+
stat.size
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class Util
|
17
|
+
def self.is_log?(fp)
|
18
|
+
append_flags = File::WRONLY | File::APPEND
|
19
|
+
|
20
|
+
! fp.closed? &&
|
21
|
+
fp.sync &&
|
22
|
+
fp.path[0] == ?/ &&
|
23
|
+
(fp.fcntl(Fcntl::F_GETFL) & append_flags) == append_flags
|
24
|
+
end
|
25
|
+
|
26
|
+
# creates and returns a new File object. The File is unlinked
|
27
|
+
# immediately, switched to binary mode, and userspace output
|
28
|
+
# buffering is disabled
|
29
|
+
def self.tmpio
|
30
|
+
fp = begin
|
31
|
+
TmpIO.open("#{Dir::tmpdir}/#{rand}",
|
32
|
+
File::RDWR|File::CREAT|File::EXCL, 0600)
|
33
|
+
rescue Errno::EEXIST
|
34
|
+
retry
|
35
|
+
end
|
36
|
+
File.unlink(fp.path)
|
37
|
+
fp.binmode
|
38
|
+
fp.sync = true
|
39
|
+
fp
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
#all this code is called inside a forked block
|
2
|
+
require 'preforker/util'
|
3
|
+
|
4
|
+
class Preforker
|
5
|
+
class Worker
|
6
|
+
class WorkerAPI
|
7
|
+
attr_reader :logger
|
8
|
+
|
9
|
+
def initialize(worker, master)
|
10
|
+
@master = master
|
11
|
+
@logger = master.logger
|
12
|
+
@worker = worker
|
13
|
+
@alive = @worker.tmp
|
14
|
+
@last_file_bit = 0
|
15
|
+
end
|
16
|
+
|
17
|
+
def wants_me_alive?
|
18
|
+
if @alive
|
19
|
+
#we are alive, let's be thankful and tell master we are alive and happy
|
20
|
+
@last_file_bit = 0 == @last_file_bit ? 1 : 0
|
21
|
+
@alive.chmod(@last_file_bit)
|
22
|
+
end
|
23
|
+
@alive
|
24
|
+
end
|
25
|
+
|
26
|
+
def kill
|
27
|
+
if @alive
|
28
|
+
@worker.log "Exiting gracefully"
|
29
|
+
@alive = false
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
attr_reader :tmp
|
35
|
+
attr_accessor :pid
|
36
|
+
|
37
|
+
def initialize(worker_block, master)
|
38
|
+
@worker_block = worker_block
|
39
|
+
@master = master
|
40
|
+
@tmp = Util.tmpio
|
41
|
+
end
|
42
|
+
|
43
|
+
def init_self_pipe!
|
44
|
+
@read_pipe, @write_pipe = IO.pipe
|
45
|
+
[@read_pipe, @write_pipe].each { |io| io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) }
|
46
|
+
end
|
47
|
+
|
48
|
+
def work
|
49
|
+
log "Created"
|
50
|
+
init
|
51
|
+
|
52
|
+
@worker_block.call(@master_api) if @master_api.wants_me_alive?
|
53
|
+
end
|
54
|
+
|
55
|
+
def init
|
56
|
+
init_self_pipe!
|
57
|
+
handle_signals
|
58
|
+
tmp.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
|
59
|
+
@master_api = WorkerAPI.new(self, @master)
|
60
|
+
end
|
61
|
+
|
62
|
+
def handle_signals
|
63
|
+
[:TERM, :INT].each do |sig|
|
64
|
+
trap(sig) { exit!(0) }
|
65
|
+
end
|
66
|
+
|
67
|
+
[:EXIT, :QUIT].each do |sig|
|
68
|
+
trap(sig){ @master_api.kill }
|
69
|
+
end
|
70
|
+
|
71
|
+
#what usr1 does
|
72
|
+
trap(:USR1) { @read_pipe.close rescue nil }
|
73
|
+
trap(:CHLD, 'DEFAULT')
|
74
|
+
end
|
75
|
+
|
76
|
+
def proc_message(message)
|
77
|
+
full_message = ["#{@master.app_name} Child #{$$}", message].join(": ")
|
78
|
+
$0 = full_message
|
79
|
+
end
|
80
|
+
|
81
|
+
def log(message)
|
82
|
+
full_message = proc_message(message)
|
83
|
+
@master.logger.info full_message
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/preforker.rb
ADDED
@@ -0,0 +1,189 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'fcntl'
|
3
|
+
require 'preforker/pid_manager'
|
4
|
+
require 'preforker/worker'
|
5
|
+
require 'preforker/signal_processor'
|
6
|
+
require 'logger'
|
7
|
+
|
8
|
+
class Preforker
|
9
|
+
attr_reader :timeout, :app_name, :logger
|
10
|
+
attr_accessor :number_of_workers
|
11
|
+
|
12
|
+
def initialize(options = {}, &worker_block)
|
13
|
+
@app_name = options[:app_name] || "Preforker"
|
14
|
+
default_log_file = "#{@app_name.downcase}.log"
|
15
|
+
@options = {
|
16
|
+
:timeout => 5,
|
17
|
+
:workers => 10,
|
18
|
+
:app_name => "Preforker",
|
19
|
+
:stderr_path => default_log_file,
|
20
|
+
:stderr_path => default_log_file
|
21
|
+
}.merge(options)
|
22
|
+
|
23
|
+
@logger = @options[:logger] || Logger.new(default_log_file)
|
24
|
+
|
25
|
+
@timeout = @options[:timeout]
|
26
|
+
@number_of_workers = @options[:workers]
|
27
|
+
@worker_block = worker_block || lambda {}
|
28
|
+
|
29
|
+
@workers = {}
|
30
|
+
$0 = "#@app_name Master"
|
31
|
+
end
|
32
|
+
|
33
|
+
def start
|
34
|
+
launch do |ready_write|
|
35
|
+
$stdin.reopen("/dev/null")
|
36
|
+
set_stdout_path(@options[:stdout_path])
|
37
|
+
set_stderr_path(@options[:stderr_path])
|
38
|
+
|
39
|
+
logger.info "Master started"
|
40
|
+
|
41
|
+
pid_path = @options[:pid_path] || "./#{@app_name.downcase}.pid"
|
42
|
+
@pid_manager = PidManager.new(pid_path)
|
43
|
+
@signal_processor = SignalProcessor.new(self)
|
44
|
+
|
45
|
+
spawn_missing_workers do
|
46
|
+
ready_write.close
|
47
|
+
end
|
48
|
+
|
49
|
+
#tell parent we are ready
|
50
|
+
ready_write.syswrite($$.to_s)
|
51
|
+
ready_write.close rescue nil
|
52
|
+
|
53
|
+
@signal_processor.start_signal_loop
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def launch(&block)
|
58
|
+
puts "Starting server"
|
59
|
+
|
60
|
+
ready_read, ready_write = IO.pipe
|
61
|
+
[ready_read, ready_write].each { |io| io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) }
|
62
|
+
|
63
|
+
fork do
|
64
|
+
ready_read.close
|
65
|
+
|
66
|
+
Process.setsid
|
67
|
+
fork do
|
68
|
+
block.call(ready_write)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
ready_write.close
|
73
|
+
master_pid = (ready_read.readpartial(16) rescue nil).to_i
|
74
|
+
ready_read.close
|
75
|
+
if master_pid <= 1
|
76
|
+
warn "Master failed to start, check stderr log for details"
|
77
|
+
exit!(1)
|
78
|
+
else
|
79
|
+
puts "Server started successfuly"
|
80
|
+
exit(0)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def close_resources_worker_wont_use
|
85
|
+
@signal_processor.reset
|
86
|
+
@workers.values.each { |other| other.tmp.close rescue nil }
|
87
|
+
@workers.clear
|
88
|
+
end
|
89
|
+
|
90
|
+
# Terminates all workers, but does not exit master process
|
91
|
+
def stop(graceful = true)
|
92
|
+
limit = Time.now + @timeout
|
93
|
+
until @workers.empty? || Time.now > limit
|
94
|
+
signal_each_worker(graceful ? :QUIT : :TERM)
|
95
|
+
sleep(0.1)
|
96
|
+
reap_all_workers
|
97
|
+
end
|
98
|
+
signal_each_worker(:KILL)
|
99
|
+
end
|
100
|
+
|
101
|
+
def quit(graceful = true)
|
102
|
+
stop(graceful)
|
103
|
+
@pid_manager.unlink
|
104
|
+
end
|
105
|
+
|
106
|
+
def reap_all_workers
|
107
|
+
begin
|
108
|
+
loop do
|
109
|
+
worker_pid, status = Process.waitpid2(-1, Process::WNOHANG)
|
110
|
+
break unless worker_pid
|
111
|
+
worker = @workers.delete(worker_pid) and worker.tmp.close rescue nil
|
112
|
+
logger.info "reaped #{status.inspect}"
|
113
|
+
end
|
114
|
+
rescue Errno::ECHILD
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def spawn_missing_workers(new_workers_count = @number_of_workers, &init_block)
|
119
|
+
new_workers_count.times do
|
120
|
+
worker = Worker.new(@worker_block, self)
|
121
|
+
worker_pid = fork do
|
122
|
+
close_resources_worker_wont_use
|
123
|
+
init_block.call if init_block
|
124
|
+
worker.work
|
125
|
+
end
|
126
|
+
|
127
|
+
worker.pid = worker_pid
|
128
|
+
@workers[worker_pid] = worker
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def maintain_worker_count
|
133
|
+
number_of_missing_workers = @number_of_workers - @workers.size
|
134
|
+
return if number_of_missing_workers == 0
|
135
|
+
return spawn_missing_workers(number_of_missing_workers) if number_of_missing_workers > 0
|
136
|
+
@workers.values[0..(-number_of_missing_workers - 1)].each do |unneeded_worker|
|
137
|
+
signal_worker(:QUIT, unneeded_worker.pid) rescue nil
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# forcibly terminate all workers that haven't checked in in timeout
|
142
|
+
# seconds. The timeout is implemented using an unlinked File
|
143
|
+
# shared between the parent process and each worker. The worker
|
144
|
+
# runs File#chmod to modify the ctime of the File. If the ctime
|
145
|
+
# is stale for >timeout seconds, then we'll kill the corresponding
|
146
|
+
# worker.
|
147
|
+
def murder_lazy_workers
|
148
|
+
@workers.dup.each_pair do |worker_pid, worker|
|
149
|
+
stat = worker.tmp.stat
|
150
|
+
# skip workers that disable fchmod or have never fchmod-ed
|
151
|
+
next if stat.mode == 0100600
|
152
|
+
next if (diff = (Time.now - stat.ctime)) <= @timeout
|
153
|
+
logger.error "Worker=#{worker_pid} timeout (#{diff}s > #{@timeout}s), killing"
|
154
|
+
signal_worker(:KILL, worker_pid) # take no prisoners for timeout violations
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# delivers a signal to a worker and fails gracefully if the worker
|
159
|
+
# is no longer running.
|
160
|
+
def signal_worker(signal, worker_pid)
|
161
|
+
begin
|
162
|
+
Process.kill(signal, worker_pid)
|
163
|
+
rescue Errno::ESRCH
|
164
|
+
worker = @workers.delete(worker_pid) and worker.tmp.close rescue nil
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# delivers a signal to each worker
|
169
|
+
def signal_each_worker(signal)
|
170
|
+
@workers.keys.each { |worker_pid| signal_worker(signal, worker_pid) }
|
171
|
+
end
|
172
|
+
|
173
|
+
def signal_quit
|
174
|
+
signal_worker(:QUIT, @pid_manager.pid)
|
175
|
+
end
|
176
|
+
|
177
|
+
def set_stdout_path(path)
|
178
|
+
redirect_io($stdout, path)
|
179
|
+
end
|
180
|
+
|
181
|
+
def set_stderr_path(path)
|
182
|
+
redirect_io($stderr, path)
|
183
|
+
end
|
184
|
+
|
185
|
+
def redirect_io(io, path)
|
186
|
+
File.open(path, 'ab') { |fp| io.reopen(fp) } if path
|
187
|
+
io.sync = true
|
188
|
+
end
|
189
|
+
end
|
data/preforker.gemspec
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{preforker}
|
8
|
+
s.version = "0.1.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Daniel Cadenas"]
|
12
|
+
s.date = %q{2010-04-02}
|
13
|
+
s.description = %q{A gem to easily create prefork servers.}
|
14
|
+
s.email = %q{dcadenas@gmail.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
".gitignore",
|
22
|
+
"LICENSE",
|
23
|
+
"README.rdoc",
|
24
|
+
"Rakefile",
|
25
|
+
"VERSION",
|
26
|
+
"examples/amqp.rb",
|
27
|
+
"examples/amqp_client.rb",
|
28
|
+
"examples/ping_pong.rb",
|
29
|
+
"lib/preforker.rb",
|
30
|
+
"lib/preforker/pid_manager.rb",
|
31
|
+
"lib/preforker/signal_processor.rb",
|
32
|
+
"lib/preforker/util.rb",
|
33
|
+
"lib/preforker/worker.rb",
|
34
|
+
"preforker.gemspec",
|
35
|
+
"spec/integration/logging_spec.rb",
|
36
|
+
"spec/integration/timeout_spec.rb",
|
37
|
+
"spec/integration/worker_control_spec.rb",
|
38
|
+
"spec/spec.opts",
|
39
|
+
"spec/spec_helper.rb",
|
40
|
+
"spec/support/integration.rb"
|
41
|
+
]
|
42
|
+
s.homepage = %q{http://github.com/dcadenas/preforker}
|
43
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
44
|
+
s.require_paths = ["lib"]
|
45
|
+
s.rubygems_version = %q{1.3.6}
|
46
|
+
s.summary = %q{A gem to easily create prefork servers.}
|
47
|
+
s.test_files = [
|
48
|
+
"spec/integration/logging_spec.rb",
|
49
|
+
"spec/integration/timeout_spec.rb",
|
50
|
+
"spec/integration/worker_control_spec.rb",
|
51
|
+
"spec/spec_helper.rb",
|
52
|
+
"spec/support/integration.rb",
|
53
|
+
"examples/amqp.rb",
|
54
|
+
"examples/amqp_client.rb",
|
55
|
+
"examples/ping_pong.rb"
|
56
|
+
]
|
57
|
+
|
58
|
+
if s.respond_to? :specification_version then
|
59
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
60
|
+
s.specification_version = 3
|
61
|
+
|
62
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
63
|
+
s.add_development_dependency(%q<rspec>, [">= 1.3.0"])
|
64
|
+
s.add_development_dependency(%q<filetesthelper>, [">= 0.10.1"])
|
65
|
+
else
|
66
|
+
s.add_dependency(%q<rspec>, [">= 1.3.0"])
|
67
|
+
s.add_dependency(%q<filetesthelper>, [">= 0.10.1"])
|
68
|
+
end
|
69
|
+
else
|
70
|
+
s.add_dependency(%q<rspec>, [">= 1.3.0"])
|
71
|
+
s.add_dependency(%q<filetesthelper>, [">= 0.10.1"])
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
include FileTestHelper
|
4
|
+
describe "Preforker" do
|
5
|
+
sandboxed_it "should redirect stdout" do
|
6
|
+
run_preforker <<-CODE
|
7
|
+
Preforker.new(:workers => 1, :stdout_path => 'test.log') do
|
8
|
+
puts "hello"
|
9
|
+
sleep 0.3 while master.wants_me_alive?
|
10
|
+
end.start
|
11
|
+
CODE
|
12
|
+
|
13
|
+
term_server
|
14
|
+
File.read("test.log").should == "hello\n"
|
15
|
+
end
|
16
|
+
|
17
|
+
sandboxed_it "should redirect stdout to the null device" do
|
18
|
+
run_preforker <<-CODE
|
19
|
+
Preforker.new(:workers => 1, :stdout_path => '/dev/null') do
|
20
|
+
puts "hello"
|
21
|
+
sleep 0.3 while master.wants_me_alive?
|
22
|
+
end.start
|
23
|
+
CODE
|
24
|
+
|
25
|
+
term_server
|
26
|
+
File.exists?("test.log").should == false
|
27
|
+
end
|
28
|
+
|
29
|
+
sandboxed_it "should redirect stderr" do
|
30
|
+
run_preforker <<-CODE
|
31
|
+
Preforker.new(:workers => 1, :stderr_path => 'test.log') do |master|
|
32
|
+
warn "hello"
|
33
|
+
sleep 0.3 while master.wants_me_alive?
|
34
|
+
end.start
|
35
|
+
CODE
|
36
|
+
|
37
|
+
term_server
|
38
|
+
File.read("test.log").should == "hello\n"
|
39
|
+
end
|
40
|
+
|
41
|
+
sandboxed_it "should have a default logger file" do
|
42
|
+
run_preforker <<-CODE
|
43
|
+
Preforker.new(:workers => 1) do
|
44
|
+
sleep 0.3 while master.wants_me_alive?
|
45
|
+
end.start
|
46
|
+
CODE
|
47
|
+
|
48
|
+
term_server
|
49
|
+
File.read("preforker.log").should =~ /Logfile created on/
|
50
|
+
end
|
51
|
+
|
52
|
+
sandboxed_it "should be possible to use the same file for logging, stdout and stderr" do
|
53
|
+
run_preforker <<-CODE
|
54
|
+
Preforker.new(:workers => 1, :stdout_path => 'test.log', :stderr_path => 'test.log', :logger => Logger.new('test.log')) do
|
55
|
+
puts "stdout string"
|
56
|
+
warn "stderr string"
|
57
|
+
sleep 0.3 while master.wants_me_alive?
|
58
|
+
end.start
|
59
|
+
CODE
|
60
|
+
|
61
|
+
term_server
|
62
|
+
log = File.read("test.log")
|
63
|
+
log.should =~ /stdout string/
|
64
|
+
log.should =~ /stderr string/
|
65
|
+
log.should =~ /Logfile created on/
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
include FileTestHelper
|
4
|
+
describe "Preforker" do
|
5
|
+
sandboxed_it "should not respawn workers when there's not a timeout" do
|
6
|
+
run_preforker <<-CODE
|
7
|
+
Preforker.new(:timeout => 2, :workers => 1) do |master|
|
8
|
+
sleep 0.1 while master.wants_me_alive?
|
9
|
+
end.start
|
10
|
+
CODE
|
11
|
+
|
12
|
+
sleep 1
|
13
|
+
term_server
|
14
|
+
log = File.read("preforker.log")
|
15
|
+
log.should_not =~ /ERROR.*timeout/
|
16
|
+
log.scan(/Child.*Created/).size.should == 1
|
17
|
+
end
|
18
|
+
|
19
|
+
sandboxed_it "should respawn workers when there's a timeout (master checks once a second max)" do
|
20
|
+
run_preforker <<-CODE
|
21
|
+
Preforker.new(:timeout => 1, :workers => 1) do
|
22
|
+
sleep 1000
|
23
|
+
end.start
|
24
|
+
CODE
|
25
|
+
|
26
|
+
sleep 1
|
27
|
+
term_server
|
28
|
+
log = File.read("preforker.log")
|
29
|
+
log.should =~ /ERROR.*timeout/
|
30
|
+
log.scan(/Child.*Created/).size.should > 1
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
include FileTestHelper
|
4
|
+
describe "Preforker" do
|
5
|
+
sandboxed_it "should quit gracefully" do
|
6
|
+
run_preforker <<-CODE
|
7
|
+
Preforker.new(:workers => 1) do |master|
|
8
|
+
sleep 0.1 while master.wants_me_alive?
|
9
|
+
|
10
|
+
master.logger.info("Main loop ended. Dying")
|
11
|
+
end.start
|
12
|
+
CODE
|
13
|
+
|
14
|
+
quit_server
|
15
|
+
log = File.read("preforker.log")
|
16
|
+
log.should =~ /Main loop ended. Dying/
|
17
|
+
end
|
18
|
+
|
19
|
+
sandboxed_it "shouldn't quit gracefully on term signal" do
|
20
|
+
run_preforker <<-CODE
|
21
|
+
Preforker.new(:workers => 1) do |master|
|
22
|
+
sleep 0.1 while master.wants_me_alive?
|
23
|
+
|
24
|
+
master.logger.info("Main loop ended. Dying")
|
25
|
+
end.start
|
26
|
+
CODE
|
27
|
+
|
28
|
+
term_server
|
29
|
+
log = File.read("preforker.log")
|
30
|
+
log.should_not =~ /Main loop ended. Dying/
|
31
|
+
end
|
32
|
+
|
33
|
+
sandboxed_it "shouldn't quit gracefully on int signal" do
|
34
|
+
run_preforker <<-CODE
|
35
|
+
Preforker.new(:workers => 1) do |master|
|
36
|
+
sleep 0.1 while master.wants_me_alive?
|
37
|
+
|
38
|
+
master.logger.info("Main loop ended. Dying")
|
39
|
+
end.start
|
40
|
+
CODE
|
41
|
+
|
42
|
+
int_server
|
43
|
+
log = File.read("preforker.log")
|
44
|
+
log.should_not =~ /Main loop ended. Dying/
|
45
|
+
end
|
46
|
+
|
47
|
+
sandboxed_it "should add a worker on ttin" do
|
48
|
+
run_preforker <<-CODE
|
49
|
+
Preforker.new(:workers => 2) do |master|
|
50
|
+
sleep 0.1 while master.wants_me_alive?
|
51
|
+
end.start
|
52
|
+
CODE
|
53
|
+
|
54
|
+
signal_server(:TTIN)
|
55
|
+
sleep 0.5
|
56
|
+
log = File.read("preforker.log")
|
57
|
+
log.scan(/Child.*Created/).size.should == 3
|
58
|
+
end
|
59
|
+
|
60
|
+
sandboxed_it "should remove a worker on ttou" do
|
61
|
+
run_preforker <<-CODE
|
62
|
+
Preforker.new(:workers => 2) do |master|
|
63
|
+
sleep 0.1 while master.wants_me_alive?
|
64
|
+
end.start
|
65
|
+
CODE
|
66
|
+
|
67
|
+
signal_server(:TTOU)
|
68
|
+
sleep 0.5
|
69
|
+
log = File.read("preforker.log")
|
70
|
+
log.scan(/Child.*Exiting/).size.should == 1
|
71
|
+
end
|
72
|
+
|
73
|
+
sandboxed_it "should remove all workers on winch" do
|
74
|
+
run_preforker <<-CODE
|
75
|
+
Preforker.new(:workers => 2) do |master|
|
76
|
+
sleep 0.1 while master.wants_me_alive?
|
77
|
+
end.start
|
78
|
+
CODE
|
79
|
+
|
80
|
+
signal_server(:WINCH)
|
81
|
+
sleep 0.5
|
82
|
+
log = File.read("preforker.log")
|
83
|
+
log.scan(/Child.*Exiting/).size.should == 2
|
84
|
+
end
|
85
|
+
|
86
|
+
sandboxed_it "should keep creating workers when they die" do
|
87
|
+
run_preforker <<-CODE
|
88
|
+
Preforker.new(:workers => 1, :timeout => 0.2) do |master|
|
89
|
+
end.start
|
90
|
+
CODE
|
91
|
+
|
92
|
+
sleep 0.3
|
93
|
+
log = File.read("preforker.log")
|
94
|
+
log.scan(/Child.*Created/).size.should > 1
|
95
|
+
end
|
96
|
+
end
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
require 'preforker'
|
4
|
+
require 'spec'
|
5
|
+
require 'spec/autorun'
|
6
|
+
require 'rubygems'
|
7
|
+
require 'filetesthelper'
|
8
|
+
Dir[File.dirname(__FILE__) + "/support/**/*.rb"].each {|f| require f}
|
9
|
+
|
10
|
+
include Integration
|
11
|
+
Spec::Runner.configure do |config|
|
12
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Integration
|
2
|
+
PREFORKER_LIB_PATH = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'lib'))
|
3
|
+
def run_preforker(code)
|
4
|
+
File.open("test.rb", 'w') do |f|
|
5
|
+
f.write <<-TOP
|
6
|
+
$LOAD_PATH.unshift('#{PREFORKER_LIB_PATH}')
|
7
|
+
require 'preforker'
|
8
|
+
TOP
|
9
|
+
f.write(code)
|
10
|
+
end
|
11
|
+
|
12
|
+
system("ruby test.rb")
|
13
|
+
end
|
14
|
+
|
15
|
+
def sandboxed_it(desc)
|
16
|
+
it desc do
|
17
|
+
with_files do
|
18
|
+
yield
|
19
|
+
term_server
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def signal_server(signal)
|
25
|
+
Dir['*.pid'].each do |pid_file_path|
|
26
|
+
Process.kill(signal, File.read(pid_file_path).to_i)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def term_server
|
31
|
+
signal_server(:TERM)
|
32
|
+
wait_till_server_ends
|
33
|
+
end
|
34
|
+
|
35
|
+
def quit_server
|
36
|
+
signal_server(:QUIT)
|
37
|
+
wait_till_server_ends
|
38
|
+
end
|
39
|
+
|
40
|
+
def int_server
|
41
|
+
signal_server(:INT)
|
42
|
+
wait_till_server_ends
|
43
|
+
end
|
44
|
+
|
45
|
+
def wait_till_server_ends
|
46
|
+
Dir['*.pid'].each do |pid_file_path|
|
47
|
+
while File.exists?(pid_file_path) do
|
48
|
+
sleep 0.2
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
metadata
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: preforker
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 0.1.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Daniel Cadenas
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-04-02 00:00:00 -03:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: rspec
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 1
|
29
|
+
- 3
|
30
|
+
- 0
|
31
|
+
version: 1.3.0
|
32
|
+
type: :development
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: filetesthelper
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
segments:
|
42
|
+
- 0
|
43
|
+
- 10
|
44
|
+
- 1
|
45
|
+
version: 0.10.1
|
46
|
+
type: :development
|
47
|
+
version_requirements: *id002
|
48
|
+
description: A gem to easily create prefork servers.
|
49
|
+
email: dcadenas@gmail.com
|
50
|
+
executables: []
|
51
|
+
|
52
|
+
extensions: []
|
53
|
+
|
54
|
+
extra_rdoc_files:
|
55
|
+
- LICENSE
|
56
|
+
- README.rdoc
|
57
|
+
files:
|
58
|
+
- .document
|
59
|
+
- .gitignore
|
60
|
+
- LICENSE
|
61
|
+
- README.rdoc
|
62
|
+
- Rakefile
|
63
|
+
- VERSION
|
64
|
+
- examples/amqp.rb
|
65
|
+
- examples/amqp_client.rb
|
66
|
+
- examples/ping_pong.rb
|
67
|
+
- lib/preforker.rb
|
68
|
+
- lib/preforker/pid_manager.rb
|
69
|
+
- lib/preforker/signal_processor.rb
|
70
|
+
- lib/preforker/util.rb
|
71
|
+
- lib/preforker/worker.rb
|
72
|
+
- preforker.gemspec
|
73
|
+
- spec/integration/logging_spec.rb
|
74
|
+
- spec/integration/timeout_spec.rb
|
75
|
+
- spec/integration/worker_control_spec.rb
|
76
|
+
- spec/spec.opts
|
77
|
+
- spec/spec_helper.rb
|
78
|
+
- spec/support/integration.rb
|
79
|
+
has_rdoc: true
|
80
|
+
homepage: http://github.com/dcadenas/preforker
|
81
|
+
licenses: []
|
82
|
+
|
83
|
+
post_install_message:
|
84
|
+
rdoc_options:
|
85
|
+
- --charset=UTF-8
|
86
|
+
require_paths:
|
87
|
+
- lib
|
88
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
segments:
|
93
|
+
- 0
|
94
|
+
version: "0"
|
95
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
segments:
|
100
|
+
- 0
|
101
|
+
version: "0"
|
102
|
+
requirements: []
|
103
|
+
|
104
|
+
rubyforge_project:
|
105
|
+
rubygems_version: 1.3.6
|
106
|
+
signing_key:
|
107
|
+
specification_version: 3
|
108
|
+
summary: A gem to easily create prefork servers.
|
109
|
+
test_files:
|
110
|
+
- spec/integration/logging_spec.rb
|
111
|
+
- spec/integration/timeout_spec.rb
|
112
|
+
- spec/integration/worker_control_spec.rb
|
113
|
+
- spec/spec_helper.rb
|
114
|
+
- spec/support/integration.rb
|
115
|
+
- examples/amqp.rb
|
116
|
+
- examples/amqp_client.rb
|
117
|
+
- examples/ping_pong.rb
|