puma 5.0.0-java → 5.1.0-java

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.

Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +1190 -574
  3. data/README.md +28 -20
  4. data/bin/puma-wild +3 -9
  5. data/docs/compile_options.md +19 -0
  6. data/docs/deployment.md +5 -6
  7. data/docs/fork_worker.md +2 -0
  8. data/docs/jungle/README.md +0 -4
  9. data/docs/jungle/rc.d/puma +2 -2
  10. data/docs/nginx.md +1 -1
  11. data/docs/restart.md +46 -23
  12. data/docs/systemd.md +25 -3
  13. data/ext/puma_http11/ext_help.h +1 -1
  14. data/ext/puma_http11/extconf.rb +4 -5
  15. data/ext/puma_http11/http11_parser.c +64 -64
  16. data/ext/puma_http11/mini_ssl.c +39 -37
  17. data/ext/puma_http11/puma_http11.c +25 -12
  18. data/lib/puma.rb +7 -4
  19. data/lib/puma/app/status.rb +44 -46
  20. data/lib/puma/binder.rb +48 -1
  21. data/lib/puma/cli.rb +4 -0
  22. data/lib/puma/client.rb +31 -80
  23. data/lib/puma/cluster.rb +39 -202
  24. data/lib/puma/cluster/worker.rb +176 -0
  25. data/lib/puma/cluster/worker_handle.rb +86 -0
  26. data/lib/puma/configuration.rb +20 -8
  27. data/lib/puma/const.rb +11 -3
  28. data/lib/puma/control_cli.rb +71 -70
  29. data/lib/puma/dsl.rb +67 -19
  30. data/lib/puma/error_logger.rb +2 -2
  31. data/lib/puma/events.rb +21 -3
  32. data/lib/puma/json.rb +96 -0
  33. data/lib/puma/launcher.rb +61 -12
  34. data/lib/puma/minissl.rb +8 -0
  35. data/lib/puma/puma_http11.jar +0 -0
  36. data/lib/puma/queue_close.rb +26 -0
  37. data/lib/puma/reactor.rb +79 -373
  38. data/lib/puma/request.rb +451 -0
  39. data/lib/puma/runner.rb +15 -21
  40. data/lib/puma/server.rb +193 -508
  41. data/lib/puma/single.rb +3 -2
  42. data/lib/puma/state_file.rb +5 -3
  43. data/lib/puma/systemd.rb +46 -0
  44. data/lib/puma/thread_pool.rb +22 -2
  45. data/lib/puma/util.rb +12 -0
  46. metadata +9 -6
  47. data/docs/jungle/upstart/README.md +0 -61
  48. data/docs/jungle/upstart/puma-manager.conf +0 -31
  49. data/docs/jungle/upstart/puma.conf +0 -69
  50. data/lib/puma/accept_nonblock.rb +0 -29
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Puma
4
+ class Cluster < Puma::Runner
5
+ # This class is instantiated by the `Puma::Cluster` and represents a single
6
+ # worker process.
7
+ #
8
+ # At the core of this class is running an instance of `Puma::Server` which
9
+ # gets created via the `start_server` method from the `Puma::Runner` class
10
+ # that this inherits from.
11
+ class Worker < Puma::Runner
12
+ attr_reader :index, :master
13
+
14
+ def initialize(index:, master:, launcher:, pipes:, server: nil)
15
+ super launcher, launcher.events
16
+
17
+ @index = index
18
+ @master = master
19
+ @launcher = launcher
20
+ @options = launcher.options
21
+ @check_pipe = pipes[:check_pipe]
22
+ @worker_write = pipes[:worker_write]
23
+ @fork_pipe = pipes[:fork_pipe]
24
+ @wakeup = pipes[:wakeup]
25
+ @server = server
26
+ end
27
+
28
+ def run
29
+ title = "puma: cluster worker #{index}: #{master}"
30
+ title += " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
31
+ $0 = title
32
+
33
+ Signal.trap "SIGINT", "IGNORE"
34
+ Signal.trap "SIGCHLD", "DEFAULT"
35
+
36
+ Thread.new do
37
+ Puma.set_thread_name "worker check pipe"
38
+ IO.select [@check_pipe]
39
+ log "! Detected parent died, dying"
40
+ exit! 1
41
+ end
42
+
43
+ # If we're not running under a Bundler context, then
44
+ # report the info about the context we will be using
45
+ if !ENV['BUNDLE_GEMFILE']
46
+ if File.exist?("Gemfile")
47
+ log "+ Gemfile in context: #{File.expand_path("Gemfile")}"
48
+ elsif File.exist?("gems.rb")
49
+ log "+ Gemfile in context: #{File.expand_path("gems.rb")}"
50
+ end
51
+ end
52
+
53
+ # Invoke any worker boot hooks so they can get
54
+ # things in shape before booting the app.
55
+ @launcher.config.run_hooks :before_worker_boot, index, @launcher.events
56
+
57
+ server = @server ||= start_server
58
+ restart_server = Queue.new << true << false
59
+
60
+ fork_worker = @options[:fork_worker] && index == 0
61
+
62
+ if fork_worker
63
+ restart_server.clear
64
+ worker_pids = []
65
+ Signal.trap "SIGCHLD" do
66
+ wakeup! if worker_pids.reject! do |p|
67
+ Process.wait(p, Process::WNOHANG) rescue true
68
+ end
69
+ end
70
+
71
+ Thread.new do
72
+ Puma.set_thread_name "worker fork pipe"
73
+ while (idx = @fork_pipe.gets)
74
+ idx = idx.to_i
75
+ if idx == -1 # stop server
76
+ if restart_server.length > 0
77
+ restart_server.clear
78
+ server.begin_restart(true)
79
+ @launcher.config.run_hooks :before_refork, nil, @launcher.events
80
+ Puma::Util.nakayoshi_gc @events if @options[:nakayoshi_fork]
81
+ end
82
+ elsif idx == 0 # restart server
83
+ restart_server << true << false
84
+ else # fork worker
85
+ worker_pids << pid = spawn_worker(idx)
86
+ @worker_write << "f#{pid}:#{idx}\n" rescue nil
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ Signal.trap "SIGTERM" do
93
+ @worker_write << "e#{Process.pid}\n" rescue nil
94
+ restart_server.clear
95
+ server.stop
96
+ restart_server << false
97
+ end
98
+
99
+ begin
100
+ @worker_write << "b#{Process.pid}:#{index}\n"
101
+ rescue SystemCallError, IOError
102
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
103
+ STDERR.puts "Master seems to have exited, exiting."
104
+ return
105
+ end
106
+
107
+ while restart_server.pop
108
+ server_thread = server.run
109
+ stat_thread ||= Thread.new(@worker_write) do |io|
110
+ Puma.set_thread_name "stat payload"
111
+ base_payload = "p#{Process.pid}"
112
+
113
+ while true
114
+ begin
115
+ b = server.backlog || 0
116
+ r = server.running || 0
117
+ t = server.pool_capacity || 0
118
+ m = server.max_threads || 0
119
+ rc = server.requests_count || 0
120
+ payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r}, "pool_capacity":#{t}, "max_threads": #{m}, "requests_count": #{rc} }\n!
121
+ io << payload
122
+ rescue IOError
123
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
124
+ break
125
+ end
126
+ sleep Const::WORKER_CHECK_INTERVAL
127
+ end
128
+ end
129
+ server_thread.join
130
+ end
131
+
132
+ # Invoke any worker shutdown hooks so they can prevent the worker
133
+ # exiting until any background operations are completed
134
+ @launcher.config.run_hooks :before_worker_shutdown, index, @launcher.events
135
+ ensure
136
+ @worker_write << "t#{Process.pid}\n" rescue nil
137
+ @worker_write.close
138
+ end
139
+
140
+ private
141
+
142
+ def spawn_worker(idx)
143
+ @launcher.config.run_hooks :before_worker_fork, idx, @launcher.events
144
+
145
+ pid = fork do
146
+ new_worker = Worker.new index: idx,
147
+ master: master,
148
+ launcher: @launcher,
149
+ pipes: { check_pipe: @check_pipe,
150
+ worker_write: @worker_write },
151
+ server: @server
152
+ new_worker.run
153
+ end
154
+
155
+ if !pid
156
+ log "! Complete inability to spawn new workers detected"
157
+ log "! Seppuku is the only choice."
158
+ exit! 1
159
+ end
160
+
161
+ @launcher.config.run_hooks :after_worker_fork, idx, @launcher.events
162
+ pid
163
+ end
164
+
165
+ def wakeup!
166
+ return unless @wakeup
167
+
168
+ begin
169
+ @wakeup.write "!" unless @wakeup.closed?
170
+ rescue SystemCallError, IOError
171
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Puma
4
+ class Cluster < Runner
5
+ # This class represents a worker process from the perspective of the puma
6
+ # master process. It contains information about the process and its health
7
+ # and it exposes methods to control the process via IPC. It does not
8
+ # include the actual logic executed by the worker process itself. For that,
9
+ # see Puma::Cluster::Worker.
10
+ class WorkerHandle
11
+ def initialize(idx, pid, phase, options)
12
+ @index = idx
13
+ @pid = pid
14
+ @phase = phase
15
+ @stage = :started
16
+ @signal = "TERM"
17
+ @options = options
18
+ @first_term_sent = nil
19
+ @started_at = Time.now
20
+ @last_checkin = Time.now
21
+ @last_status = {}
22
+ @term = false
23
+ end
24
+
25
+ attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status, :started_at
26
+
27
+ # @version 5.0.0
28
+ attr_writer :pid, :phase
29
+
30
+ def booted?
31
+ @stage == :booted
32
+ end
33
+
34
+ def boot!
35
+ @last_checkin = Time.now
36
+ @stage = :booted
37
+ end
38
+
39
+ def term?
40
+ @term
41
+ end
42
+
43
+ def ping!(status)
44
+ @last_checkin = Time.now
45
+ captures = status.match(/{ "backlog":(?<backlog>\d*), "running":(?<running>\d*), "pool_capacity":(?<pool_capacity>\d*), "max_threads": (?<max_threads>\d*), "requests_count": (?<requests_count>\d*) }/)
46
+ @last_status = captures.names.inject({}) do |hash, key|
47
+ hash[key.to_sym] = captures[key].to_i
48
+ hash
49
+ end
50
+ end
51
+
52
+ # @see Puma::Cluster#check_workers
53
+ # @version 5.0.0
54
+ def ping_timeout
55
+ @last_checkin +
56
+ (booted? ?
57
+ @options[:worker_timeout] :
58
+ @options[:worker_boot_timeout]
59
+ )
60
+ end
61
+
62
+ def term
63
+ begin
64
+ if @first_term_sent && (Time.now - @first_term_sent) > @options[:worker_shutdown_timeout]
65
+ @signal = "KILL"
66
+ else
67
+ @term ||= true
68
+ @first_term_sent ||= Time.now
69
+ end
70
+ Process.kill @signal, @pid if @pid
71
+ rescue Errno::ESRCH
72
+ end
73
+ end
74
+
75
+ def kill
76
+ @signal = 'KILL'
77
+ term
78
+ end
79
+
80
+ def hup
81
+ Process.kill "HUP", @pid
82
+ rescue Errno::ESRCH
83
+ end
84
+ end
85
+ end
86
+ end
@@ -92,6 +92,12 @@ module Puma
92
92
  end
