Repository: puma/puma (Ruby)

Issue:
Use `available_processor_count` as worker numbers if `WEB_CONCURRENCY` is set to `"auto"`
**Is your feature request related to a problem? Please describe.**
We are trying to make easier for applications to configure better defaults to puma in Rails. One of the features we introduced in our configuration was to automatically detect the number of processors and set that as the worker count.

We later had to revert that change because some platforms lie about how many processors they provide to users. So instead we defined a new value to `WEB_CONCURRENCY` that is `"auto"`.

We had to rever that change because puma is also reading the value of that variable and that would just explode trying to parsing it that.

See https://github.com/rails/rails/commit/142e6ab2c1e3cb7df4b82e20e2a1cd676b755714

**Describe the solution you'd like**
Now that puma is taking care of reading `WEB_CONCURRENCY`, I think it would make sense to puma to implement the `"auto"` feature. This would allow Rails and non-Rails users to get better default without having to change the puma.rb file with conditionals.

**Describe alternatives you've considered**
Rails could introduce a new variable to control the concurrency of puma, but I think that would provide worse experience since people are already used with `WEB_CONCURRENCY`.



Discussion:
Seems reasonable.

Currently, code is:
```ruby

workers = ENV['WEB_CONCURRENCY']
```

Is the following what you'd like it updated to?
```ruby
workers =
  if (t = ENV['WEB_CONCURRENCY']) == 'auto' && defined?(::Concurrent)
    Concurrent.available_processor_count
  else
    t
  end
```
Yes!
In this case, the 'code' changes are simple.

I haven't written any tests yet.  Ideally, we load `concurrent-ruby`, but I don't really want to load it in the main test processes, so tests are non-trivial...

I'll look at it this weekend.
Why don't you use `Etc.nprocessors`? No external dependency required.
Why doesn't Rails?

