├── .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 [![](https://secure.travis-ci.org/TwP/servolux.png)](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 | --------------------------------------------------------------------------------