Class: CertificateDepot::Server
- Inherits:
-
Object
- Object
- CertificateDepot::Server
- Defined in:
- lib/certificate_depot/server.rb
Overview
The CertificateDepot server is a pre-forking server. This basically means that it forks a pre-configured number of workers.
Server
|_ worker
|_ worker
|_ ...
Workers hang around until something connects to the socket. The first worker to accept the request serves it. When workers die the server starts spawning new processes until it matches the configured process count. Workers are identified by their process ID or PID.
The server creates a pipe between itself and each of the workers. We call these pipes lifelines. When a worker goes down the lifeline is severed. This is used as a signal by the server to spawn new workers. If the server goes down the workers use the same trick to notice this.
Constant Summary collapse
- POSSIBLE_PID_FILES =
['/var/run/depot.pid', File.('~/.depot.pid')]
- POSSIBLE_LOG_FILES =
['/var/log/depot.log', File.('~/depot.log')]
- READ_BUFFER_SIZE =
16 * 1024
- DEFAULTS =
{ :host => '127.0.0.1', :port => 35553, :process_count => 2, :max_connection_queue => 10 }
Instance Attribute Summary collapse
-
#depot ⇒ Object
Returns the value of attribute depot.
-
#socket ⇒ Object
Returns the value of attribute socket.
Class Method Summary collapse
-
.start(depot, options = {}) ⇒ Object
Creates a new server instance and starts listening on its configured host and port.
-
.stop(options = {}) ⇒ Object
Finds the server PID and kills it causing the workers to go down as well.
Instance Method Summary collapse
-
#cleanup ⇒ Object
Cleanup all server resources.
-
#despawn_worker(pid) ⇒ Object
Deletes references to workers from the server instance.
-
#initialize(depot, options = {}) ⇒ Server
constructor
Create a new server instance.
-
#kill ⇒ Object
Sends the QUIT signal to the server process.
-
#load_pid_from_file ⇒ Object
Reads the PID of the process with the mainloop from the filesystem.
-
#log ⇒ Object
Returns a Log object for the server.
-
#missing_workers ⇒ Object
Returns the number of workers that need to be created in order to get to the configured process count.
-
#reap_workers ⇒ Object
Figures out if any workers died and deletes them from internal structures if they did.
-
#remove_pid_file ⇒ Object
Removes all possible PID files.
-
#reroute_stdio ⇒ Object
Make all output of interpreter go to the logfile.
-
#run ⇒ Object
Start behaving like a server.
-
#save_pid_to_file(pid) ⇒ Object
Write the PID of the process with the mainloop to the filesystem so we read it later on to signal the server to shutdown.
-
#setup_socket ⇒ Object
Creates the socket the server listens on and binds it to the configured host and port.
-
#signals_want_shutdown? ⇒ Boolean
Returns true when the signals received by the process demand a shutdown.
-
#sleep ⇒ Object
Sleeps until someone wants the server main loop to wake up or when 2 seconds go by.
-
#spawn_workers ⇒ Object
Figures out how many workers are currently running and creates new ones if needed.
-
#trap_signals ⇒ Object
Installs signal traps to listen for incoming signals to the process.
Constructor Details
#initialize(depot, options = {}) ⇒ Server
Create a new server instance. The first argument is a CertificateDepot instance. The second argument contains overrides to the DEFAULTS.
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# File 'lib/certificate_depot/server.rb', line 37 def initialize(depot, ={}) @depot = depot # Override the default with user supplied options. @options = .dup DEFAULTS.keys.each do |key| @options[key] ||= DEFAULTS[key] end # If someone specifies a PID file we have to try that instead of the # default. if pid_file = @options.delete(:pid_file) @options[:possible_pid_files] = [pid_file] else @options[:possible_pid_files] = POSSIBLE_PID_FILES end # If someone specifies a log file we have to try that instead of the # default. if log_file = @options.delete(:log_file) @options[:possible_log_files] = [log_file] else @options[:possible_log_files] = POSSIBLE_LOG_FILES end # Contains the lifelines to all the workers. They are indexed by the # worker's PID. @lifelines = {} # Workers are instances of CertificateDepot::Worker. They are indexed by # their own PID. @workers = {} # Signals received by the process @signals = [] end |
Instance Attribute Details
#depot ⇒ Object
Returns the value of attribute depot.
23 24 25 |
# File 'lib/certificate_depot/server.rb', line 23 def depot @depot end |
#socket ⇒ Object
Returns the value of attribute socket.
23 24 25 |
# File 'lib/certificate_depot/server.rb', line 23 def socket @socket end |
Class Method Details
.start(depot, options = {}) ⇒ Object
Creates a new server instance and starts listening on its configured host and port. Returns once the server was started.
276 277 278 279 |
# File 'lib/certificate_depot/server.rb', line 276 def self.start(depot, ={}) server = new(depot, ) server.run end |
.stop(options = {}) ⇒ Object
Finds the server PID and kills it causing the workers to go down as well.
282 283 284 285 |
# File 'lib/certificate_depot/server.rb', line 282 def self.stop(={}) server = new(nil, ) server.kill end |
Instance Method Details
#cleanup ⇒ Object
Cleanup all server resources.
238 239 240 241 242 243 244 245 246 247 248 |
# File 'lib/certificate_depot/server.rb', line 238 def cleanup log.info("Shutting down") @lifelines.each do |pid, lifeline| begin lifeline.close rescue IOError end end socket.close remove_pid_file end |
#despawn_worker(pid) ⇒ Object
Deletes references to workers from the server instance
186 187 188 189 190 |
# File 'lib/certificate_depot/server.rb', line 186 def despawn_worker(pid) log.debug("Removing worker #{pid}") @workers.delete(pid) @lifelines.delete(pid) end |
#kill ⇒ Object
Sends the QUIT signal to the server process.
267 268 269 270 271 272 |
# File 'lib/certificate_depot/server.rb', line 267 def kill Process.kill(:QUIT, load_pid_from_file) true rescue Errno::ESRCH false end |
#load_pid_from_file ⇒ Object
Reads the PID of the process with the mainloop from the filesystem. Used for sending signals to a running server.
146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/certificate_depot/server.rb', line 146 def load_pid_from_file best_match = @options[:possible_pid_files].inject([]) do |matches, pid_file| begin log.debug("Considering reading PID from `#{pid_file}'") possibility = [File.atime(pid_file), File.read(pid_file).to_i] log.debug(" - created #{possibility[0]}, contains PID: #{possibility[1]}") matches << possibility rescue Errno::EACCES, Errno::ENOENT end; matches end.compact.sort.last best_match[1] if best_match end |
#log ⇒ Object
Returns a Log object for the server
75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/certificate_depot/server.rb', line 75 def log if @log.nil? @options[:possible_log_files].each do |log_file| begin file = File.open(log_file, File::WRONLY|File::APPEND|File::CREAT) @log = CertificateDepot::Log.new(file) rescue Errno::EACCES end end end; @log end |
#missing_workers ⇒ Object
Returns the number of workers that need to be created in order to get to the configured process count.
262 263 264 |
# File 'lib/certificate_depot/server.rb', line 262 def missing_workers @options[:process_count] - @workers.length end |
#reap_workers ⇒ Object
Figures out if any workers died and deletes them from internal structures if they did.
172 173 174 175 176 177 178 179 180 181 182 183 |
# File 'lib/certificate_depot/server.rb', line 172 def reap_workers # Don't try to find more dead workers than the process count @workers.length.times do # We use +waitpid+ to find any child process which has exited. It # immediately returns when there aren't any dead processes. if pid = Process.waitpid(-1, Process::WNOHANG) despawn_worker(pid) else return # Stop when we don't find any end end end |
#remove_pid_file ⇒ Object
Removes all possible PID files.
160 161 162 163 164 165 166 167 168 |
# File 'lib/certificate_depot/server.rb', line 160 def remove_pid_file @options[:possible_pid_files].each do |pid_file| begin File.unlink(pid_file) log.debug("Removed PID file `#{pid_file}'") rescue Errno::EACCES, Errno::ENOENT end end end |
#reroute_stdio ⇒ Object
Make all output of interpreter go to the logfile
126 127 128 129 |
# File 'lib/certificate_depot/server.rb', line 126 def reroute_stdio $stdout = log.file $stderr = log.file end |
#run ⇒ Object
Start behaving like a server. This method returns once the server has completely started.
Forks a process and starts a runloop in the fork. The runloop does worker housekeeping. It does so in three phases. First it removes all non-functional workers from its internal structures. After that it spawns new workers if it needs to. Finally it sleeps for a while so the the runloop doesn’t keep busy all the time.
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/certificate_depot/server.rb', line 95 def run log.info("Starting Certificate Depot server") trap_signals setup_socket save_pid_to_file(fork do # Generate a new process group so we're no longer a child process # of the TTY. Process.setsid reroute_stdio loop do break if signals_want_shutdown? reap_workers spawn_workers sleep end cleanup end) end |
#save_pid_to_file(pid) ⇒ Object
Write the PID of the process with the mainloop to the filesystem so we read it later on to signal the server to shutdown.
133 134 135 136 137 138 139 140 141 142 |
# File 'lib/certificate_depot/server.rb', line 133 def save_pid_to_file(pid) @options[:possible_pid_files].each do |pid_file| begin File.open(pid_file, 'w') { |file| file.write(pid.to_s) } log.debug("Writing PID to `#{pid_file}'") return pid_file rescue Errno::EACCES end end end |
#setup_socket ⇒ Object
Creates the socket the server listens on and binds it to the configured host and port.
252 253 254 255 256 257 258 |
# File 'lib/certificate_depot/server.rb', line 252 def setup_socket self.socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0) address = Socket.pack_sockaddr_in(@options[:port], @options[:host]) socket.bind(address) socket.listen(@options[:max_connection_queue]) log.info("Listening on #{@options[:host]}:#{@options[:port]}") end |
#signals_want_shutdown? ⇒ Boolean
Returns true when the signals received by the process demand a shutdown
121 122 123 |
# File 'lib/certificate_depot/server.rb', line 121 def signals_want_shutdown? !@signals.empty? end |
#sleep ⇒ Object
Sleeps until someone wants the server main loop to wake up or when 2 seconds go by. Workers can wake the server in two ways, either by writing anything to their lifeline or by severing the lifeline. The lifeline is severed when the worker dies.
223 224 225 226 227 228 229 230 231 232 233 234 235 |
# File 'lib/certificate_depot/server.rb', line 223 def sleep # Returns with active IO objects if any of them are written to. # Otherwise it times out after two seconds. if needy = IO.select(@lifelines.values, nil, @lifelines.values, 2) log.debug("Detected activity on: #{needy.inspect}") # Read everything coming in on the lifelines and discard it because # the contents doesn't matter. needy.flatten.each do |lifeline| loop { lifeline.read_nonblock(READ_BUFFER_SIZE) } unless lifeline.closed? end if needy end rescue EOFError, Errno::EAGAIN, Errno::EINTR, Errno::EBADF, IOError end |
#spawn_workers ⇒ Object
Figures out how many workers are currently running and creates new ones if needed.
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 |
# File 'lib/certificate_depot/server.rb', line 194 def spawn_workers missing_workers.times do worker = CertificateDepot::Worker.new(self) lifeline = IO.pipe pid = fork do # We close the server side of the pipe in this process otherwise we # don't get a EOF when reading from it. lifeline.first.close worker.lifeline = lifeline.last worker.run end @workers[pid] = worker @lifelines[pid] = lifeline.first # We close the client side of the pipe in this process otherwise we # don't get an EOF when reading from it. lifeline.last.close log.debug("Spawned worker #{pid}") end end |
#trap_signals ⇒ Object
Installs signal traps to listen for incoming signals to the process.
115 116 117 118 |
# File 'lib/certificate_depot/server.rb', line 115 def trap_signals trap(:QUIT) { @signals << :QUIT } trap(:EXIT) { @signals << :EXIT } end |