puma 6.0.0 → 6.2.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of puma might be problematic. Click here for more details.

data/lib/puma/const.rb CHANGED
@@ -5,7 +5,6 @@ module Puma
5
5
  class UnsupportedOption < RuntimeError
6
6
  end
7
7
 
8
-
9
8
  # Every standard HTTP code mapped to the appropriate message. These are
10
9
  # used so frequently that they are placed directly in Puma for easy
11
10
  # access rather than Puma::Const itself.
@@ -100,10 +99,10 @@ module Puma
100
99
  # too taxing on performance.
101
100
  module Const
102
101
 
103
- PUMA_VERSION = VERSION = "6.0.0".freeze
104
- CODE_NAME = "Sunflower".freeze
102
+ PUMA_VERSION = VERSION = "6.2.0"
103
+ CODE_NAME = "Speaking of Now"
105
104
 
106
- PUMA_SERVER_STRING = ['puma', PUMA_VERSION, CODE_NAME].join(' ').freeze
105
+ PUMA_SERVER_STRING = ["puma", PUMA_VERSION, CODE_NAME].join(" ").freeze
107
106
 
108
107
  FAST_TRACK_KA_TIMEOUT = 0.2
109
108
 
@@ -112,28 +111,28 @@ module Puma
112
111
  WRITE_TIMEOUT = 10
113
112
 
114
113
  # The original URI requested by the client.
115
- REQUEST_URI= 'REQUEST_URI'.freeze
116
- REQUEST_PATH = 'REQUEST_PATH'.freeze
117
- QUERY_STRING = 'QUERY_STRING'.freeze
118
- CONTENT_LENGTH = "CONTENT_LENGTH".freeze
114
+ REQUEST_URI= "REQUEST_URI"
115
+ REQUEST_PATH = "REQUEST_PATH"
116
+ QUERY_STRING = "QUERY_STRING"
117
+ CONTENT_LENGTH = "CONTENT_LENGTH"
119
118
 
120
- PATH_INFO = 'PATH_INFO'.freeze
119
+ PATH_INFO = "PATH_INFO"
121
120
 
122
- PUMA_TMP_BASE = "puma".freeze
121
+ PUMA_TMP_BASE = "puma"
123
122
 
124
123
  ERROR_RESPONSE = {
125
124
  # Indicate that we couldn't parse the request
126
- 400 => "HTTP/1.1 400 Bad Request\r\n\r\n".freeze,
125
+ 400 => "HTTP/1.1 400 Bad Request\r\n\r\n",
127
126
  # The standard empty 404 response for bad requests. Use Error4040Handler for custom stuff.
128
- 404 => "HTTP/1.1 404 Not Found\r\nConnection: close\r\nServer: Puma #{PUMA_VERSION}\r\n\r\nNOT FOUND".freeze,
127
+ 404 => "HTTP/1.1 404 Not Found\r\nConnection: close\r\nServer: Puma #{PUMA_VERSION}\r\n\r\nNOT FOUND",
129
128
  # The standard empty 408 response for requests that timed out.
130
- 408 => "HTTP/1.1 408 Request Timeout\r\nConnection: close\r\nServer: Puma #{PUMA_VERSION}\r\n\r\n".freeze,
129
+ 408 => "HTTP/1.1 408 Request Timeout\r\nConnection: close\r\nServer: Puma #{PUMA_VERSION}\r\n\r\n",
131
130
  # Indicate that there was an internal error, obviously.
132
- 500 => "HTTP/1.1 500 Internal Server Error\r\n\r\n".freeze,
131
+ 500 => "HTTP/1.1 500 Internal Server Error\r\n\r\n",
133
132
  # Incorrect or invalid header value
134
- 501 => "HTTP/1.1 501 Not Implemented\r\n\r\n".freeze,
133
+ 501 => "HTTP/1.1 501 Not Implemented\r\n\r\n",
135
134
  # A common header for indicating the server is too busy. Not used yet.
136
- 503 => "HTTP/1.1 503 Service Unavailable\r\n\r\nBUSY".freeze
135
+ 503 => "HTTP/1.1 503 Service Unavailable\r\n\r\nBUSY"
137
136
  }.freeze
