puma 2.2.0 → 8.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. checksums.yaml +7 -0
  2. data/History.md +3334 -0
  3. data/LICENSE +23 -20
  4. data/README.md +387 -100
  5. data/bin/puma-wild +25 -0
  6. data/bin/pumactl +1 -1
  7. data/docs/5.0-Upgrade.md +98 -0
  8. data/docs/6.0-Upgrade.md +56 -0
  9. data/docs/7.0-Upgrade.md +52 -0
  10. data/docs/8.0-Upgrade.md +100 -0
  11. data/docs/architecture.md +74 -0
  12. data/docs/compile_options.md +55 -0
  13. data/docs/deployment.md +137 -0
  14. data/docs/fork_worker.md +41 -0
  15. data/docs/grpc.md +62 -0
  16. data/docs/images/favicon.svg +1 -0
  17. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  18. data/docs/images/puma-connection-flow.png +0 -0
  19. data/docs/images/puma-general-arch.png +0 -0
  20. data/docs/images/running-puma.svg +1 -0
  21. data/docs/images/standard-logo.svg +1 -0
  22. data/docs/java_options.md +54 -0
  23. data/docs/jungle/README.md +9 -0
  24. data/docs/jungle/rc.d/README.md +74 -0
  25. data/docs/jungle/rc.d/puma +61 -0
  26. data/docs/jungle/rc.d/puma.conf +10 -0
  27. data/docs/kubernetes.md +73 -0
  28. data/docs/nginx.md +5 -5
  29. data/docs/plugins.md +42 -0
  30. data/docs/rails_dev_mode.md +28 -0
  31. data/docs/restart.md +65 -0
  32. data/docs/signals.md +98 -0
  33. data/docs/stats.md +148 -0
  34. data/docs/systemd.md +253 -0
  35. data/docs/testing_benchmarks_local_files.md +150 -0
  36. data/docs/testing_test_rackup_ci_files.md +36 -0
  37. data/ext/puma_http11/PumaHttp11Service.java +2 -2
  38. data/ext/puma_http11/extconf.rb +59 -2
  39. data/ext/puma_http11/http11_parser.c +319 -487
  40. data/ext/puma_http11/http11_parser.h +15 -13
  41. data/ext/puma_http11/http11_parser.java.rl +64 -94
  42. data/ext/puma_http11/http11_parser.rl +27 -24
  43. data/ext/puma_http11/http11_parser_common.rl +8 -8
  44. data/ext/puma_http11/mini_ssl.c +696 -38
  45. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  46. data/ext/puma_http11/org/jruby/puma/EnvKey.java +241 -0
  47. data/ext/puma_http11/org/jruby/puma/Http11.java +241 -145
  48. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +174 -221
  49. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +413 -193
  50. data/ext/puma_http11/puma_http11.c +183 -175
  51. data/lib/puma/app/status.rb +77 -29
  52. data/lib/puma/binder.rb +349 -119
  53. data/lib/puma/cli.rb +163 -801
  54. data/lib/puma/client.rb +627 -144
  55. data/lib/puma/client_env.rb +171 -0
  56. data/lib/puma/cluster/worker.rb +183 -0
  57. data/lib/puma/cluster/worker_handle.rb +127 -0
  58. data/lib/puma/cluster.rb +634 -0
  59. data/lib/puma/cluster_accept_loop_delay.rb +91 -0
  60. data/lib/puma/commonlogger.rb +115 -0
  61. data/lib/puma/configuration.rb +420 -198
  62. data/lib/puma/const.rb +266 -115
  63. data/lib/puma/control_cli.rb +219 -120
  64. data/lib/puma/detect.rb +56 -2
  65. data/lib/puma/dsl.rb +1562 -0
  66. data/lib/puma/error_logger.rb +115 -0
  67. data/lib/puma/events.rb +44 -60
  68. data/lib/puma/io_buffer.rb +48 -5
  69. data/lib/puma/jruby_restart.rb +2 -51
  70. data/lib/puma/json_serialization.rb +96 -0
  71. data/lib/puma/launcher/bundle_pruner.rb +102 -0
  72. data/lib/puma/launcher.rb +501 -0
  73. data/lib/puma/log_writer.rb +153 -0
  74. data/lib/puma/minissl/context_builder.rb +96 -0
  75. data/lib/puma/minissl.rb +355 -37
  76. data/lib/puma/null_io.rb +79 -12
  77. data/lib/puma/plugin/systemd.rb +90 -0
  78. data/lib/puma/plugin/tmp_restart.rb +36 -0
  79. data/lib/puma/plugin.rb +111 -0
  80. data/lib/puma/rack/builder.rb +297 -0
  81. data/lib/puma/rack/urlmap.rb +93 -0
  82. data/lib/puma/rack_default.rb +21 -4
  83. data/lib/puma/reactor.rb +102 -133
  84. data/lib/puma/response.rb +532 -0
  85. data/lib/puma/runner.rb +211 -0
  86. data/lib/puma/sd_notify.rb +146 -0
  87. data/lib/puma/server.rb +582 -445
  88. data/lib/puma/server_plugin_control.rb +32 -0
  89. data/lib/puma/single.rb +72 -0
  90. data/lib/puma/state_file.rb +69 -0
  91. data/lib/puma/thread_pool.rb +389 -57
  92. data/lib/puma/util.rb +125 -0
  93. data/lib/puma.rb +80 -6
  94. data/lib/rack/handler/puma.rb +125 -47
  95. data/tools/Dockerfile +26 -0
  96. data/tools/trickletest.rb +44 -0
  97. metadata +88 -148
  98. data/COPYING +0 -55
  99. data/Gemfile +0 -10
  100. data/History.txt +0 -302
  101. data/Manifest.txt +0 -60
  102. data/Rakefile +0 -165
  103. data/TODO +0 -5
  104. data/docs/config.md +0 -0
  105. data/ext/puma_http11/ext_help.h +0 -15
  106. data/ext/puma_http11/io_buffer.c +0 -154
  107. data/lib/puma/accept_nonblock.rb +0 -23
  108. data/lib/puma/capistrano.rb +0 -34
  109. data/lib/puma/compat.rb +0 -11
  110. data/lib/puma/daemon_ext.rb +0 -20
  111. data/lib/puma/delegation.rb +0 -11
  112. data/lib/puma/java_io_buffer.rb +0 -45
  113. data/lib/puma/rack_patch.rb +0 -25
  114. data/puma.gemspec +0 -45
  115. data/test/test_app_status.rb +0 -88
  116. data/test/test_cli.rb +0 -171
  117. data/test/test_config.rb +0 -16
  118. data/test/test_http10.rb +0 -27
  119. data/test/test_http11.rb +0 -126
  120. data/test/test_integration.rb +0 -154
  121. data/test/test_iobuffer.rb +0 -38
  122. data/test/test_minissl.rb +0 -25
  123. data/test/test_null_io.rb +0 -31
  124. data/test/test_persistent.rb +0 -238
  125. data/test/test_puma_server.rb +0 -224
  126. data/test/test_rack_handler.rb +0 -10
  127. data/test/test_rack_server.rb +0 -141
  128. data/test/test_thread_pool.rb +0 -146
  129. data/test/test_unix_socket.rb +0 -39
  130. data/test/test_ws.rb +0 -89
  131. data/tools/jungle/init.d/README.md +0 -54
  132. data/tools/jungle/init.d/puma +0 -332
  133. data/tools/jungle/init.d/run-puma +0 -3
  134. data/tools/jungle/upstart/README.md +0 -61
  135. data/tools/jungle/upstart/puma-manager.conf +0 -31
  136. data/tools/jungle/upstart/puma.conf +0 -52
@@ -1,57 +1,296 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+ require 'uri'
5
+
6
+ require_relative 'plugin'
7
+ require_relative 'const'
8
+ require_relative 'dsl'
9
+ require_relative 'events'
10
+
1
11
  module Puma
12
+ # A class used for storing "leveled" configuration options.
13
+ #
14
+ # In this class any "user" specified options take precedence over any
15
+ # "file" specified options, take precedence over any "default" options.
16
+ #
17
+ # User input is preferred over "defaults":
18
+ # user_options = { foo: "bar" }
19
+ # default_options = { foo: "zoo" }
20
+ # options = UserFileDefaultOptions.new(user_options, default_options)
21
+ # puts options[:foo]
22
+ # # => "bar"
23
+ #
24
+ # All values can be accessed via `all_of`
25
+ #
26
+ # puts options.all_of(:foo)
27
+ # # => ["bar", "zoo"]
28
+ #
29
+ # A "file" option can be set. This config will be preferred over "default" options
30
+ # but will defer to any available "user" specified options.
31
+ #
32
+ # user_options = { foo: "bar" }
33
+ # default_options = { rackup: "zoo.rb" }
34
+ # options = UserFileDefaultOptions.new(user_options, default_options)
35
+ # options.file_options[:rackup] = "sup.rb"
36
+ # puts options[:rackup]
37
+ # # => "sup.rb"
38
+ #
39
+ # The "default" options can be set via procs. These are resolved during runtime
40
+ # via calls to `finalize_values`
41
+ class UserFileDefaultOptions
42
+ def initialize(user_options, default_options)
43
+ @user_options = user_options
44
+ @file_options = {}
45
+ @default_options = default_options
46
+ end
47
+
48
+ attr_reader :user_options, :file_options, :default_options
49
+
50
+ def [](key)
51
+ fetch(key)
52
+ end
53
+
54
+ def []=(key, value)
55
+ user_options[key] = value
56
+ end
57
+
58
+ def fetch(key, default_value = nil)
59
+ return user_options[key] if user_options.key?(key)
60
+ return file_options[key] if file_options.key?(key)
61
+ return default_options[key] if default_options.key?(key)
62
+
63
+ default_value
64
+ end
2
65
 
