-
Notifications
You must be signed in to change notification settings - Fork 220
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fixes #25293 - Support Puma #774
base: develop
Are you sure you want to change the base?
Conversation
@@ -0,0 +1,29 @@ | |||
module Proxy | |||
class App |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For naming, perhaps this is more of a Router than an app?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't mind App
here. No big deal for me.
This now adds Puma support in the launcher, converting the settings to a puma config. It still misses a few things, such as the HTTPS client certificate checking. I've chosen to implement a different class for each launcher to clearly separate the two. It also doesn't install the USR1 signal handler. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the biggest difference is that spawning Puma is not done in an extra thread, but in the main thread so it's blocking. And also that puma can now be started using "puma" command.
Shall we simply deprecate the bin/smart-proxy
binary and only keep it for optional (temporary) fallback to Webrick? I don't expect issues with Puma migration for Smart Proxy, the code has been thread safe for years.
@@ -0,0 +1,29 @@ | |||
module Proxy | |||
class App |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't mind App
here. No big deal for me.
end | ||
|
||
def launch | ||
::Puma::Launcher.new(conf).run |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Offtopic: I noticed that the spawned thread in DHCP plugin does not have any shutdown implementation. It provides stop
method however it's never called except tests (?), I tested this using pry
(DHCP plugin must be enabled and working). This can be probably workarounded using at_exit
block with calling stop and waiting until the thread joins.
Anyway, this is not relevant, I was just investigating how shutdown works in puma - it performs graceful shutdown of the Puma's resources and that's about it. Although I had impression we do some thread joining for webrick case, we only do this for server threads (not background plugin DHCP thread).
end | ||
|
||
{ | ||
:app => app, | ||
:server => :webrick, | ||
:DoNotListen => true, | ||
:Port => http_port, # only being used to correctly log http port being used | ||
:Port => settings.http_port, # only being used to correctly log http port being used | ||
:Logger => ::Proxy::LogBuffer::TraceDecorator.instance, | ||
:AccessLog => [], | ||
:ServerSoftware => "foreman-proxy/#{Proxy::VERSION}", | ||
:daemonize => false, | ||
} | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Off topic: I have learned there is a MaxClients
setting which limits maximum amount of threads (thus simultaneous clients). By default it's 100, I think we should make this configurable for customers struggling with choking process - this feels like too high even when we don't use database (lots of other IO however).
Interestingly, puma defaults to 5 max treads for MRI, 16 for other interpreters. From various articles this looks like the sweet-spot in regard to GIL for typical web applications (database): https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server
def https_enabled? | ||
settings.ssl_private_key && settings.ssl_certificate && settings.ssl_ca_file | ||
end | ||
Proxy::SignalHandler.install_traps |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line is missing for Puma.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My comment is actually a leftover from my shutdown investigation, it's not relevant. These traps are only for webrick, maybe we should rename this method to show these are only for Webrick to prevent confusion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You found it out before I could reply. That's exactly the reasoning. Perhaps the SignalHandler should be merged in the Webrick launcher or at least moved to Proxy::Launcher::Webrick::SignalHandler
.
|
||
def settings | ||
launcher.settings | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some change with settings are causing dynflow to fail:
2020-10-06T10:47:24 [W] Error details for Error during startup, terminating: <NoMethodError>: undefined method `settings' for #<Hash:0x0000000002748d38>
/home/lzap/work/smart_proxy_dynflow/lib/smart_proxy_dynflow_core/settings.rb:82:in `load_from_proxy'
(eval):8:in `block (2 levels) in build_http_app'
/home/lzap/work/smart-proxy/lib/proxy/launcher/webrick.rb:46:in `instance_eval'
/home/lzap/work/smart-proxy/lib/proxy/launcher/webrick.rb:46:in `block (2 levels) in build_http_app'
/home/lzap/work/smart-proxy/lib/proxy/launcher/webrick.rb:46:in `each'
/home/lzap/work/smart-proxy/lib/proxy/launcher/webrick.rb:46:in `block in build_http_app'
/home/lzap/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/rack-2.2.3/lib/rack/builder.rb:125:in `instance_eval'
/home/lzap/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/rack-2.2.3/lib/rack/builder.rb:125:in `initialize'
/home/lzap/work/smart-proxy/lib/proxy/launcher/webrick.rb:45:in `new'
/home/lzap/work/smart-proxy/lib/proxy/launcher/webrick.rb:45:in `build_http_app'
/home/lzap/work/smart-proxy/lib/proxy/launcher/webrick.rb:14:in `launch'
/home/lzap/work/smart-proxy/lib/proxy/launcher.rb:93:in `launch'
bin/smart-proxy:7:in `<top (required)>'
/home/lzap/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/bundler-1.17.2/lib/bundler/cli/exec.rb:74:in `load'
/home/lzap/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/bundler-1.17.2/lib/bundler/cli/exec.rb:74:in `kernel_load'
/home/lzap/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/bundler-1.17.2/lib/bundler/cli/exec.rb:28:in `run'
/home/lzap/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/bundler-1.17.2/lib/bundler/cli.rb:463:in `exec'
/home/lzap/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/bundler-1.17.2/lib/bundler/vendor/thor/lib/thor/command.rb:27:in `run'
/home/lzap/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/bundler-1.17.2/lib/bundler/vendor/thor/lib/thor/invocation.rb:126:in `invoke_command'
/home/lzap/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/bundler-1.17.2/lib/bundler/vendor/thor/lib/thor.rb:387:in `dispatch'
/home/lzap/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/bundler-1.17.2/lib/bundler/cli.rb:27:in `dispatch'
/home/lzap/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/bundler-1.17.2/lib/bundler/vendor/thor/lib/thor/base.rb:466:in `start'
/home/lzap/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/bundler-1.17.2/lib/bundler/cli.rb:18:in `start'
/home/lzap/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/bundler-1.17.2/exe/bundle:30:in `block in <top (required)>'
/home/lzap/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/bundler-1.17.2/lib/bundler/friendly_errors.rb:124:in `with_friendly_errors'
/home/lzap/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/bundler-1.17.2/exe/bundle:22:in `<top (required)>'
/home/lzap/.rbenv/versions/2.7.1/bin/bundle:23:in `load'
/home/lzap/.rbenv/versions/2.7.1/bin/bundle:23:in `<main>'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't know the smart proxy used these internals. I'll take a look.
launcher = ::Launcher::Webrick.new(self) | ||
when 'puma' | ||
require 'proxy/launcher/puma' | ||
launcher = ::Launcher::Puma.new(self) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this needed? Aren't we actually switching from using our own startup binary to calling "puma" directly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was a bit of my confusion as well trying to understand the layers and I think @lzap worded the question better than I was going to. I attempted to read up on the Rack pattern and it's uses in places and apply it to here. I'm going to attempt to write out what I think I understand (and am probably wrong about in some way):
Puma -> Rack Middleware -> Smart Proxy Plugins
Where Puma serves as the application server handling spawning processes and threads to handle incoming requests.
Rack middleware (Proxy::App) handles mounting each Smart Proxy plugin as a Rack app as an http, https or both endpoint.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this needed? Aren't we actually switching from using our own startup binary to calling "puma" directly?
Both are valid. The Puma launcher keeps everything compatible from a packaging perspective: no need to change all config files and service files.
Puma -> Rack Middleware -> Smart Proxy Plugins
This is slightly incorrect. More correct would be:
Application server -> Rack application
Passenger would also be a valid application server since there is a config.ru
file. Any Rack application can also apply middleware (we don't do this today).
If you note, previously it also built a Rack application here:
Lines 47 to 49 in 88fbc8e
app = Rack::Builder.new do | |
plugins.each { |p| instance_eval(p.http_rackup) } | |
end |
And the HTTPS one here:
Lines 69 to 71 in 88fbc8e
app = Rack::Builder.new do | |
plugins.each { |p| instance_eval(p.https_rackup) } | |
end |
So the solution I chose was to write a third Rack application that embeds boths those and switches it based on the received protocol.
Note that a Rack application is defined as a class that has a call(env)
method. Env is a simple Ruby hash. To get a request, you can call Rack::Request.new(env)
. It's surprisingly simple.
https://github.com/rack/rack/blob/master/lib/rack/lobster.rb is the example they ship.
This matches the module layout
To support Puma, this takes the approach of introducing a Rack application that understands the HTTP request scheme. Depending on that, it responds either with the HTTP or HTTPS app. This means all the responsibility of binding to HTTP and HTTPS is shifted to the application server. To use this: bundle exec puma -b tcp://127.0.0.1:8000 -b 'ssl://127.0.0.1:8443?key=config/key.pem&cert=config/cert.pem' Note that this doesn't set the secure ciphers nor protocols. It also requires Puma to be built with OpenSSL extensions to be able to bind on HTTPS.
Rebased just so the big |
Another question I had: do we need to keep the daemonize code in? We don't use it with systemd and not for Windows (AFAIK). It may only be useful on BSD but there are other solutions to that. |
I vote for removing this, it's okay to run software non-daemonized or even in tmux/screen. What solutions are on your mind? Technically speaking, a daemon is a process that has some unique attributes and no external tool can daemonize a process. There are tools however which can help supervising such processes like DJ's daemontools or even systemd these days. I am totally fine that smart-proxy would not support daemonization in true UNIX sense, supervisor should be enough. |
https://software.clapper.org/daemonize/ is one of the first things to show up when I look for bsd daemonize. So roughly the ones you mentioned.
Let's start with that then. It'll make this PR smaller. |
I opened #777 to remove the daemonize code. |
This seems to completely break
When running
|
@ekohl What remains to be done here? The needed rebase is fairly trivial, smart_proxy_dynflow_core does not exist anymore and so it doesn't cause any issues. |
To support Puma, this takes the approach of introducing a Rack application that understands the HTTP request scheme. Depending on that, it responds either with the HTTP or HTTPS app.
This means all the responsibility of binding to HTTP and HTTPS is shifted to the application server.
To use this:
Note that this doesn't set the secure ciphers nor protocols. It also requires Puma to be built with OpenSSL extensions to be able to bind on HTTPS.
I verified this with by setting the BMC module to enabled only on HTTPS:
Then running verification: