puma 7.2.0-java → 8.0.1-java

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.
data/lib/puma/server.rb CHANGED
@@ -11,7 +11,7 @@ require_relative 'reactor'
11
11
  require_relative 'client'
12
12
  require_relative 'binder'
13
13
  require_relative 'util'
14
- require_relative 'request'
14
+ require_relative 'response'
15
15
  require_relative 'configuration'
16
16
  require_relative 'cluster_accept_loop_delay'
17
17
 
@@ -31,15 +31,15 @@ module Puma
31
31
  # Each `Puma::Server` will have one reactor and one thread pool.
32
32
  class Server
33
33
  module FiberPerRequest
34
- def handle_request(client, requests)
34
+ def handle_request(processor, client, requests)
35
35
  Fiber.new do
36
36
  super
37
37
  end.resume
38
38
  end
39
39
  end
40
40
 
41
- include Puma::Const
42
- include Request
41
+ include Const
42
+ include Response
43
43
 
44
44
  attr_reader :options
45
45
  attr_reader :thread
@@ -259,7 +259,9 @@ module Puma
259
259
 
260
260
  @status = :run
261
261
 
262
- @thread_pool = ThreadPool.new(thread_name, options, server: self) { |client| process_client client }
262
+ @thread_pool = ThreadPool.new(thread_name, options, server: self) do |processor, client|
263
+ process_client(processor, client)
264
+ end
263
265
 
264
266
  if @queue_requests
