From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on dcvr.yhbt.net X-Spam-Level: X-Spam-ASN: AS62744 199.249.224.0/24 X-Spam-Status: No, score=-2.9 required=3.0 tests=AWL,BAYES_00, RCVD_IN_MSPIKE_BL,RCVD_IN_MSPIKE_ZBI,RCVD_IN_XBL,RDNS_NONE,SPF_FAIL, SPF_HELO_FAIL,TO_EQ_FM_DOM_SPF_FAIL,WEIRD_PORT shortcircuit=no autolearn=no autolearn_force=no version=3.4.0 Received: from 80x24.org (unknown [199.249.224.42]) by dcvr.yhbt.net (Postfix) with ESMTP id 2757620966 for ; Wed, 5 Apr 2017 18:40:40 +0000 (UTC) From: Eric Wong To: spew@80x24.org Subject: [PATCH] wip Date: Wed, 5 Apr 2017 18:40:37 +0000 Message-Id: <20170405184037.15716-1-e@80x24.org> List-Id: --- lib/yahns/config.rb | 23 +++++++- lib/yahns/http_context.rb | 4 ++ lib/yahns/rack_proxy.rb | 143 ++++++++++++++++++++++++++++++++++++++++++++++ lib/yahns/submaster.rb | 12 ++++ 4 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 lib/yahns/rack_proxy.rb create mode 100644 lib/yahns/submaster.rb diff --git a/lib/yahns/config.rb b/lib/yahns/config.rb index e545d59..1f4d17e 100644 --- a/lib/yahns/config.rb +++ b/lib/yahns/config.rb @@ -436,6 +436,27 @@ def commit!(server) server.__send__("#{var}=", val) if val != :unset end - @app_ctx.each { |app| app.logger ||= server.logger } + # count extra workers for rack_proxy (and maybe others) + submasters = 0 + afc = @set[:atfork_child] + afc = [] if afc == :unset + + nsm = 0 + @app_ctx.each do |ctx| + ctx.logger ||= server.logger + next unless ctx.respond_to?(:submasters) + cmd_list = ctx.submasters or next + require_relative 'submaster' + submasters += cmd_list.size + cmd_list.each do |cmd| + afc << Yahns::Submaster.new(cmd, nsm += 1) + end + end + if submasters > 0 + wp = @set[:worker_processes] + wp = 1 if wp == :unset # gotta have one worker + server.__send__(:worker_processes=, wp + submasters) + server.__send__(:atfork_child=, afc) + end end end diff --git a/lib/yahns/http_context.rb b/lib/yahns/http_context.rb index 40f2c58..70a75d7 100644 --- a/lib/yahns/http_context.rb +++ b/lib/yahns/http_context.rb @@ -91,4 +91,8 @@ def tmpio_for(len, env) end tmp end + + def submasters + @yahns_rack.respond_to?(:submasters) ? @yahns_rack.submasters : nil + end end diff --git a/lib/yahns/rack_proxy.rb b/lib/yahns/rack_proxy.rb new file mode 100644 index 0000000..1dee36a --- /dev/null +++ b/lib/yahns/rack_proxy.rb @@ -0,0 +1,143 @@ +# -*- encoding: binary -*- +# Copyright (C) 2017 all contributors +# License: GPL-3.0+ +# frozen_string_literal: true +require_relative 'rack' +require_relative 'proxy_pass' +require 'socket' + +# Basically, a lazy way to setup ProxyPass to hand off some (or all) +# requests to any HTTP server backend (e.g. varnish, etc) +class Yahns::RackProxy < Yahns::Rack # :nodoc: + + # the key is the destination returned by the top-level config.ru + # and the value is a splattable array for spawning another process + # via Process.exec + # { + # # [ key, backend URL, ] => %w(splattable array for Process.exec), + # [:pass, 'http://127.0.0.1:9292/' ] => %w(rackup /path/to/config.ru) + # [:lsock, 'unix:/path/to/sock' ] => %w(bleh -l /path/to/sock ...) + # + # # Users of Ruby 2.3+ can shorten their config when + # # running systemd-aware daemons which will bind to + # # a random TCP port: + # :pri => %w(blah -c conf.rb config.ru), + # :alt => %w(blah -c /path/to/alt.conf.rb alt.ru), + # :psgi => %w(blah foo.psgi), + # ... + # } + + # By default, proxy all requests by using the :pass return value + # Users can selectively process requests for non-buggy code in + # the core yahns processes. + PROXY_ALL = lambda { |env| :pass } # :nodoc: + attr_reader :submasters + + def initialize(ru = PROXY_ALL, mapping, opts = {}) + @submasters = [] + case mapping + when Hash # multiple HTTP backends running different commands + # nothing to do { key: splattable array for Process.spawn } + when Array # only one backend + mapping = { pass: cmd } + else + raise ArgumentError, "#{cmd.inspect} must be an Array or Hash" + end + env = nil + + @proxy_pass_map = {} + mapping.each do |key, cmd| + case key + when Array # undocumented for now.. + key, url, ppopts = *key + when Symbol # OK + ppopts = {} + else + raise ArgumentError, "#{key.inspect} is not a symbol" + end + Array === cmd or raise ArgumentError, + "#{cmd.inspect} must be a splattable array for Process.exec" + @proxy_pass_map[key] and raise ArgumentError, + "#{key.inspect} may not be repeated in mapping" + + cmd = cmd.dup + unless url + if RUBY_VERSION.to_f < 2.3 && env.nil? # only warn once + warn "Ruby < 2.3 may crash when emulating systemd to pass FDs\n", +" http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-core/69895\n" + end + + # nope, no UNIXServer support, maybe not worth it to deal + # with FS perms in containers. + # Also, we can use TCP Fast Open support under Linux + srv = random_tcp_listener(ppopts) + addr = srv.addr + url = "http://#{addr[3]}:#{addr[1]}/" + + # pretend to be systemd for sd_listen_fds(3) users + # submaster will #call any env values before calling Process.exec, + # so we can lazy-expand LISTEN_PID, here: + env ||= { 'LISTEN_FDS' => '1', 'LISTEN_PID' => Process.method(:pid) } + case cmd[0] + when Hash + cmd[0] = cmd[0].merge(env) + else + cmd.unshift(env) + end + + rdr = { 3 => srv } + case cmd[-1] + when Hash + cmd[-1] = cmd[-1].merge(rdr) + else + cmd << rdr + end + end + + @submasters << cmd + @proxy_pass_map[key] = Yahns::ProxyPass.new(url, ppopts) + end + super(ru, opts) # Yahns::Rack#initialize + end + + def build_app! + super # Yahns::Rack#build_app! + proxy_app = @app + + # wrap the (possibly-)user-supplied app + @app = lambda do |env| + res = proxy_app.call(env) + + # standard Rack responses may be handled in yahns proper: + Array === res and return res + + # the response is :pass or another Symbol, not a proper Rack response! + # shove the env over to the appropriate Yahns::ProxyPass which + # talks to a backend HTTP process: + ppass = @proxy_pass_map[res] and return ppass.call(env) + + # oops, user screwed up :< + logger = env['rack.logger'] and + logger.error("bad response from user-supplied proxy: #{res.inspect}") + + [ 500, [ %w(Content-Type text/plain) ], [] ] + end + end + + def random_tcp_listener(opts) # TODO: should we support options? + srv = TCPServer.new('127.0.0.1', 0) # 0: bind random port + srv.setsockopt(:SOL_SOCKET, :SO_KEEPALIVE, 1) + srv.setsockopt(:IPPROTO_TCP, :TCP_NODELAY, 1) + + # Deferring accepts slows down core yahns, but it's useful for + # less-sophisticated upstream (backend) servers: + Socket.const_defined?(:TCP_DEFER_ACCEPT) and + srv.setsockopt(:IPPROTO_TCP, :TCP_DEFER_ACCEPT, 1) + + srv.listen(1024) + srv + end +end + +# register ourselves +Yahns::Config::APP_CLASS[:rack_proxy] = Yahns::RackProxy diff --git a/lib/yahns/submaster.rb b/lib/yahns/submaster.rb new file mode 100644 index 0000000..b86b425 --- /dev/null +++ b/lib/yahns/submaster.rb @@ -0,0 +1,12 @@ +# -*- encoding: binary -*- +# Copyright (C) 2017 all contributors +# License: GPL-3.0+ +# frozen_string_literal: true +class Yahns::Submaster < Struct.new(cmd, idx) + + # atfork_child + def call(worker_nr) + return if worker_nr != idx + exec(*cmd) + end +end -- EW