├── .gitignore
├── .travis.yml
├── History.txt
├── README.md
├── Rakefile
├── examples
├── beanstalk.rb
├── echo.rb
├── preforking_server.rb
└── server_beanstalk.rb
├── lib
├── servolux.rb
└── servolux
│ ├── child.rb
│ ├── daemon.rb
│ ├── null_logger.rb
│ ├── pid_file.rb
│ ├── piper.rb
│ ├── prefork.rb
│ ├── server.rb
│ ├── threaded.rb
│ └── version.rb
├── script
└── bootstrap
└── spec
├── child_spec.rb
├── daemon_spec.rb
├── pid_file_spec.rb
├── piper_spec.rb
├── prefork_spec.rb
├── server_spec.rb
├── servolux_spec.rb
├── spec_helper.rb
└── threaded_spec.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | # The list of files that should be ignored by Mr Bones.
2 | # Lines that start with '#' are comments.
3 | #
4 | # A .gitignore file can be used instead by setting it as the ignore
5 | # file in your Rakefile:
6 | #
7 | # PROJ.ignore_file = '.gitignore'
8 | #
9 | # For a project with a C extension, the following would be a good set of
10 | # exclude patterns (uncomment them if you want to use them):
11 | # *.[oa]
12 | *~
13 | *.sw[op]
14 | announcement.txt
15 | coverage
16 | doc
17 | pkg
18 | tags
19 | .yardoc
20 | .rvmrc
21 | vendor
22 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 |
3 | notifications:
4 | email: false
5 |
6 | before_install: "gem install bones"
7 | install: "rake gem:install_dependencies"
8 |
9 | script: "rake"
10 |
11 | rvm:
12 | - 2.1.10
13 | - 2.2.6
14 | - 2.3.3
15 | - 2.4.0
16 |
--------------------------------------------------------------------------------
/History.txt:
--------------------------------------------------------------------------------
1 | == 0.13.0 / 2017-03-18
2 |
3 | * Minor Enhancements
4 | * Using a "self pipe" for signal handling in the Server
5 | * Bug Fixes
6 | * Test cleanup
7 | * Better cleanup of child processes across the board
8 |
9 | == 0.12.0 / 2015-06-07
10 |
11 | * Minor Enhancements
12 | * Adding support for a `PidFile` class [pr #19]
13 | * Bug Fixes
14 |
15 | == 0.11.0 / 2015-05-29
16 |
17 | * Minor Enhancements
18 | * Fixing server shutdown and signal handling in Ruby 2.0 and up [pr #16]
19 | * Improving error handling [pr #17]
20 | * Added worker options [pr #18]
21 | * Added a `boostrap` script for easier development
22 | * Bug Fixes
23 | * Typo and documentation fixes
24 |
25 | == 0.10.0 / 2012-02-18
26 |
27 | * Minor Enhancements
28 | * Add in the ability to vary the Prefork worker pool size [issue #8]
29 | * Pass original child exception backtrace up the exception chain [issue #7]
30 | * Improved child process wellness checks in Piper and Child classes
31 | * Bug Fixes
32 | * Typo and documentation fixes [issue #6]
33 |
34 | == 0.9.7 / 2012-01-19
35 |
36 | * Minor Enhancements
37 | * Added `after_fork` and `before_exec` handlers for the Daemon class [issue #4]
38 | * Bug Fixes
39 | * ThreadError when stopping threaded objects [issue #5]
40 |
41 | == 0.9.6 / 2011-01-02
42 |
43 | * Minor Enhancements
44 | * Threaded objects run immediately (sleep after running)
45 | * Added a "timed_out?" method in the Prefork::Worker class
46 |
47 | == 0.9.5 / 2010-07-15
48 |
49 | * Minor Enhancements
50 | * Numeric value can be given as the "shutdown_command" for the Servolux::Daemon
51 |
52 | == 0.9.4 / 2010-04-18
53 |
54 | * Bug Fixes
55 | * Elminated race condition in logfile watching [Avdi Grimm]
56 | * Made program exit optionalx [Avdi Grimm]
57 |
58 | == 0.9.3 / 2010-03-10
59 |
60 | * Updated to the latest version of Mr Bones
61 |
62 | == 0.9.2 / 2010-02-10
63 |
64 | * Bug Fixes
65 | * Removing "rescue nil" from code that handles user callbacks
66 |
67 | == 0.9.1 / 2010-01-21
68 |
69 | * Bug Fixes
70 | * Addressing daemon startup issues
71 | * Calling a non-existent piper method from the daemon class
72 |
73 | == 0.9.0 / 2009-11-30
74 |
75 | * Minor Enhancements
76 | * Moving towards yard style documentation
77 | * Adding tests for the Prefork class
78 | * Bug Fixes
79 | * Fixes for Ruby 1.9
80 | * Ensuring the examples run and shutdown properly
81 | * Other numerous bug fixes
82 |
83 | == 0.8.1 / 2009-11-12
84 |
85 | * 3 Bug Fixes
86 | * Attempting to wakeup a dead thread in Prefork
87 | * Shutdown error between the Prefork parent/child
88 | * Restart was not working properly
89 |
90 | == 0.8.0 / 2009-11-12
91 |
92 | * 2 Major Enhancements
93 | * Preforking worker pool
94 | * 1 Minor Enhancement
95 | * Default return value for Piper#gets (useful for detecting timeouts)
96 | * 2 Bug Fixes
97 | * The piper is now using a socket pair for bidirectional communication
98 | * The piper now defaults to a blocking mode (nil timeout)
99 |
100 | == 0.7.1 / 2009-11-06
101 |
102 | * 1 Bug Fix
103 | * Server shutdown would hang in Ruby 1.9.1
104 |
105 | == 0.7.0 / 2009-11-06
106 |
107 | * 1 Minor Enhancement
108 | * Added a permissions mask for the server PID file [Avdi Grimm]
109 | * 1 Bug Fix
110 | * Fixing up some development / runtime dependencies
111 |
112 | == 0.6.2 / 2009-07-16
113 |
114 | * 1 Minor Enhancement
115 | * Added a flag to the server startup method to wait for shutdown
116 | before returning
117 |
118 | == 0.6.1 / 2009-07-13
119 |
120 | * 1 Minor Enhancement
121 | * Added a method to wait for server shutdown in the Server class
122 |
123 | == 0.6.0 / 2009-07-07
124 |
125 | * 2 Minor Enhancements
126 | * Threaded objects can be set to run only a given number of times
127 | * Threaded objects can now continue on error
128 |
129 | == 0.5.0 / 2009-06-30
130 |
131 | * 2 Minor Enhancements
132 | * Added tests for the Child class
133 | * Updating documentation in preperation for a release
134 |
135 | == 0.4.0 / 2009-06-29
136 |
137 | * 1 Minor Enhancement
138 | * Added a "Child" class for working with child processes
139 | * 1 Bug Fix
140 | * Thread#join has a small bug in JRuby - implemented workaround
141 |
142 | == 0.3.0 / 2009-06-24
143 |
144 | * 2 Minor Enhancements
145 | * Documentation
146 | * Unit tests
147 |
148 | == 0.2.0 / 2009-06-19
149 |
150 | * 1 Minor Enhancement
151 | * Added a signal method to the Piper class for signaling the child
152 | * 1 Bug Fix
153 | * Fixed a race condition in the Threaded#stop method
154 |
155 | == 0.1.0 / 2009-06-18
156 |
157 | * 1 Major Enhancement
158 | * Birthday!
159 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Serv-O-Lux
2 | by Tim Pease [](http://travis-ci.org/TwP/servolux)
3 |
4 | * [Homepage](http://rubygems.org/gems/servolux)
5 | * [Github Project](http://github.com/TwP/servolux)
6 |
7 | ### Description
8 |
9 | Serv-O-Lux is a collection of Ruby classes that are useful for daemon and
10 | process management, and for writing your own Ruby services. The code is well
11 | documented and tested. It works with Ruby and JRuby supporting 1.9 and 2.0
12 | interpreters.
13 |
14 | ### Features
15 |
16 | [Servolux::Threaded](http://www.rubydoc.info/github/TwP/servolux/Servolux/Threaded)
17 | -- when included into your own class, it gives you an activity thread that will
18 | run some code at a regular interval. Provides methods to start and stop the
19 | thread, report on the running state, and join the thread to wait for it to
20 | complete.
21 |
22 | [Servolux::Server](http://www.rubydoc.info/github/TwP/servolux/Servolux/Server)
23 | -- a template server class that handles the mundane work of creating / deleting
24 | a PID file, reporting running state, logging errors, starting the service, and
25 | gracefully shutting down the service.
26 |
27 | [Servolux::Piper](http://www.rubydoc.info/github/TwP/servolux/Servolux/Piper)
28 | -- an extension of the standard Ruby fork method that opens a pipe for
29 | communication between parent and child processes. Ruby objects are passed
30 | between parent and child allowing, for example, exceptions in the child process
31 | to be passed to the parent and raised there.
32 |
33 | [Servolux::Daemon](http://www.rubydoc.info/github/TwP/servolux/Servolux/Daemon)
34 | -- a robust class for starting and stopping daemon processes.
35 |
36 | [Servolux::Child](http://www.rubydoc.info/github/TwP/servolux/Servolux/Child)
37 | -- adds some much needed functionality to child processes created via Ruby's
38 | IO#popen method. Specifically, a timeout thread is used to signal the child
39 | process to die if it does not exit in a given amount of time.
40 |
41 | [Servolux::Prefork](http://www.rubydoc.info/github/TwP/servolux/Servolux/Prefork)
42 | -- provides a pre-forking worker pool for executing tasks in parallel using
43 | multiple processes.
44 |
45 | [Servolux::PidFile](http://www.rubydoc.info/github/TwP/servolux/Servolux/PidFile)
46 | -- provides PID file management and process signaling and liveness checks.
47 |
48 | All the documentation is available online at http://rdoc.info/projects/TwP/servolux
49 |
50 | ### Install
51 |
52 | gem install servolux
53 |
54 | ### License
55 |
56 | The MIT License
57 |
58 | Copyright (c) 2015 Tim Pease
59 |
60 | Permission is hereby granted, free of charge, to any person obtaining
61 | a copy of this software and associated documentation files (the
62 | 'Software'), to deal in the Software without restriction, including
63 | without limitation the rights to use, copy, modify, merge, publish,
64 | distribute, sublicense, and/or sell copies of the Software, and to
65 | permit persons to whom the Software is furnished to do so, subject to
66 | the following conditions:
67 |
68 | The above copyright notice and this permission notice shall be
69 | included in all copies or substantial portions of the Software.
70 |
71 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
72 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
73 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
74 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
75 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
76 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
77 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
78 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 |
2 | begin
3 | require 'bones'
4 | rescue LoadError
5 | abort '### please install the "bones" gem ###'
6 | end
7 |
8 | lib = File.expand_path('../lib', __FILE__)
9 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
10 | require 'servolux/version'
11 |
12 | task :default => 'spec:run'
13 | task 'gem:release' => 'spec:run'
14 |
15 | Bones {
16 | name 'servolux'
17 | summary 'A collection of tools for working with processes'
18 | authors 'Tim Pease'
19 | email 'tim.pease@gmail.com'
20 | url 'http://rubygems.org/gems/servolux'
21 | version Servolux::VERSION
22 |
23 | spec.opts << '--color' << '--format documentation'
24 |
25 | depend_on 'bones-rspec', '~> 2.0', :development => true
26 | depend_on 'bones-git', '~> 1.3', :development => true
27 | depend_on 'logging', '~> 2.0', :development => true
28 | }
29 |
--------------------------------------------------------------------------------
/examples/beanstalk.rb:
--------------------------------------------------------------------------------
1 | # Preforking Beanstalkd job runner using Servolux.
2 | #
3 | # In this example, we prefork 7 processes each of which connect to our
4 | # Beanstalkd queue and then wait for jobs to process. We are using a module so
5 | # that we can connect to the beanstalk queue before executing and then
6 | # disconnect from the beanstalk queue after exiting. These methods are called
7 | # exactly once per child process.
8 | #
9 | # A variation on this is to load source code in the before_executing method
10 | # and initialize an object that will process jobs. This is advantageous because
11 | # now you can send SIGHUP to a child process and it will restart, loading your
12 | # Ruby libraries before executing. Now you can do a rolling deploy of new
13 | # code.
14 | #
15 | # def before_executing
16 | # Kernel.load '/your/source/code.rb'
17 | # @job_runner = Your::Source::Code::JobRunner.new
18 | # end
19 | # --------
20 |
21 | require 'servolux'
22 | require 'beanstalk-client'
23 |
24 | module JobProcessor
25 | # Open a connection to our beanstalk queue. This method is called once just
26 | # before entering the child run loop.
27 | def before_executing
28 | host = config[:host]
29 | port = config[:port]
30 | @beanstalk = Beanstalk::Pool.new(["#{host}:#{port}"])
31 | end
32 |
33 | # Close the connection to our beanstalk queue. This method is called once
34 | # just after the child run loop stops and just before the child exits.
35 | def after_executing
36 | @beanstalk.close
37 | end
38 |
39 | # Close the beanstalk socket when we receive SIGHUP. This allows the execute
40 | # thread to return processing back to the child run loop; the child run loop
41 | # will gracefully shutdown the process.
42 | def hup
43 | @beanstalk.close if @job.nil?
44 | @thread.wakeup
45 | end
46 |
47 | # We want to do the same thing when we receive SIGTERM.
48 | alias :term :hup
49 |
50 | # Reserve a job from the beanstalk queue, and processes jobs as we receive
51 | # them. We have a timeout set for 2 minutes so that we can send a heartbeat
52 | # back to the parent process even if the beanstalk queue is empty.
53 | #
54 | # This method is called repeatedly by the child run loop until the child is
55 | # killed via SIGHUP or SIGTERM or halted by the parent.
56 | def execute
57 | @job = nil
58 | @job = @beanstalk.reserve(120) rescue nil
59 | if @job
60 | $stdout.puts "[C] #{Process.pid} processing job #{@job.inspect}"
61 | # ... do more processing here
62 | end
63 | rescue Beanstalk::TimedOut
64 | ensure
65 | @job.delete rescue nil if @job
66 | end
67 | end
68 |
69 | # Create our preforking worker pool. Each worker will run the code found in
70 | # the JobProcessor module.
71 | #
72 | # The `:config` Hash is passed to each worker when it is created. The values
73 | # here are available to the JobProcessor module. We use this config hash to pass
74 | # the `:host` and `:port` where the beanstalkd server can be found.
75 | #
76 | # We set a timeout of 10 minutes for the worker pool. The child process
77 | # must send a "heartbeat" message to the parent within this timeout period;
78 | # otherwise, the parent will halt the child process.
79 | #
80 | # Our execute code in the JobProcessor takes this into account. It will wakeup
81 | # every 2 minutes, if no jobs are reserved from the beanstalk queue, and send
82 | # the heartbeat message.
83 | #
84 | # This also means that if any job processed by a worker takes longer than 10
85 | # minutes to run, that child worker will be killed.
86 | pool = Servolux::Prefork.new \
87 | :timeout => 600,
88 | :module => JobProcessor,
89 | :config => {:host => '127.0.0.1', :port => 11300}
90 |
91 | # Start up 7 child processes to handle jobs
92 | pool.start 7
93 |
94 | # When SIGINT is received, kill all child process and then reap the child PIDs
95 | # from the proc table.
96 | trap('INT') {
97 | pool.signal 'KILL'
98 | pool.reap
99 | }
100 | Process.waitall
101 |
102 |
--------------------------------------------------------------------------------
/examples/echo.rb:
--------------------------------------------------------------------------------
1 | # Pre-forking echo server using Servolux
2 | #
3 | # Run this code using "ruby echo.rb"
4 | #
5 | # You can test the server using NetCat from a separate terminal window.
6 | #
7 | # echo "hello world" | nc localhost 4242
8 | #
9 | # This example was stolen from Ryan Tomayko and modified to demonstrate the
10 | # Servolux gem. The original can be found here:
11 | #
12 | # http://tomayko.com/writings/unicorn-is-unix
13 | # --------
14 |
15 | require 'servolux'
16 |
17 | # Create a socket, bind it to localhost:4242, and start listening.
18 | # Runs once in the parent; all forked children inherit the socket's
19 | # file descriptor.
20 | acceptor = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
21 | address = Socket.pack_sockaddr_in(4242, '0.0.0.0')
22 | acceptor.bind(address)
23 | acceptor.listen(10)
24 |
25 | # Close the socket when we exit the parent or any child process. This
26 | # only closes the file descriptor in the calling process, it does not
27 | # take the socket out of the listening state (until the last fd is
28 | # closed).
29 | #
30 | # The trap is guaranteed to happen, and guaranteed to happen only
31 | # once, right before the process exits for any reason (unless
32 | # it's terminated with a SIGKILL).
33 | trap('EXIT') { acceptor.close }
34 |
35 | # Create the worker pool passing in the code to execute in each child
36 | # process.
37 | pool = Servolux::Prefork.new {
38 | socket,_ = acceptor.accept
39 | socket.write "child #$$ echo> "
40 | socket.flush
41 | message = socket.gets
42 | socket.write message
43 | socket.close
44 | puts "child #$$ echo'd: '#{message.strip}'"
45 | }
46 |
47 | # Start up 3 child process to handle echo requests on the socket.
48 | pool.start 3
49 |
50 | # Stop the child processes when SIGINT is received.
51 | trap('INT') { pool.signal 'KILL' }
52 | Process.waitall
53 |
--------------------------------------------------------------------------------
/examples/preforking_server.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'servolux'
3 | require 'logger'
4 |
5 | ###############################################################################
6 | # This is an example script that creates uses both Prefork and Server
7 | #
8 | # The Server will manage the Prefork pool and respond to signals to add new
9 | # workers to the pool
10 | ###############################################################################
11 |
12 | # The child script that will be executed, this is just a shell script that
13 | # will be execed by each worker pool member.
14 | module ExecChild
15 | def self.the_script
16 | <<__sh__
17 | #!/bin/sh
18 | #
19 | trap "echo I am process $$" SIGUSR1
20 | trap "exit 0" SIGHUP
21 | trap "exit 1" SIGTERM
22 |
23 | echo "[$$] I am a child program"
24 | while true
25 | do
26 | sleep 1
27 | done
28 | __sh__
29 | end
30 |
31 | def self.script_name
32 | "/tmp/exec-child-worker.sh"
33 | end
34 |
35 | def self.write_script
36 | File.open( script_name, "w+", 0750 ) do |f|
37 | f.write( the_script )
38 | end
39 | end
40 |
41 | def self.remove_script
42 | File.unlink( script_name )
43 | end
44 |
45 | def execute
46 | exec ExecChild.script_name
47 | end
48 | end
49 |
50 | class PreforkingServerExample < ::Servolux::Server
51 |
52 | # Create a preforking server that has the given minimum and maximum boundaries
53 | #
54 | def initialize( min_workers = 2, max_workers = 10 )
55 | @logger = ::Logger.new( $stderr )
56 | super( self.class.name, :interval => 2, :logger => @logger )
57 | @pool = Servolux::Prefork.new( :module => ExecChild, :timeout => nil,
58 | :min_workers => min_workers, :max_workers => max_workers )
59 | end
60 |
61 | def log( msg )
62 | logger.info msg
63 | end
64 |
65 | def log_pool_status
66 | log "Pool status : #{@pool.worker_counts.inspect} living pids #{live_worker_pids.join(' ')}"
67 | end
68 |
69 | def live_worker_pids
70 | pids = []
71 | @pool.each_worker { |w| pids << w.pid if w.alive? }
72 | return pids
73 | end
74 |
75 | def shutdown_workers
76 | log "Shutting down all workers"
77 | @pool.stop
78 | loop do
79 | log_pool_status
80 | break if @pool.live_worker_count <= 0
81 | sleep 0.25
82 | end
83 | end
84 |
85 | def log_worker_status( worker )
86 | if not worker.alive? then
87 | worker.wait
88 | if worker.exited? then
89 | log "Worker #{worker.pid} exited with status #{worker.exitstatus}"
90 | elsif worker.signaled? then
91 | log "Worker #{worker.pid} signaled by #{worker.termsig}"
92 | elsif worker.stopped? then
93 | log "Worker #{worker.pid} stopped by #{worker.stopsig}"
94 | else
95 | log "I have no clue #{worker.inspect}"
96 | end
97 | end
98 | end
99 |
100 |
101 | #############################################################################
102 | # Implementations of parts of the Servolux::Server API
103 | #############################################################################
104 |
105 | # this is run once before the Server's run loop
106 | def before_starting
107 | ExecChild.write_script
108 | log "Starting up the Pool"
109 | @pool.start( 1 )
110 | log "Send a USR1 to add a worker (kill -usr1 #{Process.pid})"
111 | log "Send a USR2 to kill all the workers (kill -usr2 #{Process.pid})"
112 | log "Send a INT (Ctrl-C) or TERM to shutdown the server (kill -term #{Process.pid})"
113 | end
114 |
115 | # Add a worker to the pool when USR1 is received
116 | def usr1
117 | log "Adding a worker"
118 | @pool.add_workers
119 | end
120 |
121 | # kill all the current workers with a usr2, the run loop will respawn up to
122 | # the min_worker count
123 | #
124 | def usr2
125 | shutdown_workers
126 | end
127 |
128 | # By default, Servolux::Server will capture the TERM signal and call its
129 | # +shutdown+ method. After that +shutdown+ method is called it will call
130 | # +after_shutdown+ we're going to hook into that so that all the workers get
131 | # cleanly shutdown before the parent process exits
132 | def after_stopping
133 | shutdown_workers
134 | ExecChild.remove_script
135 | end
136 |
137 | # This is the method that is executed during the run loop
138 | #
139 | def run
140 | log_pool_status
141 | @pool.each_worker do |worker|
142 | log_worker_status( worker )
143 | end
144 | @pool.ensure_worker_pool_size
145 | end
146 | end
147 |
148 | if $0 == __FILE__ then
149 | pse = PreforkingServerExample.new
150 | pse.startup
151 | end
152 |
--------------------------------------------------------------------------------
/examples/server_beanstalk.rb:
--------------------------------------------------------------------------------
1 |
2 | require 'rubygems'
3 | require 'servolux'
4 | require 'beanstalk-client'
5 | require 'logger'
6 |
7 | # The END block is executed at the *end* of the script. It is here only
8 | # because this is the meat of the running code, and it makes the example more
9 | # "exemplary".
10 | END {
11 |
12 | # Create a new Servolux::Server and augment it with our BeanstalkWorkerPool
13 | # methods. The run loop will be executed every 30 seconds by this server.
14 | server = Servolux::Server.new('BeanstalkWorkerPool', :logger => Logger.new($stdout), :interval => 30)
15 | server.extend BeanstalkWorkerPool
16 |
17 | # Startup the server. The "before_starting" method will be called and the run
18 | # loop will begin executing. This method will not return until a SIGINT or
19 | # SIGTERM is sent to the server process.
20 | server.startup
21 |
22 | }
23 |
24 | # The worker pool is managed as a Servolux::Server instance. This allows the
25 | # pool to be gracefully stopped and to be monitored by the server thread. This
26 | # monitoring involves reaping child processes that have died and reporting on
27 | # errors raised by children. It is also possible to respawn dead child
28 | # workers, but this should be thoroughly thought through (ha, unintentional
29 | # alliteration) before doing so [if the CPU is thrashing, then respawning dead
30 | # child workers will only contribute to the thrash].
31 | module BeanstalkWorkerPool
32 | # Before we start the server run loop, allocate our pool of child workers
33 | # and prefork seven JobProcessors to pull work from the beanstalk queue.
34 | def before_starting
35 | @pool = Servolux::Prefork.new \
36 | :module => JobProcessor,
37 | :config => {:host => '127.0.0.1', :port => 11300}
38 | @pool.start 7
39 | end
40 |
41 | # This run loop will be called at a fixed interval by the server thread. If
42 | # the pool has any child processes that have died or restarted, then the
43 | # expired PIDs are read from the proc table. If any workers in the pool
44 | # have reported an error, then display those errors on STDOUT; these are
45 | # errors raised from the child process that caused the child to terminate.
46 | def run
47 | @pool.reap
48 | @pool.each_worker { |worker|
49 | $stdout.puts "[P] #{Process.pid} child error: #{worker.error.inspect}" if worker.error
50 | }
51 | end
52 |
53 | # After the server run loop exits, stop all children in the pool of workers.
54 | def after_stopping
55 | @pool.stop
56 | end
57 | end
58 |
59 | # See the beanstalk.rb example for an explanation of the JobProcessor
60 | module JobProcessor
61 | def before_executing
62 | host = config[:host]
63 | port = config[:port]
64 | @beanstalk = Beanstalk::Pool.new(["#{host}:#{port}"])
65 | end
66 |
67 | def after_executing
68 | @beanstalk.close
69 | end
70 |
71 | def hup
72 | @beanstalk.close if @job.nil?
73 | @thread.wakeup
74 | end
75 | alias :term :hup
76 |
77 | def execute
78 | @job = nil
79 | @job = @beanstalk.reserve(120) rescue nil
80 | if @job
81 | $stdout.puts "[C] #{Process.pid} processing job #{@job.inspect}"
82 | end
83 | rescue Beanstalk::TimedOut
84 | ensure
85 | @job.delete rescue nil if @job
86 | end
87 | end
88 |
89 |
--------------------------------------------------------------------------------
/lib/servolux.rb:
--------------------------------------------------------------------------------
1 | module Servolux
2 |
3 | # :stopdoc:
4 | LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
5 | PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
6 | # :startdoc:
7 |
8 | # Generic Servolux Error class.
9 | Error = Class.new(StandardError)
10 |
11 | # Returns the library path for the module. If any arguments are given,
12 | # they will be joined to the end of the library path using
13 | # File.join.
14 | #
15 | # @return [String] absolute servolux 'lib' path
16 | #
17 | def self.libpath( *args )
18 | args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
19 | end
20 |
21 | # Returns the lpath for the module. If any arguments are given,
22 | # they will be joined to the end of the servolux base path using
23 | # File.join.
24 | #
25 | # @return [String] absolute servolux base path
26 | #
27 | def self.path( *args )
28 | args.empty? ? PATH : ::File.join(PATH, args.flatten)
29 | end
30 |
31 | # Returns +true+ if the execution platform supports fork.
32 | #
33 | # @return [Boolean]
34 | #
35 | def self.fork?
36 | RUBY_PLATFORM != 'java' and test(?e, '/dev/null')
37 | end
38 | end
39 |
40 | %w[version threaded child daemon null_logger pid_file piper prefork server].each do |lib|
41 | require Servolux.libpath('servolux', lib)
42 | end
43 |
--------------------------------------------------------------------------------
/lib/servolux/child.rb:
--------------------------------------------------------------------------------
1 |
2 | # == Synopsis
3 | # Manage a child process spawned via IO#popen and provide a timeout
4 | # mechanism to kill the process after some amount time.
5 | #
6 | # == Details
7 | # Ruby provides the IO#popen method to spawn a child process and return an
8 | # IO instance connected to the child's stdin and stdout (with stderr
9 | # redirected to stdout). The Servolux::Child class adds to this a timeout
10 | # thread that will signal the child process after some number of seconds.
11 | # If the child exits cleanly before the timeout expires then no signals are
12 | # sent to the child.
13 | #
14 | # A list of signals can be provided which will be sent in succession to the
15 | # child until one of them causes the child to exit. The current Ruby thread
16 | # suspends for a few seconds to allow each signal to be processed by the
17 | # child. By default these signals are SIGTERM, SIGQUIT, SIGKILL and the time
18 | # to wait between signals is four seconds.
19 | #
20 | # The +stop+ method is used to stop the child process (if running) and to
21 | # reset the state of the Child instance so that it can be started again.
22 | # Stopping the Child instance closes the IO between parent and child
23 | # process.
24 | #
25 | # The +wait+ method is used to wait for the child process to exit. The
26 | # Process::Status object is retrieved by the Child and stored as an instance
27 | # variable. The +exitstatus+ method (and the other process related methods)
28 | # will return non-nil values after the wait method is called.
29 | #
30 | # == Examples
31 | #
32 | # child = Servolux::Child.new(:command => 'sleep 120', :timeout => 10)
33 | # child.start
34 | # child.wait
35 | #
36 | # child.timed_out? #=> true
37 | # child.signaled? #=> true
38 | # child.exitstatus #=> nil
39 | #
40 | class Servolux::Child
41 |
42 | attr_accessor :command
43 | attr_accessor :timeout
44 | attr_accessor :signals
45 | attr_accessor :suspend
46 | attr_reader :io
47 | attr_reader :pid
48 |
49 | # Create a new Child that will execute and manage the +command+ string as
50 | # a child process.
51 | #
52 | # @option opts [String] :command
53 | # The command that will be executed via IO#popen.
54 | #
55 | # @option opts [Numeric] :timeout (nil)
56 | # The number of seconds to wait before terminating the child process.
57 | # No action is taken if the child process exits normally before the
58 | # timeout expires.
59 | #
60 | # @option opts [Array] :signals (['TERM', 'QUIT', 'KILL'])
61 | # A list of signals that will be sent to the child process when the
62 | # timeout expires. The signals increase in severity with SIGKILL being
63 | # the signal of last resort.
64 | #
65 | # @option opts [Numeric] :suspend (4)
66 | # The number of seconds to wait for the child process to respond to a
67 | # signal before trying the next one in the list.
68 | #
69 | def initialize( opts = {} )
70 | @command = opts.fetch(:command, nil)
71 | @timeout = opts.fetch(:timeout, nil)
72 | @signals = opts.fetch(:signals, %w[TERM QUIT KILL])
73 | @suspend = opts.fetch(:suspend, 4)
74 | @io = @pid = @status = @thread = @timed_out = nil
75 | yield self if block_given?
76 | end
77 |
78 | # Runs the +command+ string as a subprocess; the subprocess’s
79 | # standard input and output will be connected to the returned IO object.
80 | # The default mode for the new file object is "r", but mode may be set to
81 | # any of the modes listed in the description for class IO.
82 | #
83 | # If a block is given, Ruby will run the +command+ as a child connected to
84 | # Ruby with a pipe. Ruby’s end of the pipe will be passed as a parameter
85 | # to the block. In this case the value of the block is returned.
86 | #
87 | # @param [String] mode The mode flag used to open the child process via
88 | # IO#popen.
89 | # @yield [IO] Execute the block of call passing in the communication pipe
90 | # with the child process.
91 | # @yieldreturn Returns the result of the block.
92 | # @return [IO] The communication pipe with the child process or the return
93 | # value from the block if one was given.
94 | #
95 | def start( mode = 'r', &block )
96 | start_timeout_thread if @timeout
97 |
98 | @io = IO::popen @command, mode
99 | @pid = @io.pid
100 | @status = nil
101 |
102 | return block.call(@io) unless block.nil?
103 | @io
104 | end
105 |
106 | # Stop the child process if it is alive. A sequence of +signals+ are sent
107 | # to the process until it dies with SIGKILL being the signal of last
108 | # resort.
109 | #
110 | # After this method returns, the IO pipe to the child will be closed and
111 | # the stored child PID is set to +nil+. The +start+ method can be safely
112 | # called again.
113 | #
114 | # @return self
115 | #
116 | def stop
117 | unless @thread.nil?
118 | t, @thread = @thread, nil
119 | t[:stop] = true
120 | t.wakeup.join if t.status
121 | end
122 |
123 | kill if alive?
124 | @io.close unless @io.nil? || @io.closed?
125 | @io = nil
126 | self
127 | end
128 |
129 | # Waits for the child process to exit and returns its exit status. The
130 | # global variable $? is set to a Process::Status object containing
131 | # information on the child process.
132 | #
133 | # You can get more information about how the child status exited by calling
134 | # the following methods on the piper instance:
135 | #
136 | # * coredump?
137 | # * exited?
138 | # * signaled?
139 | # * stopped?
140 | # * success?
141 | # * exitstatus
142 | # * stopsig
143 | # * termsig
144 | #
145 | # @param [Integer] flags Bit flags that will be passed to the system level
146 | # wait call. See the Ruby core documentation for Process#wait for more
147 | # information on these flags.
148 | # @return [Integer, nil] The exit status of the child process or +nil+ if
149 | # the child process is not running.
150 | #
151 | def wait( flags = 0 )
152 | return if @pid.nil?
153 | _, @status = Process.wait2(@pid, flags) unless @status
154 | exitstatus
155 | end
156 |
157 | # Returns +true+ if the child process is alive. Returns +nil+ if the child
158 | # process has not been started.
159 | #
160 | # @return [Boolean]
161 | #
162 | def alive?
163 | return if @pid.nil?
164 | wait(Process::WNOHANG|Process::WUNTRACED)
165 | Process.kill(0, @pid)
166 | true
167 | rescue Errno::ESRCH, Errno::ENOENT
168 | false
169 | end
170 |
171 | # Returns +true+ if the child process was killed by the timeout thread.
172 | #
173 | # @return [Boolean]
174 | #
175 | def timed_out?
176 | @timed_out
177 | end
178 |
179 | %w[coredump? exited? signaled? stopped? success? exitstatus stopsig termsig].
180 | each { |method|
181 | self.class_eval <<-CODE
182 | def #{method}
183 | return if @status.nil?
184 | @status.#{method}
185 | end
186 | CODE
187 | }
188 |
189 |
190 | private
191 |
192 | # Attempt to kill the child process by sending the configured +signals+
193 | # and waiting for +suspend+ seconds between each signal; this gives the
194 | # child time to respond to the signal.
195 | #
196 | # Returns +true+ if the child died. Returns +false+ if the child is still
197 | # not dead after the last signal was sent. Returns +nil+ if the child was
198 | # not running in the first place.
199 | #
200 | def kill
201 | return if @io.nil?
202 |
203 | existed = false
204 | @signals.each do |sig|
205 | begin
206 | Process.kill sig, @pid
207 | existed = true
208 | rescue Errno::ESRCH, Errno::ENOENT
209 | return(existed ? nil : true)
210 | end
211 | return true unless alive?
212 | sleep @suspend
213 | return true unless alive?
214 | end
215 | return !alive?
216 | end
217 |
218 | def start_timeout_thread
219 | @timed_out = false
220 | @thread = Thread.new(self) { |child|
221 | sleep @timeout
222 | unless Thread.current[:stop]
223 | if child.alive?
224 | child.instance_variable_set(:@timed_out, true)
225 | child.__send__(:kill)
226 | end
227 | end
228 | }
229 | end
230 |
231 | end
232 |
233 |
--------------------------------------------------------------------------------
/lib/servolux/daemon.rb:
--------------------------------------------------------------------------------
1 | require 'ostruct'
2 |
3 | # == Synopsis
4 | # The Daemon takes care of the work of creating and managing daemon
5 | # processes from Ruby.
6 | #
7 | # == Details
8 | # A daemon process is a long running process on a UNIX system that is
9 | # detached from a TTY -- i.e. it is not tied to a user session. These types
10 | # of processes are notoriously difficult to setup correctly. This Daemon
11 | # class encapsulates some best practices to ensure daemons startup properly
12 | # and can be shutdown gracefully.
13 | #
14 | # Starting a daemon process involves forking a child process, setting the
15 | # child as a session leader, forking again, and detaching from the current
16 | # working directory and standard in/out/error file descriptors. Because of
17 | # this separation between the parent process and the daemon process, it is
18 | # difficult to know if the daemon started properly.
19 | #
20 | # The Daemon class opens a pipe between the parent and the daemon. The PID
21 | # of the daemon is sent to the parent through this pipe. The PID is used to
22 | # check if the daemon is alive. Along with the PID, any errors from the
23 | # daemon process are marshalled through the pipe back to the parent. These
24 | # errors are wrapped in a StartupError and then raised in the parent.
25 | #
26 | # If no errors are passed up the pipe, the parent process waits till the
27 | # daemon starts. This is determined by sending a signal to the daemon
28 | # process.
29 | #
30 | # If a log file is given to the Daemon instance, then it is monitored for a
31 | # change in size and mtime. This lets the Daemon instance know that the
32 | # daemon process is updating the log file. Furthermore, the log file can be
33 | # watched for a specific pattern; this pattern signals that the daemon
34 | # process is up and running.
35 | #
36 | # Shutting down the daemon process is a little simpler. An external shutdown
37 | # command can be used, or the Daemon instance will send an INT or TERM
38 | # signal to the daemon process.
39 | #
40 | # Again, the Daemon instance will wait till the daemon process shuts down.
41 | # This is determined by attempting to signal the daemon process PID and then
42 | # returning when this signal fails -- i.e. then the daemon process has died.
43 | #
44 | # == Examples
45 | #
46 | # ==== Bad Example
47 | # This is a bad example. The daemon will not start because the startup
48 | # command "/usr/bin/no-command-by-this-name" cannot be found on the file
49 | # system. The daemon process will send an Errno::ENOENT through the pipe
50 | # back to the parent which gets wrapped in a StartupError
51 | #
52 | # daemon = Servolux::Daemon.new(
53 | # :name => 'Bad Example',
54 | # :pid_file => '/dev/null',
55 | # :startup_command => '/usr/bin/no-command-by-this-name'
56 | # )
57 | # daemon.startup #=> raises StartupError
58 | #
59 | # ==== Good Example
60 | # This is a simple Ruby server that prints the time to a file every minute.
61 | # So, it's not really a "good" example, but it will work.
62 | #
63 | # server = Servolux::Server.new('TimeStamp', :interval => 60)
64 | # class << server
65 | # def file() @fd ||= File.open('timestamps.txt', 'w'); end
66 | # def run() file.puts Time.now; end
67 | # end
68 | #
69 | # daemon = Servolux::Daemon.new(:server => server, :log_file => 'timestamps.txt')
70 | # daemon.startup
71 | #
72 | class Servolux::Daemon
73 |
74 | Error = Class.new(::Servolux::Error)
75 | Timeout = Class.new(Error)
76 | StartupError = Class.new(Error)
77 |
78 | attr_accessor :name
79 | attr_accessor :logger
80 | attr_reader :pid_file
81 | attr_reader :startup_command
82 | attr_accessor :shutdown_command
83 | attr_accessor :timeout
84 | attr_accessor :nochdir
85 | attr_accessor :noclose
86 | attr_reader :log_file
87 | attr_reader :look_for
88 | attr_accessor :after_fork
89 | attr_accessor :before_exec
90 |
91 | # Create a new Daemon that will manage the +startup_command+ as a daemon
92 | # process.
93 | #
94 | # @option opts [String] :name
95 | # The name of the daemon process. This name will appear in log messages.
96 | # [required]
97 | #
98 | # @option opts [Logger] :logger
99 | # The Logger instance used to output messages. [required]
100 | #
101 | # @option opts [String] :pid_file
102 | # Location of the PID file. This is used to determine if the daemon
103 | # process is running, and to send signals to the daemon process.
104 | # [required]
105 | #
106 | # @option opts [String, Array, Proc, Method, Servolux::Server] :startup_command
107 | # Assign the startup command. Different calling semantics are used for
108 | # each type of command. See the {Daemon#startup_command= startup_command}
109 | # method for more details. [required]
110 | #
111 | # @option opts [Numeric] :timeout (30)
112 | # The time (in seconds) to wait for the daemon process to either startup
113 | # or shutdown. An error is raised when this timeout is exceeded.
114 | #
115 | # @option opts [Boolean] :nochdir (false)
116 | # When set to true this flag directs the daemon process to keep the
117 | # current working directory. By default, the process of daemonizing will
118 | # cause the current working directory to be changed to the root folder
119 | # (thus preventing the daemon process from holding onto the directory
120 | # inode).
121 | #
122 | # @option opts [Boolean] :noclose (false)
123 | # When set to true this flag keeps the standard input/output streams from
124 | # being reopened to /dev/null when the daemon process is created. Reopening
125 | # the standard input/output streams frees the file descriptors which are
126 | # still being used by the parent process. This prevents zombie processes.
127 | #
128 | # @option opts [Numeric, String, Array, Proc, Method, Servolux::Server] :shutdown_command (nil)
129 | # Assign the shutdown command. Different calling semantics are used for
130 | # each type of command.
131 | #
132 | # @option opts [String] :log_file (nil)
133 | # This log file will be monitored to determine if the daemon process has
134 | # successfully started.
135 | #
136 | # @option opts [String, Regexp] :look_for (nil)
137 | # This can be either a String or a Regexp. It defines a phrase to search
138 | # for in the log_file. When the daemon process is started, the parent
139 | # process will not return until this phrase is found in the log file. This
140 | # is a useful check for determining if the daemon process is fully
141 | # started.
142 | #
143 | # @option opts [Proc, lambda] :after_fork (nil)
144 | # This proc will be called in the child process immediately after forking.
145 | #
146 | # @option opts [Proc, lambda] :before_exec (nil)
147 | # This proc will be called in the child process immediately before calling
148 | # `exec` to execute the desired process. This proc will be called after
149 | # the :after_fork proc if present.
150 | #
151 | # @yield [self] Block used to configure the daemon instance
152 | #
153 | def initialize( opts = {} )
154 | @piper = nil
155 | @logfile_reader = nil
156 | @pid_file = nil
157 |
158 | self.name = opts.fetch(:name, nil)
159 | self.logger = opts.fetch(:logger, Servolux::NullLogger())
160 | self.startup_command = opts.fetch(:server, nil) || opts.fetch(:startup_command, nil)
161 | self.shutdown_command = opts.fetch(:shutdown_command, nil)
162 | self.timeout = opts.fetch(:timeout, 30)
163 | self.nochdir = opts.fetch(:nochdir, false)
164 | self.noclose = opts.fetch(:noclose, false)
165 | self.log_file = opts.fetch(:log_file, nil)
166 | self.look_for = opts.fetch(:look_for, nil)
167 | self.after_fork = opts.fetch(:after_fork, nil)
168 | self.before_exec = opts.fetch(:before_exec, nil)
169 |
170 | self.pid_file = opts.fetch(:pid_file, name) if pid_file.nil?
171 |
172 | yield self if block_given?
173 |
174 | ary = %w[name logger pid_file startup_command].map { |var|
175 | self.send(var).nil? ? var : nil
176 | }.compact
177 | raise Error, "These variables are required: #{ary.join(', ')}." unless ary.empty?
178 | end
179 |
180 | # Assign the startup command. This can be either a String, an Array of
181 | # strings, a Proc, a bound Method, or a Servolux::Server instance.
182 | # Different calling semantics are used for each type of command.
183 | #
184 | # If the startup command is a String or an Array of strings, then
185 | # Kernel#exec is used to run the command. Therefore, the string (or array)
186 | # should be a system command that is either fully qualified or can be
187 | # found on the current environment path.
188 | #
189 | # If the startup command is a Proc or a bound Method then it is invoked
190 | # using the +call+ method on the object. No arguments are passed to the
191 | # +call+ invocation.
192 | #
193 | # Lastly, if the startup command is a Servolux::Server then its +startup+
194 | # method is called.
195 | #
196 | # @param [String, Array, Proc, Method, Servolux::Server] val The startup
197 | # command to invoke when daemonizing.
198 | #
199 | def startup_command=( val )
200 | @startup_command = val
201 | return unless val.is_a?(::Servolux::Server)
202 |
203 | self.name = val.name
204 | self.logger = val.logger
205 | self.pid_file = val.pid_file
206 | @shutdown_command = nil
207 | end
208 | alias :server= :startup_command=
209 | alias :server :startup_command
210 |
211 | # Set the PID file to the given `value`. If a PidFile instance is given, then
212 | # it is used. If a name is given, then that name is used to create a PifFile
213 | # instance.
214 | #
215 | # value - The PID file name or a PidFile instance.
216 | #
217 | # Raises an ArgumentError if the `value` cannot be used as a PID file.
218 | def pid_file=( value )
219 | @pid_file =
220 | case value
221 | when Servolux::PidFile
222 | value
223 | when String
224 | path = File.dirname(value)
225 | fn = File.basename(value, ".pid")
226 | Servolux::PidFile.new(:name => fn, :path => path, :logger => logger)
227 | else
228 | raise ArgumentError, "#{value.inspect} cannot be used as a PID file"
229 | end
230 | end
231 |
232 | # Assign the log file name. This log file will be monitored to determine
233 | # if the daemon process is running.
234 | #
235 | # @param [String] filename The name of the log file to monitor
236 | #
237 | def log_file=( filename )
238 | return if filename.nil?
239 | @logfile_reader ||= LogfileReader.new
240 | @logfile_reader.filename = filename
241 | end
242 |
243 | # A string or regular expression to search for in the log file. When the
244 | # daemon process is started, the parent process will not return until this
245 | # phrase is found in the log file. This is a useful check for determining
246 | # if the daemon process is fully started.
247 | #
248 | # If no phrase is given to look for, then the log file will simply be
249 | # watched for a change in size and a modified timestamp.
250 | #
251 | # @param [String, Regexp] val The phrase in the log file to search for
252 | #
253 | def look_for=( val )
254 | return if val.nil?
255 | @logfile_reader ||= LogfileReader.new
256 | @logfile_reader.look_for = val
257 | end
258 |
259 | # Start the daemon process. Passing in +false+ to this method will prevent
260 | # the parent from exiting after the daemon process starts.
261 | #
262 | # @return [Daemon] self
263 | #
264 | def startup( do_exit = true )
265 | raise Error, "Fork is not supported in this Ruby environment." unless ::Servolux.fork?
266 | return if alive?
267 |
268 | logger.debug "About to fork ..."
269 | @piper = ::Servolux::Piper.daemon(nochdir, noclose)
270 |
271 | # Make sure we have an idea of the state of the log file BEFORE the child
272 | # gets a chance to write to it.
273 | @logfile_reader.updated? if @logfile_reader
274 |
275 | @piper.parent {
276 | @piper.timeout = 0.1
277 | wait_for_startup
278 | exit!(0) if do_exit
279 | }
280 |
281 | @piper.child { run_startup_command }
282 | self
283 | end
284 |
285 | # Stop the daemon process. If a shutdown command has been defined, it will
286 | # be called to stop the daemon process. Otherwise, SIGINT will be sent to
287 | # the daemon process to terminate it.
288 | #
289 | # @return [Daemon] self
290 | #
291 | def shutdown
292 | return unless alive?
293 |
294 | case shutdown_command
295 | when nil; kill
296 | when Integer; kill(shutdown_command)
297 | when String; exec(shutdown_command)
298 | when Array; exec(*shutdown_command)
299 | when Proc, Method; shutdown_command.call
300 | when ::Servolux::Server; shutdown_command.shutdown
301 | else
302 | raise Error, "Unrecognized shutdown command #{shutdown_command.inspect}"
303 | end
304 |
305 | wait_for_shutdown
306 | end
307 |
308 | # Returns +true+ if the daemon process is currently running. Returns
309 | # +false+ if this is not the case. The status of the process is determined
310 | # by sending a signal to the process identified by the +pid_file+.
311 | #
312 | # @return [Boolean]
313 | #
314 | def alive?
315 | pid_file.alive?
316 | end
317 |
318 | # Send a signal to the daemon process identified by the PID file. The
319 | # default signal to send is 'INT' (2). The signal can be given either as a
320 | # string or a signal number.
321 | #
322 | # @param [String, Integer] signal The kill signal to send to the daemon
323 | # process
324 | # @return [Daemon] self
325 | #
326 | def kill( signal = 'INT' )
327 | pid_file.kill signal
328 | end
329 |
330 | private
331 |
332 | def run_startup_command
333 | after_fork.call if after_fork.respond_to? :call
334 |
335 | case startup_command
336 | when String; exec(startup_command)
337 | when Array; exec(*startup_command)
338 | when Proc, Method; startup_command.call
339 | when ::Servolux::Server; startup_command.startup
340 | else
341 | raise Error, "Unrecognized startup command #{startup_command.inspect}"
342 | end
343 |
344 | rescue Exception => err
345 | unless err.is_a?(SystemExit)
346 | logger.fatal err
347 | @piper.puts err
348 | end
349 | ensure
350 | @piper.close
351 | end
352 |
353 | def exec( *args )
354 | logger.debug "Calling: exec(*#{args.inspect})"
355 |
356 | skip = [STDIN, STDOUT, STDERR]
357 | skip << @piper.socket if @piper
358 | ObjectSpace.each_object(IO) { |io|
359 | next if skip.include? io
360 | io.close unless io.closed?
361 | }
362 |
363 | before_exec.call if before_exec.respond_to? :call
364 | Kernel.exec(*args)
365 | end
366 |
367 | def retrieve_pid
368 | @piper ? @piper.pid : pid_file.pid
369 | end
370 |
371 | def started?
372 | return false unless alive?
373 | return true if @logfile_reader.nil?
374 | @logfile_reader.updated?
375 | end
376 |
377 | def wait_for_startup
378 | logger.debug "Waiting for #{name.inspect} to startup."
379 |
380 | started = wait_for {
381 | rv = started?
382 | err = @piper.gets
383 | raise StartupError, "Child raised error: #{err.inspect}", err.backtrace unless err.nil?
384 | rv
385 | }
386 |
387 | raise Timeout, "#{name.inspect} failed to startup in a timely fashion. " \
388 | "The timeout is set at #{timeout} seconds." unless started
389 |
390 | logger.info 'Server has daemonized.'
391 | ensure
392 | @piper.close
393 | end
394 |
395 | def wait_for_shutdown
396 | logger.debug "Waiting for #{name.inspect} to shutdown."
397 | return self if wait_for { !alive? }
398 | raise Timeout, "#{name.inspect} failed to shutdown in a timely fashion. " \
399 | "The timeout is set at #{timeout} seconds."
400 | end
401 |
402 | def wait_for
403 | start = Time.now
404 | nap_time = 0.2
405 |
406 | loop do
407 | sleep nap_time
408 |
409 | diff = Time.now - start
410 | nap_time = 2*nap_time
411 | nap_time = 0.2 if nap_time > 1.6
412 |
413 | break true if yield
414 | break false if diff >= timeout
415 | end
416 | end
417 |
418 | # :stopdoc:
419 | # @private
420 | class LogfileReader
421 | attr_accessor :filename
422 | attr_reader :look_for
423 |
424 | def initialize
425 | @filename = nil
426 | @look_for = nil
427 | end
428 |
429 | def look_for=( val )
430 | case val
431 | when nil; @look_for = nil
432 | when String; @look_for = Regexp.new(Regexp.escape(val))
433 | when Regexp; @look_for = val
434 | else
435 | raise Error,
436 | "Don't know how to look for #{val.inspect} in the logfile"
437 | end
438 | end
439 |
440 | def stat
441 | s = File.stat(@filename) if @filename && test(?f, @filename)
442 | s || OpenStruct.new(:mtime => Time.at(0), :size => 0)
443 | end
444 |
445 | def updated?
446 | s = stat
447 | @stat ||= s
448 |
449 | return false if s.nil?
450 | return false if @stat.mtime == s.mtime and @stat.size == s.size
451 | return true if @look_for.nil?
452 |
453 | File.open(@filename, 'r') do |fd|
454 | fd.seek @stat.size, IO::SEEK_SET
455 | while line = fd.gets
456 | return true if line =~ @look_for
457 | end
458 | end
459 |
460 | return false
461 | ensure
462 | @stat = s
463 | end
464 | end
465 | # :startdoc:
466 | end
467 |
--------------------------------------------------------------------------------
/lib/servolux/null_logger.rb:
--------------------------------------------------------------------------------
1 | require "singleton"
2 | require "logger"
3 |
4 | module Servolux
5 | # A "do nothing" implementation of the standard Ruby Logger class.
6 | class NullLogger < Logger
7 | include Singleton
8 | def initialize(*args); end
9 | def add(*args, &block); end
10 | end
11 |
12 | # Syntactic sugar for getting the null logger instance.
13 | #
14 | # Servolux::NullLogger() #=> NullLogger singleton instance
15 | #
16 | def self.NullLogger
17 | NullLogger.instance
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/servolux/pid_file.rb:
--------------------------------------------------------------------------------
1 | # == Synopsis
2 | # The PidFile manages the lifecycle of a PID file.
3 | #
4 | # == Details
5 | # A PID file contains the process ID of a given program. This file can be used
6 | # by the program to indicate that it started successfully. The file can be used
7 | # programmatically to look up the process ID and send signals to the program.
8 | # The file can be used to ensure two instances of the same program are not
9 | # started at the same time.
10 | #
11 | # The PidFile class supports creating and deleting PID files. Methods are
12 | # provided to check if the program associated with the PID is `alive?`. Signals
13 | # can be sent to the program using the `kill` method.
14 | #
15 | # == Examples
16 | #
17 | # Here is a simple example creating a PID file in the "/var/run" directory.
18 | #
19 | # pid_file = Servolux::PidFile.new(:name => "test", :path => "/var/run")
20 | # pid_file.filename #=> "/var/run/test.pid"
21 | # pid_file.write
22 | #
23 | # From another process we can access this PID file and send a `HUP` signal to
24 | # the program.
25 | #
26 | # pid_file = Servolux::PidFile.new(:name => "test", :path => "/var/run")
27 | # pid_file.kill("HUP") if pid_file.alive?
28 | #
29 | class Servolux::PidFile
30 |
31 | DEFAULT_MODE = 0640
32 |
33 | attr_accessor :name # the process name
34 | attr_accessor :path # the path to the PID file
35 | attr_accessor :mode # PID file permissions mode
36 | attr_accessor :logger # logger for outputting messages
37 |
38 | # Create a new PID file instance.
39 | #
40 | # opts - The options Hash
41 | # :name - the name of the program
42 | # :path - path to the PID file location
43 | # :mode - file permissions mode
44 | # :logger - logger for outputting messages
45 | #
46 | def initialize( opts = {} )
47 | @name = opts.fetch(:name, $0)
48 | @path = opts.fetch(:path, ".")
49 | @mode = opts.fetch(:mode, DEFAULT_MODE)
50 | @logger = opts.fetch(:logger, Servolux::NullLogger())
51 |
52 | yield self if block_given?
53 | end
54 |
55 | # Returns the full name of the PID file including path and extension.
56 | def filename
57 | fn = name.to_s.downcase.tr(" ","_") + ".pid"
58 | fn = File.join(path, fn) unless path.nil?
59 | fn
60 | end
61 |
62 | # Writes the given `pid` to the PID file. The `pid` defaults to the current
63 | # process ID.
64 | #
65 | # pid - The process ID to write to the file
66 | #
67 | # Returns the filename of PID file.
68 | # Raises Errno::EACCESS if you do not have permission to write the file.
69 | def write( pid = Process.pid )
70 | fn = filename
71 | logger.debug "Writing pid file #{fn.inspect}"
72 | File.open(fn, 'w', mode) { |fd| fd.write(pid.to_s) }
73 | fn
74 | end
75 |
76 | # Delete the PID file if it exists. This method first checks that the current
77 | # process PID is the same as the PID stored in the file. If the PIDs do not
78 | # match, then this method returns `nil` without taking any action.
79 | #
80 | # Returns the filename of the deleted file or `nil` if no action was taken.
81 | # Raises Errno::EACCESS if you do not have permission to delete the file.
82 | def delete
83 | return unless pid == Process.pid
84 | fn = filename
85 | logger.debug "Deleting pid file #{fn.inspect}"
86 | File.delete fn
87 | fn
88 | end
89 |
90 | # Forcibly delete the PID file if it exists. This method does NOT check that
91 | # the current process PID against the PID stored in the file.
92 | #
93 | # Returns the filename of the deleted file or `nil` if no action was taken.
94 | # Raises Errno::EACCESS if you do not have permission to delete the file.
95 | def delete!
96 | return unless exist?
97 | fn = filename
98 | logger.debug "Deleting pid file #{fn.inspect}"
99 | File.delete fn
100 | fn
101 | end
102 |
103 | # Returns `true` if the PID file exists. Returns `false` otherwise.
104 | def exist?
105 | File.exist? filename
106 | end
107 |
108 | # Returns the numeric PID read from the file or `nil` if the file does not
109 | # exist. If you do not have permission to access the file `nil` is returned.
110 | def pid
111 | fn = filename
112 | Integer(File.read(fn).strip) if File.exist?(fn)
113 | rescue Errno::EACCES => err
114 | logger.error "You do not have access to the PID file at " \
115 | "#{fn.inspect}: #{err.message}"
116 | nil
117 | end
118 |
119 | # Returns `true` if the process is currently running. Returns `false` if this
120 | # is not the case. The status of the process is determined by sending signal 0
121 | # to the process.
122 | def alive?
123 | pid = self.pid
124 | return if pid.nil?
125 |
126 | Process.kill(0, pid)
127 | true
128 | rescue Errno::ESRCH, Errno::ENOENT
129 | false
130 | end
131 |
132 | # Send a signal the process identified by the PID file. The default signal to
133 | # send is 'INT' (2). The signal can be given either as a string or a signal
134 | # number.
135 | #
136 | # signal - The signal to send to the process (String or Integer)
137 | #
138 | # Returns an Integer or `nil` if an error was encountered.
139 | def kill( signal = 'INT' )
140 | pid = self.pid
141 | return if pid.nil?
142 |
143 | signal = Signal.list.invert[signal] if signal.is_a?(Integer)
144 | logger.info "Killing PID #{pid} with #{signal}"
145 | Process.kill(signal, pid)
146 |
147 | rescue Errno::EINVAL
148 | logger.error "Failed to kill PID #{pid} with #{signal}: " \
149 | "'#{signal}' is an invalid or unsupported signal number."
150 | nil
151 | rescue Errno::EPERM
152 | logger.error "Failed to kill PID #{pid} with #{signal}: " \
153 | "Insufficient permissions."
154 | nil
155 | rescue Errno::ESRCH
156 | logger.error "Failed to kill PID #{pid} with #{signal}: " \
157 | "Process is deceased or zombie."
158 | nil
159 | rescue Errno::EACCES => err
160 | logger.error err.message
161 | nil
162 | rescue Errno::ENOENT => err
163 | logger.error "Could not find a PID file at #{pid_file.inspect}. " \
164 | "Most likely the process is no longer running."
165 | nil
166 | rescue Exception => err
167 | unless err.is_a?(SystemExit)
168 | logger.error "Failed to kill PID #{pid} with #{signal}: #{err.message}"
169 | end
170 | nil
171 | end
172 | end
173 |
--------------------------------------------------------------------------------
/lib/servolux/piper.rb:
--------------------------------------------------------------------------------
1 | require 'socket'
2 |
3 | # == Synopsis
4 | # A Piper is used to fork a child process and then establish a communication
5 | # pipe between the parent and child. This communication pipe is used to pass
6 | # Ruby objects between the two.
7 | #
8 | # == Details
9 | # When a new piper instance is created, the Ruby process is forked into two
10 | # processes - the parent and the child. Each continues execution from the
11 | # point of the fork. The piper establishes a pipe for communication between
12 | # the parent and the child. This communication pipe can be opened as read /
13 | # write / read-write (from the perspective of the parent).
14 | #
15 | # Communication over the pipe is handled by marshalling Ruby objects through
16 | # the pipe. This means that nearly any Ruby object can be passed between the
17 | # two processes. For example, exceptions from the child process can be
18 | # marshalled back to the parent and raised there.
19 | #
20 | # Object passing is handled by use of the +puts+ and +gets+ methods defined
21 | # on the Piper. These methods use a +timeout+ and the Kernel#select method
22 | # to ensure a timely return.
23 | #
24 | # == Examples
25 | #
26 | # piper = Servolux::Piper.new('r', :timeout => 5)
27 | #
28 | # piper.parent {
29 | # $stdout.puts "parent pid #{Process.pid}"
30 | # $stdout.puts "child pid #{piper.pid} [from fork]"
31 | #
32 | # child_pid = piper.gets
33 | # $stdout.puts "child pid #{child_pid} [from child]"
34 | #
35 | # msg = piper.gets
36 | # $stdout.puts "message from child #{msg.inspect}"
37 | # }
38 | #
39 | # piper.child {
40 | # sleep 2
41 | # piper.puts Process.pid
42 | # sleep 3
43 | # piper.puts "The time is #{Time.now}"
44 | # }
45 | #
46 | # piper.close
47 | #
48 | class Servolux::Piper
49 |
50 | # :stopdoc:
51 | SIZEOF_INT = [42].pack('I').size # @private
52 | # :startdoc:
53 |
54 | # Creates a new Piper with the child process configured as a daemon. The
55 | # +pid+ method of the piper returns the PID of the daemon process.
56 | #
57 | # By default a daemon process will release its current working directory
58 | # and the stdout/stderr/stdin file descriptors. This allows the parent
59 | # process to exit cleanly. This behavior can be overridden by setting the
60 | # _nochdir_ and _noclose_ flags to true. The first will keep the current
61 | # working directory; the second will keep stdout/stderr/stdin open.
62 | #
63 | # @param [Boolean] nochdir Do not change working directories
64 | # @param [Boolean] noclose Do not close stdin, stdout, and stderr
65 | # @return [Piper]
66 | #
67 | def self.daemon( nochdir = false, noclose = false )
68 | piper = self.new(:timeout => 1)
69 | piper.parent {
70 | pid = piper.gets
71 | raise ::Servolux::Error, 'Could not get the child PID.' if pid.nil?
72 |
73 | piper.wait # reap the child process
74 | piper.instance_variable_set(:@child_pid, pid) # adopt the grandchild
75 | }
76 | piper.child {
77 | Process.setsid # Become session leader.
78 | exit!(0) if fork # Zap session leader.
79 |
80 | Dir.chdir '/' unless nochdir # Release old working directory.
81 | File.umask 0000 # Ensure sensible umask.
82 |
83 | unless noclose
84 | STDIN.reopen '/dev/null' # Free file descriptors and
85 | STDOUT.reopen '/dev/null', 'a' # point them somewhere sensible.
86 | STDERR.reopen '/dev/null', 'a'
87 | end
88 |
89 | piper.puts Process.pid
90 | }
91 | return piper
92 | end
93 |
94 | # The timeout in seconds to wait for puts / gets commands.
95 | attr_accessor :timeout
96 |
97 | # The underlying socket the piper is using for communication.
98 | attr_reader :socket
99 |
100 | # @overload Piper.new( mode = 'r', opts = {} )
101 | # Creates a new Piper instance with the communication pipe configured
102 | # using the provided _mode_. The default mode is read-only (from the
103 | # parent, and write-only from the child). The supported modes are as
104 | # follows:
105 | #
106 | # Mode | Parent View | Child View
107 | # -------------------------------
108 | # r read-only write-only
109 | # w write-only read-only
110 | # rw read-write read-write
111 | #
112 | # @param [String] mode The communication mode of the pipe.
113 | # @option opts [Numeric] :timeout (nil)
114 | # The number of seconds to wait for a +puts+ or +gets+ to succeed. If not
115 | # specified, calls through the pipe will block forever until data is
116 | # available. You can configure the +puts+ and +gets+ to be non-blocking
117 | # by setting the timeout to +0+.
118 | # @return [Piper]
119 | #
120 | def initialize( *args )
121 | opts = args.last.is_a?(Hash) ? args.pop : {}
122 | mode = args.first || 'r'
123 |
124 | unless %w[r w rw].include? mode
125 | raise ArgumentError, "Unsupported mode #{mode.inspect}"
126 | end
127 |
128 | @status = nil
129 | @timeout = opts.fetch(:timeout, nil)
130 | socket_pair = Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM, 0)
131 | @child_pid = Kernel.fork
132 |
133 | if child?
134 | @socket = socket_pair[1]
135 | socket_pair[0].close
136 |
137 | case mode
138 | when 'r'; @socket.close_read
139 | when 'w'; @socket.close_write
140 | end
141 | else
142 | @socket = socket_pair[0]
143 | socket_pair[1].close
144 |
145 | case mode
146 | when 'r'; @socket.close_write
147 | when 'w'; @socket.close_read
148 | end
149 | end
150 | end
151 |
152 | # Close both the communications socket. This only affects the process from
153 | # which it was called -- the parent or the child.
154 | #
155 | # @return [Piper] self
156 | #
157 | def close
158 | @socket.close unless @socket.closed?
159 | self
160 | end
161 |
162 | # Returns +true+ if the piper has been closed. Returns +false+ otherwise.
163 | #
164 | # @return [Boolean]
165 | #
166 | def closed?
167 | @socket.closed?
168 | end
169 |
170 | # Returns +true+ if the communications pipe is readable from the process
171 | # and there is data waiting to be read.
172 | #
173 | # @return [Boolean]
174 | #
175 | def readable?
176 | return false if @socket.closed?
177 | r,_,_ = Kernel.select([@socket], nil, nil, @timeout) rescue nil
178 | return !(r.nil? or r.empty?)
179 | end
180 |
181 | # Returns +true+ if the communications pipe is writeable from the process
182 | # and the write buffer can accept more data.
183 | #
184 | # @return [Boolean]
185 | #
186 | def writeable?
187 | return false if @socket.closed?
188 | _,w,_ = Kernel.select(nil, [@socket], nil, @timeout) rescue nil
189 | return !(w.nil? or w.empty?)
190 | end
191 |
192 | # Execute the _block_ only in the child process. This method returns
193 | # immediately when called from the parent process. The piper instance is
194 | # passed to the block if the arity is non-zero.
195 | #
196 | # @yield [self] Execute the block in the child process
197 | # @yieldparam [Piper] self The piper instance (optional)
198 | # @return The return value from the block or +nil+ when called from the
199 | # parent.
200 | #
201 | def child( &block )
202 | return unless child?
203 | raise ArgumentError, "A block must be supplied" if block.nil?
204 |
205 | if block.arity > 0
206 | block.call(self)
207 | else
208 | block.call
209 | end
210 | end
211 |
212 | # Returns +true+ if this is the child process and +false+ otherwise.
213 | #
214 | # @return [Boolean]
215 | #
216 | def child?
217 | @child_pid.nil?
218 | end
219 |
220 | # Execute the _block_ only in the parent process. This method returns
221 | # immediately when called from the child process. The piper instance is
222 | # passed to the block if the arity is non-zero.
223 | #
224 | # @yield [self] Execute the block in the parent process
225 | # @yieldparam [Piper] self The piper instance (optional)
226 | # @return The return value from the block or +nil+ when called from the
227 | # child.
228 | #
229 | def parent( &block )
230 | return unless parent?
231 | raise ArgumentError, "A block must be supplied" if block.nil?
232 |
233 | if block.arity > 0
234 | block.call(self)
235 | else
236 | block.call
237 | end
238 | end
239 |
240 | # Returns +true+ if this is the parent process and +false+ otherwise.
241 | #
242 | # @return [Boolean]
243 | #
244 | def parent?
245 | !@child_pid.nil?
246 | end
247 |
248 | # Returns the PID of the child process when called from the parent.
249 | # Returns +nil+ when called from the child.
250 | #
251 | # @return [Integer, nil] The PID of the child process or +nil+
252 | #
253 | def pid
254 | @child_pid
255 | end
256 |
257 | # Read an object from the communication pipe. If data is available then it
258 | # is un-marshalled and returned as a Ruby object. If the pipe is closed for
259 | # reading or if no data is available then the _default_ value is returned.
260 | # You can pass in the _default_ value; otherwise it will be +nil+.
261 | #
262 | # This method will block until the +timeout+ is reached or data can be
263 | # read from the pipe.
264 | #
265 | def gets( default = nil )
266 | return default unless readable?
267 |
268 | data = @socket.read SIZEOF_INT
269 | return default if data.nil?
270 |
271 | size = data.unpack('I').first
272 | data = @socket.read size
273 | return default if data.nil?
274 |
275 | Marshal.load(data) rescue data
276 | rescue SystemCallError
277 | return default
278 | end
279 |
280 | # Write an object to the communication pipe. Returns +nil+ if the pipe is
281 | # closed for writing or if the write buffer is full. The _obj_ is
282 | # marshalled and written to the pipe (therefore, procs and other
283 | # un-marshallable Ruby objects cannot be passed through the pipe).
284 | #
285 | # If the write is successful, then the number of bytes written to the pipe
286 | # is returned. If this number is zero it means that the _obj_ was
287 | # unsuccessfully communicated (sorry).
288 | #
289 | # @param [Object] obj The data to send to the "other" process. The object
290 | # must be marshallable by Ruby (no Proc objects or lambdas).
291 | # @return [Integer, nil] The number of bytes written to the pipe or +nil+ if
292 | # there was an error or the pipe is not writeable.
293 | #
294 | def puts( obj )
295 | return unless writeable?
296 |
297 | data = Marshal.dump(obj)
298 | @socket.write([data.size].pack('I')) + @socket.write(data)
299 | rescue SystemCallError
300 | return nil
301 | end
302 |
303 | # Send the given signal to the child process. The signal may be an integer
304 | # signal number or a POSIX signal name (either with or without a +SIG+
305 | # prefix).
306 | #
307 | # This method does nothing when called from the child process.
308 | #
309 | # @param [String, Integer] sig The signal to send to the child process.
310 | # @return [Integer, nil] The result of Process#kill or +nil+ if called from
311 | # the child process.
312 | #
313 | def signal( sig )
314 | return if child?
315 | return unless alive?
316 | Process.kill(sig, @child_pid)
317 | end
318 |
319 | # Waits for the child process to exit and returns its exit status. The
320 | # global variable $? is set to a Process::Status object containing
321 | # information on the child process.
322 | #
323 | # Always returns +nil+ when called from the child process.
324 | #
325 | # You can get more information about how the child status exited by calling
326 | # the following methods on the piper instance:
327 | #
328 | # * coredump?
329 | # * exited?
330 | # * signaled?
331 | # * stopped?
332 | # * success?
333 | # * exitstatus
334 | # * stopsig
335 | # * termsig
336 | #
337 | # @param [Integer] flags Bit flags that will be passed to the system level
338 | # wait call. See the Ruby core documentation for Process#wait for more
339 | # information on these flags.
340 | # @return [Integer, nil] The exit status of the child process or +nil+ if
341 | # the child process is not running.
342 | #
343 | def wait( flags = 0 )
344 | return if child?
345 | _, @status = Process.wait2(@child_pid, flags) unless @status
346 | exitstatus
347 | rescue Errno::ECHILD
348 | nil
349 | end
350 |
351 | # Returns +true+ if the child process is alive. Returns +nil+ if the child
352 | # process has not been started.
353 | #
354 | # Always returns +nil+ when called from the child process.
355 | #
356 | # @return [Boolean, nil]
357 | #
358 | def alive?
359 | return if child?
360 | wait(Process::WNOHANG|Process::WUNTRACED)
361 | Process.kill(0, @child_pid)
362 | true
363 | rescue Errno::ESRCH, Errno::ENOENT, Errno::ECHILD
364 | false
365 | end
366 |
367 | %w[coredump? exited? signaled? stopped? success? exitstatus stopsig termsig].
368 | each { |method|
369 | self.class_eval <<-CODE
370 | def #{method}
371 | return if @status.nil?
372 | @status.#{method}
373 | end
374 | CODE
375 | }
376 | end
377 |
378 |
--------------------------------------------------------------------------------
/lib/servolux/prefork.rb:
--------------------------------------------------------------------------------
1 |
2 | # == Synopsis
3 | # The Prefork class provides a pre-forking worker pool for executing tasks in
4 | # parallel using multiple processes.
5 | #
6 | # == Details
7 | # A pre-forking worker pool is a technique for executing code in parallel in a
8 | # UNIX environment. Each worker in the pool forks a child process and then
9 | # executes user supplied code in that child process. The child process can
10 | # pull jobs from a queue (beanstalkd for example) or listen on a socket for
11 | # network requests.
12 | #
13 | # The code to execute in the child processes is passed as a block to the
14 | # Prefork initialize method. The child processes executes this code in a loop;
15 | # that is, your code block should not worry about keeping itself alive. This
16 | # is handled by the library.
17 | #
18 | # If your code raises an exception, it will be captured by the library code
19 | # and marshalled back to the parent process. This will halt the child process.
20 | # The Prefork worker pool does not restart dead workers. A method is provided
21 | # to iterate over workers that have errors, and it is up to the user to handle
22 | # errors as they please.
23 | #
24 | # Instead of passing a block to the initialize method, you can provide a Ruby
25 | # module that defines an "execute" method. This method will be executed in the
26 | # child process' run loop. When using a module, you also have the option of
27 | # defining a "before_executing" method and an "after_executing" method. These
28 | # methods will be called before the child starts the execute loop and after
29 | # the execute loop finishes. Each method will be called exactly once. Both
30 | # methods are optional.
31 | #
32 | # Sending a SIGHUP to a child process will cause that child to stop and
33 | # restart. The child will send a signal to the parent asking to be shutdown.
34 | # The parent will gracefully halt the child and then start a new child process
35 | # to replace it. If you define a "hup" method in your worker module, it will
36 | # be executed when SIGHUP is received by the child. Your "hup" method will be
37 | # the last method executed in the signal handler.
38 | #
39 | # This has the advantage of calling your before/after_executing methods again
40 | # and reloading any code or resources your worker code will use. The SIGHUP
41 | # will call Thread#wakeup on the main child process thread; please write your
42 | # code to respond accordingly to this wakeup call (a thread waiting on a
43 | # Queue#pop will not return when wakeup is called on the thread).
44 | #
45 | # == Examples
46 | #
47 | # A pre-forking echo server: http://github.com/TwP/servolux/blob/master/examples/echo.rb
48 | #
49 | # Pulling jobs from a beanstalkd work queue: http://github.com/TwP/servolux/blob/master/examples/beanstalk.rb
50 | #
51 | # ==== Before / After Executing
52 | # In this example, we are creating 42 worker processes that will log the
53 | # process ID and the current time to a file. Each worker will do this every 2
54 | # seconds. The before/after_executing methods are used to open the file before
55 | # the run loop starts and to close the file after the run loop completes. The
56 | # execute method uses the stored file descriptor when logging the message.
57 | #
58 | # module RunMe
59 | # def before_executing
60 | # @fd = File.open("#{Process.pid}.txt", 'w')
61 | # end
62 | #
63 | # def after_executing
64 | # @fd.close
65 | # end
66 | #
67 | # def execute
68 | # @fd.puts "Process #{Process.pid} @ #{Time.now}"
69 | # sleep 2
70 | # end
71 | # end
72 | #
73 | # pool = Servolux::Prefork.new(:module => RunMe)
74 | # pool.start 42
75 | #
76 | # ==== Heartbeat
77 | # When a :timeout is supplied to the constructor, a "heartbeat" is setup
78 | # between the parent and the child worker. Each loop through the child's
79 | # execute code must return before :timeout seconds have elapsed. If one
80 | # iteration through the loop takes longer than :timeout seconds, then the
81 | # parent process will halt the child worker. An error will be raised in the
82 | # parent process.
83 | #
84 | # pool = Servolux::Prefork.new(:timeout => 2) {
85 | # puts "Process #{Process.pid} is running."
86 | # sleep(rand * 5)
87 | # }
88 | # pool.start 42
89 | #
90 | # Eventually all 42 child processes will be killed by their parents. The
91 | # random number generator will eventually cause the child to sleep longer than
92 | # two seconds.
93 | #
94 | # What is happening here is that each time the child processes executes the
95 | # block of code, the Servolux library code will send a "heartbeat" message to
96 | # the parent. The parent is using a Kernel#select call on the communications
97 | # pipe to wait for this message. The timeout is passed to the select call, and
98 | # this will cause it to return +nil+ -- this is the error condition the
99 | # heartbeat prevents.
100 | #
101 | # Use the heartbeat with caution -- allow margins for timing issues and
102 | # processor load spikes.
103 | #
104 | # ==== Signals
105 | # Forked child processes are configured to respond to two signals: SIGHUP and
106 | # SIGTERM. The SIGHUP signal when sent to a child process is used to restart
107 | # just that one child. The SIGTERM signal when sent to a child process is used
108 | # to forcibly kill the child; it will not be restarted. The parent process
109 | # uses SIGTERM to halt all the children when it is stopping.
110 | #
111 | # SIGHUP
112 | # Child processes are restarted by sending a SIGHUP signal to the child. This
113 | # will shutdown the child worker and then start up a new one to replace it.
114 | # For the child to shutdown gracefully, it needs to return from the "execute"
115 | # method when it receives the signal. Define a "hup" method that will wake the
116 | # execute thread from any pending operations -- listening on a socket, reading
117 | # a file, polling a queue, etc. When the execute method returns, the child
118 | # will exit.
119 | #
120 | # SIGTERM
121 | # Child processes are stopped by the prefork parent by sending a SIGTERM
122 | # signal to the child. For the child to shutdown gracefully, it needs to
123 | # return from the "execute" method when it receives the signal. Define a
124 | # "term" method that will wake the execute thread from any pending operations
125 | # -- listening on a socket, reading a file, polling a queue, etc. When the
126 | # execute method returns, the child will exit.
127 | #
128 | class Servolux::Prefork
129 |
130 | CommunicationError = Class.new(::Servolux::Error)
131 | UnknownSignal = Class.new(::Servolux::Error)
132 | UnknownResponse = Class.new(::Servolux::Error)
133 |
134 | # :stopdoc:
135 | START = "\000START".freeze # @private
136 | HALT = "\000HALT".freeze # @private
137 | ERROR = "\000SHIT".freeze # @private
138 | HEARTBEAT = "\000<3".freeze # @private
139 | # :startdoc:
140 |
141 | attr_accessor :timeout # Communication timeout in seconds.
142 | attr_accessor :min_workers # Minimum number of workers
143 | attr_accessor :max_workers # Maximum number of workers
144 | attr_accessor :config # Worker configuration options (a Hash)
145 |
146 | # call-seq:
147 | # Prefork.new { block }
148 | # Prefork.new( :module => Module )
149 | #
150 | # Create a new pre-forking worker pool. You must provide a block of code for
151 | # the workers to execute in their child processes. This code block can be
152 | # passed either as a block to this method or as a module via the :module
153 | # option.
154 | #
155 | # If a :timeout is given, then each worker will setup a "heartbeat" between
156 | # the parent process and the child process. If the child does not respond to
157 | # the parent within :timeout seconds, then the child process will be halted.
158 | # If you do not want to use the heartbeat then leave the :timeout unset or
159 | # manually set it to +nil+.
160 | #
161 | # Additionally, :min_workers and :max_workers options are avilable. If
162 | # :min_workers is given, the method +ensure_worker_pool_size+ will guarantee
163 | # that at least :min_workers are up and running. If :max_workers is given,
164 | # then +add_workers+ will NOT allow ou to spawn more workers than
165 | # :max_workers.
166 | #
167 | # The pre-forking worker pool makes no effort to restart dead workers. It is
168 | # left to the user to implement this functionality.
169 | #
170 | def initialize( opts = {}, &block )
171 | @timeout = opts.fetch(:timeout, nil)
172 | @module = opts.fetch(:module, nil)
173 | @max_workers = opts.fetch(:max_workers, nil)
174 | @min_workers = opts.fetch(:min_workers, nil)
175 | @config = opts.fetch(:config, {})
176 | @module = Module.new { define_method :execute, &block } if block
177 | @workers = []
178 |
179 | raise ArgumentError, 'No code was given to execute by the workers.' unless @module
180 | end
181 |
182 | # Start up the given _number_ of workers. Each worker will create a child
183 | # process and run the user supplied code in that child process.
184 | #
185 | # @param [Integer] number The number of workers to prefork
186 | # @return [Prefork] self
187 | #
188 | def start( number )
189 | @workers.clear
190 | add_workers( number )
191 | self
192 | end
193 |
194 | # Stop all workers. The current process will wait for each child process to
195 | # exit before this method will return. The worker instances are not
196 | # destroyed by this method; this means that the +each_worker+ and the
197 | # +errors+ methods will still function correctly after stopping the workers.
198 | #
199 | def stop
200 | @workers.each { |worker| worker.stop; pause }
201 | reap
202 | self
203 | end
204 |
205 | # This method should be called periodically in order to clear the return
206 | # status from child processes that have either died or been restarted (via a
207 | # HUP signal). This will remove zombie children from the process table.
208 | #
209 | # @return [Prefork] self
210 | #
211 | def reap
212 | @workers.each { |worker| worker.reap }
213 | self
214 | end
215 |
216 | # Send this given _signal_ to all child process. The default signal is
217 | # 'TERM'. The method waits for a short period of time after the signal is
218 | # sent to each child; this is done to alleviate a flood of signals being
219 | # sent simultaneously and overwhelming the CPU.
220 | #
221 | # @param [String, Integer] signal The signal to send to child processes.
222 | # @return [Prefork] self
223 | #
224 | def signal( signal = 'TERM' )
225 | @workers.each { |worker| worker.signal(signal); pause }
226 | self
227 | end
228 | alias :kill :signal
229 |
230 | # call-seq:
231 | # each_worker { |worker| block }
232 | #
233 | # Iterates over all the workers and yields each, in turn, to the given
234 | # _block_.
235 | #
236 | def each_worker( &block )
237 | @workers.each(&block)
238 | self
239 | end
240 |
241 | # call-seq:
242 | # add_workers( number = 1 )
243 | #
244 | # Adds additional workers to the pool. It will not add more workers than
245 | # the number set in :max_workers
246 | #
247 | def add_workers( number = 1 )
248 | number.times do
249 | break if at_max_workers?
250 | worker = Worker.new(self, @config)
251 | worker.extend @module
252 | worker.start
253 | @workers << worker
254 | pause
255 | end
256 | end
257 |
258 | # call-seq:
259 | # prune_workers()
260 | #
261 | # Remove workers that are no longer alive from the worker pool
262 | #
263 | def prune_workers
264 | new_workers = @workers.find_all { |w| w.reap.alive? }
265 | @workers = new_workers
266 | end
267 |
268 | # call-seq:
269 | # ensure_worker_pool_size()
270 | #
271 | # Make sure that the worker pool has >= the minimum number of workers and less
272 | # than the maximum number of workers.
273 | #
274 | # Generally, this means prune the number of workers and then spawn workers up
275 | # to the min_worker level. If min is not set, then we only prune
276 | #
277 | def ensure_worker_pool_size
278 | prune_workers
279 | while below_minimum_workers? do
280 | add_workers
281 | end
282 | end
283 |
284 | # call-seq:
285 | # below_minimum_workers?
286 | #
287 | # Report if the number of workers is below the minimum threshold
288 | #
289 | def below_minimum_workers?
290 | return false unless @min_workers
291 | return @workers.size < @min_workers
292 | end
293 |
294 | # call-seq:
295 | # at_max_workers?
296 | #
297 | # Return true or false if we are currently at or above the maximum number of
298 | # workers allowed.
299 | #
300 | def at_max_workers?
301 | return false unless @max_workers
302 | return @workers.size >= @max_workers
303 | end
304 |
305 | # call-seq:
306 | # errors { |worker| block }
307 | #
308 | # Iterates over all the workers and yields the worker to the given _block_
309 | # only if the worker has an error condition.
310 | #
311 | def errors
312 | @workers.each { |worker| yield worker unless worker.error.nil? }
313 | self
314 | end
315 |
316 | # call-seq:
317 | # worker_counts -> { :alive => 2, :dead => 1 }
318 | #
319 | # Returns a hash containing the counts of alive and dead workers
320 | def worker_counts
321 | counts = { :alive => 0, :dead => 0 }
322 | each_worker do |worker|
323 | state = worker.alive? ? :alive : :dead
324 | counts[state] += 1
325 | end
326 | return counts
327 | end
328 |
329 | # call-seq:
330 | # live_worker_count -> Integer
331 | #
332 | # Returns the number of live workers in the pool
333 | def live_worker_count
334 | worker_counts[:alive]
335 | end
336 |
337 | # call-seq:
338 | # dead_worker_count -> Integer
339 | #
340 | # Returns the number of dead workers in the pool
341 | def dead_worker_count
342 | worker_counts[:dead]
343 | end
344 |
345 | private
346 |
347 | # Pause script execution for a random time interval between 0.1 and 0.4
348 | # seconds. This method is used to slow down the starting and stopping of
349 | # child processes in order to avoid the "thundering herd" problem.
350 | # http://en.wikipedia.org/wiki/Thundering_herd_problem
351 | #
352 | def pause
353 | sleep(0.1 + 0.3*rand)
354 | end
355 |
356 | # The worker encapsulates the forking of the child process and communication
357 | # between the parent and the child. Each worker instance is extended with
358 | # the block or module supplied to the pre-forking pool that created the
359 | # worker.
360 | #
361 | class Worker
362 |
363 | attr_reader :error
364 |
365 | attr_reader :config
366 |
367 | # Create a new worker that belongs to the _prefork_ pool.
368 | #
369 | # @param [Prefork] prefork The prefork pool that created this worker.
370 | # @param [Hash] config The worker configuration options.
371 | #
372 | def initialize( prefork, config )
373 | @timeout = prefork.timeout
374 | @config = config
375 | @thread = nil
376 | @piper = nil
377 | @error = nil
378 | @pid_list = []
379 | end
380 |
381 | # Start this worker. A new process will be forked, and the code supplied
382 | # by the user to the prefork pool will be executed in the child process.
383 | #
384 | # @return [Worker] self
385 | #
386 | def start
387 | @pid_list << @piper.pid if @piper
388 | @error = nil
389 | @piper = ::Servolux::Piper.new('rw', :timeout => @timeout)
390 | @piper.parent? ? parent : child
391 | self
392 | end
393 |
394 | # Stop this worker. The internal worker thread is stopped and a 'HUP'
395 | # signal is sent to the child process. This method will return immediately
396 | # without waiting for the child process to exit. Use the +wait+ method
397 | # after calling +stop+ if your code needs to know when the child exits.
398 | #
399 | # @return [Worker, nil] self
400 | #
401 | def stop
402 | return if @thread.nil? or @piper.nil? or @piper.child?
403 |
404 | @thread[:stop] = true
405 | @thread.wakeup if @thread.status
406 | close_parent
407 | signal 'TERM'
408 | @thread.join(0.5)
409 | @thread = nil
410 | self
411 | end
412 |
413 | # Wait for the child process to exit. This method returns immediately when
414 | # called from the child process or if the child process has not yet been
415 | # forked.
416 | #
417 | def wait
418 | return if @piper.nil? or @piper.child?
419 | @piper.wait(Process::WUNTRACED)
420 | end
421 |
422 | # Send this given _signal_ to the child process. The default signal is
423 | # 'TERM'. This method will return immediately.
424 | #
425 | # @param [String, Integer] signal The signal to send to the child process.
426 | # @return [Integer, nil] The result of Process#kill or +nil+ if called from
427 | # the child process.
428 | #
429 | def signal( signal = 'TERM' )
430 | return if @piper.nil?
431 | @piper.signal signal
432 | rescue Errno::ESRCH, Errno::ENOENT
433 | return nil
434 | end
435 | alias :kill :signal
436 |
437 | # Returns +true+ if the child process is alive. Returns +nil+ if the child
438 | # process has not been started.
439 | #
440 | # Always returns +nil+ when called from the child process.
441 | #
442 | # @return [Boolean, nil]
443 | #
444 | def alive?
445 | return if @piper.nil?
446 | @piper.alive?
447 | end
448 |
449 | # Internal: Attempt to reap any child processes spawned by this worker. If a
450 | # child has exited, then we remove it from our PID list.
451 | #
452 | # @return [Worker] this worker instance.
453 | def reap
454 | @piper.alive? unless @piper.nil?
455 | @pid_list.dup.each do |pid|
456 | @pid_list.delete(pid) if reap?(pid)
457 | end
458 | self
459 | end
460 |
461 | # Internal: Check the return status of the given child PID. This will reap
462 | # the process from the kernel process table if the child has exited.
463 | #
464 | # @return [Boolean] true if the PID has exited; false otherwise.
465 | def reap?(pid)
466 | _, cstatus = Process.wait2(pid, Process::WNOHANG|Process::WUNTRACED)
467 | return true if cstatus
468 | Process.kill(0, pid)
469 | false
470 | rescue Errno::ESRCH, Errno::ENOENT, Errno::ECHILD
471 | true
472 | end
473 |
474 | # Returns +true+ if communication with the child process timed out.
475 | # Returns +nil+ if the child process has not been started.
476 | #
477 | # Always returns +nil+ when called from the child process.
478 | #
479 | # @return [Boolean, nil]
480 | #
481 | def timed_out?
482 | return if @piper.nil? or @piper.child?
483 | CommunicationError === @error
484 | end
485 |
486 | %w[pid coredump? exited? signaled? stopped? success? exitstatus stopsig termsig].
487 | each { |method|
488 | self.class_eval <<-CODE
489 | def #{method}
490 | return if @piper.nil?
491 | @piper.#{method}
492 | end
493 | CODE
494 | }
495 |
496 | private
497 |
498 | def close_parent
499 | @piper.timeout = 0.5
500 | @piper.puts HALT rescue nil
501 | @piper.close
502 | end
503 |
504 | # This code should only be executed in the parent process.
505 | #
506 | def parent
507 | @thread = Thread.new {
508 | begin
509 | @piper.puts START
510 | Thread.current[:stop] = false
511 | response = parent_loop
512 | rescue StandardError => err
513 | @error = err
514 | ensure
515 | close_parent
516 | start if START == response and !Thread.current[:stop]
517 | end
518 | }
519 | Thread.pass until @thread[:stop] == false
520 | end
521 |
522 | def parent_loop
523 | response = nil
524 | loop {
525 | break if Thread.current[:stop]
526 | break unless @piper.alive?
527 | @piper.puts HEARTBEAT
528 | response = @piper.gets(ERROR)
529 | break if Thread.current[:stop]
530 |
531 | case response
532 | when HEARTBEAT; next
533 | when START; break
534 | when ERROR
535 | raise CommunicationError,
536 | "Unable to read data from Child process. Possible timeout, closing of pipe and/or child death."
537 | when Exception
538 | @error = response
539 | break
540 | else
541 | raise UnknownResponse,
542 | "Child returned unknown response: #{response.inspect}"
543 | end
544 | }
545 | return response
546 | end
547 |
548 | # This code should only be executed in the child process. It wraps the
549 | # user supplied "execute" code in a loop, and takes care of handling
550 | # signals and communication with the parent.
551 | #
552 | def child
553 |
554 | # if we get a HUP signal, then tell the parent process to stop this
555 | # child process and start a new one to replace it
556 | Signal.trap('HUP') {
557 | @piper.timeout = 0.5
558 | @piper.puts START rescue nil
559 | hup if self.respond_to? :hup
560 | }
561 |
562 | Signal.trap('TERM') {
563 | @piper.close
564 | term if self.respond_to? :term
565 | }
566 |
567 | @thread = Thread.new {
568 | begin
569 | :wait until @piper.gets == START
570 | before_executing if self.respond_to? :before_executing
571 | child_loop
572 | rescue Exception => err
573 | @piper.puts err rescue nil
574 | ensure
575 | after_executing if self.respond_to? :after_executing
576 | @piper.close
577 | end
578 | }
579 | @thread.join
580 | ensure
581 | exit!
582 | end
583 |
584 | def child_loop
585 | loop {
586 | signal = @piper.gets(ERROR)
587 | case signal
588 | when HEARTBEAT
589 | execute
590 | @piper.puts HEARTBEAT
591 | when HALT
592 | break
593 | when ERROR
594 | raise CommunicationError,
595 | "Unable to read data from Parent process. Possible timeout, closing of pipe and/or parent death."
596 | else
597 | raise UnknownSignal,
598 | "Child received unknown signal: #{signal.inspect}"
599 | end
600 | }
601 | end
602 | end
603 | end
604 |
605 |
--------------------------------------------------------------------------------
/lib/servolux/server.rb:
--------------------------------------------------------------------------------
1 |
2 | require 'thread'
3 |
4 | # == Synopsis
5 | # The Server class makes it simple to create a server-type application in
6 | # Ruby. A server in this context is any process that should run for a long
7 | # period of time either in the foreground or as a daemon.
8 | #
9 | # == Details
10 | # The Server class provides for standard server features: process ID file
11 | # management, signal handling, run loop, logging, etc. All that you need to
12 | # provide is a +run+ method that will be called by the server's run loop.
13 | # Optionally, you can provide a block to the +new+ method and it will be
14 | # called within the run loop instead of a run method.
15 | #
16 | # SIGINT and SIGTERM are handled by default. These signals will gracefully
17 | # shutdown the server by calling the +shutdown+ method (provided by default,
18 | # too). A few other signals can be handled by defining a few methods on your
19 | # server instance. For example, SIGINT is handled by the +int+ method (an
20 | # alias for +shutdown+). Likewise, SIGTERM is handled by the +term+ method
21 | # (another alias for +shutdown+). The following signal methods are
22 | # recognized by the Server class:
23 | #
24 | # Method | Signal | Default Action
25 | # --------+----------+----------------
26 | # hup SIGHUP none
27 | # int SIGINT shutdown
28 | # term SIGTERM shutdown
29 | # usr1 SIGUSR1 none
30 | # usr2 SIGUSR2 none
31 | #
32 | # In order to handle SIGUSR1 you would define a usr1 method for your
33 | # server.
34 | #
35 | # There are a few other methods that are useful and should be mentioned. Two
36 | # methods are called before and after the run loop starts: +before_starting+
37 | # and +after_starting+. The first is called just before the run loop thread
38 | # is created and started. The second is called just after the run loop
39 | # thread has been created (no guarantee is made that the run loop thread has
40 | # actually been scheduled).
41 | #
42 | # Likewise, two other methods are called before and after the run loop is
43 | # shutdown: +before_stopping+ and +after_stopping+. The first is called just
44 | # before the run loop thread is signaled for shutdown. The second is called
45 | # just after the run loop thread has died; the +after_stopping+ method is
46 | # guaranteed to NOT be called till after the run loop thread is well and
47 | # truly dead.
48 | #
49 | # == Usage
50 | # For simple, quick and dirty servers just pass a block to the Server
51 | # initializer. This block will be used as the run method.
52 | #
53 | # server = Servolux::Server.new('Basic', :interval => 1) {
54 | # puts "I'm alive and well @ #{Time.now}"
55 | # }
56 | # server.startup
57 | #
58 | # For more complex services you will need to define your own server methods:
59 | # the +run+ method, signal handlers, and before/after methods. Any pattern
60 | # that Ruby provides for defining methods on objects can be used to define
61 | # these methods. In a nutshell:
62 | #
63 | # Inheritance
64 | #
65 | # class MyServer < Servolux::Server
66 | # def run
67 | # puts "I'm alive and well @ #{Time.now}"
68 | # end
69 | # end
70 | # server = MyServer.new('MyServer', :interval => 1)
71 | # server.startup
72 | #
73 | # Extension
74 | #
75 | # module MyServer
76 | # def run
77 | # puts "I'm alive and well @ #{Time.now}"
78 | # end
79 | # end
80 | # server = Servolux::Server.new('Module', :interval => 1)
81 | # server.extend MyServer
82 | # server.startup
83 | #
84 | # Singleton Class
85 | #
86 | # server = Servolux::Server.new('Singleton', :interval => 1)
87 | # class << server
88 | # def run
89 | # puts "I'm alive and well @ #{Time.now}"
90 | # end
91 | # end
92 | # server.startup
93 | #
94 | # == Examples
95 | #
96 | # === Signals
97 | # This example shows how to change the log level of the server when SIGUSR1
98 | # is sent to the process. The log level toggles between "debug" and the
99 | # original log level each time SIGUSR1 is sent to the server process. Since
100 | # this is a module, it can be used with any Servolux::Server instance.
101 | #
102 | # module DebugSignal
103 | # def usr1
104 | # @old_log_level ||= nil
105 | # if @old_log_level
106 | # logger.level = @old_log_level
107 | # @old_log_level = nil
108 | # else
109 | # @old_log_level = logger.level
110 | # logger.level = :debug
111 | # end
112 | # end
113 | # end
114 | #
115 | # server = Servolux::Server.new('Debugger', :interval => 2) {
116 | # logger.info "Running @ #{Time.now}"
117 | # logger.debug "hey look - a debug message"
118 | # }
119 | # server.extend DebugSignal
120 | # server.startup
121 | #
122 | class Servolux::Server
123 | include ::Servolux::Threaded
124 |
125 | # :stopdoc:
126 | SIGNALS = %w[HUP INT TERM USR1 USR2] & Signal.list.keys
127 | SIGNALS.each {|sig| sig.freeze}.freeze
128 | # :startdoc:
129 |
130 | Error = Class.new(::Servolux::Error)
131 |
132 | attr_reader :name
133 | attr_accessor :logger
134 | attr_reader :pid_file
135 |
136 | # call-seq:
137 | # Server.new( name, options = {} ) { block }
138 | #
139 | # Creates a new server identified by _name_ and configured from the
140 | # _options_ hash. The _block_ is run inside a separate thread that will
141 | # loop at the configured interval.
142 | #
143 | # ==== Options
144 | # * logger :: The logger instance this server will use
145 | # * pid_file :: Location of the PID file
146 | # * interval :: Sleep interval between invocations of the _block_
147 | #
148 | def initialize( name, opts = {}, &block )
149 | @name = name
150 | @mutex = Mutex.new
151 | @shutdown = nil
152 |
153 | self.logger = opts.fetch(:logger, Servolux::NullLogger())
154 | self.interval = opts.fetch(:interval, 0)
155 | self.pid_file = opts.fetch(:pid_file, name)
156 |
157 | if block
158 | eg = class << self; self; end
159 | eg.__send__(:define_method, :run, &block)
160 | end
161 |
162 | ary = %w[name logger pid_file].map { |var|
163 | self.send(var).nil? ? var : nil
164 | }.compact
165 | raise Error, "These variables are required: #{ary.join(', ')}." unless ary.empty?
166 | end
167 |
168 | # Start the server running using it's own internal thread. This method
169 | # will not return until the server is shutdown.
170 | #
171 | # Startup involves creating a PID file, registering signal handlers to
172 | # shutdown the server, starting and joining the server thread. The PID
173 | # file is deleted when this method returns.
174 | #
175 | # If +true+ is passed to this method, then it will not return until the
176 | # +wait_for_shutdown+ method has been called from another thread. This
177 | # flag is used to ensure that the server has shutdown completely when
178 | # shutdown by a signal.
179 | #
180 | # @return [Server] self
181 | #
182 | def startup( wait = false )
183 | return self if running?
184 | @mutex.synchronize {
185 | @shutdown = ConditionVariable.new
186 | }
187 |
188 | begin
189 | trap_signals
190 | pid_file.write
191 | start
192 | join
193 | wait_for_shutdown if wait
194 | ensure
195 | pid_file.delete
196 | halt_signal_processing
197 | end
198 | return self
199 | end
200 |
201 | # Stop the server if it is running. This method will return after three
202 | # things have occurred:
203 | #
204 | # 1) The 'before_stopping' method has returned.
205 | # 2) The server's activity thread has stopped.
206 | # 3) The 'after_stopping' method has returned.
207 | #
208 | # It is entirely possible that the activity thread will stop before either
209 | # the +before_stopping+ or +after_stopping+ methods return. To make sure
210 | # the server is completely stopped, use the +wait_for_shutdown+ method to
211 | # be notified when the this +shutdown+ method is finished executing.
212 | #
213 | # @return [Server] self
214 | #
215 | def shutdown
216 | return self unless running?
217 | stop
218 | @mutex.synchronize {
219 | @shutdown.signal
220 | @shutdown = nil
221 | }
222 | self
223 | end
224 |
225 | # If the server has been started, this method waits till the +shutdown+
226 | # method has been called and has completed. The current thread will be
227 | # blocked until the server has been safely stopped.
228 | #
229 | def wait_for_shutdown
230 | @mutex.synchronize {
231 | @shutdown.wait(@mutex) unless @shutdown.nil?
232 | }
233 | self
234 | end
235 |
236 | alias :int :shutdown # handles the INT signal
237 | alias :term :shutdown # handles the TERM signal
238 | private :start, :stop
239 |
240 | # Set the PID file to the given `value`. If a PidFile instance is given, then
241 | # it is used. If a name is given, then that name is used to create a PifFile
242 | # instance.
243 | #
244 | # value - The PID file name or a PidFile instance.
245 | #
246 | # Raises an ArgumentError if the `value` cannot be used as a PID file.
247 | def pid_file=( value )
248 | @pid_file =
249 | case value
250 | when Servolux::PidFile
251 | value
252 | when String
253 | path = File.dirname(value)
254 | fn = File.basename(value, ".pid")
255 | Servolux::PidFile.new(:name => fn, :path => path, :logger => logger)
256 | else
257 | raise ArgumentError, "#{value.inspect} cannot be used as a PID file"
258 | end
259 | end
260 |
261 | private
262 |
263 | def halt_signal_processing
264 | if defined?(@wr) && !@wr.nil? && !@wr.closed?
265 | @queue << :halt
266 | @wr.write("!")
267 | @wr.flush
268 | end
269 | end
270 |
271 | def trap_signals
272 | @queue = []
273 | @rd, @wr = IO.pipe
274 |
275 | SIGNALS.each do |sig|
276 | method = sig.downcase.to_sym
277 | if self.respond_to? method
278 | Signal.trap(sig) do
279 | begin
280 | @queue << method
281 | @wr.write_nonblock("!")
282 | rescue StandardError => err
283 | logger.error "Exception in signal handler: #{method}"
284 | logger.error err
285 | end
286 | end
287 | end
288 | end
289 |
290 | Thread.new {
291 | :run while process_signals
292 | @rd.close if !@rd.nil? && !@rd.closed?
293 | @wr.close if !@wr.nil? && !@wr.closed?
294 | logger.info "Signal processing thread has stopped"
295 | }
296 | end
297 |
298 | def process_signals
299 | IO.select([@rd])
300 | @rd.read_nonblock(42)
301 |
302 | while !@queue.empty?
303 | method = @queue.shift
304 | next if method.nil?
305 | return false if method == :halt
306 |
307 | self.send(method)
308 | end
309 | return true
310 | rescue IO::WaitReadable
311 | return true
312 | rescue IOError, EOFError, Errno::EBADF
313 | return false
314 | rescue StandardError => err
315 | logger.error "Exception in signal handler: #{method}"
316 | logger.error err
317 | return false
318 | end
319 | end
320 |
--------------------------------------------------------------------------------
/lib/servolux/threaded.rb:
--------------------------------------------------------------------------------
1 |
2 | # == Synopsis
3 | # The Threaded module is used to perform some activity at a specified
4 | # interval.
5 | #
6 | # == Details
7 | # Sometimes it is useful for an object to have its own thread of execution
8 | # to perform a task at a recurring interval. The Threaded module
9 | # encapsulates this functionality so you don't have to write it yourself. It
10 | # can be used with any object that responds to the +run+ method.
11 | #
12 | # The threaded object is run by calling the +start+ method. This will create
13 | # a new thread that will invoke the +run+ method at the desired interval.
14 | # Just before the thread is created the +before_starting+ method will be
15 | # called (if it is defined by the threaded object). Likewise, after the
16 | # thread is created the +after_starting+ method will be called (if it is
17 | # defined by the threaded object).
18 | #
19 | # The threaded object is stopped by calling the +stop+ method. This sets an
20 | # internal flag and then wakes up the thread. The thread gracefully exits
21 | # after checking the flag. Like the start method, before and after methods
22 | # are defined for stopping as well. Just before the thread is stopped the
23 | # +before_stopping+ method will be called (if it is defined by the threaded
24 | # object). Likewise, after the thread has died the +after_stopping+ method
25 | # will be called (if it is defined by the threaded object).
26 | #
27 | # Calling the +join+ method on a threaded object will cause the calling
28 | # thread to wait until the threaded object has stopped. An optional timeout
29 | # parameter can be given.
30 | #
31 | # == Examples
32 | # Take a look at the Servolux::Server class for an example of a threaded
33 | # object.
34 | #
35 | module Servolux::Threaded
36 |
37 | # This method will be called by the activity thread at the desired
38 | # interval. Implementing classes are expect to provide this
39 | # functionality.
40 | #
41 | def run
42 | raise NotImplementedError,
43 | 'The run method must be defined by the threaded object.'
44 | end
45 |
46 | # Start the activity thread. If already started this method will return
47 | # without taking any action.
48 | #
49 | # If the including class defines a 'before_starting' method, it will be
50 | # called before the thread is created and run. Likewise, if the
51 | # including class defines an 'after_starting' method, it will be called
52 | # after the thread is created.
53 | #
54 | def start
55 | return self if _activity_thread.running?
56 | logger.debug "Starting"
57 |
58 | before_starting if self.respond_to?(:before_starting)
59 | @_activity_thread.start self
60 | after_starting if self.respond_to?(:after_starting)
61 | self
62 | end
63 |
64 | # Stop the activity thread. If already stopped this method will return
65 | # without taking any action.
66 | #
67 | # If the including class defines a 'before_stopping' method, it will be
68 | # called before the thread is stopped. Likewise, if the including class
69 | # defines an 'after_stopping' method, it will be called after the thread
70 | # has stopped.
71 | #
72 | def stop
73 | return self unless _activity_thread.running?
74 | logger.debug "Stopping"
75 |
76 | before_stopping if self.respond_to?(:before_stopping)
77 | @_activity_thread.stop
78 | self
79 | end
80 |
81 | # Wait on the activity thread. If the thread is already stopped, this
82 | # method will return without taking any action. Otherwise, this method
83 | # does not return until the activity thread has stopped, or a specific
84 | # number of iterations has passed since this method was called.
85 | #
86 | def wait( limit = nil )
87 | return self unless _activity_thread.running?
88 | initial_iterations = @_activity_thread.iterations
89 | loop {
90 | break unless @_activity_thread.running?
91 | break if limit and @_activity_thread.iterations > ( initial_iterations + limit )
92 | Thread.pass
93 | }
94 | end
95 |
96 | # If the activity thread is running, the calling thread will suspend
97 | # execution and run the activity thread. This method does not return until
98 | # the activity thread is stopped or until _limit_ seconds have passed.
99 | #
100 | # If the activity thread is not running, this method returns immediately
101 | # with +nil+.
102 | #
103 | def join( limit = nil )
104 | _activity_thread.join(limit) ? self : nil
105 | end
106 |
107 | # Returns +true+ if the activity thread is running. Returns +false+
108 | # otherwise.
109 | #
110 | def running?
111 | _activity_thread.running?
112 | end
113 |
114 | # Returns +true+ if the activity thread has finished its maximum
115 | # number of iterations or the thread is no longer running.
116 | # Returns +false+ otherwise.
117 | #
118 | def finished_iterations?
119 | return true unless _activity_thread.running?
120 | @_activity_thread.finished_iterations?
121 | end
122 |
123 | # Returns the status of threaded object.
124 | #
125 | # 'sleep' : sleeping or waiting on I/O
126 | # 'run' : executing
127 | # 'aborting' : aborting
128 | # false : not running or terminated normally
129 | # nil : terminated with an exception
130 | #
131 | # If this method returns +nil+, then calling join on the threaded object
132 | # will cause the exception to be raised in the calling thread.
133 | #
134 | def status
135 | return false if _activity_thread.thread.nil?
136 | @_activity_thread.thread.status
137 | end
138 |
139 | # Sets the number of seconds to sleep between invocations of the
140 | # threaded object's 'run' method.
141 | #
142 | def interval=( value )
143 | value = Float(value)
144 | raise ArgumentError, "Sleep interval must be >= 0" unless value >= 0
145 | _activity_thread.interval = value
146 | end
147 |
148 | # Returns the number of seconds to sleep between invocations of the
149 | # threaded object's 'run' method.
150 | #
151 | def interval
152 | _activity_thread.interval
153 | end
154 |
155 | # Signals the activity thread to treat the sleep interval with strict
156 | # semantics. The time it takes for the 'run' method to execute will be
157 | # subtracted from the sleep interval.
158 | #
159 | # If the sleep interval is 60 seconds and the 'run' method takes 2.2 seconds
160 | # to execute, then the activity thread will sleep for 57.2 seconds. The
161 | # subsequent invocation of the 'run' will occur as close as possible to 60
162 | # seconds after the previous invocation.
163 | #
164 | def use_strict_interval=( value )
165 | _activity_thread.use_strict_interval = (value ? true : false)
166 | end
167 |
168 | # When true, the activity thread will treat the sleep interval with strict
169 | # semantics. See the setter method for an in depth explanation.
170 | #
171 | def use_strict_interval
172 | _activity_thread.use_strict_interval
173 | end
174 | alias :use_strict_interval? :use_strict_interval
175 |
176 | # Sets the maximum number of invocations of the threaded object's
177 | # 'run' method
178 | #
179 | def maximum_iterations=( value )
180 | unless value.nil?
181 | value = Integer(value)
182 | raise ArgumentError, "maximum iterations must be >= 1" unless value >= 1
183 | end
184 |
185 | _activity_thread.maximum_iterations = value
186 | end
187 |
188 | # Returns the maximum number of invocations of the threaded
189 | # object's 'run' method
190 | #
191 | def maximum_iterations
192 | _activity_thread.maximum_iterations
193 | end
194 |
195 | # Returns the number of iterations of the threaded object's 'run' method
196 | # completed thus far.
197 | #
198 | def iterations
199 | _activity_thread.iterations
200 | end
201 |
202 | # Set to +true+ to continue running the threaded object even if an error
203 | # is raised by the +run+ method. The default behavior is to stop the
204 | # activity thread when an error is raised by the run method.
205 | #
206 | # A SystemExit will never be caught; it will always cause the Ruby
207 | # interpreter to exit.
208 | #
209 | def continue_on_error=( value )
210 | _activity_thread.continue_on_error = (value ? true : false)
211 | end
212 |
213 | # Returns +true+ if the threaded object should continue running even if an
214 | # error is raised by the run method. The default is to return +false+. The
215 | # threaded object will stop running when an error is raised.
216 | #
217 | def continue_on_error?
218 | _activity_thread.continue_on_error
219 | end
220 |
221 | # :stopdoc:
222 | def _activity_thread
223 | @_activity_thread ||= ::Servolux::Threaded::ThreadContainer.new(60, false, 0, nil, false);
224 | end # @private
225 |
226 | # @private
227 | ThreadContainer = Struct.new( :interval, :use_strict_interval, :iterations, :maximum_iterations, :continue_on_error, :thread, :running ) {
228 | def start( threaded )
229 | self.running = true
230 | self.iterations = 0
231 | self.thread = Thread.new { run threaded }
232 | Thread.pass
233 | end # @private
234 |
235 | def stop
236 | self.running = false
237 | thread.wakeup if thread.alive?
238 | end # @private
239 |
240 | def run( threaded )
241 | loop do
242 | begin
243 | mark_time
244 | break unless running?
245 | threaded.run
246 |
247 | if maximum_iterations
248 | self.iterations += 1
249 | if finished_iterations?
250 | self.running = false
251 | break
252 | end
253 | end
254 |
255 | sleep if running?
256 | rescue SystemExit; raise
257 | rescue Exception => err
258 | if continue_on_error
259 | threaded.logger.error err
260 | else
261 | threaded.logger.fatal err
262 | raise err
263 | end
264 | end
265 | end
266 | ensure
267 | if threaded.respond_to?(:after_stopping) and !self.running
268 | threaded.after_stopping
269 | end
270 | self.running = false
271 | end # @private
272 |
273 | def join( limit = nil )
274 | return if thread.nil?
275 | limit ? thread.join(limit) : thread.join
276 | end # @private
277 |
278 | def finished_iterations?
279 | return true if maximum_iterations and (iterations >= maximum_iterations)
280 | return false
281 | end # @private
282 |
283 | alias :running? :running
284 |
285 | # Mark the start time of the run loop.
286 | #
287 | def mark_time
288 | @mark = Time.now if use_strict_interval
289 | end # @private
290 |
291 | # Sleep for "interval" seconds adjusting for the run time of the "run"
292 | # method if the "use_strict_interval" flag is set. If the run time of the
293 | # "run" method exceeds our sleep "interval", then log a warning and just
294 | # use the interval as normal for this sleep period.
295 | #
296 | def sleep
297 | time_to_sleep = interval
298 |
299 | if use_strict_interval and interval > 0
300 | diff = Time.now - @mark
301 | time_to_sleep = interval - diff
302 |
303 | if time_to_sleep < 0
304 | time_to_sleep = interval
305 | logger.warn "Run time [#{diff} s] exceeded strict interval [#{interval} s]"
306 | end
307 | end
308 |
309 | ::Kernel.sleep time_to_sleep
310 | end # @private
311 | }
312 | # :startdoc:
313 |
314 | end
315 |
316 |
--------------------------------------------------------------------------------
/lib/servolux/version.rb:
--------------------------------------------------------------------------------
1 | module Servolux
2 | VERSION = "0.13.0".freeze
3 |
4 | # Returns the version string for the library.
5 | def self.version
6 | VERSION
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/script/bootstrap:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | gem list -i bones >/dev/null 2>&1
4 | rc=$?
5 | if [ "$rc" != "0" ]; then
6 | gem install bones
7 | fi
8 |
9 | rake gem:install_dependencies
10 |
--------------------------------------------------------------------------------
/spec/child_spec.rb:
--------------------------------------------------------------------------------
1 |
2 | require File.expand_path('../spec_helper', __FILE__)
3 |
4 | describe Servolux::Child do
5 | before :all do
6 | @child = Servolux::Child.new
7 | end
8 |
9 | after :each do
10 | @child.stop
11 | end
12 |
13 | it 'has some sensible defaults' do
14 | expect(@child.command).to be_nil
15 | expect(@child.timeout).to be_nil
16 | expect(@child.signals).to eq(%w[TERM QUIT KILL])
17 | expect(@child.suspend).to eq(4)
18 | expect(@child.pid).to be_nil
19 | expect(@child.io).to be_nil
20 | end
21 |
22 | it 'starts a child process' do
23 | @child.command = 'echo `pwd`'
24 | @child.start
25 |
26 | expect(@child.pid).to_not be_nil
27 | @child.wait
28 | expect(@child.io.read.strip).to eq(Dir.pwd)
29 | expect(@child.success?).to be true
30 | end
31 |
32 | it 'kills a child process after some timeout' do
33 | @child.command = 'sleep 5; echo `pwd`'
34 | @child.timeout = 0.25
35 | @child.start
36 |
37 | expect(@child.pid).to_not be_nil
38 | @child.wait
39 |
40 | expect(@child.io.read.strip).to be_empty
41 |
42 | expect(@child.signaled?).to be true
43 | expect(@child.exited?).to be false
44 | expect(@child.exitstatus).to be_nil
45 | expect(@child.success?).to be_nil
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/spec/daemon_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../spec_helper', __FILE__)
2 | require 'fileutils'
3 |
4 | if Servolux.fork?
5 | describe Servolux::Daemon do
6 | TestServer = Module.new {
7 | def before_starting() @counter = 0; end
8 | def after_stopping() exit!(0); end
9 | def run
10 | @counter += 1
11 | logger.info "executing run loop [#@counter]"
12 | end
13 | }
14 |
15 | log_fn = File.join(Dir.pwd, 'tmp.log')
16 | pid_fn = File.join(Dir.pwd, 'tmp.pid')
17 |
18 | before(:each) do
19 | FileUtils.rm_f [log_fn, pid_fn]
20 | @logger = Logger.new log_fn
21 | end
22 |
23 | after(:each) do
24 | @daemon.shutdown if defined? @daemon && @daemon
25 | FileUtils.rm_f [log_fn, pid_fn]
26 | end
27 |
28 | it 'waits for an updated logfile when starting' do
29 | server = Servolux::Server.new('Hey You', :logger => @logger, :interval => 2, :pid_file => pid_fn)
30 | server.extend TestServer
31 | @daemon = Servolux::Daemon.new(:server => server, :log_file => log_fn, :timeout => 8)
32 |
33 | @daemon.startup false
34 | expect(@daemon).to be_alive
35 | end
36 |
37 | it 'waits for a particular line to appear in the log file' do
38 | server = Servolux::Server.new('Hey You', :logger => @logger, :interval => 1, :pid_file => pid_fn)
39 | server.extend TestServer
40 | @daemon = Servolux::Daemon.new(:server => server, :log_file => log_fn, :look_for => 'executing run loop [2]', :timeout => 8)
41 |
42 | @daemon.startup false
43 | expect(@daemon).to be_alive
44 | end
45 |
46 | it 'raises an error if the startup timeout is exceeded' do
47 | server = Servolux::Server.new('Hey You', :logger => @logger, :interval => 3600, :pid_file => pid_fn)
48 | server.extend TestServer
49 | @daemon = Servolux::Daemon.new(:server => server, :log_file => log_fn, :look_for => 'executing run loop [42]', :timeout => 4)
50 |
51 | expect { @daemon.startup }.to raise_error(Servolux::Daemon::Timeout)
52 | end
53 | end
54 | end # if Servolux.fork?
55 |
--------------------------------------------------------------------------------
/spec/pid_file_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../spec_helper', __FILE__)
2 |
3 | describe Servolux::PidFile do
4 | before :all do
5 | tmp = Tempfile.new "servolux-pid-file"
6 | @path = tmp.path; tmp.unlink
7 | FileUtils.mkdir @path
8 | end
9 |
10 | after :all do
11 | FileUtils.rm_rf @path
12 | end
13 |
14 | before :each do
15 | FileUtils.rm_f Dir.glob("#@path/*.pid")
16 | @pid_file = Servolux::PidFile.new \
17 | :name => "test",
18 | :path => @path,
19 | :logger => Logging.logger['Servolux']
20 |
21 | @filename = @pid_file.filename
22 | end
23 |
24 | describe "filename" do
25 | it "normalizes the process name" do
26 | pid = Servolux::PidFile.new :name => "Test Server"
27 | expect(pid.filename).to eq("./test_server.pid")
28 | end
29 |
30 | it "includes the path" do
31 | pid = Servolux::PidFile.new :name => "Test Server", :path => @path
32 | expect(pid.filename).to eq("#@path/test_server.pid")
33 | end
34 | end
35 |
36 | describe "creating" do
37 | it "writes a PID file" do
38 | expect(test(?e, @filename)).to be false
39 |
40 | @pid_file.write(123456)
41 | expect(test(?e, @filename)).to be true
42 | expect(@log_output.readline.chomp).to \
43 | eq(%Q{DEBUG Servolux : Writing pid file "#@path/test.pid"})
44 |
45 | pid = Integer(File.read(@filename).strip)
46 | expect(pid).to eq(123456)
47 | end
48 |
49 | it "uses mode rw-r----- by default" do
50 | expect(test(?e, @filename)).to be false
51 |
52 | @pid_file.write
53 | expect(test(?e, @filename)).to be true
54 | expect(@log_output.readline.chomp).to \
55 | eq(%Q{DEBUG Servolux : Writing pid file "#@path/test.pid"})
56 |
57 | mode = File.stat(@filename).mode & 0777
58 | expect(mode).to eq(0640)
59 | end
60 |
61 | it "uses the given mode" do
62 | @pid_file.mode = 0400
63 | expect(test(?e, @filename)).to be false
64 |
65 | @pid_file.write
66 | expect(test(?e, @filename)).to be true
67 | expect(@log_output.readline.chomp).to \
68 | eq(%Q{DEBUG Servolux : Writing pid file "#@path/test.pid"})
69 |
70 | mode = File.stat(@filename).mode & 0777
71 | expect(mode).to eq(0400)
72 | end
73 | end
74 |
75 | describe "deleting" do
76 | it "removes a PID file" do
77 | expect(test(?e, @filename)).to be false
78 | expect { @pid_file.delete }.not_to raise_error
79 |
80 | @pid_file.write
81 | expect(test(?e, @filename)).to be true
82 | expect(@log_output.readline.chomp).to \
83 | eq(%Q{DEBUG Servolux : Writing pid file "#@path/test.pid"})
84 |
85 | @pid_file.delete
86 | expect(test(?e, @filename)).to be false
87 | expect(@log_output.readline.chomp).to \
88 | eq(%Q{DEBUG Servolux : Deleting pid file "#@path/test.pid"})
89 | end
90 |
91 | it "removes the PID file only from the same process" do
92 | @pid_file.write(654321)
93 | expect(test(?e, @filename)).to be true
94 | expect(@log_output.readline.chomp).to \
95 | eq(%Q{DEBUG Servolux : Writing pid file "#@path/test.pid"})
96 |
97 | @pid_file.delete
98 | expect(test(?e, @filename)).to be true
99 | expect(@log_output.readline).to be_nil
100 | end
101 |
102 | it "can forcibly remove a PID file" do
103 | @pid_file.write(135790)
104 | expect(test(?e, @filename)).to be true
105 | expect(@log_output.readline.chomp).to \
106 | eq(%Q{DEBUG Servolux : Writing pid file "#@path/test.pid"})
107 |
108 | @pid_file.delete!
109 | expect(test(?e, @filename)).to be false
110 | expect(@log_output.readline.chomp).to \
111 | eq(%Q{DEBUG Servolux : Deleting pid file "#@path/test.pid"})
112 | end
113 | end
114 |
115 | it "returns the PID from the file" do
116 | expect(@pid_file.pid).to be_nil
117 |
118 | File.open(@filename, 'w') { |fd| fd.write(314159) }
119 | expect(@pid_file.pid).to eq(314159)
120 |
121 | File.delete(@filename)
122 | expect(@pid_file.pid).to be_nil
123 | end
124 |
125 | it "reports if the process is alive" do
126 | expect(@pid_file.pid).to be_nil # there is no PID file yet
127 | expect(@pid_file).not_to be_alive # and so we cannot determine
128 | # if the process is alive
129 | @pid_file.write
130 | expect(@pid_file.pid).not_to be_nil
131 | expect(@pid_file).to be_alive
132 | end
133 | end
134 |
--------------------------------------------------------------------------------
/spec/piper_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../spec_helper', __FILE__)
2 |
3 | if Servolux.fork?
4 | describe Servolux::Piper do
5 | before :each do
6 | @piper = nil
7 | end
8 |
9 | after :each do
10 | next if @piper.nil?
11 | @piper.puts :die rescue nil
12 | @piper.close
13 | @piper = nil
14 | end
15 |
16 | it 'only understands three file modes' do
17 | %w[r w rw].each do |mode|
18 | expect {
19 | piper = Servolux::Piper.new(mode)
20 | piper.child { piper.close; exit! }
21 | piper.parent { piper.close }
22 | }.not_to raise_error
23 | end
24 |
25 | expect {
26 | Servolux::Piper.new('f')
27 | }.to raise_error(ArgumentError, 'Unsupported mode "f"')
28 | end
29 |
30 | it 'enables communication between parents and children' do
31 | @piper = Servolux::Piper.new 'rw', :timeout => 2
32 |
33 | @piper.child {
34 | loop {
35 | obj = @piper.gets
36 | if :die == obj
37 | @piper.close; exit!
38 | end
39 | @piper.puts obj unless obj.nil?
40 | }
41 | exit!
42 | }
43 |
44 | @piper.parent {
45 | @piper.puts 'foo bar baz'
46 | expect(@piper.gets).to eq('foo bar baz')
47 |
48 | @piper.puts %w[one two three]
49 | expect(@piper.gets).to eq(%w[one two three])
50 |
51 | expect(@piper.puts('Returns # of bytes written')).to be > 0
52 | expect(@piper.gets).to eq('Returns # of bytes written')
53 |
54 | @piper.puts 1
55 | @piper.puts 2
56 | @piper.puts 3
57 | expect(@piper.gets).to eq(1)
58 | expect(@piper.gets).to eq(2)
59 | expect(@piper.gets).to eq(3)
60 |
61 | @piper.timeout = 0.1
62 | expect(@piper).not_to be_readable
63 | }
64 | end
65 |
66 | it 'sends signals from parent to child' do
67 | @piper = Servolux::Piper.new 'rw', :timeout => 2
68 |
69 | @piper.child {
70 | Signal.trap('USR2') { @piper.puts "'USR2' was received" rescue nil }
71 | Signal.trap('INT') {
72 | @piper.puts "'INT' was received" rescue nil
73 | @piper.close
74 | exit!
75 | }
76 | Thread.new { sleep 7; exit! }
77 | @piper.puts :ready
78 | loop { sleep }
79 | exit!
80 | }
81 |
82 | @piper.parent {
83 | expect(@piper.gets).to eq(:ready)
84 |
85 | @piper.signal 'USR2'
86 | expect(@piper.gets).to eq("'USR2' was received")
87 |
88 | @piper.signal 'INT'
89 | expect(@piper.gets).to eq("'INT' was received")
90 | }
91 | end
92 |
93 | it 'creates a daemon process' do
94 | @piper = Servolux::Piper.daemon(true, true)
95 |
96 | @piper.child {
97 | @piper.puts Process.ppid
98 | @piper.close
99 | exit!
100 | }
101 |
102 | @piper.parent {
103 | expect(@piper.gets).not_to eq(Process.pid)
104 | }
105 | end
106 | end
107 | end # if Servolux.fork?
108 |
109 |
--------------------------------------------------------------------------------
/spec/prefork_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../spec_helper', __FILE__)
2 | require 'tempfile'
3 | require 'fileutils'
4 | require 'enumerator'
5 |
6 | if Servolux.fork?
7 | describe Servolux::Prefork do
8 | def pids
9 | workers.map! { |w| w.pid }
10 | end
11 |
12 | def workers
13 | ary = []
14 | return ary if @prefork.nil?
15 | @prefork.each_worker { |w| ary << w }
16 | ary
17 | end
18 |
19 | def worker_count
20 | Dir.glob(@glob).length
21 | end
22 |
23 | def alive?( pid )
24 | Process.kill(0, pid)
25 | true
26 | rescue Errno::ESRCH, Errno::ENOENT, Errno::ECHILD
27 | false
28 | end
29 |
30 | def wait_until( seconds = 5 )
31 | start = Time.now
32 | sleep 0.250 until ((Time.now - start) > seconds) || yield
33 | end
34 |
35 | before :all do
36 | tmp = Tempfile.new 'servolux-prefork'
37 | @path = tmp.path; tmp.unlink
38 | @glob = @path + '/*.txt'
39 | FileUtils.mkdir @path
40 |
41 | @worker = Module.new do
42 | def before_executing() @fd = File.open("#{config[:path]}/#$$.txt", 'w'); end
43 | def after_executing() @fd.close; FileUtils.rm_f @fd.path; end
44 | def execute() @fd.puts Time.now; sleep 2; end
45 | def hup() @thread.wakeup; end
46 | alias :term :hup
47 | end
48 | end
49 |
50 | after :all do
51 | FileUtils.rm_rf @path
52 | end
53 |
54 | before :each do
55 | @prefork = nil
56 | FileUtils.rm_f "#@path/*.txt"
57 | end
58 |
59 | after :each do
60 | next if @prefork.nil?
61 | @prefork.stop
62 | @prefork.each_worker { |worker| worker.signal('KILL') }
63 | @prefork = nil
64 | FileUtils.rm_f "#@path/*.txt"
65 | end
66 |
67 | it "starts up a single worker" do
68 | @prefork = Servolux::Prefork.new :module => @worker, :config => {:path => @path}
69 | @prefork.start 1
70 | ary = workers
71 | wait_until { ary.all? { |w| w.alive? } }
72 | wait_until { worker_count >= 1 }
73 |
74 | ary = Dir.glob(@glob)
75 | expect(ary.length).to eq(1)
76 | expect(File.basename(ary.first).to_i).to eq(pids.first)
77 | end
78 |
79 | it "starts up a number of workers" do
80 | @prefork = Servolux::Prefork.new :module => @worker, :config => {:path => @path}
81 | @prefork.start 8
82 | ary = workers
83 | wait_until { ary.all? { |w| w.alive? } }
84 | wait_until { worker_count >= 8 }
85 |
86 | ary = Dir.glob(@glob)
87 | expect(ary.length).to eq(8)
88 |
89 | ary.map! { |fn| File.basename(fn).to_i }.sort!
90 | expect(ary).to eq(pids.sort)
91 | end
92 |
93 | it "stops workers gracefullly" do
94 | @prefork = Servolux::Prefork.new :module => @worker, :config => {:path => @path}
95 | @prefork.start 3
96 | ary = workers
97 | wait_until { ary.all? { |w| w.alive? } }
98 | wait_until { worker_count >= 3 }
99 |
100 | ary = Dir.glob(@glob)
101 | expect(ary.length).to eq(3)
102 |
103 | @prefork.stop
104 | wait_until { Dir.glob(@glob).length == 0 }
105 | workers.each { |w| w.wait rescue nil }
106 |
107 | rv = workers.all? { |w| !w.alive? }
108 | expect(rv).to be true
109 | end
110 |
111 | it "restarts a worker via SIGHUP" do
112 | @prefork = Servolux::Prefork.new :module => @worker, :config => {:path => @path}
113 | @prefork.start 2
114 | ary = workers
115 | wait_until { ary.all? { |w| w.alive? } }
116 | wait_until { worker_count >= 2 }
117 |
118 | pid = pids.last
119 | ary.last.signal 'HUP'
120 | wait_until { !alive? pid }
121 | @prefork.reap
122 | wait_until { ary.all? { |w| w.alive? } }
123 |
124 | expect(pid).not_to eq(pids.last)
125 | end
126 |
127 | it "starts up a stopped worker" do
128 | @prefork = Servolux::Prefork.new :module => @worker, :config => {:path => @path}
129 | @prefork.start 2
130 | ary = workers
131 | wait_until { ary.all? { |w| w.alive? } }
132 | wait_until { worker_count >= 2 }
133 |
134 | pid = pids.last
135 | ary.last.signal 'TERM'
136 |
137 | wait_until { !alive? pid }
138 | @prefork.reap
139 |
140 | @prefork.each_worker do |worker|
141 | worker.start unless worker.alive?
142 | end
143 | wait_until { ary.all? { |w| w.alive? } }
144 | expect(pid).not_to eq(pids.last)
145 | end
146 |
147 | it "adds a new worker to the worker pool" do
148 | @prefork = Servolux::Prefork.new :module => @worker, :config => {:path => @path}
149 | @prefork.start 2
150 | ary = workers
151 | wait_until { ary.all? { |w| w.alive? } }
152 | wait_until { worker_count >= 2 }
153 |
154 |
155 | @prefork.add_workers( 2 )
156 | wait_until { worker_count >= 4 }
157 | expect(workers.size).to eq(4)
158 | end
159 |
160 | it "only adds workers up to the max_workers value" do
161 | @prefork = Servolux::Prefork.new :module => @worker, :max_workers => 3, :config => {:path => @path}
162 | @prefork.start 2
163 | ary = workers
164 | wait_until { ary.all? { |w| w.alive? } }
165 | wait_until { worker_count >= 2 }
166 |
167 | @prefork.add_workers( 2 )
168 | wait_until { worker_count >= 3 }
169 | expect(workers.size).to eq(3)
170 | end
171 |
172 | it "prunes workers that are no longer running" do
173 | @prefork = Servolux::Prefork.new :module => @worker, :config => {:path => @path}
174 | @prefork.start 2
175 | ary = workers
176 | wait_until { ary.all? { |w| w.alive? } }
177 | wait_until { worker_count >= 2 }
178 |
179 | @prefork.add_workers( 2 )
180 | wait_until { worker_count >= 3 }
181 | expect(workers.size).to eq(4)
182 |
183 | workers[0].stop
184 | wait_until { !workers[0].alive? }
185 |
186 | @prefork.prune_workers
187 | expect(workers.size).to eq(3)
188 | end
189 |
190 | it "ensures that there are minimum number of workers" do
191 | @prefork = Servolux::Prefork.new :module => @worker, :min_workers => 3, :config => {:path => @path}
192 | @prefork.start 1
193 | ary = workers
194 | wait_until { ary.all? { |w| w.alive? } }
195 | wait_until { worker_count >= 1 }
196 |
197 | @prefork.ensure_worker_pool_size
198 | wait_until { worker_count >= 3 }
199 | expect(workers.size).to eq(3)
200 | end
201 | end
202 | end # Servolux.fork?
203 |
--------------------------------------------------------------------------------
/spec/server_spec.rb:
--------------------------------------------------------------------------------
1 |
2 | require File.expand_path('../spec_helper', __FILE__)
3 |
4 | describe Servolux::Server do
5 |
6 | def wait_until( seconds = 5 )
7 | start = Time.now
8 | sleep 0.250 until ((Time.now - start) > seconds) || yield
9 | end
10 |
11 | def readlog
12 | @log_output.readline
13 | end
14 |
15 | base = Class.new(Servolux::Server) do
16 | def initialize
17 | super('Test Server', :logger => Logging.logger['Servolux'])
18 | end
19 | def run() sleep; end
20 | end
21 |
22 | before :each do
23 | @server = base.new
24 | @server.pid_file.delete
25 | end
26 |
27 | after :each do
28 | @server.shutdown
29 | @server.wait_for_shutdown
30 | wait_until { @t.status == false } if @t && @t.alive?
31 | end
32 |
33 | it 'generates a PID file' do
34 | expect(@server.pid_file).to_not exist
35 |
36 | @t = Thread.new {@server.startup}
37 | wait_until { @server.running? and @t.status == 'sleep' }
38 | expect(@server.pid_file).to exist
39 |
40 | @server.shutdown
41 | wait_until { @t.status == false }
42 | expect(@server.pid_file).to_not exist
43 | end
44 |
45 | it 'generates a PID file with mode rw-r----- by default' do
46 | @t = Thread.new {@server.startup}
47 | wait_until { @server.running? and @t.status == 'sleep' }
48 | expect(@server.pid_file).to exist
49 |
50 | expect(readlog.chomp).to eq(%q(DEBUG Servolux : Writing pid file "./test_server.pid"))
51 | expect(readlog.chomp).to eq(%q(DEBUG Servolux : Starting))
52 |
53 | filename = @server.pid_file.filename
54 | mode = File.stat(filename).mode & 0777
55 | expect(mode).to eq(0640)
56 |
57 | @server.shutdown
58 | wait_until { @t.status == false }
59 | expect(@server.pid_file).to_not exist
60 | end
61 |
62 | it 'generates PID file with the specified permissions' do
63 | @server.pid_file.mode = 0400
64 | @t = Thread.new {@server.startup}
65 | wait_until { @server.running? and @t.status == 'sleep' }
66 | expect(@server.pid_file).to exist
67 |
68 | expect(readlog.chomp).to eq(%q(DEBUG Servolux : Writing pid file "./test_server.pid"))
69 | expect(readlog.chomp).to eq(%q(DEBUG Servolux : Starting))
70 |
71 | filename = @server.pid_file.filename
72 | mode = File.stat(filename).mode & 0777
73 | expect(mode).to eq(0400)
74 |
75 | @server.shutdown
76 | wait_until { @t.status == false }
77 | expect(@server.pid_file).to_not exist
78 | end
79 |
80 | it 'shuts down gracefully when signaled' do
81 | @t = Thread.new {@server.startup}
82 | wait_until { @server.running? and @t.status == 'sleep' }
83 | expect(@server).to be_running
84 |
85 | Process.kill 'SIGINT', $$
86 | wait_until { @t.status == false }
87 | expect(@server).to_not be_running
88 | end
89 |
90 | it 'responds to signals that have defined handlers' do
91 | class << @server
92 | def hup() logger.info 'hup was called'; end
93 | def usr1() logger.info 'usr1 was called'; end
94 | def usr2() logger.info 'usr2 was called'; end
95 | end
96 |
97 | @t = Thread.new {@server.startup}
98 | wait_until { @server.running? and @t.status == 'sleep' }
99 | readlog
100 | expect(readlog.strip).to eq('DEBUG Servolux : Starting')
101 |
102 | line = nil
103 | Process.kill 'SIGUSR1', $$
104 | wait_until { line = readlog }
105 | expect(line).to_not be_nil
106 | expect(line.strip).to eq('INFO Servolux : usr1 was called')
107 |
108 | line = nil
109 | Process.kill 'SIGHUP', $$
110 | wait_until { line = readlog }
111 | expect(line).to_not be_nil
112 | expect(line.strip).to eq('INFO Servolux : hup was called')
113 |
114 | line = nil
115 | Process.kill 'SIGUSR2', $$
116 | wait_until { line = readlog }
117 | expect(line).to_not be_nil
118 | expect(line.strip).to eq('INFO Servolux : usr2 was called')
119 |
120 | Process.kill 'SIGTERM', $$
121 | wait_until { @t.status == false }
122 | expect(@server).to_not be_running
123 | end
124 |
125 | it 'captures exceptions raised by the signal handlers' do
126 | class << @server
127 | def usr2() raise 'Ooops!'; end
128 | end
129 |
130 | @t = Thread.new {@server.startup}
131 | wait_until { @server.running? and @t.status == 'sleep' }
132 | readlog
133 | expect(readlog.strip).to eq('DEBUG Servolux : Starting')
134 |
135 | line = nil
136 | Process.kill 'SIGUSR2', $$
137 | wait_until { line = readlog }
138 | expect(line).to_not be_nil
139 | expect(line.strip).to eq('ERROR Servolux : Exception in signal handler: usr2')
140 |
141 | line = nil
142 | wait_until { line = readlog }
143 | expect(line).to_not be_nil
144 | expect(line.strip).to eq('ERROR Servolux : Ooops!')
145 | end
146 |
147 | it 'logs when the signal handler thread exits' do
148 | class << @server
149 | def hup() logger.info 'hup was called'; end
150 | end
151 |
152 | @t = Thread.new {@server.startup}
153 | wait_until { @server.running? and @t.status == 'sleep' }
154 | readlog
155 | expect(readlog.strip).to eq('DEBUG Servolux : Starting')
156 |
157 | line = nil
158 | @server.__send__(:halt_signal_processing)
159 | wait_until { line = readlog }
160 | expect(line).to_not be_nil
161 | expect(line.strip).to eq('INFO Servolux : Signal processing thread has stopped')
162 |
163 | line = nil
164 | Process.kill 'SIGHUP', $$
165 | wait_until { line = readlog }
166 | expect(line).to_not be_nil
167 | expect(line.strip).to eq('ERROR Servolux : Exception in signal handler: hup')
168 | expect(readlog.strip).to eq('ERROR Servolux : closed stream')
169 | end
170 | end
171 |
--------------------------------------------------------------------------------
/spec/servolux_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../spec_helper', __FILE__)
2 |
3 | describe Servolux do
4 | before :all do
5 | @root_dir = File.expand_path(File.join(File.dirname(__FILE__), '..'))
6 | end
7 |
8 | it "finds things releative to 'lib'" do
9 | expect(Servolux.libpath(%w[servolux threaded])).to eq(File.join(@root_dir, %w[lib servolux threaded]))
10 | end
11 |
12 | it "finds things releative to 'root'" do
13 | expect(Servolux.path('Rakefile')).to eq(File.join(@root_dir, 'Rakefile'))
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | unless defined? SERVOLUX_SPEC_HELPER
2 | SERVOLUX_SPEC_HELPER = true
3 |
4 | require 'rubygems'
5 | require 'logging'
6 | require 'rspec'
7 | require 'rspec/logging_helper'
8 |
9 | require File.expand_path('../../lib/servolux', __FILE__)
10 |
11 | include Logging.globally
12 |
13 | RSpec.configure do |config|
14 | include RSpec::LoggingHelper
15 | config.capture_log_messages
16 | end
17 | end # unless defined?
18 |
--------------------------------------------------------------------------------
/spec/threaded_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../spec_helper', __FILE__)
2 |
3 | describe Servolux::Threaded do
4 | base = Class.new do
5 | include Servolux::Threaded
6 | def initialize
7 | self.interval = 0
8 | @mutex = Mutex.new
9 | @signal = ConditionVariable.new
10 | end
11 | def pass( val = 'sleep' )
12 | Thread.pass until status == val
13 | end
14 | def send_signal
15 | @mutex.synchronize {
16 | @signal.signal
17 | @signal = nil
18 | }
19 | end
20 | def wait_signal
21 | @mutex.synchronize {
22 | @signal.wait(@mutex) unless @signal.nil?
23 | }
24 | end
25 | end
26 |
27 | it "let's you know that it is running" do
28 | klass = Class.new(base) do
29 | def run() sleep 1; end
30 | end
31 |
32 | obj = klass.new
33 | obj.interval = 0
34 | expect(obj.running?).to be_nil
35 |
36 | obj.start
37 | expect(obj).to be_running
38 | obj.pass
39 |
40 | obj.stop.join(2)
41 | expect(obj).to_not be_running
42 | end
43 |
44 | it "stops even when sleeping in the run method" do
45 | klass = Class.new(base) do
46 | attr_reader :stopped
47 | def run() sleep; end
48 | def after_starting() @stopped = false; end
49 | def after_stopping() @stopped = true; end
50 | end
51 |
52 | obj = klass.new
53 | obj.interval = 0
54 | expect(obj.stopped).to be_nil
55 |
56 | obj.start
57 | expect(obj.stopped).to be false
58 | obj.pass
59 |
60 | obj.stop.join(2)
61 | expect(obj.stopped).to be true
62 | end
63 |
64 | it "calls all the before and after hooks" do
65 | klass = Class.new(base) do
66 | attr_accessor :ary
67 | def run() sleep 1; end
68 | def before_starting() ary << 1; end
69 | def after_starting() ary << 2; end
70 | def before_stopping() ary << 3; end
71 | def after_stopping() ary << 4; end
72 | end
73 |
74 | obj = klass.new
75 | obj.interval = 86400
76 | obj.ary = []
77 |
78 | obj.start
79 | expect(obj.ary).to eq([1,2])
80 | obj.pass
81 |
82 | obj.stop.join(2)
83 | expect(obj.ary).to eq([1,2,3,4])
84 | end
85 |
86 | it "dies when an exception is thrown" do
87 | klass = Class.new(base) do
88 | def run() raise 'ni'; end
89 | end
90 |
91 | obj = klass.new
92 |
93 | obj.start
94 | obj.pass nil
95 |
96 | expect(obj).to_not be_running
97 | @log_output.readline
98 | expect(@log_output.readline.chomp).to eq("FATAL Object : ni")
99 |
100 | expect { obj.join }.to raise_error(RuntimeError, 'ni')
101 | end
102 |
103 | it "lives if told to continue on error" do
104 | klass = Class.new(base) do
105 | def run()
106 | @sleep ||= false
107 | if @sleep
108 | send_signal
109 | sleep
110 | else
111 | @sleep = true
112 | raise 'ni'
113 | end
114 | end
115 | end
116 |
117 | obj = klass.new
118 | obj.continue_on_error = true
119 |
120 | obj.start
121 | obj.wait_signal
122 |
123 | expect(obj).to be_running
124 | @log_output.readline
125 | expect(@log_output.readline.chomp).to eq("ERROR Object : ni")
126 |
127 | obj.stop.join(2)
128 | expect(obj).to_not be_running
129 | end
130 |
131 | it "complains loudly if you don't have a run method" do
132 | obj = base.new
133 | obj.start
134 | obj.pass nil
135 |
136 | @log_output.readline
137 | expect(@log_output.readline.chomp).to eq("FATAL Object : The run method must be defined by the threaded object.")
138 |
139 | expect { obj.join }.to raise_error(NotImplementedError, 'The run method must be defined by the threaded object.')
140 | end
141 |
142 | # --------------------------------------------------------------------------
143 | describe 'when setting maximum iterations' do
144 |
145 | it "stops after a limited number of iterations" do
146 | klass = Class.new( base ) do
147 | def run() ; end
148 | end
149 |
150 | obj = klass.new
151 | obj.maximum_iterations = 5
152 | expect(obj.iterations).to eq(0)
153 |
154 | obj.start
155 | obj.wait
156 | expect(obj.iterations).to eq(5)
157 | end
158 |
159 | it "runs the 'after_stopping' method" do
160 | klass = Class.new( base ) do
161 | attr_accessor :ary
162 | def run() ; end
163 | def after_stopping() ary << 4; end
164 | end
165 |
166 | obj = klass.new
167 | obj.maximum_iterations = 5
168 | obj.ary = []
169 |
170 | obj.start
171 | obj.wait
172 | expect(obj.ary).to eq([4])
173 | end
174 |
175 | it "should not increment iterations if maximum iterations has not been set" do
176 | klass = Class.new( base ) do
177 | def run() ; end
178 | end
179 |
180 | obj = klass.new
181 | expect(obj.iterations).to eq(0)
182 |
183 | obj.start
184 | sleep 0.1
185 | obj.stop.join(2)
186 | expect(obj.iterations).to eq(0)
187 | end
188 |
189 | it "complains loudly if you attempt to set a maximum number of iterations < 1" do
190 | obj = base.new
191 | expect { obj.maximum_iterations = -1 }.to raise_error( ArgumentError, "maximum iterations must be >= 1" )
192 | end
193 | end
194 |
195 | # --------------------------------------------------------------------------
196 | context 'when running with a strict interval' do
197 |
198 | it "logs a warning if the strict interval is exceeded" do
199 | klass = Class.new( base ) do
200 | def run() sleep 0.5; end
201 | end
202 |
203 | obj = klass.new
204 | obj.interval = 0.250
205 | obj.use_strict_interval = true
206 | obj.maximum_iterations = 2
207 |
208 | obj.start
209 | obj.wait
210 |
211 | @log_output.readline
212 | expect(@log_output.readline.chomp).to match(%r/ WARN Servolux::Threaded::ThreadContainer : Run time \[\d+\.\d+ s\] exceeded strict interval \[0\.25 s\]/)
213 | end
214 |
215 | it "ignores the strict flag if the interval is zero" do
216 | klass = Class.new( base ) do
217 | def run() sleep 0.250; end
218 | end
219 |
220 | obj = klass.new
221 | obj.interval = 0
222 | obj.use_strict_interval = true
223 | obj.maximum_iterations = 2
224 |
225 | obj.start
226 | obj.wait
227 |
228 | @log_output.readline
229 | expect(@log_output.readline).to be_nil
230 | end
231 | end
232 | end
233 |
--------------------------------------------------------------------------------