265
267
  @reactor = Reactor.new(@io_selector_backend) { |c|
@@ -304,7 +306,7 @@ module Puma
304
306
  # The method checks to see if it has the full header and body with
305
307
  # the `Puma::Client#try_to_finish` method. If the full request has been sent,
306
308
  # then the request is passed to the ThreadPool (`@thread_pool << client`)
307
- # so that a "worker thread" can pick up the request and begin to execute application logic.
309
+ # so that a "processor thread" can pick up the request and begin to execute application logic.
308
310
  # The Client is then removed from the reactor (return `true`).
309
311
  #
310
312
  # If a client object times out, a 408 response is written, its connection is closed,
@@ -401,6 +403,7 @@ module Puma
401
403
  next
402
404
  end
403
405
  drain += 1 if shutting_down?
406
+
404
407
  client = new_client(io, sock)
405
408
  client.send(addr_send_name, addr_value) if addr_value
406
409
  pool << client
@@ -414,7 +417,7 @@ module Puma
414
417
  end
415
418
  end
416
419
 
417
- @log_writer.debug "Drained #{drain} additional connections." if drain
420
+ @log_writer.debug { "Drained #{drain} additional connections." } if drain
418
421
  @events.fire :state, @status
419
422
 
420
423
  if queue_requests
@@ -444,7 +447,9 @@ module Puma
444
447
  def new_client(io, sock)
445
448
  client = Client.new(io, @binder.env(sock))
446
449
  client.listener = sock
450
+ client.env_set_http_version = @env_set_http_version
447
451
  client.http_content_length_limit = @http_content_length_limit
452
+ client.supported_http_methods = @supported_http_methods
448
453
  client
449
454
  end
450
455
 
@@ -470,14 +475,14 @@ module Puma
470
475
  # Given a connection on +client+, handle the incoming requests,
471
476
  # or queue the connection in the Reactor if no request is available.
472
477
  #
473
- # This method is called from a ThreadPool worker thread.
478
+ # This method is called from a ThreadPool processor thread.
474
479
  #
475
480
  # This method supports HTTP Keep-Alive so it may, depending on if the client
476
481
  # indicates that it supports keep alive, wait for another request before
477
482
  # returning.
478
483
  #
479
484
  # Return true if one or more requests were processed.
480
- def process_client(client)
485
+ def process_client(processor, client)
481
486
  close_socket = true
482
487
 
483
488
  requests = 0
@@ -500,7 +505,7 @@ module Puma
500
505
  while can_loop
501
506
  can_loop = false
502
507
  @requests_count += 1
503
- case handle_request(client, requests + 1)
508
+ case handle_request(processor, client, requests + 1)
504
509
  when :close
505
510
  when :async
506
511
  close_socket = false
@@ -577,7 +582,7 @@ module Puma
577
582
  lowlevel_error(e, client.env)
578
583
  @log_writer.ssl_error e, client.io
579
584
  when HttpParserError
580
- response_to_error(client, requests, e, 400)
585
+ response_to_error(client, requests, e, client.error_status_code || 400)
581
586
  @log_writer.parse_error e, client
582
587
  when HttpParserError501
583
588
  response_to_error(client, requests, e, 501)
@@ -610,7 +615,15 @@ module Puma
610
615
  end
611
616
 
612
617
  def response_to_error(client, requests, err, status_code)
613
- status, headers, res_body = lowlevel_error(err, client.env, status_code)
618
+ # @todo remove sometime later
619
+ if status_code == 413
620
+ status = 413
621
+ res_body = ["Payload Too Large"]
622
+ headers = {}
623
+ headers[CONTENT_LENGTH2] = 17
624
+ else
625
+ status, headers, res_body = lowlevel_error(err, client.env, status_code)
626
+ end
614
627
  prepare_response(status, headers, res_body, requests, client)
615
628
  end
616
629
  private :response_to_error
@@ -618,32 +631,11 @@ module Puma
618
631
  # Wait for all outstanding requests to finish.
619
632
  #
620
633
  def graceful_shutdown
621
- if options[:shutdown_debug]
622
- threads = Thread.list
623
- total = threads.size
624
-
625
- pid = Process.pid
626
-
627
- $stdout.syswrite "#{pid}: === Begin thread backtrace dump ===\n"
628
-
629
- threads.each_with_index do |t,i|
630
- $stdout.syswrite "#{pid}: Thread #{i+1}/#{total}: #{t.inspect}\n"
631
- $stdout.syswrite "#{pid}: #{t.backtrace.join("\n#{pid}: ")}\n\n"
632
- end
633
- $stdout.syswrite "#{pid}: === End thread backtrace dump ===\n"
634
- end
635
-
636
634
  if @status != :restart
637
635
  @binder.close
638
636
  end
639
637
 
640
- if @thread_pool
641
- if timeout = options[:force_shutdown_after]
642
- @thread_pool.shutdown timeout.to_f
643
- else
644
- @thread_pool.shutdown
645
- end
646
- end
638
+ @thread_pool.shutdown(options[:force_shutdown_after])
647
639
  end
648
640
 
649
641
  def notify_safely(message)
@@ -660,7 +652,7 @@ module Puma
660
652
  end
661
653
  private :notify_safely
662
654
 
663
- # Stops the acceptor thread and then causes the worker threads to finish
655
+ # Stops the acceptor thread and then causes the processor threads to finish
664
656
  # off the request queue before finally exiting.
665
657
 
666
658
  def stop(sync=false)
@@ -730,6 +722,49 @@ module Puma
730
722
  @binder.add_unix_listener path, umask, mode, backlog
731
723
  end
732
724
 
725
+ # Updates the minimum and maximum number of threads in the thread pool.
726
+ #
727
+ # This method allows dynamic adjustment of the thread pool size while the server
728
+ # is running. It validates the provided values and updates both the thread pool
729
+ # and the server's thread configuration.
730
+ #
731
+ # @param min [Integer] The minimum number of threads to maintain in the pool.
732
+ # Defaults to the current minimum if not specified. Must be greater than 0
733
+ # and less than or equal to max.
734
+ # @param max [Integer] The maximum number of threads allowed in the pool.
735
+ # Defaults to the current maximum if not specified. Must be greater than or
736
+ # equal to min.
737
+ #
738
+ # @return [void]
739
+ #
740
+ # @note If validation fails, a warning message is logged and no changes are made.
741
+ #
742
+ # @example Update both min and max threads
743
+ # server.update_thread_pool_min_max(min: 2, max: 8)
744
+ #
745
+ # @example Update only the minimum threads
746
+ # server.update_thread_pool_min_max(min: 4)
747
+ #
748
+ # @example Update only the maximum threads
749
+ # server.update_thread_pool_min_max(max: 16)
750
+ #
751
+ def update_thread_pool_min_max(min: @min_threads, max: @max_threads)
752
+ if min > max
753
+ @log_writer.log "`min' value cannot be greater than `max' value."
754
+ return
755
+ end
756
+
757
+ if min < 0
758
+ @log_writer.log "`min' value cannot be less than 0"
759
+ return
760
+ end
761
+
762
+ @thread_pool&.with_mutex do
763
+ @thread_pool.min, @thread_pool.max = min, max
764
+ @min_threads, @max_threads = min, max
765
+ end
766
+ end
767
+
733
768
  # @!attribute [r] connected_ports
734
769
  def connected_ports
735
770
  @binder.connected_ports
@@ -0,0 +1,32 @@
1
+ module Puma
2
+ # ServerPluginControl provides a control interface for server plugins to
3
+ # interact with and manage server settings dynamically.
4
+ #
5
+ # This class acts as a facade between plugins and the Puma server,
6
+ # allowing plugins to safely modify server configuration and thread pool
7
+ # settings without direct access to the server's internal state.
8
+ #
9
+ class ServerPluginControl
10
+ def initialize(server)
11
+ @server = server
12
+ end
13
+
14
+ # Returns the maximum number of threads in the thread pool.
15
+ def max_threads
16
+ @server.max_threads
17
+ end
18
+
19
+ # Returns the minimum number of threads in the thread pool.
20
+ def min_threads
21
+ @server.min_threads
22
+ end
23
+
24
+ # Updates the minimum and maximum number of threads in the thread pool.
25
+ #
26
+ # @see Puma::Server#update_thread_pool_min_max
27
+ #
28
+ def update_thread_pool_min_max(min: max_threads, max: min_threads)
29
+ @server.update_thread_pool_min_max(min: min, max: max)
30
+ end
31
+ end
32
+ end
@@ -3,6 +3,7 @@
3
3
  require 'thread'
4
4
 
5
5
  require_relative 'io_buffer'
6
+ require_relative 'server_plugin_control'
6
7
 
7
8
  module Puma
8
9
 
@@ -24,6 +25,51 @@ module Puma
24
25
  class ForceShutdown < RuntimeError
25
26
  end
26
27
 
28
+ class ProcessorThread
29
+ attr_accessor :thread
30
+ attr_writer :marked_as_io_thread
31
+
32
+ def initialize(pool)
33
+ @pool = pool
34
+ @thread = nil
35
+ @marked_as_io_thread = false
36
+ end
37
+
38
+ def mark_as_io_thread!
39
+ unless @marked_as_io_thread
40
+ @marked_as_io_thread = true
41
+
42
+ # Immediately signal the pool that it can spawn a new thread
43
+ # if there's some work in the queue.
44
+ @pool.spawn_thread_if_needed
45
+ end
46
+ end
47
+
48
+ def marked_as_io_thread?
49
+ @marked_as_io_thread
50
+ end
51
+
52
+ def alive?
53
+ @thread&.alive?
54
+ end
55
+
56
+ def join(...)
57
+ @thread.join(...)
58
+ end
59
+
60
+ def kill(...)
61
+ @thread.kill(...)
62
+ end
63
+
64
+ def [](key)
65
+ @thread[key]
66
+ end
67
+
68
+ def raise(...)
69
+ @thread.raise(...)
70
+ end
71
+ end
72
+
27
73
  # How long, after raising the ForceShutdown of a thread during
28
74
  # forced shutdown mode, to wait for the thread to try and finish
29
75
  # up its work before leaving the thread to die on the vine.
@@ -52,10 +98,13 @@ module Puma
52
98
  @name = name
53
99
  @min = Integer(options[:min_threads])
54
100
  @max = Integer(options[:max_threads])
101
+ @max_io_threads = Integer(options[:max_io_threads] || 0)
102
+
55
103
  # Not an 'exposed' option, options[:pool_shutdown_grace_time] is used in CI
56
104
  # to shorten @shutdown_grace_time from SHUTDOWN_GRACE_TIME. Parallel CI
57
105
  # makes stubbing constants difficult.
58
106
  @shutdown_grace_time = Float(options[:pool_shutdown_grace_time] || SHUTDOWN_GRACE_TIME)
107
+ @shutdown_debug = options[:shutdown_debug]
59
108
  @block = block
60
109
  @out_of_band = options[:out_of_band]
61
110
  @out_of_band_running = false
@@ -70,7 +119,7 @@ module Puma
70
119
  @trim_requested = 0
71
120
  @out_of_band_pending = false
72
121
 
73
- @workers = []
122
+ @processors = []
74
123
 
75
124
  @auto_trim = nil
76
125
  @reaper = nil
@@ -87,6 +136,7 @@ module Puma
87
136
  end
88
137
 
89
138
  attr_reader :spawned, :trim_requested, :waiting
139
+ attr_accessor :min, :max
90
140
 
91
141
  # generate stats hash so as not to perform multiple locks
92
142
  # @return [Hash] hash containing stat info from ThreadPool
@@ -96,8 +146,9 @@ module Puma
96
146
  @backlog_max = 0
97
147
  { backlog: @todo.size,
98
148
  running: @spawned,
99
- pool_capacity: @waiting + (@max - @spawned),
149
+ pool_capacity: pool_capacity,
100
150
  busy_threads: @spawned - @waiting + @todo.size,
151
+ io_threads: @processors.count(&:marked_as_io_thread?),
101
152
  backlog_max: temp
102
153
  }
103
154
  end
@@ -121,7 +172,7 @@ module Puma
121
172
 
122
173
  # @!attribute [r] pool_capacity
123
174
  def pool_capacity
124
- waiting + (@max - spawned)
175
+ (waiting + (@max - spawned)).clamp(0, Float::INFINITY)
125
176
  end
126
177
 
127
178
  # @!attribute [r] busy_threads
@@ -138,7 +189,8 @@ module Puma
138
189
  @spawned += 1
139
190
 
140
191
  trigger_before_thread_start_hooks
141
- th = Thread.new(@spawned) do |spawned|
192
+ processor = ProcessorThread.new(self)
193
+ processor.thread = Thread.new(processor, @spawned) do |processor, spawned|
142
194
  Puma.set_thread_name '%s tp %03i' % [@name, spawned]
143
195
  # Advertise server into the thread
144
196
  Thread.current.puma_server = @server
@@ -153,11 +205,23 @@ module Puma
153
205
  work = nil
154
206
 
155
207
  mutex.synchronize do
208
+ if processor.marked_as_io_thread?
209
+ if @processors.count { |t| !t.marked_as_io_thread? } < @max
210
+ # We're not at max processor threads, so the io thread can rejoin the normal population.
211
+ processor.marked_as_io_thread = false
212
+ else
213
+ # We're already at max threads, so we exit the extra io thread.
214
+ @processors.delete(processor)
215
+ trigger_before_thread_exit_hooks
216
+ Thread.exit
217
+ end
218
+ end
219
+
156
220
  while todo.empty?
157
221
  if @trim_requested > 0
158
222
  @trim_requested -= 1
159
223
  @spawned -= 1
160
- @workers.delete th
224
+ @processors.delete(processor)
161
225
  not_full.signal
162
226
  trigger_before_thread_exit_hooks
163
227
  Thread.exit
@@ -179,16 +243,16 @@ module Puma
179
243
  end
180
244
 
181
245
  begin
182
- @out_of_band_pending = true if block.call(work)
246
+ @out_of_band_pending = true if block.call(processor, work)
183
247
  rescue Exception => e
184
248
  STDERR.puts "Error reached top of thread-pool: #{e.message} (#{e.class})"
185
249
  end
186
250
  end
187
251
  end
188
252
 
189
- @workers << th
253
+ @processors << processor
190
254
 
191
- th
255
+ processor
192
256
  end
193
257
 
194
258
  private :spawn_thread
@@ -198,7 +262,7 @@ module Puma
198
262
 
199
263
  @before_thread_start.each do |b|
200
264
  begin
201
- b[:block].call
265
+ b[:block].call(ServerPluginControl.new(@server))
202
266
  rescue Exception => e
203
267
  STDERR.puts "WARNING before_thread_start hook failed with exception (#{e.class}) #{e.message}"
204
268
  end
@@ -257,6 +321,16 @@ module Puma
257
321
  @mutex.synchronize(&block)
258
322
  end
259
323
 
324
+ # :nodoc:
325
+ #
326
+ # Must be called with @mutex held!
327
+ #
328
+ def can_spawn_processor?
329
+ io_processors_count = @processors.count(&:marked_as_io_thread?)
330
+ extra_io_processors_count = io_processors_count > @max_io_threads ? io_processors_count - @max_io_threads : 0
331
+ (@spawned - io_processors_count) < (@max - extra_io_processors_count)
332
+ end
333
+
260
334
  # Add +work+ to the todo list for a Thread to pickup and process.
261
335
  def <<(work)
262
336
  with_mutex do
@@ -268,12 +342,21 @@ module Puma
268
342
  t = @todo.size
269
343
  @backlog_max = t if t > @backlog_max
270
344
 
271
- if @waiting < @todo.size and @spawned < @max
345
+ if @waiting < @todo.size and can_spawn_processor?
272
346
  spawn_thread
273
347
  end
274
348
 
275
349
  @not_empty.signal
276
350
  end
351
+ self
352
+ end
353
+
354
+ def spawn_thread_if_needed # :nodoc:
355
+ with_mutex do
356
+ if @waiting < @todo.size and can_spawn_processor?
357
+ spawn_thread
358
+ end
359
+ end
277
360
  end
278
361
 
279
362
  # If there are any free threads in the pool, tell one to go ahead
@@ -294,16 +377,12 @@ module Puma
294
377
  # spawned counter so that new healthy threads could be created again.
295
378
  def reap
296
379
  with_mutex do
297
- dead_workers = @workers.reject(&:alive?)
380
+ @processors, dead_processors = @processors.partition(&:alive?)
298
381
 
299
- dead_workers.each do |worker|
300
- worker.kill
382
+ dead_processors.each do |processor|
383
+ processor.kill
301
384
  @spawned -= 1
302
385
  end
303
-
304
- @workers.delete_if do |w|
305
- dead_workers.include?(w)
306
- end
307
386
  end
308
387
  end
309
388
 
@@ -362,7 +441,7 @@ module Puma
362
441
  # Next, wait an extra +@shutdown_grace_time+ seconds then force-kill remaining
363
442
  # threads. Finally, wait 1 second for remaining threads to exit.
364
443
  #
365
- def shutdown(timeout=-1)
444
+ def shutdown(timeout)
366
445
  threads = with_mutex do
367
446
  @shutdown = true
368
447
  @trim_requested = @spawned
@@ -371,8 +450,12 @@ module Puma
371
450
 
372
451
  @auto_trim&.stop
373
452
  @reaper&.stop
374
- # dup workers so that we join them all safely
375
- @workers.dup
453
+ # dup processors so that we join them all safely
454
+ @processors.dup
455
+ end
456
+
457
+ if @shutdown_debug == true
458
+ shutdown_debug("Shutdown initiated")
376
459
  end
377
460
 
378
461
  if timeout == -1
@@ -382,13 +465,16 @@ module Puma
382
465
  join = ->(inner_timeout) do
383
466
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
384
467
  threads.reject! do |t|
385
- elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
386
- t.join inner_timeout - elapsed
468
+ remaining = inner_timeout - (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start)
469
+ remaining > 0 && t.join(remaining)
387
470
  end
388
471
  end
389
472
 
390
473
  # Wait +timeout+ seconds for threads to finish.
391
474
  join.call(timeout)
475
+ if @shutdown_debug == :on_force && !threads.empty?
476
+ shutdown_debug("Shutdown timeout exceeded")
477
+ end
392
478
 
393
479
  # If threads are still running, raise ForceShutdown and wait to finish.
394
480
  @shutdown_mutex.synchronize do
@@ -398,6 +484,9 @@ module Puma
398
484
  end
399
485
  end
400
486
  join.call(@shutdown_grace_time)
487
+ if @shutdown_debug == :on_force && !threads.empty?
488
+ shutdown_debug("Shutdown grace timeout exceeded")
489
+ end
401
490
 
402
491
  # If threads are _still_ running, forcefully kill them and wait to finish.
403
492
  threads.each(&:kill)
@@ -405,7 +494,24 @@ module Puma
405
494
  end
406
495
 
407
496
  @spawned = 0
408
- @workers = []
497
+ @processors = []
498
+ end
499
+
500
+ private
501
+
502
+ def shutdown_debug(message)
503
+ pid = Process.pid
504
+ threads = Thread.list
505
+
506
+ $stdout.syswrite "#{pid}: #{message}\n"
507
+ $stdout.syswrite "#{pid}: === Begin thread backtrace dump ===\n"
508
+
509
+ threads.each_with_index do |thread, index|
510
+ $stdout.syswrite "#{pid}: Thread #{index + 1}/#{threads.size}: #{thread.inspect}\n"
511
+ $stdout.syswrite "#{pid}: #{(thread.backtrace || []).join("\n#{pid}: ")}\n\n"
512
+ end
513
+
514
+ $stdout.syswrite "#{pid}: === End thread backtrace dump ===\n"
409
515
  end
410
516
  end
411
517
  end
@@ -109,7 +109,7 @@ module Puma
109
109
  end
110
110
 
111
111
  if port
112
- host ||= ::Puma::Configuration::DEFAULTS[:tcp_host]
112
+ host ||= ::Puma::Configuration.default_tcp_host
113
113
  config.port port, host
114
114
  end
115
115
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: puma
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.2.0
4
+ version: 8.0.1
5
5
  platform: java
6
6
  authors:
7
7
  - Evan Phoenix
@@ -42,13 +42,21 @@ files:
42
42
  - bin/puma
43
43
  - bin/puma-wild
44
44
  - bin/pumactl
45
+ - docs/5.0-Upgrade.md
46
+ - docs/6.0-Upgrade.md
47
+ - docs/7.0-Upgrade.md
48
+ - docs/8.0-Upgrade.md
45
49
  - docs/architecture.md
46
50
  - docs/compile_options.md
47
51
  - docs/deployment.md
48
52
  - docs/fork_worker.md
53
+ - docs/grpc.md
54
+ - docs/images/favicon.svg
49
55
  - docs/images/puma-connection-flow-no-reactor.png
50
56
  - docs/images/puma-connection-flow.png
51
57
  - docs/images/puma-general-arch.png
58
+ - docs/images/running-puma.svg
59
+ - docs/images/standard-logo.svg
52
60
  - docs/java_options.md
53
61
  - docs/jungle/README.md
54
62
  - docs/jungle/rc.d/README.md
@@ -73,6 +81,7 @@ files:
73
81
  - ext/puma_http11/http11_parser_common.rl
74
82
  - ext/puma_http11/mini_ssl.c
75
83
  - ext/puma_http11/no_ssl/PumaHttp11Service.java
84
+ - ext/puma_http11/org/jruby/puma/EnvKey.java
76
85
  - ext/puma_http11/org/jruby/puma/Http11.java
77
86
  - ext/puma_http11/org/jruby/puma/Http11Parser.java
78
87
  - ext/puma_http11/org/jruby/puma/MiniSSL.java
@@ -82,6 +91,7 @@ files:
82
91
  - lib/puma/binder.rb
83
92
  - lib/puma/cli.rb
84
93
  - lib/puma/client.rb
94
+ - lib/puma/client_env.rb
85
95
  - lib/puma/cluster.rb
86
96
  - lib/puma/cluster/worker.rb
87
97
  - lib/puma/cluster/worker_handle.rb
@@ -111,10 +121,11 @@ files:
111
121
  - lib/puma/rack/urlmap.rb
112
122
  - lib/puma/rack_default.rb
113
123
  - lib/puma/reactor.rb
114
- - lib/puma/request.rb
124
+ - lib/puma/response.rb
115
125
  - lib/puma/runner.rb
116
126
  - lib/puma/sd_notify.rb
117
127
  - lib/puma/server.rb
128
+ - lib/puma/server_plugin_control.rb
118
129
  - lib/puma/single.rb
119
130
  - lib/puma/state_file.rb
120
131
  - lib/puma/thread_pool.rb
@@ -146,7 +157,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
146
157
  - !ruby/object:Gem::Version
147
158
  version: '0'
148
159
  requirements: []
149
- rubygems_version: 3.6.9
160
+ rubygems_version: 4.0.3
150
161
  specification_version: 4
151
162
  summary: A Ruby/Rack web server built for parallelism.
152
163
  test_files: []