3
- # The CLI exports it's Configuration object here to allow
4
- # apps to pick it up. An app needs to use it conditionally though
5
- # since it is not set if the app is launched via another
6
- # mechanism than the CLI class.
66
+ def all_of(key)
67
+ user = user_options[key]
68
+ file = file_options[key]
69
+ default = default_options[key]
7
70
 
8
- class << self
9
- attr_accessor :cli_config
71
+ user = [user] unless user.is_a?(Array)
72
+ file = [file] unless file.is_a?(Array)
73
+ default = [default] unless default.is_a?(Array)
74
+
75
+ user.compact!
76
+ file.compact!
77
+ default.compact!
78
+
79
+ user + file + default
80
+ end
81
+
82
+ def finalize_values
83
+ @default_options.each do |k,v|
84
+ if v.respond_to? :call
85
+ @default_options[k] = v.call
86
+ end
87
+ end
88
+ end
89
+
90
+ def final_options
91
+ default_options
92
+ .merge(file_options)
93
+ .merge(user_options)
94
+ end
10
95
  end
11
96
 
97
+ # The main configuration class of Puma.
98
+ #
99
+ # It can be initialized with a set of "user" options and "default" options.
100
+ # Defaults will be merged with `Configuration.puma_default_options`.
101
+ #
102
+ # This class works together with 2 main other classes the `UserFileDefaultOptions`
103
+ # which stores configuration options in order so the precedence is that user
104
+ # set configuration wins over "file" based configuration wins over "default"
105
+ # configuration. These configurations are set via the `DSL` class. This
106
+ # class powers the Puma config file syntax and does double duty as a configuration
107
+ # DSL used by the `Puma::CLI` and Puma rack handler.
108
+ #
109
+ # It also handles loading plugins.
110
+ #
111
+ # [Note:]
112
+ # `:port` and `:host` are not valid keys. By the time they make it to the
113
+ # configuration options they are expected to be incorporated into a `:binds` key.
114
+ # Under the hood the DSL maps `port` and `host` calls to `:binds`
115
+ #
116
+ # config = Configuration.new({}) do |user_config, file_config, default_config|
117
+ # user_config.port 3003
118
+ # end
119
+ # config.clamp
120
+ # puts config.options[:port]
121
+ # # => 3003
122
+ #
123
+ # It is expected that `load` is called on the configuration instance after setting
124
+ # config. This method expands any values in `config_file` and puts them into the
125
+ # correct configuration option hash.
126
+ #
127
+ # Once all configuration is complete it is expected that `clamp` will be called
128
+ # on the instance. This will expand any procs stored under "default" values. This
129
+ # is done because an environment variable may have been modified while loading
130
+ # configuration files.
12
131
  class Configuration
