Class: CertificateDepot::Server

Inherits:
Object
  • Object
show all
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.expand_path('~/.depot.pid')]
POSSIBLE_LOG_FILES =
['/var/log/depot.log', File.expand_path('~/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

Class Method Summary collapse

Instance Method Summary collapse

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, options={})
  @depot = depot
  
  # Override the default with user supplied options.
  @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

#depotObject

Returns the value of attribute depot


23
24
25
# File 'lib/certificate_depot/server.rb', line 23

def depot
  @depot
end

#socketObject

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, options={})
  server = new(depot, options)
  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(options={})
  server = new(nil, options)
  server.kill
end

Instance Method Details

#cleanupObject

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

#killObject

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_fileObject

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

#logObject

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_workersObject

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_workersObject

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_fileObject

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_stdioObject

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

#runObject

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_socketObject

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

Returns:

  • (Boolean)

121
122
123
# File 'lib/certificate_depot/server.rb', line 121

def signals_want_shutdown?
  !@signals.empty?
end

#sleepObject

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_workersObject

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_signalsObject

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