dumping ground for random patches and texts
 help / color / mirror / Atom feed
From: Eric Wong <e@80x24.org>
To: spew@80x24.org
Subject: [PATCH] wip
Date: Wed,  5 Apr 2017 18:40:37 +0000	[thread overview]
Message-ID: <20170405184037.15716-1-e@80x24.org> (raw)

---
 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 <yahns-public@yhbt.net>
+# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
+# 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 <yahns-public@yhbt.net>
+# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
+# 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


             reply	other threads:[~2017-04-05 18:40 UTC|newest]

Thread overview: 23+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2017-04-05 18:40 Eric Wong [this message]
  -- strict thread matches above, loose matches on Subject: below --
2021-10-27 20:16 [PATCH] wip Eric Wong
2021-06-05 19:58 Eric Wong
2021-04-05  7:42 Eric Wong
2021-03-08  7:11 Eric Wong
2021-01-21  4:24 [PATCH] WIP Eric Wong
2021-01-03 22:57 [PATCH] wip Eric Wong
2020-12-27 11:36 [PATCH] WIP Eric Wong
2020-11-15  7:35 [PATCH] wip Eric Wong
2020-04-23  4:27 Eric Wong
2020-04-20  7:14 Eric Wong
2020-01-13  9:24 [PATCH] WIP Eric Wong
2019-05-11 22:55 Eric Wong
2019-01-02  9:21 [PATCH] wip Eric Wong
2018-07-06 21:31 Eric Wong
2018-06-24 11:55 Eric Wong
2018-06-24  8:39 Eric Wong
2017-07-15  1:42 [PATCH] WIP Eric Wong
2017-04-12 20:17 [PATCH] wip Eric Wong
2016-08-23 20:07 Eric Wong
2016-08-18  2:16 Eric Wong
2016-06-26  3:46 Eric Wong
2015-12-22  0:15 Eric Wong

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20170405184037.15716-1-e@80x24.org \
    --to=e@80x24.org \
    --cc=spew@80x24.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).