93
93
  end
94
94
  end
95
+
96
+ def final_options
97
+ default_options
98
+ .merge(file_options)
99
+ .merge(user_options)
100
+ end
95
101
  end
96
102
 
97
103
  # The main configuration class of Puma.
@@ -108,16 +114,17 @@ module Puma
108
114
  #
109
115
  # It also handles loading plugins.
110
116
  #
111
- # > Note: `:port` and `:host` are not valid keys. By the time they make it to the
117
+ # [Note:]
118
+ # `:port` and `:host` are not valid keys. By the time they make it to the
112
119
  # configuration options they are expected to be incorporated into a `:binds` key.
113
120
  # Under the hood the DSL maps `port` and `host` calls to `:binds`
114
121
  #
115
- # config = Configuration.new({}) do |user_config, file_config, default_config|
116
- # user_config.port 3003
117
- # end
118
- # config.load
119
- # puts config.options[:port]
120
- # # => 3003
122
+ # config = Configuration.new({}) do |user_config, file_config, default_config|
123
+ # user_config.port 3003
124
+ # end
125
+ # config.load
126
+ # puts config.options[:port]
127
+ # # => 3003
121
128
  #
122
129
  # It is expected that `load` is called on the configuration instance after setting
123
130
  # config. This method expands any values in `config_file` and puts them into the