138
137
 
139
138
  # The basic max request size we'll try to read.
@@ -146,95 +145,88 @@ module Puma
146
145
  # Maximum request body size before it is moved out of memory and into a tempfile for reading.
147
146
  MAX_BODY = MAX_HEADER
148
147
 
149
- REQUEST_METHOD = "REQUEST_METHOD".freeze
150
- HEAD = "HEAD".freeze
151
- GET = "GET".freeze
152
- POST = "POST".freeze
153
- PUT = "PUT".freeze
154
- DELETE = "DELETE".freeze
155
- OPTIONS = "OPTIONS".freeze
156
- TRACE = "TRACE".freeze
157
- PATCH = "PATCH".freeze
158
- SUPPORTED_HTTP_METHODS = [HEAD, GET, POST, PUT, DELETE, OPTIONS, TRACE, PATCH].freeze
148
+ REQUEST_METHOD = "REQUEST_METHOD"
149
+ HEAD = "HEAD"
150
+ SUPPORTED_HTTP_METHODS = %w[HEAD GET POST PUT DELETE OPTIONS TRACE PATCH].freeze
159
151
  # ETag is based on the apache standard of hex mtime-size-inode (inode is 0 on win32)
160
- LINE_END = "\r\n".freeze
161
- REMOTE_ADDR = "REMOTE_ADDR".freeze
162
- HTTP_X_FORWARDED_FOR = "HTTP_X_FORWARDED_FOR".freeze
163
- HTTP_X_FORWARDED_SSL = "HTTP_X_FORWARDED_SSL".freeze
164
- HTTP_X_FORWARDED_SCHEME = "HTTP_X_FORWARDED_SCHEME".freeze
165
- HTTP_X_FORWARDED_PROTO = "HTTP_X_FORWARDED_PROTO".freeze
152
+ LINE_END = "\r\n"
153
+ REMOTE_ADDR = "REMOTE_ADDR"
154
+ HTTP_X_FORWARDED_FOR = "HTTP_X_FORWARDED_FOR"
155
+ HTTP_X_FORWARDED_SSL = "HTTP_X_FORWARDED_SSL"
156
+ HTTP_X_FORWARDED_SCHEME = "HTTP_X_FORWARDED_SCHEME"
157
+ HTTP_X_FORWARDED_PROTO = "HTTP_X_FORWARDED_PROTO"
166
158
 
167
- SERVER_NAME = "SERVER_NAME".freeze
168
- SERVER_PORT = "SERVER_PORT".freeze
169
- HTTP_HOST = "HTTP_HOST".freeze
170
- PORT_80 = "80".freeze
171
- PORT_443 = "443".freeze
172
- LOCALHOST = "localhost".freeze
173
- LOCALHOST_IPV4 = "127.0.0.1".freeze
174
- LOCALHOST_IPV6 = "::1".freeze
175
- UNSPECIFIED_IPV4 = "0.0.0.0".freeze
176
- UNSPECIFIED_IPV6 = "::".freeze
159
+ SERVER_NAME = "SERVER_NAME"
160
+ SERVER_PORT = "SERVER_PORT"
161
+ HTTP_HOST = "HTTP_HOST"
162
+ PORT_80 = "80"
163
+ PORT_443 = "443"
164
+ LOCALHOST = "localhost"
165
+ LOCALHOST_IPV4 = "127.0.0.1"
166
+ LOCALHOST_IPV6 = "::1"
167
+ UNSPECIFIED_IPV4 = "0.0.0.0"
168
+ UNSPECIFIED_IPV6 = "::"
177
169
 