13
- DefaultRackup = "config.ru"
132
+ class NotLoadedError < StandardError; end
133
+ class NotClampedError < StandardError; end
134
+
135
+ DEFAULTS = {
136
+ auto_trim_time: 30,
137
+ binds: ['tcp://[::]:9292'.freeze],
138
+ debug: false,
139
+ early_hints: nil,
140
+ enable_keep_alives: true,
141
+ environment: 'development'.freeze,
142
+ fiber_per_request: !!ENV.fetch("PUMA_FIBER_PER_REQUEST", false),
143
+ # Number of seconds to wait until we get the first data for the request.
144
+ first_data_timeout: 30,
145
+ force_shutdown_after: -1,
146
+ http_content_length_limit: nil,
147
+ # Number of seconds to wait until the next request before shutting down.
148
+ idle_timeout: nil,
149
+ io_selector_backend: :auto,
150
+ log_requests: false,
151
+ logger: STDOUT,
152
+ # Limits how many requests a keep alive connection can make.
153
+ # The connection will be closed after it reaches `max_keep_alive`
154
+ # requests.
155
+ max_io_threads: 0,
156
+ max_keep_alive: 999,
157
+ max_threads: Puma.mri? ? 5 : 16,
158
+ min_threads: 0,
159
+ mode: :http,
160
+ mutate_stdout_and_stderr_to_sync_on_write: true,
161
+ out_of_band: [],
162
+ # Number of seconds for another request within a persistent session.
163
+ persistent_timeout: 65, # PUMA_PERSISTENT_TIMEOUT
164
+ prune_bundler: false,
165
+ queue_requests: true,
166
+ rackup: 'config.ru'.freeze,
167
+ raise_exception_on_sigterm: true,
168
+ reaping_time: 1,
169
+ remote_address: :socket,
170
+ silence_fork_callback_warning: false,
171
+ silence_single_worker_warning: false,
172
+ tag: File.basename(Dir.getwd),
173
+ tcp_host: '::'.freeze,
174
+ tcp_port: 9292,
175
+ wait_for_less_busy_worker: 0.005,
176
+ worker_boot_timeout: 60,
177
+ worker_check_interval: 5,
178
+ worker_culling_strategy: :youngest,
179
+ worker_shutdown_timeout: 30,
180
+ worker_timeout: 60,
181
+ workers: 0,
182
+ }
183
+
184
+ def initialize(user_options={}, default_options = {}, env = ENV, &block)
185
+ default_options = self.puma_default_options(env).merge(events: Events.new).merge(default_options)
186
+
187
+ @_options = UserFileDefaultOptions.new(user_options, default_options)
188
+ @plugins = PluginLoader.new
189
+ @events = @_options[:events] || Events.new
190
+ @hooks = {}
191
+ @user_dsl = DSL.new(@_options.user_options, self)
192
+ @file_dsl = DSL.new(@_options.file_options, self)
193
+ @default_dsl = DSL.new(@_options.default_options, self)
194
+
195
+ @puma_bundler_pruned = env.key? 'PUMA_BUNDLER_PRUNED'
196
+
197
+ if block
198
+ configure(&block)
199
+ end
200
+
201
+ @loaded = false
202
+ @clamped = false
203
+ end
204
+
205
+ attr_reader :plugins, :events, :hooks, :_options
14
206
 
15
- DefaultTCPHost = "0.0.0.0"
16
- DefaultTCPPort = 9292
207
+ def options
208
+ raise NotClampedError, "ensure clamp is called before accessing options" unless @clamped
17
209
 
18
- def initialize(options)
19
- @options = options
20
- @options[:binds] ||= []
21
- @options[:on_restart] ||= []
210
+ @_options
22
211
  end
23
212
 
24
- attr_reader :options
213
+ def configure
214
+ yield @user_dsl, @file_dsl, @default_dsl
215
+ ensure
216
+ @user_dsl._offer_plugins
217
+ @file_dsl._offer_plugins
218
+ @default_dsl._offer_plugins
219
+ end
25
220
 
26
221
  def initialize_copy(other)
