diff options
Diffstat (limited to 'lib/unicorn/http_server.rb')
-rw-r--r-- | lib/unicorn/http_server.rb | 142 |
1 files changed, 68 insertions, 74 deletions
diff --git a/lib/unicorn/http_server.rb b/lib/unicorn/http_server.rb index 5334fa0..08fbe40 100644 --- a/lib/unicorn/http_server.rb +++ b/lib/unicorn/http_server.rb @@ -1,4 +1,5 @@ # -*- encoding: binary -*- +# frozen_string_literal: false # This is the process manager of Unicorn. This manages worker # processes which in turn handle the I/O and application process. @@ -6,7 +7,7 @@ # forked worker children. # # Users do not need to know the internals of this class, but reading the -# {source}[https://bogomips.org/unicorn.git/tree/lib/unicorn/http_server.rb] +# {source}[https://yhbt.net/unicorn.git/tree/lib/unicorn/http_server.rb] # is education for programmers wishing to learn how unicorn works. # See Unicorn::Configurator for information on how to configure unicorn. class Unicorn::HttpServer @@ -15,7 +16,7 @@ class Unicorn::HttpServer :before_fork, :after_fork, :before_exec, :listener_opts, :preload_app, :orig_app, :config, :ready_pipe, :user, - :default_middleware + :default_middleware, :early_hints attr_writer :after_worker_exit, :after_worker_ready, :worker_exec attr_reader :pid, :logger @@ -69,7 +70,6 @@ class Unicorn::HttpServer # incoming requests on the socket. def initialize(app, options = {}) @app = app - @request = Unicorn::HttpRequest.new @reexec_pid = 0 @default_middleware = true options = options.dup @@ -78,6 +78,7 @@ class Unicorn::HttpServer options[:use_defaults] = true self.config = Unicorn::Configurator.new(options) self.listener_opts = {} + @immortal = [] # immortal inherited sockets from systemd # We use @self_pipe differently in the master and worker processes: # @@ -111,9 +112,7 @@ class Unicorn::HttpServer @worker_data = if worker_data = ENV['UNICORN_WORKER'] worker_data = worker_data.split(',').map!(&:to_i) - worker_data[1] = worker_data.slice!(1..2).map do |i| - Kgio::Pipe.for_fd(i) - end + worker_data[1] = worker_data.slice!(1..2).map { |i| IO.for_fd(i) } worker_data end end @@ -159,6 +158,7 @@ class Unicorn::HttpServer end set_names = listener_names(listeners) dead_names.concat(cur_names - set_names).uniq! + dead_names -= @immortal.map { |io| sock_name(io) } LISTENERS.delete_if do |io| if dead_names.include?(sock_name(io)) @@ -188,7 +188,8 @@ class Unicorn::HttpServer rescue Errno::EEXIST retry end - fp.syswrite("#$$\n") + fp.sync = true + fp.write("#$$\n") File.rename(fp.path, path) fp.close end @@ -241,10 +242,6 @@ class Unicorn::HttpServer tries = opt[:tries] || 5 begin io = bind_listen(address, opt) - unless Kgio::TCPServer === io || Kgio::UNIXServer === io - io.autoclose = false - io = server_cast(io) - end logger.info "listening on addr=#{sock_name(io)} fd=#{io.fileno}" LISTENERS << io io @@ -387,12 +384,13 @@ class Unicorn::HttpServer # the Ruby itself and not require a separate malloc (on 32-bit MRI 1.9+). # Most reads are only one byte here and uncommon, so it's not worth a # persistent buffer, either: - @self_pipe[0].kgio_tryread(11) + @self_pipe[0].read_nonblock(11, exception: false) end def awaken_master return if $$ != @master_pid - @self_pipe[1].kgio_trywrite('.') # wakeup master process from select + # wakeup master process from select + @self_pipe[1].write_nonblock('.', exception: false) end # reaps all unreaped workers @@ -446,11 +444,6 @@ class Unicorn::HttpServer Dir.chdir(START_CTX[:cwd]) cmd = [ START_CTX[0] ].concat(START_CTX[:argv]) - # avoid leaking FDs we don't know about, but let before_exec - # unset FD_CLOEXEC, if anything else in the app eventually - # relies on FD inheritence. - close_sockets_on_exec(listener_fds) - # exec(command, hash) works in at least 1.9.1+, but will only be # required in 1.9.4/2.0.0 at earliest. cmd << listener_fds @@ -472,29 +465,15 @@ class Unicorn::HttpServer worker_info = [worker.nr, worker.to_io.fileno, worker.master.fileno] env['UNICORN_WORKER'] = worker_info.join(',') - close_sockets_on_exec(listener_fds) - Process.spawn(env, START_CTX[0], *START_CTX[:argv], listener_fds) end def listener_sockets listener_fds = {} - LISTENERS.each do |sock| - sock.close_on_exec = false - listener_fds[sock.fileno] = sock - end + LISTENERS.each { |sock| listener_fds[sock.fileno] = sock } listener_fds end - def close_sockets_on_exec(sockets) - (3..1024).each do |io| - next if sockets.include?(io) - io = IO.for_fd(io) rescue next - io.autoclose = false - io.close_on_exec = true - end - end - # forcibly terminate all workers that haven't checked in in timeout seconds. The timeout is implemented using an unlinked File def murder_lazy_workers next_sleep = @timeout - 1 @@ -582,16 +561,25 @@ class Unicorn::HttpServer 500 end if code - client.kgio_trywrite(err_response(code, @request.response_start_sent)) + code = err_response(code, @request.response_start_sent) + client.write_nonblock(code, exception: false) end client.close rescue end + def e103_response_write(client, headers) + rss = @request.response_start_sent + buf = rss ? "103 Early Hints\r\n" : "HTTP/1.1 103 Early Hints\r\n" + headers.each { |key, value| append_header(buf, key, value) } + buf << (rss ? "\r\nHTTP/1.1 ".freeze : "\r\n".freeze) + client.write(buf) + end + def e100_response_write(client, env) # We use String#freeze to avoid allocations under Ruby 2.1+ # Not many users hit this code path, so it's better to reduce the - # constant table sizes even for 1.9.3-2.0 users who'll hit extra + # constant table sizes even for Ruby 2.0 users who'll hit extra # allocations here. client.write(@request.response_start_sent ? "100 Continue\r\n\r\nHTTP/1.1 ".freeze : @@ -601,8 +589,19 @@ class Unicorn::HttpServer # once a client is accepted, it is processed in its entirety here # in 3 easy steps: read request, call app, write app response - def process_client(client) - status, headers, body = @app.call(env = @request.read(client)) + def process_client(client, ai) + @request = Unicorn::HttpRequest.new + env = @request.read_headers(client, ai) + + if early_hints + env["rack.early_hints"] = lambda do |headers| + e103_response_write(client, headers) + end + end + + env["rack.after_reply"] = [] + + status, headers, body = @app.call(env) begin return if @request.hijacked? @@ -624,6 +623,8 @@ class Unicorn::HttpServer end rescue => e handle_error(client, e) + ensure + env["rack.after_reply"].each(&:call) if env end def nuke_listeners!(readers) @@ -654,7 +655,6 @@ class Unicorn::HttpServer LISTENERS.each { |sock| sock.close_on_exec = true } worker.user(*user) if user.kind_of?(Array) && ! worker.switched - self.timeout /= 2.0 # halve it for select() @config = nil build_app! unless preload_app @after_fork = @listener_opts = @orig_app = nil @@ -668,58 +668,54 @@ class Unicorn::HttpServer logger.info "worker=#{worker_nr} reopening logs..." Unicorn::Util.reopen_logs logger.info "worker=#{worker_nr} done reopening logs" + false rescue => e logger.error(e) rescue nil exit!(77) # EX_NOPERM in sysexits.h end + def prep_readers(readers) + wtr = Unicorn::Waiter.prep_readers(readers) + @timeout *= 500 # to milliseconds for epoll, but halved + wtr + rescue + require_relative 'select_waiter' + @timeout /= 2.0 # halved for IO.select + Unicorn::SelectWaiter.new + end + # runs inside each forked worker, this sits around and waits # for connections and doesn't die until the parent dies (or is # given a INT, QUIT, or TERM signal) def worker_loop(worker) - ppid = @master_pid readers = init_worker_process(worker) - nr = 0 # this becomes negative if we need to reopen logs + waiter = prep_readers(readers) + reopen = false # this only works immediately if the master sent us the signal # (which is the normal case) - trap(:USR1) { nr = -65536 } + trap(:USR1) { reopen = true } ready = readers.dup @after_worker_ready.call(self, worker) begin - nr < 0 and reopen_worker_logs(worker.nr) - nr = 0 + reopen = reopen_worker_logs(worker.nr) if reopen worker.tick = time_now.to_i - tmp = ready.dup - while sock = tmp.shift - # Unicorn::Worker#kgio_tryaccept is not like accept(2) at all, - # but that will return false - if client = sock.kgio_tryaccept - process_client(client) - nr += 1 + while sock = ready.shift + client_ai = sock.accept_nonblock(exception: false) + if client_ai != :wait_readable + process_client(*client_ai) worker.tick = time_now.to_i end - break if nr < 0 + break if reopen end - # make the following bet: if we accepted clients this round, - # we're probably reasonably busy, so avoid calling select() - # and do a speculative non-blocking accept() on ready listeners - # before we sleep again in select(). - unless nr == 0 - tmp = ready.dup - redo - end - - ppid == Process.ppid or return - - # timeout used so we can detect parent death: + # timeout so we can .tick and keep parent from SIGKILL-ing us worker.tick = time_now.to_i - ret = IO.select(readers, nil, nil, @timeout) and ready = ret[0] + waiter.get_readers(ready, readers, @timeout) rescue => e - redo if nr < 0 && readers[0] + redo if reopen && readers[0] Unicorn.log_error(@logger, "listen loop error", e) if readers[0] end while readers[0] end @@ -807,21 +803,21 @@ class Unicorn::HttpServer def inherit_listeners! # inherit sockets from parents, they need to be plain Socket objects - # before they become Kgio::UNIXServer or Kgio::TCPServer inherited = ENV['UNICORN_FD'].to_s.split(',') + immortal = [] # emulate sd_listen_fds() for systemd sd_pid, sd_fds = ENV.values_at('LISTEN_PID', 'LISTEN_FDS') if sd_pid.to_i == $$ # n.b. $$ can never be zero # 3 = SD_LISTEN_FDS_START - inherited.concat((3...(3 + sd_fds.to_i)).to_a) + immortal = (3...(3 + sd_fds.to_i)).to_a + inherited.concat(immortal) end # to ease debugging, we will not unset LISTEN_PID and LISTEN_FDS inherited.map! do |fd| io = Socket.for_fd(fd.to_i) - io.autoclose = false - io = server_cast(io) + @immortal << io if immortal.include?(fd) set_server_sockopt(io, listener_opts[sock_name(io)]) logger.info "inherited addr=#{sock_name(io)} fd=#{io.fileno}" io @@ -830,11 +826,9 @@ class Unicorn::HttpServer config_listeners = config[:listeners].dup LISTENERS.replace(inherited) - # we start out with generic Socket objects that get cast to either - # Kgio::TCPServer or Kgio::UNIXServer objects; but since the Socket - # objects share the same OS-level file descriptor as the higher-level - # *Server objects; we need to prevent Socket objects from being - # garbage-collected + # we only use generic Socket objects for aggregate Socket#accept_nonblock + # return value [ Socket, Addrinfo ]. This allows us to avoid having to + # make getpeername(2) syscalls later on to fill in env['REMOTE_ADDR'] config_listeners -= listener_names if config_listeners.empty? && LISTENERS.empty? config_listeners << Unicorn::Const::DEFAULT_LISTEN |