178
- SERVER_PROTOCOL = "SERVER_PROTOCOL".freeze
179
- HTTP_11 = "HTTP/1.1".freeze
170
+ SERVER_PROTOCOL = "SERVER_PROTOCOL"
171
+ HTTP_11 = "HTTP/1.1"
180
172
 
181
- SERVER_SOFTWARE = "SERVER_SOFTWARE".freeze
182
- GATEWAY_INTERFACE = "GATEWAY_INTERFACE".freeze
183
- CGI_VER = "CGI/1.2".freeze
173
+ SERVER_SOFTWARE = "SERVER_SOFTWARE"
174
+ GATEWAY_INTERFACE = "GATEWAY_INTERFACE"
175
+ CGI_VER = "CGI/1.2"
184
176
 
185
- STOP_COMMAND = "?".freeze
186
- HALT_COMMAND = "!".freeze
187
- RESTART_COMMAND = "R".freeze
177
+ STOP_COMMAND = "?"
178
+ HALT_COMMAND = "!"
179
+ RESTART_COMMAND = "R"
188
180
 
189
- RACK_INPUT = "rack.input".freeze
190
- RACK_URL_SCHEME = "rack.url_scheme".freeze
191
- RACK_AFTER_REPLY = "rack.after_reply".freeze
192
- PUMA_SOCKET = "puma.socket".freeze
193
- PUMA_CONFIG = "puma.config".freeze
194
- PUMA_PEERCERT = "puma.peercert".freeze
181
+ RACK_INPUT = "rack.input"
182
+ RACK_URL_SCHEME = "rack.url_scheme"
183
+ RACK_AFTER_REPLY = "rack.after_reply"
184
+ PUMA_SOCKET = "puma.socket"
185
+ PUMA_CONFIG = "puma.config"
186
+ PUMA_PEERCERT = "puma.peercert"
195
187
 
196
- HTTP = "http".freeze
197
- HTTPS = "https".freeze
188
+ HTTP = "http"
189
+ HTTPS = "https"
198
190
 
199
- HTTPS_KEY = "HTTPS".freeze
191
+ HTTPS_KEY = "HTTPS"
200
192
 
201
- HTTP_VERSION = "HTTP_VERSION".freeze
202
- HTTP_CONNECTION = "HTTP_CONNECTION".freeze
203
- HTTP_EXPECT = "HTTP_EXPECT".freeze
204
- CONTINUE = "100-continue".freeze
193
+ HTTP_VERSION = "HTTP_VERSION"
194
+ HTTP_CONNECTION = "HTTP_CONNECTION"
195
+ HTTP_EXPECT = "HTTP_EXPECT"
196
+ CONTINUE = "100-continue"
205
197
 
206
- HTTP_11_100 = "HTTP/1.1 100 Continue\r\n\r\n".freeze
207
- HTTP_11_200 = "HTTP/1.1 200 OK\r\n".freeze
208
- HTTP_10_200 = "HTTP/1.0 200 OK\r\n".freeze
198
+ HTTP_11_100 = "HTTP/1.1 100 Continue\r\n\r\n"
199
+ HTTP_11_200 = "HTTP/1.1 200 OK\r\n"
200
+ HTTP_10_200 = "HTTP/1.0 200 OK\r\n"
209
201
 
210
- CLOSE = "close".freeze
211
- KEEP_ALIVE = "keep-alive".freeze
202
+ CLOSE = "close"
203
+ KEEP_ALIVE = "keep-alive"
212
204
 
213
- CONTENT_LENGTH2 = "content-length".freeze
214
- CONTENT_LENGTH_S = "Content-Length: ".freeze
215
- TRANSFER_ENCODING = "transfer-encoding".freeze
216
- TRANSFER_ENCODING2 = "HTTP_TRANSFER_ENCODING".freeze
205
+ CONTENT_LENGTH2 = "content-length"
206
+ CONTENT_LENGTH_S = "Content-Length: "
207
+ TRANSFER_ENCODING = "transfer-encoding"
208
+ TRANSFER_ENCODING2 = "HTTP_TRANSFER_ENCODING"
217
209
 