27
- @options = @options.dup
222
+ @conf = nil
223
+ @cli_options = nil
224
+ @_options = @_options.dup
225
+ end
226
+
227
+ def flatten
228
+ dup.flatten!
229
+ end
230
+
231
+ def flatten!
232
+ @_options = @_options.flatten
233
+ self
234
+ end
235
+
236
+ def puma_default_options(env = ENV)
237
+ defaults = DEFAULTS.dup
238
+ defaults[:tcp_host] = self.class.default_tcp_host
239
+ defaults[:binds] = [self.class.default_tcp_bind]
240
+ puma_options_from_env(env).each { |k,v| defaults[k] = v if v }
241
+ defaults
242
+ end
243
+
244
+ def puma_options_from_env(env = ENV)
245
+ min = env['PUMA_MIN_THREADS'] || env['MIN_THREADS']
246
+ max = env['PUMA_MAX_THREADS'] || env['MAX_THREADS']
247
+ persistent_timeout = env['PUMA_PERSISTENT_TIMEOUT']
248
+ workers_env = env['WEB_CONCURRENCY']
249
+ workers = workers_env && workers_env.strip != "" ? parse_workers(workers_env.strip) : nil
250
+
251
+ {
252
+ min_threads: min && min != "" && Integer(min),
253
+ max_threads: max && max != "" && Integer(max),
254
+ persistent_timeout: persistent_timeout && persistent_timeout != "" && Integer(persistent_timeout),
255
+ workers: workers,
256
+ environment: env['APP_ENV'] || env['RACK_ENV'] || env['RAILS_ENV'],
257
+ }
28
258
  end
29
259
 
30
260
  def load
31
- if path = @options[:config_file]
32
- DSL.new(@options)._load_from path
33
- end
261
+ @loaded = true
262
+ config_files.each { |config_file| @file_dsl._load_from(config_file) }
263
+ @_options
264
+ end
34
265
 
35
- # Rakeup default option support
36
- if host = @options[:Host]
37
- port = @options[:Port] || DefaultTCPPort
266
+ def config_files
267
+ raise NotLoadedError, "ensure load is called before accessing config_files" unless @loaded
38
268
 
39
- @options[:binds] << "tcp://#{host}:#{port}"
40
- end
269
+ files = @_options.all_of(:config_files)
41
270
 
42
- if @options[:binds].empty?
43
- @options[:binds] << "tcp://#{DefaultTCPHost}:#{DefaultTCPPort}"
44
- end
271
+ return [] if files == ['-']
272
+ return files if files.any?
45
273
 
