Repository: puma/puma (Ruby)

Issue:
Support for shutdown on idle
**Is your feature request related to a problem? Please describe.**
In order to reduce resource consumption, infrequently utilised Rails/Rack apps should not occupy memory when they are not actively used. Socket activation, which Puma already supports, solves part of this; applications can be started on demand as soon as a request comes in. However, there doesn't seem to be a way to shut down the server again when the app is no longer active.

**Describe the solution you'd like**
There should be an (opt-in) setting that allows to specify a timeout. If no new request is received inside that timeout, the server should shut down and exit cleanly. Care must be taken not to close the sockets received from systemd.

**Describe alternatives you've considered**
This feature cannot be implemented in the service manager (systemd), as it doesn't keep track of activity on the sockets it created after the service has been started. This information is only available to Puma itself.

(I'd be willing to implement this if someone can point me at the right place to start.)



Fix (patch):
diff --git a/lib/puma/cli.rb b/lib/puma/cli.rb
index 55ac41a143..ba423b4d9d 100644
--- a/lib/puma/cli.rb
+++ b/lib/puma/cli.rb
@@ -144,6 +144,10 @@ def setup_options
             $LOAD_PATH.unshift(*arg.split(':'))
           end
 
+          o.on "--idle-timeout SECONDS", "Number of seconds until the next request before automatic shutdown" do |arg|
+            user_config.idle_timeout arg
+          end
+
           o.on "-p", "--port PORT", "Define the TCP port to bind to",
             "Use -b for more advanced options" do |arg|
             user_config.bind "tcp://#{Configuration::DEFAULTS[:tcp_host]}:#{arg}"
diff --git a/lib/puma/configuration.rb b/lib/puma/configuration.rb
index 94a3d51add..b560721f8c 100644
--- a/lib/puma/configuration.rb
+++ b/lib/puma/configuration.rb
@@ -133,8 +133,10 @@ class Configuration
       debug: false,
       early_hints: nil,
       environment: 'development'.freeze,
-      # Number of seconds to wait until we get the first data for the request
+      # Number of seconds to wait until we get the first data for the request.
       first_data_timeout: 30,
+      # Number of seconds to wait until the next request before shutting down.
+      idle_timeout: nil,
       io_selector_backend: :auto,
       log_requests: false,
       logger: STDOUT,
diff --git a/lib/puma/dsl.rb b/lib/puma/dsl.rb
index 04f13d8675..5fe8f5d573 100644
--- a/lib/puma/dsl.rb
+++ b/lib/puma/dsl.rb
@@ -315,16 +315,22 @@ def port(port, host=nil)
       bind URI::Generic.build(scheme: 'tcp', host: host, port: Integer(port)).to_s
     end
 
+    # Define how long the tcp socket stays open, if no data has been received.
+    # @see Puma::Server.new
+    def first_data_timeout(seconds)
+      @options[:first_data_timeout] = Integer(seconds)
+    end
+
     # Define how long persistent connections can be idle before Puma closes them.
     # @see Puma::Server.new
     def persistent_timeout(seconds)
       @options[:persistent_timeout] = Integer(seconds)
     end
 
-    # Define how long the tcp socket stays open, if no data has been received.
+    # If a new request is not received within this number of seconds, begin shutting down.
     # @see Puma::Server.new
-    def first_data_timeout(seconds)
-      @options[:first_data_timeout] = Integer(seconds)
+    def idle_timeout(seconds)
+      @options[:idle_timeout] = Integer(seconds)
     end
 
     # Work around leaky apps that leave garbage in Thread locals
diff --git a/lib/puma/server.rb b/lib/puma/server.rb
index 807213a008..5d23ddca94 100644
--- a/lib/puma/server.rb
+++ b/lib/puma/server.rb
@@ -86,15 +86,16 @@ def initialize(app, events = nil, options = {})
         UserFileDefaultOptions.new(options, Configuration::DEFAULTS)
       end
 
-      @log_writer          = @options.fetch :log_writer, LogWriter.stdio
-      @early_hints         = @options[:early_hints]
-      @first_data_timeout  = @options[:first_data_timeout]
-      @min_threads         = @options[:min_threads]
-      @max_threads         = @options[:max_threads]
-      @persistent_timeout  = @options[:persistent_timeout]
-      @queue_requests      = @options[:queue_requests]
-      @max_fast_inline     = @options[:max_fast_inline]
-      @io_selector_backend = @options[:io_selector_backend]
+      @log_writer                = @options.fetch :log_writer, LogWriter.stdio
+      @early_hints               = @options[:early_hints]
+      @first_data_timeout        = @options[:first_data_timeout]
+      @persistent_timeout        = @options[:persistent_timeout]
+      @idle_timeout              = @options[:idle_timeout]
+      @min_threads               = @options[:min_threads]
+      @max_threads               = @options[:max_threads]
+      @queue_requests            = @options[:queue_requests]
+      @max_fast_inline           = @options[:max_fast_inline]
+      @io_selector_backend       = @options[:io_selector_backend]
       @http_content_length_limit = @options[:http_content_length_limit]
 
       # make this a hash, since we prefer `key?` over `include?`
@@ -330,8 +331,12 @@ def handle_servers
 
         while @status == :run || (drain && shutting_down?)
           begin
-            ios = IO.select sockets, nil, nil, (shutting_down? ? 0 : nil)
-            break unless ios
+            ios = IO.select sockets, nil, nil, (shutting_down? ? 0 : @idle_timeout)
+            unless ios
+              @status = :stop unless shutting_down?
+              break
+            end
+
             ios.first.each do |sock|
               if sock == check
                 break if handle_check