(maybe this? https://github.com/rails/rails/commit/f719787c582839fd2fcd886d70b43da3ddad2ceb)
Actually I don't know the answer to that, but I suppose IMHO, if `Etc.nprocessors` is not doing the right thing, maybe we should fix that in CRuby... it seems a bit weird if it doesn't work correctly.
It doesn't take in consideration cpu quotas, also JRuby, but yeah, that might be enough. The way the PR was implemented doesn't force the dependency, but if you have use it



Patch:
diff --git a/README.md b/README.md
index dc0714f5f1..01843eed5a 100644
--- a/README.md
+++ b/README.md
@@ -118,6 +118,8 @@ $ WEB_CONCURRENCY=3 puma -t 8:32
 
 Note that threads are still used in clustered mode, and the `-t` thread flag setting is per worker, so `-w 2 -t 16:16` will spawn 32 threads in total, with 16 in each worker process.
 
+If the `WEB_CONCURRENCY` environment variable is set to `"auto"` and the `concurrent-ruby` gem is available in your application, Puma will set the worker process count to the result of [available processors](https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent.html#available_processor_count-class_method).
+
 For an in-depth discussion of the tradeoffs of thread and process count settings, [see our docs](https://github.com/puma/puma/blob/9282a8efa5a0c48e39c60d22ca70051a25df9f55/docs/kubernetes.md#workers-per-pod-and-other-config-issues).
 
 In clustered mode, Puma can "preload" your application. This loads all the application code *prior* to forking. Preloading reduces total memory usage of your application via an operating system feature called [copy-on-write](https://en.wikipedia.org/wiki/Copy-on-write).
diff --git a/lib/puma/configuration.rb b/lib/puma/configuration.rb
index 085ee7dac4..4cd1cd807c 100644
--- a/lib/puma/configuration.rb
+++ b/lib/puma/configuration.rb
@@ -227,7 +227,12 @@ def puma_default_options(env = ENV)
     def puma_options_from_env(env = ENV)
       min = env['PUMA_MIN_THREADS'] || env['MIN_THREADS']
       max = env['PUMA_MAX_THREADS'] || env['MAX_THREADS']
-      workers = env['WEB_CONCURRENCY']
+      workers = if env['WEB_CONCURRENCY'] == 'auto'
+        require_processor_counter
+        ::Concurrent.available_processor_count
+      else
+        env['WEB_CONCURRENCY']
+      end
 
       {
         min_threads: min && Integer(min),
@@ -341,6 +346,16 @@ def self.temp_path
 
     private
 
+    def require_processor_counter
+      require 'concurrent/utility/processor_counter'
+    rescue LoadError
+      warn <<~MESSAGE
+        WEB_CONCURRENCY=auto requires the "concurrent-ruby" gem to be installed.
+        Please add "concurrent-ruby" to your Gemfile.
+      MESSAGE
+      raise
+    end
+
     # Load and use the normal Rack builder if we can, otherwise
     # fallback to our minimal version.
     def rack_builder


Test patch:
diff --git a/test/helpers/integration.rb b/test/helpers/integration.rb
index 9810001e60..eac9c23d61 100644
--- a/test/helpers/integration.rb
+++ b/test/helpers/integration.rb
@@ -76,6 +76,15 @@ def silent_and_checked_system_command(*args)
     assert(system(*args, out: File::NULL, err: File::NULL))
   end
 
+  def with_unbundled_env
+    bundler_ver = Gem::Version.new(Bundler::VERSION)
+    if bundler_ver < Gem::Version.new('2.1.0')
+      Bundler.with_clean_env { yield }
+    else
+      Bundler.with_unbundled_env { yield }
+    end
+  end
+
   def cli_server(argv,  # rubocop:disable Metrics/ParameterLists
       unix: false,      # uses a UNIXSocket for the server listener when true
       config: nil,      # string to use for config file
diff --git a/test/test_web_concurrency_auto.rb b/test/test_web_concurrency_auto.rb
new file mode 100644
index 0000000000..1e53330c59
--- /dev/null
+++ b/test/test_web_concurrency_auto.rb
@@ -0,0 +1,29 @@
+require_relative "helper"
+require_relative "helpers/integration"
+
+class TestWebConcurrencyAuto < TestIntegration
+
+  def teardown
+    return if skipped?
+    super
+  end
+
+  def test_web_concurrency_with_concurrent_ruby_available
+    skip_unless :fork
+    env = {
+      "BUNDLE_GEMFILE" => "#{__dir__}/web_concurrency_test/Gemfile",
+      "WEB_CONCURRENCY" => "auto"
+    }
+    Dir.chdir("#{__dir__}/web_concurrency_test") do
+      with_unbundled_env do
+        silent_and_checked_system_command("bundle config --local path vendor/bundle")
+        silent_and_checked_system_command("bundle install")
+      end
+      cli_server set_pumactl_args, env: env
+    end
+
+    connection = connect("/worker_count")
+    body = read_body(connection, 1)
+    assert_equal(get_stats.fetch("workers").to_s, body)
+  end
+end
diff --git a/test/test_worker_gem_independence.rb b/test/test_worker_gem_independence.rb
index 129817d4e3..48030206c0 100644
--- a/test/test_worker_gem_independence.rb
+++ b/test/test_worker_gem_independence.rb
@@ -138,13 +138,4 @@ def start_phased_restart
 
     true while @server.gets !~ /booted in [.0-9]+s, phase: 1/
   end
-
-  def with_unbundled_env
-    bundler_ver = Gem::Version.new(Bundler::VERSION)
-    if bundler_ver < Gem::Version.new('2.1.0')
-      Bundler.with_clean_env { yield }
-    else
-      Bundler.with_unbundled_env { yield }
-    end
-  end
 end
diff --git a/test/web_concurrency_test/Gemfile b/test/web_concurrency_test/Gemfile
new file mode 100644
index 0000000000..58f8134b4b
--- /dev/null
+++ b/test/web_concurrency_test/Gemfile
@@ -0,0 +1,5 @@
+source "https://rubygems.org"
+
+gem 'puma', path: '../..'
+
+gem 'concurrent-ruby', '~> 1.0'
diff --git a/test/web_concurrency_test/config.ru b/test/web_concurrency_test/config.ru
new file mode 100644
index 0000000000..79561d6d50
--- /dev/null
+++ b/test/web_concurrency_test/config.ru
@@ -0,0 +1,5 @@
+map "/worker_count" do
+  run ->(env) {
+    [200, {}, [Concurrent.available_processor_count.to_i.to_s]]
+  }
+end