@@ -197,7 +204,8 @@ module Puma
197
204
  :logger => STDOUT,
198
205
  :persistent_timeout => Const::PERSISTENT_TIMEOUT,
199
206
  :first_data_timeout => Const::FIRST_DATA_TIMEOUT,
200
- :raise_exception_on_sigterm => true
207
+ :raise_exception_on_sigterm => true,
208
+ :max_fast_inline => Const::MAX_FAST_INLINE
201
209
  }
202
210
  end
203
211
 
@@ -288,6 +296,10 @@ module Puma
288
296
  end
289
297
  end
290
298
 
299
+ def final_options
300
+ @options.final_options
301
+ end
302
+
291
303
  def self.temp_path
292
304
  require 'tmpdir'
293
305
 
@@ -100,8 +100,8 @@ module Puma
100
100
  # too taxing on performance.
101
101
  module Const
102
102
 
103
- PUMA_VERSION = VERSION = "5.0.0".freeze
104
- CODE_NAME = "Spoony Bard".freeze
103
+ PUMA_VERSION = VERSION = "5.1.0".freeze
104
+ CODE_NAME = "At Your Service".freeze
105
105
 
106
106
  PUMA_SERVER_STRING = ['puma', PUMA_VERSION, CODE_NAME].join(' ').freeze