218
- CONNECTION_CLOSE = "Connection: close\r\n".freeze
219
- CONNECTION_KEEP_ALIVE = "Connection: Keep-Alive\r\n".freeze
210
+ CONNECTION_CLOSE = "Connection: close\r\n"
211
+ CONNECTION_KEEP_ALIVE = "Connection: Keep-Alive\r\n"
220
212
 
221
- TRANSFER_ENCODING_CHUNKED = "Transfer-Encoding: chunked\r\n".freeze
222
- CLOSE_CHUNKED = "0\r\n\r\n".freeze
213
+ TRANSFER_ENCODING_CHUNKED = "Transfer-Encoding: chunked\r\n"
214
+ CLOSE_CHUNKED = "0\r\n\r\n"
223
215
 
224
- CHUNKED = "chunked".freeze
216
+ CHUNKED = "chunked"
225
217
 
226
- COLON = ": ".freeze
218
+ COLON = ": "
227
219
 
228
- NEWLINE = "\n".freeze
220
+ NEWLINE = "\n"
229
221
 
230
- HIJACK_P = "rack.hijack?".freeze
231
- HIJACK = "rack.hijack".freeze
232
- HIJACK_IO = "rack.hijack_io".freeze
222
+ HIJACK_P = "rack.hijack?"
223
+ HIJACK = "rack.hijack"
224
+ HIJACK_IO = "rack.hijack_io"
233
225
 
234
- EARLY_HINTS = "rack.early_hints".freeze
226
+ EARLY_HINTS = "rack.early_hints"
235
227
 
236
228
  # Illegal character in the key or value of response header
237
- DQUOTE = "\"".freeze
229
+ DQUOTE = "\""
238
230
  HTTP_HEADER_DELIMITER = Regexp.escape("(),/:;<=>?@[]{}\\").freeze