46
- if @options[:control_url] == "auto"
47
- path = Configuration.temp_path
48
- @options[:control_url] = "unix://#{path}"
49
- @options[:control_url_temp] = path
274
+ first_default_file = %W(config/puma/#{@_options[:environment]}.rb config/puma.rb).find do |f|
275
+ File.exist?(f)
50
276
  end
51
277
 
52
- unless @options[:control_auth_token]
53
- setup_random_token
54
- end
278
+ [first_default_file]
279
+ end
280
+
281
+ # Call once all configuration (included from rackup files)
282
+ # is loaded to finalize defaults and lock in the configuration.
283
+ #
284
+ # This also calls load if it hasn't been called yet.
285
+ def clamp
286
+ load unless @loaded
287
+ run_mode_hooks
288
+ set_conditional_default_options
289
+ @_options.finalize_values
290
+ rewrite_unavailable_ipv6_binds!
291
+ @clamped = true
292
+ warn_hooks
293
+ options
55
294
  end
56
295
 
57
296
  # Injects the Configuration object into the env
@@ -70,68 +309,78 @@ module Puma
70
309
  # Indicate if there is a properly configured app
71
310
  #
72
311
  def app_configured?
73
- @options[:app] || File.exists?(rackup)
312
+ options[:app] || File.exist?(rackup)
74
313
  end
75
314
 
76
315
  def rackup
77
- @options[:rackup] || DefaultRackup
316
+ options[:rackup]
78
317
  end
79
318
 
80
- # Load the specified rackup file, pull an options from
319
+ # Load the specified rackup file, pull options from
81
320
  # the rackup file, and set @app.
82
321
  #
83
322
  def app
84
- app = @options[:app]
85
-
86
- unless app
87
- unless File.exists?(rackup)
88
- raise "Missing rackup file '#{rackup}'"
89
- end
323
+ found = options[:app] || load_rackup
90
324
 
91
- app, options = Rack::Builder.parse_file rackup
92
- @options.merge! options
93
-
94
- options.each do |key,val|
95
- if key.to_s[0,4] == "bind"
96
- @options[:binds] << val
97
- end
98
- end
325
+ if options[:log_requests]
326
+ require_relative 'commonlogger'
327
+ logger = options[:custom_logger] ? options[:custom_logger] : options[:logger]
328
+ found = CommonLogger.new(found, logger)
99
329
  end
100
330
 
101
- if !@options[:quiet] and @options[:environment] == "development"
102
- logger = @options[:logger] || STDOUT
103
- app = Rack::CommonLogger.new(app, logger)
104
- end
331
+ ConfigMiddleware.new(self, found)
332
+ end
105
333
 
106
- return ConfigMiddleware.new(self, app)
334
+ # Return which environment we're running in
335
+ def environment
336
+ options[:environment]
107
337
  end
108
338
 
109
- def setup_random_token
110
- begin
111
- require 'openssl'
112
- rescue LoadError
339
+ def load_plugin(name)
340
+ @plugins.create name
341
+ end
342
+
343
+ # @param key [:Symbol] hook to run
344
+ # @param arg [Launcher, Int] `:before_restart` passes Launcher
345
+ #
346
+ def run_hooks(key, arg, log_writer, hook_data = nil)
347
+ log_writer.debug { "Running #{key} hooks" }
348
+
349
+ options.all_of(key).each do |hook_options|
350
+ begin
351
+ block = hook_options[:block]
352
+ if id = hook_options[:id]
353
+ hook_data[id] ||= Hash.new
354
+ block.call arg, hook_data[id]
355
+ else
356
+ block.call arg
357
+ end
358
+ rescue => e
359
+ log_writer.log "WARNING hook #{key} failed with exception (#{e.class}) #{e.message}"
360
+ log_writer.debug e.backtrace.join("\n")
361
+ end
113
362
  end
363
+ end
114
364
 
115
- count = 16
365
+ def final_options
366
+ options.final_options
367
+ end
116
368
 
117
- bytes = nil
369
+ def self.default_tcp_host
370
+ ipv6_interface_available? ? Const::UNSPECIFIED_IPV6 : Const::UNSPECIFIED_IPV4
371
+ end
118
372
 
119
- if defined? OpenSSL::Random
120
- bytes = OpenSSL::Random.random_bytes(count)
121
- elsif File.exists?("/dev/urandom")
122
- File.open("/dev/urandom") do |f|
123
- bytes = f.read(count)
124
- end
125
- end
373
+ def self.default_tcp_bind(port = DEFAULTS[:tcp_port])
374
+ URI::Generic.build(scheme: 'tcp', host: default_tcp_host, port: Integer(port)).to_s
375
+ end
126
376
 
127
- if bytes
128
- token = ""
129
- bytes.each_byte { |b| token << b.to_s(16) }
130
- else
131
- token = (0..count).to_a.map { rand(255).to_s(16) }.join
377
+ def self.ipv6_interface_available?
378
+ Socket.getifaddrs.any? do |ifaddr|
379
+ addr = ifaddr.addr
380
+ addr&.ipv6? && !addr&.ipv6_loopback?
132
381
  end
133
-
134
- @options[:control_auth_token] = token
382
+ rescue StandardError
383
+ false
135
384
  end
136
385
 
137
386
  def self.temp_path
@@ -141,159 +390,132 @@ module Puma
141
390
  "#{Dir.tmpdir}/puma-status-#{t}-#{$$}"
142
391
  end
143
392
 
144
- # The methods that are available for use inside the config file.
145
- #
146
- class DSL
147
- def initialize(options)
148
- @options = options
149
- end
393
+ def self.random_token
394
+ require 'securerandom' unless defined?(SecureRandom)
150
395
 
151
- def _load_from(path)
152
- instance_eval File.read(path), path, 1
153
- end
396
+ SecureRandom.hex(16)
397
+ end
154
398
 
155
- # Use +obj+ or +block+ as the Rack app. This allows a config file to
156
- # be the app itself.
157
- #
158
- def app(obj=nil, &block)
159
- obj ||= block
399
+ private
160
400
 
161
- raise "Provide either a #call'able or a block" unless obj
401
+ def rewrite_unavailable_ipv6_binds!
402
+ return if self.class.ipv6_interface_available?
162
403
 
163
- @options[:app] = obj
164
- end
404
+ tried_ipv6_bind = false
405
+ bind_schemes = ['tcp', 'ssl']
165
406
 
166
- # Start the Puma control rack app on +url+. This app can be communicated
167
- # with to control the main server.
168
- #
169
- def activate_control_app(url="auto", opts=nil)
170
- @options[:control_url] = url
407
+ @_options[:binds] = Array(@_options[:binds]).map do |bind|
408
+ uri = URI.parse(bind)
409
+ next bind unless bind_schemes.include?(uri.scheme)
171
410
 
172
- if opts
173
- if tok = opts[:auth_token]
174
- @options[:control_auth_token] = tok
175
- end
411
+ host = uri.host&.delete_prefix('[')&.delete_suffix(']')
412
+ next bind unless host&.include?(':')
176
413
 
177
- if opts[:no_token]
178
- @options[:control_auth_token] = :none
179
- end
180
- end
181
- end
414
+ tried_ipv6_bind = true
415
+ next bind unless host == Const::UNSPECIFIED_IPV6
182
416
 
183
- # Bind the server to +url+. tcp:// and unix:// are the only accepted
184
- # protocols.
185
- #
186
- def bind(url)
187
- @options[:binds] << url
417
+ uri.host = Const::UNSPECIFIED_IPV4
418
+ uri.to_s
419
+ rescue URI::InvalidURIError
420
+ bind
188
421
  end
189
422
 
190
- # Daemonize the server into the background. Highly suggest that
191
- # this be combined with +pidfile+ and +stdout_redirect+.
192
- def daemonize(which=true)
193
- @options[:daemon] = which
194
- end
423
+ warn "WARNING: IPv6 bind requested but no non-loopback IPv6 interface was detected" if tried_ipv6_bind
424
+ end
195
425
 
196
- # Set the environment in which the Rack's app will run.
197
- def environment(environment)
198
- @options[:environment] = environment
199
- end
426
+ def require_processor_counter
427
+ require 'concurrent/utility/processor_counter'
428
+ rescue LoadError
429
+ warn <<~MESSAGE
430
+ WEB_CONCURRENCY=auto or workers(:auto) requires the "concurrent-ruby" gem to be installed.
431
+ Please add "concurrent-ruby" to your Gemfile.
432
+ MESSAGE
433
+ raise
434
+ end
200
435
 
201
- # Code to run before doing a restart. This code should
202
- # close logfiles, database connections, etc.
203
- #
204
- # This can be called multiple times to add code each time.
205
- #
206
- def on_restart(&blk)
207
- @options[:on_restart] << blk
436
+ def parse_workers(value)
437
+ if value == :auto || value == 'auto'
438
+ require_processor_counter
439
+ Integer(::Concurrent.available_processor_count)
440
+ else
441
+ Integer(value)
208
442
  end
443
+ rescue ArgumentError, TypeError
444
+ raise ArgumentError, "workers must be an Integer or :auto"
445
+ end
209
446
 
210
- # Command to use to restart puma. This should be just how to
211
- # load puma itself (ie. 'ruby -Ilib bin/puma'), not the arguments
212
- # to puma, as those are the same as the original process.
213
- #
214
- def restart_command(cmd)
215
- @options[:request_cmd] = cmd
447
+ # Load and use the normal Rack builder if we can, otherwise
448
+ # fallback to our minimal version.
449
+ def rack_builder
450
+ # Load bundler now if we can so that we can pickup rack from
451
+ # a Gemfile
452
+ if @puma_bundler_pruned
453
+ begin
454
+ require 'bundler/setup'
455
+ rescue LoadError
456
+ end
216
457
  end
217
458
 
218
- # Store the pid of the server in the file at +path+.
219
- def pidfile(path)
220
- @options[:pidfile] = path
459
+ begin
460
+ require 'rack'
461
+ require 'rack/builder'
462
+ ::Rack::Builder
463
+ rescue LoadError
464
+ require_relative 'rack/builder'
465
+ Puma::Rack::Builder
221
466
  end
467
+ end
222
468
 
223
- # Disable request logging.
224
- #
225
- def quiet
226
- @options[:quiet] = true
227
- end
469
+ def load_rackup
470
+ raise "Missing rackup file '#{rackup}'" unless File.exist?(rackup)
228
471
 
229
- # Load +path+ as a rackup file.
230
- #
231
- def rackup(path)
232
- @options[:rackup] = path.to_s
233
- end
472
+ rack_app, rack_options = rack_builder.parse_file(rackup)
473
+ rack_options = rack_options || {}
234
474
 
235
- # Redirect STDOUT and STDERR to files specified.
236
- def stdout_redirect(stdout=nil, stderr=nil, append=false)
237
- @options[:redirect_stdout] = stdout
238
- @options[:redirect_stderr] = stderr
239
- @options[:redirect_append] = append
240
- end
241
-
242
- # Configure +min+ to be the minimum number of threads to use to answer
243
- # requests and +max+ the maximum.
244
- #
245
- def threads(min, max)
246
- if min > max
247
- raise "The minimum number of threads must be less than the max"
248
- end
475
+ options.file_options.merge!(rack_options)
249
476
 
250
- @options[:min_threads] = min
251
- @options[:max_threads] = max
477
+ config_ru_binds = []
478
+ rack_options.each do |k, v|
479
+ config_ru_binds << v if k.to_s.start_with?("bind")
252
480
  end
253
481
 
254
- def ssl_bind(host, port, opts)
255
- o = [
256
- "cert=#{opts[:cert]}",
257
- "key=#{opts[:key]}"
258
- ]
482
+ options.file_options[:binds] = config_ru_binds unless config_ru_binds.empty?
259
483
 
260
- @options[:binds] << "ssl://#{host}:#{port}?#{o.join('&')}"
261
- end
484
+ rack_app
485
+ end
262
486
 
263
- # Use +path+ as the file to store the server info state. This is
264
- # used by pumactl to query and control the server.
265
- #
266
- def state_path(path)
267
- @options[:state] = path.to_s
268
- end
487
+ def run_mode_hooks
488
+ workers_before = @_options[:workers]
489
+ key = workers_before > 0 ? :cluster : :single
269
490
 
270
- # *Cluster mode only* How many worker processes to run.
271
- #
272
- def workers(count)
273
- @options[:workers] = count.to_i
274
- end
491
+ @_options.all_of(key).each(&:call)
275
492
 
276
- # *Cluster mode only* Code to run when a worker boots to setup
277
- # the process before booting the app.
278
- #
279
- # This can be called multiple times to add hooks.
280
- #
281
- def on_worker_boot(&block)
282
- @options[:worker_boot] << block
493
+ unless @_options[:workers] == workers_before
494
+ raise "cannot change the number of workers inside a #{key} configuration hook"
283
495
  end
496
+ end
284
497
 
285
- # The directory to operate out of.
286
- def directory(dir)
287
- @options[:directory] = dir.to_s
288
- @options[:worker_directory] = dir.to_s
289
- end
498
+ def set_conditional_default_options
499
+ @_options.default_options[:preload_app] = !@_options[:prune_bundler] &&
500
+ (@_options[:workers] > 1) && Puma.forkable?
501
+ end
290
502
 
291
- # *Cluster mode only* Preload the application before starting
292
- # the workers and setting up the listen ports. This conflicts
293
- # with using the phased restart feature, you can't use both.
294
- #
295
- def preload_app!(answer=true)
296
- @options[:preload_app] = answer
503
+ def warn_hooks
504
+ return if options[:workers] > 0
505
+ return if options[:silence_fork_callback_warning]
506
+
507
+ log_writer = LogWriter.stdio
508
+ @hooks.each_key do |hook|
509
+ options.all_of(hook).each do |hook_options|
510
+ next unless hook_options[:cluster_only]
511
+
512
+ log_writer.log(<<~MSG.tr("\n", " "))
513
+ Warning: The code in the `#{hook}` block will not execute
514
+ in the current Puma configuration. The `#{hook}` block only
515
+ executes in Puma's cluster mode. To fix this, either remove the
516
+ `#{hook}` call or increase Puma's worker count above zero.
517
+ MSG
518
+ end
297
519
  end
298
520
  end
299
521
  end