107
107
 
@@ -228,7 +228,6 @@ module Puma
228
228
  COLON = ": ".freeze
229
229
 
230
230
  NEWLINE = "\n".freeze
231
- HTTP_INJECTION_REGEX = /[\r\n]/.freeze
232
231
 
233
232
  HIJACK_P = "rack.hijack?".freeze
234
233
  HIJACK = "rack.hijack".freeze
@@ -239,5 +238,14 @@ module Puma
239
238
  # Mininum interval to checks worker health
240
239
  WORKER_CHECK_INTERVAL = 5
241
240
 
241
+ # Illegal character in the key or value of response header
242
+ DQUOTE = "\"".freeze
243
+ HTTP_HEADER_DELIMITER = Regexp.escape("(),/:;<=>?@[]{}\\").freeze
244
+ ILLEGAL_HEADER_KEY_REGEX = /[\x00-\x20#{DQUOTE}#{HTTP_HEADER_DELIMITER}]/.freeze
245
+ # header values can contain HTAB?
246
+ ILLEGAL_HEADER_VALUE_REGEX = /[\x00-\x08\x0A-\x1F]/.freeze
247
+
248
+ # Banned keys of response header
249
+ BANNED_HEADER_KEY = /rack.|status/.freeze
242
250
  end
243
251
  end
@@ -11,10 +11,32 @@ require 'socket'
11
11
  module Puma
12
12
  class ControlCLI
13
13
 
14
- COMMANDS = %w{halt restart phased-restart start stats status stop reload-worker-directory gc gc-stats thread-backtraces refork}
14
+ # values must be string or nil
15
+ # value of `nil` means command cannot be processed via signal
16
+ # @version 5.0.3
17
+ CMD_PATH_SIG_MAP = {
18
+ 'gc' => nil,
19
+ 'gc-stats' => nil,
20
+ 'halt' => 'SIGQUIT',
21
+ 'phased-restart' => 'SIGUSR1',
22
+ 'refork' => 'SIGURG',
23
+ 'reload-worker-directory' => nil,
24
+ 'restart' => 'SIGUSR2',
25
+ 'start' => nil,
26
+ 'stats' => nil,
27
+ 'status' => '',
28
+ 'stop' => 'SIGTERM',
29
+ 'thread-backtraces' => nil
30
+ }.freeze
31
+
32
+ # @deprecated 6.0.0
33
+ COMMANDS = CMD_PATH_SIG_MAP.keys.freeze
34
+
35
+ # commands that cannot be used in a request
36
+ NO_REQ_COMMANDS = %w{refork}.freeze
15
37
 
16
38
  # @version 5.0.0
17
- PRINTABLE_COMMANDS = %w{gc-stats stats thread-backtraces}
39
+ PRINTABLE_COMMANDS = %w{gc-stats stats thread-backtraces}.freeze
18
40
 
19
41
  def initialize(argv, stdout=STDOUT, stderr=STDERR)
20
42
  @state = nil
@@ -33,7 +55,7 @@ module Puma
33
55
  @cli_options = {}
34
56
 
35
57
  opts = OptionParser.new do |o|
36
- o.banner = "Usage: pumactl (-p PID | -P pidfile | -S status_file | -C url -T token | -F config.rb) (#{COMMANDS.join("|")})"
58
+ o.banner = "Usage: pumactl (-p PID | -P pidfile | -S status_file | -C url -T token | -F config.rb) (#{CMD_PATH_SIG_MAP.keys.join("|")})"
37
59
 
38
60
  o.on "-S", "--state PATH", "Where the state file to use is" do |arg|
39
61
  @state = arg
@@ -74,7 +96,7 @@ module Puma
74
96
  end
75
97
 
76
98
  o.on_tail("-V", "--version", "Show version") do
77
- puts Const::PUMA_VERSION
99
+ @stdout.puts Const::PUMA_VERSION
78
100
  exit
79
101
  end
80
102
  end
@@ -86,10 +108,10 @@ module Puma
86
108
 
87
109
  # check presence of command
88
110
  unless @command
89
- raise "Available commands: #{COMMANDS.join(", ")}"
111
+ raise "Available commands: #{CMD_PATH_SIG_MAP.keys.join(", ")}"
90
112
  end
91
113
 
92
- unless COMMANDS.include? @command
114
+ unless CMD_PATH_SIG_MAP.key? @command
93
115
  raise "Invalid command: #{@command}"
94
116
  end
95
117
 
@@ -134,7 +156,7 @@ module Puma
134
156
  @pid = sf.pid
135
157
  elsif @pidfile
136
158
  # get pid from pid_file
137
- File.open(@pidfile) { |f| @pid = f.read.to_i }
159
+ @pid = File.read(@pidfile, mode: 'rb:UTF-8').to_i
138
160
  end
139
161
  end
140
162
 
@@ -142,24 +164,27 @@ module Puma
142
164
  uri = URI.parse @control_url
143
165
 
144
166
  # create server object by scheme
145
- server = case uri.scheme
146
- when "ssl"
147
- require 'openssl'
148
- OpenSSL::SSL::SSLSocket.new(
149
- TCPSocket.new(uri.host, uri.port),
150
- OpenSSL::SSL::SSLContext.new)
151
- .tap { |ssl| ssl.sync_close = true } # default is false
152
- .tap(&:connect)
153
- when "tcp"
154
- TCPSocket.new uri.host, uri.port
155
- when "unix"
156
- UNIXSocket.new "#{uri.host}#{uri.path}"
157
- else
158
- raise "Invalid scheme: #{uri.scheme}"
159
- end
160
-
161
- if @command == "status"
162
- message "Puma is started"
167
+ server =
168
+ case uri.scheme
169
+ when 'ssl'
170
+ require 'openssl'
171
+ OpenSSL::SSL::SSLSocket.new(
172
+ TCPSocket.new(uri.host, uri.port),
173
+ OpenSSL::SSL::SSLContext.new)
174
+ .tap { |ssl| ssl.sync_close = true } # default is false
175
+ .tap(&:connect)
176
+ when 'tcp'
177
+ TCPSocket.new uri.host, uri.port
178
+ when 'unix'
179
+ UNIXSocket.new "#{uri.host}#{uri.path}"
180
+ else
181
+ raise "Invalid scheme: #{uri.scheme}"
182
+ end
183
+
184
+ if @command == 'status'
185
+ message 'Puma is started'
186
+ elsif NO_REQ_COMMANDS.include? @command
187
+ raise "Invalid request command: #{@command}"
163
188
  else
164
189
  url = "/#{@command}"
165
190
 
@@ -167,10 +192,10 @@ module Puma
167
192
  url = url + "?token=#{@control_auth_token}"
168
193
  end
169
194
 
170
- server << "GET #{url} HTTP/1.0\r\n\r\n"
195
+ server.syswrite "GET #{url} HTTP/1.0\r\n\r\n"
171
196
 
172
197
  unless data = server.read
173
- raise "Server closed connection before responding"
198
+ raise 'Server closed connection before responding'
174
199
  end
175
200
 
176
201
  response = data.split("\r\n")
@@ -179,13 +204,13 @@ module Puma
179
204
  raise "Server sent empty response"
180
205
  end
181
206
 
182
- (@http,@code,@message) = response.first.split(" ",3)
207
+ @http, @code, @message = response.first.split(' ',3)
183
208
 
184
- if @code == "403"
185
- raise "Unauthorized access to server (wrong auth token)"
186
- elsif @code == "404"
209
+ if @code == '403'
210
+ raise 'Unauthorized access to server (wrong auth token)'
211
+ elsif @code == '404'
187
212
  raise "Command error: #{response.last}"
188
- elsif @code != "200"
213
+ elsif @code != '200'
189
214
  raise "Bad response from server: #{@code}"
190
215
  end
191
216
 
@@ -194,7 +219,7 @@ module Puma
194
219
  end
195
220
  ensure
196
221
  if server
197
- if uri.scheme == "ssl"
222
+ if uri.scheme == 'ssl'
198
223
  server.sysclose
199
224
  else
200
225
  server.close unless server.closed?
@@ -204,51 +229,28 @@ module Puma
204
229
 
205
230
  def send_signal
206
231
  unless @pid
207
- raise "Neither pid nor control url available"
232
+ raise 'Neither pid nor control url available'
208
233
  end
209
234
 
210
235
  begin
236
+ sig = CMD_PATH_SIG_MAP[@command]
211
237
 
212
- case @command
213
- when "restart"
214
- Process.kill "SIGUSR2", @pid
215
-
216
- when "halt"
217
- Process.kill "QUIT", @pid
218
-
219
- when "stop"
220
- Process.kill "SIGTERM", @pid
221
-
222
- when "stats"
223
- puts "Stats not available via pid only"
224
- return
225
-
226
- when "reload-worker-directory"
227
- puts "reload-worker-directory not available via pid only"
238
+ if sig.nil?
239
+ @stdout.puts "'#{@command}' not available via pid only"
228
240
  return
229
-
230
- when "phased-restart"
231
- Process.kill "SIGUSR1", @pid
232
-
233
- when "status"
241
+ elsif sig.start_with? 'SIG'
242
+ Process.kill sig, @pid
243
+ elsif @command == 'status'
234
244
  begin
235
245
  Process.kill 0, @pid
236
- puts "Puma is started"
246
+ @stdout.puts 'Puma is started'
237
247
  rescue Errno::ESRCH
238
- raise "Puma is not running"
248
+ raise 'Puma is not running'
239
249
  end
240
-
241
- return
242
-
243
- when "refork"
244
- Process.kill "SIGURG", @pid
245
-
246
- else
247
250
  return
248
251
  end
249
-
250
252
  rescue SystemCallError
251
- if @command == "restart"
253
+ if @command == 'restart'
252
254
  start
253
255
  else
254
256
  raise "No pid '#{@pid}' found"
@@ -259,14 +261,13 @@ module Puma
259
261
  end
260
262
 
261
263
  def run
262
- return start if @command == "start"
263
-
264
+ return start if @command == 'start'
264
265
  prepare_configuration
265
266
 
266
- if Puma.windows?
267
+ if Puma.windows? || @control_url
267
268
  send_request
268
269
  else
269
- @control_url ? send_request : send_signal
270
+ send_signal
270
271
  end
271
272
 
272
273
  rescue => e