239
231
  ILLEGAL_HEADER_KEY_REGEX = /[\x00-\x20#{DQUOTE}#{HTTP_HEADER_DELIMITER}]/.freeze
240
232
  # header values can contain HTAB?
data/lib/puma/detect.rb CHANGED
@@ -17,6 +17,8 @@ module Puma
17
17
  IS_WINDOWS = !!(RUBY_PLATFORM =~ /mswin|ming|cygwin/) ||
18
18
  IS_JRUBY && RUBY_DESCRIPTION.include?('mswin')
19
19
 
20
+ IS_LINUX = !(IS_OSX || IS_WINDOWS)
21
+
20
22
  # @version 5.2.0
21
23
  IS_MRI = (RUBY_ENGINE == 'ruby' || RUBY_ENGINE.nil?)
22
24
 
data/lib/puma/dsl.rb CHANGED
@@ -65,6 +65,7 @@ module Puma
65
65
 
66
66
  ca_additions = "&ca=#{Puma::Util.escape(opts[:ca])}" if ['peer', 'force_peer'].include?(verify)
67
67
 
68
+ low_latency_str = opts.key?(:low_latency) ? "&low_latency=#{opts[:low_latency]}" : ''
68
69
  backlog_str = opts[:backlog] ? "&backlog=#{Integer(opts[:backlog])}" : ''
69
70
 
70
71
  if defined?(JRUBY_VERSION)
@@ -114,7 +115,7 @@ module Puma
114
115
  end
115
116
 
116
117
  "ssl://#{host}:#{port}?#{cert_flags}#{key_flags}#{ssl_cipher_filter}" \
117
- "#{reuse_flag}&verify_mode=#{verify}#{tls_str}#{ca_additions}#{v_flags}#{backlog_str}"
118
+ "#{reuse_flag}&verify_mode=#{verify}#{tls_str}#{ca_additions}#{v_flags}#{backlog_str}#{low_latency_str}"
118
119
  end
119
120
  end
120
121
 
@@ -250,6 +251,7 @@ module Puma
250
251
  #
251
252
  # * Set the socket backlog depth with +backlog+, default is 1024.
252
253
  # * Set up an SSL certificate with +key+ & +cert+.
254
+ # * Set up an SSL certificate for mTLS with +key+, +cert+, +ca+ and +verify_mode+.
253
255
  # * Set whether to optimize for low latency instead of throughput with
254
256
  # +low_latency+, default is to not optimize for low latency. This is done
255
257
  # via +Socket::TCP_NODELAY+.
@@ -259,6 +261,8 @@ module Puma
259
261
  # bind 'unix:///var/run/puma.sock?backlog=512'
260
262
  # @example SSL cert
261
263
  # bind 'ssl://127.0.0.1:9292?key=key.key&cert=cert.pem'
264
+ # @example SSL cert for mutual TLS (mTLS)
265
+ # bind 'ssl://127.0.0.1:9292?key=key.key&cert=cert.pem&ca=ca.pem&verify_mode=force_peer'
262
266
  # @example Disable optimization for low latency
263
267
  # bind 'tcp://0.0.0.0:9292?low_latency=false'
264
268
  # @example Socket permissions
@@ -415,6 +419,11 @@ module Puma
415
419
  @options[:log_requests] = which
416
420
  end
417
421
 
422
+ # Pass in a custom logging class instance
423
+ def custom_logger(custom_logger)
424
+ @options[:custom_logger] = custom_logger
425
+ end
426
+
418
427
  # Show debugging info
419
428
  #
420
429
  def debug
@@ -581,6 +590,11 @@ module Puma
581
590
  @options[:silence_single_worker_warning] = true
582
591
  end
583
592
 
593
+ # Disable warning message when running single mode with callback hook defined.
594
+ def silence_fork_callback_warning
595
+ @options[:silence_fork_callback_warning] = true
596
+ end
597
+
584
598
  # Code to run immediately before master process
585
599
  # forks workers (once on boot). These hooks can block if necessary
586
600
  # to wait for background operations unknown to Puma to finish before
@@ -596,6 +610,8 @@ module Puma
596
610
  # puts "Starting workers..."
597
611
  # end
598
612
  def before_fork(&block)
613
+ warn_if_in_single_mode('before_fork')
614
+
599
615
  @options[:before_fork] ||= []
600
616
  @options[:before_fork] << block
601
617
  end
@@ -611,6 +627,8 @@ module Puma
611
627
  # puts 'Before worker boot...'
612
628
  # end
613
629
  def on_worker_boot(key = nil, &block)
630
+ warn_if_in_single_mode('on_worker_boot')
631
+
614
632
  process_hook :before_worker_boot, key, block, 'on_worker_boot'
615
633
  end
616
634
 
@@ -627,6 +645,8 @@ module Puma
627
645
  # puts 'On worker shutdown...'
628
646
  # end
629
647
  def on_worker_shutdown(key = nil, &block)
648
+ warn_if_in_single_mode('on_worker_shutdown')
649
+
630
650
  process_hook :before_worker_shutdown, key, block, 'on_worker_shutdown'
631
651
  end
632
652
 
@@ -641,6 +661,8 @@ module Puma
641
661
  # puts 'Before worker fork...'
642
662
  # end
643
663
  def on_worker_fork(&block)
664
+ warn_if_in_single_mode('on_worker_fork')
665
+
644
666
  process_hook :before_worker_fork, nil, block, 'on_worker_fork'
645
667
  end
646
668
 
@@ -655,11 +677,23 @@ module Puma
655
677
  # puts 'After worker fork...'
656
678
  # end
657
679
  def after_worker_fork(&block)
680
+ warn_if_in_single_mode('after_worker_fork')
681
+
658
682
  process_hook :after_worker_fork, nil, block, 'after_worker_fork'
659
683
  end
660
684
 
661
685
  alias_method :after_worker_boot, :after_worker_fork
662
686
 
687
+ # Code to run after puma is booted (works for both: single and clustered)
688
+ #
689
+ # @example
690
+ # on_booted do
691
+ # puts 'After booting...'
692
+ # end
693
+ def on_booted(&block)
694
+ @config.options[:events].on_booted(&block)
695
+ end
696
+
663
697
  # When `fork_worker` is enabled, code to run in Worker 0
664
698
  # before all other workers are re-forked from this process,
665
699
  # after the server has temporarily stopped serving requests
@@ -1019,6 +1053,19 @@ module Puma
1019
1053
  @options[:mutate_stdout_and_stderr_to_sync_on_write] = enabled
1020
1054
  end
1021
1055
 
1056
+ # Specify how big the request payload should be, in bytes.
1057
+ # This limit is compared against Content-Length HTTP header.
1058
+ # If the payload size (CONTENT_LENGTH) is larger than http_content_length_limit,
1059
+ # HTTP 413 status code is returned.
1060
+ #
1061
+ # When no Content-Length http header is present, it is compared against the
1062
+ # size of the body of the request.
1063
+ #
1064
+ # The default value for http_content_length_limit is nil.
1065
+ def http_content_length_limit(limit)
1066
+ @options[:http_content_length_limit] = limit
1067
+ end
1068
+
1022
1069
  private
1023
1070
 
1024
1071
  # To avoid adding cert_pem and key_pem as URI params, we store them on the
@@ -1046,7 +1093,20 @@ module Puma
1046
1093
  elsif key.nil?
1047
1094
  @options[options_key] << block
1048
1095
  else
1049
- raise "'#{method}' key must be String or Symbol"
1096
+ raise "'#{meth}' key must be String or Symbol"
1097
+ end
1098
+ end
1099
+
1100
+ def warn_if_in_single_mode(hook_name)
1101
+ return if @options[:silence_fork_callback_warning]
1102
+
1103
+ if (@options[:workers] || 0) == 0
1104
+ log_string =
1105
+ "Warning: You specified code to run in a `#{hook_name}` block, " \
1106
+ "but Puma is configured to run in cluster mode, " \
1107
+ "so your `#{hook_name}` block did not run"
1108
+
1109
+ LogWriter.stdio.log(log_string)
1050
1110
  end
1051
1111
  end
1052
1112
  end
@@ -102,7 +102,8 @@ module Puma
102
102
  @ioerr.is_a?(IO) and @ioerr.wait_writable(1)
103
103
  @ioerr.write "#{w_str}\n"
104
104
  @ioerr.flush unless @ioerr.sync
105
- rescue Errno::EPIPE, Errno::EBADF, IOError
105
+ rescue Errno::EPIPE, Errno::EBADF, IOError, Errno::EINVAL
106
+ # 'Invalid argument' (Errno::EINVAL) may be raised by flush
106
107
  end
107
108
  end
108
109
  rescue ThreadError
@@ -22,6 +22,16 @@ module Puma
22
22
  read
23
23
  end
24
24
 
25
+ # Read & Reset - returns contents and resets
26
+ # @return [String] StringIO contents
27
+ def read_and_reset
28
+ rewind
29
+ str = read
30
+ truncate 0
31
+ rewind
32
+ str
33
+ end
34
+
25
35
  alias_method :clear, :reset
26
36
 
27
37
  # before Ruby 2.5, `write` would only take one argument
data/lib/puma/launcher.rb CHANGED
@@ -59,6 +59,13 @@ module Puma
59
59
 
60
60
  @environment = conf.environment
61
61
 
62
+ # Load the systemd integration if we detect systemd's NOTIFY_SOCKET.
63
+ # Skip this on JRuby though, because it is incompatible with the systemd
64
+ # integration due to https://github.com/jruby/jruby/issues/6504
65
+ if ENV["NOTIFY_SOCKET"] && !Puma.jruby?
66
+ @config.plugins.create('systemd')
67
+ end
68
+
62
69
  if @config.options[:bind_to_activated_sockets]
63
70
  @config.options[:binds] = @binder.synthesize_binds_from_activated_fs(
64
71
  @config.options[:binds],
@@ -72,6 +79,8 @@ module Puma
72
79
  @log_writer.formatter = LogWriter::PidFormatter.new if clustered?
73
80
  @log_writer.formatter = options[:log_formatter] if @options[:log_formatter]
74
81
 
82
+ @log_writer.custom_logger = options[:custom_logger] if @options[:custom_logger]
83
+
75
84
  generate_restart_data
76
85
 
77
86
  if clustered? && !Puma.forkable?
@@ -180,7 +189,6 @@ module Puma
180
189
 
181
190
  setup_signals
182
191
  set_process_title
183
- integrate_with_systemd
184
192
 
185
193
  # This blocks until the server is stopped
186
194
  @runner.run
@@ -311,27 +319,6 @@ module Puma
311
319
  @runner.reload_worker_directory if @runner.respond_to?(:reload_worker_directory)
312
320
  end
313
321
 
314
- # Puma's systemd integration allows Puma to inform systemd:
315
- # 1. when it has successfully started
316
- # 2. when it is starting shutdown
317
- # 3. periodically for a liveness check with a watchdog thread
318
- def integrate_with_systemd
319
- return unless ENV["NOTIFY_SOCKET"]
320
-
321
- begin
322
- require_relative 'systemd'
323
- rescue LoadError
324
- log "Systemd integration failed. It looks like you're trying to use systemd notify but don't have sd_notify gem installed"
325
- return
326
- end
327
-
328
- log "* Enabling systemd notification integration"
329
-
330
- systemd = Systemd.new(@log_writer, @events)
331
- systemd.hook_events
332
- systemd.start_watchdog
333
- end
334
-
335
322
  def log(str)
336
323
  @log_writer.log(str)
337
324
  end
@@ -28,11 +28,12 @@ module Puma
28
28
  attr_reader :stdout,
29
29
  :stderr
30
30
 
31
- attr_accessor :formatter
31
+ attr_accessor :formatter, :custom_logger
32
32
 
33
33
  # Create a LogWriter that prints to +stdout+ and +stderr+.
34
34
  def initialize(stdout, stderr)
35
35
  @formatter = DefaultFormatter.new
36
+ @custom_logger = nil
36
37
  @stdout = stdout
37
38
  @stderr = stderr
38
39
 
@@ -59,7 +60,11 @@ module Puma
59
60
 
60
61
  # Write +str+ to +@stdout+
61
62
  def log(str)
62
- internal_write "#{@formatter.call str}\n"
63
+ if @custom_logger&.respond_to?(:write)
64
+ @custom_logger.write(format(str))
65
+ else
66
+ internal_write "#{@formatter.call str}\n"
67
+ end
63
68
  end
64
69
 
65
70
  def write(str)
@@ -73,13 +78,18 @@ module Puma
73
78
  @stdout.is_a?(IO) and @stdout.wait_writable(1)
74
79
  @stdout.write w_str
75
80
  @stdout.flush unless @stdout.sync
76
- rescue Errno::EPIPE, Errno::EBADF, IOError
81
+ rescue Errno::EPIPE, Errno::EBADF, IOError, Errno::EINVAL
82
+ # 'Invalid argument' (Errno::EINVAL) may be raised by flush
77
83
  end
78
84
  end
79
85
  rescue ThreadError
80
86
  end
81
87
  private :internal_write
82
88
 
89
+ def debug?
90
+ @debug
91
+ end
92
+
83
93
  def debug(str)
84
94
  log("% #{str}") if @debug
85
95
  end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../plugin'
4
+
5
+ # Puma's systemd integration allows Puma to inform systemd:
6
+ # 1. when it has successfully started
7
+ # 2. when it is starting shutdown
8
+ # 3. periodically for a liveness check with a watchdog thread
9
+ # 4. periodically set the status
10
+ Puma::Plugin.create do
11
+ def start(launcher)
12
+ require_relative '../sd_notify'
13
+
14
+ launcher.log_writer.log "* Enabling systemd notification integration"
15
+
16
+ # hook_events
17
+ launcher.events.on_booted { Puma::SdNotify.ready }
18
+ launcher.events.on_stopped { Puma::SdNotify.stopping }
19
+ launcher.events.on_restart { Puma::SdNotify.reloading }
20
+
21
+ # start watchdog
22
+ if Puma::SdNotify.watchdog?
23
+ ping_f = watchdog_sleep_time
24
+
25
+ in_background do
26
+ launcher.log_writer.log "Pinging systemd watchdog every #{ping_f.round(1)} sec"
27
+ loop do
28
+ sleep ping_f
29
+ Puma::SdNotify.watchdog
30
+ end
31
+ end
32
+ end
33
+
34
+ # start status loop
35
+ instance = self
36
+ sleep_time = 1.0
37
+ in_background do
38
+ launcher.log_writer.log "Sending status to systemd every #{sleep_time.round(1)} sec"
39
+
40
+ loop do
41
+ sleep sleep_time
42
+ # TODO: error handling?
43
+ Puma::SdNotify.status(instance.status)
44
+ end
45
+ end
46
+ end
47
+
48
+ def status
49
+ if clustered?
50
+ messages = stats[:worker_status].map do |worker|
51
+ common_message(worker[:last_status])
52
+ end.join(',')
53
+
54
+ "Puma #{Puma::Const::VERSION}: cluster: #{booted_workers}/#{workers}, worker_status: [#{messages}]"
55
+ else
56
+ "Puma #{Puma::Const::VERSION}: worker: #{common_message(stats)}"
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def watchdog_sleep_time
63
+ usec = Integer(ENV["WATCHDOG_USEC"])
64
+
65
+ sec_f = usec / 1_000_000.0
66
+ # "It is recommended that a daemon sends a keep-alive notification message
67
+ # to the service manager every half of the time returned here."
68
+ sec_f / 2
69
+ end
70
+
71
+ def stats
72
+ Puma.stats_hash
73
+ end
74
+
75
+ def clustered?
76
+ stats.has_key?(:workers)
77
+ end
78
+
79
+ def workers
80
+ stats.fetch(:workers, 1)
81
+ end
82
+
83
+ def booted_workers
84
+ stats.fetch(:booted_workers, 1)
85
+ end
86
+
87
+ def common_message(stats)
88
+ "{ #{stats[:running]}/#{stats[:max_threads]} threads, #{stats[:pool_capacity]} available, #{stats[:backlog]} backlog }"
89
+ end
90
+ end
@@ -2,8 +2,23 @@
2
2
 
3
3
  require_relative '../rack/handler/puma'
4
4
 
5
- module Rack::Handler
6
- def self.default(options = {})
7
- Rack::Handler::Puma
5
+ # rackup was removed in Rack 3, it is now a separate gem
6
+ if Object.const_defined? :Rackup
7
+ module Rackup
8
+ module Handler
9
+ def self.default(options = {})
10
+ ::Rackup::Handler::Puma
11
+ end
12
+ end
8
13
  end
14
+ elsif Object.const_defined?(:Rack) && Rack::RELEASE < '3'
15
+ module Rack
16
+ module Handler
17
+ def self.default(options = {})
18
+ ::Rack::Handler::Puma
19
+ end
20
+ end
21
+ end
22
+ else
23
+ raise "Rack 3 must be used with the Rackup gem"
9
24
  end
data/lib/puma/reactor.rb CHANGED
@@ -50,7 +50,7 @@ module Puma
50
50
  @input << client
51
51
  @selector.wakeup
52
52
  true
53
- rescue ClosedQueueError
53
+ rescue ClosedQueueError, IOError # Ignore if selector is already closed
54
54
  false
55
55
  end
56
56