puma 5.6.4 → 6.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +372 -6
  3. data/LICENSE +0 -0
  4. data/README.md +79 -29
  5. data/bin/puma-wild +1 -1
  6. data/docs/architecture.md +0 -0
  7. data/docs/compile_options.md +34 -0
  8. data/docs/deployment.md +0 -0
  9. data/docs/fork_worker.md +1 -3
  10. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  11. data/docs/images/puma-connection-flow.png +0 -0
  12. data/docs/images/puma-general-arch.png +0 -0
  13. data/docs/jungle/README.md +0 -0
  14. data/docs/jungle/rc.d/README.md +0 -0
  15. data/docs/jungle/rc.d/puma.conf +0 -0
  16. data/docs/kubernetes.md +12 -0
  17. data/docs/nginx.md +1 -1
  18. data/docs/plugins.md +0 -0
  19. data/docs/rails_dev_mode.md +0 -0
  20. data/docs/restart.md +1 -0
  21. data/docs/signals.md +0 -0
  22. data/docs/stats.md +0 -0
  23. data/docs/systemd.md +3 -6
  24. data/docs/testing_benchmarks_local_files.md +150 -0
  25. data/docs/testing_test_rackup_ci_files.md +36 -0
  26. data/ext/puma_http11/PumaHttp11Service.java +0 -0
  27. data/ext/puma_http11/ext_help.h +0 -0
  28. data/ext/puma_http11/extconf.rb +22 -10
  29. data/ext/puma_http11/http11_parser.c +1 -1
  30. data/ext/puma_http11/http11_parser.h +1 -1
  31. data/ext/puma_http11/http11_parser.java.rl +2 -2
  32. data/ext/puma_http11/http11_parser.rl +2 -2
  33. data/ext/puma_http11/http11_parser_common.rl +2 -2
  34. data/ext/puma_http11/mini_ssl.c +153 -27
  35. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +0 -0
  36. data/ext/puma_http11/org/jruby/puma/Http11.java +3 -3
  37. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +1 -1
  38. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +167 -65
  39. data/ext/puma_http11/puma_http11.c +17 -9
  40. data/lib/puma/app/status.rb +7 -4
  41. data/lib/puma/binder.rb +51 -54
  42. data/lib/puma/cli.rb +16 -18
  43. data/lib/puma/client.rb +100 -26
  44. data/lib/puma/cluster/worker.rb +18 -11
  45. data/lib/puma/cluster/worker_handle.rb +4 -1
  46. data/lib/puma/cluster.rb +102 -40
  47. data/lib/puma/commonlogger.rb +21 -14
  48. data/lib/puma/configuration.rb +77 -59
  49. data/lib/puma/const.rb +129 -92
  50. data/lib/puma/control_cli.rb +33 -23
  51. data/lib/puma/detect.rb +7 -4
  52. data/lib/puma/dsl.rb +251 -53
  53. data/lib/puma/error_logger.rb +18 -9
  54. data/lib/puma/events.rb +6 -126
  55. data/lib/puma/io_buffer.rb +39 -4
  56. data/lib/puma/jruby_restart.rb +2 -1
  57. data/lib/puma/json_serialization.rb +0 -0
  58. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  59. data/lib/puma/launcher.rb +113 -175
  60. data/lib/puma/log_writer.rb +147 -0
  61. data/lib/puma/minissl/context_builder.rb +26 -12
  62. data/lib/puma/minissl.rb +113 -15
  63. data/lib/puma/null_io.rb +21 -2
  64. data/lib/puma/plugin/systemd.rb +90 -0
  65. data/lib/puma/plugin/tmp_restart.rb +1 -1
  66. data/lib/puma/plugin.rb +0 -0
  67. data/lib/puma/rack/builder.rb +6 -6
  68. data/lib/puma/rack/urlmap.rb +1 -1
  69. data/lib/puma/rack_default.rb +19 -4
  70. data/lib/puma/reactor.rb +19 -10
  71. data/lib/puma/request.rb +365 -166
  72. data/lib/puma/runner.rb +56 -20
  73. data/lib/puma/sd_notify.rb +149 -0
  74. data/lib/puma/server.rb +137 -87
  75. data/lib/puma/single.rb +13 -11
  76. data/lib/puma/state_file.rb +4 -6
  77. data/lib/puma/thread_pool.rb +57 -19
  78. data/lib/puma/util.rb +12 -14
  79. data/lib/puma.rb +12 -11
  80. data/lib/rack/handler/puma.rb +113 -86
  81. data/tools/Dockerfile +2 -2
  82. data/tools/trickletest.rb +0 -0
  83. metadata +11 -6
  84. data/lib/puma/queue_close.rb +0 -26
  85. data/lib/puma/systemd.rb +0 -46
data/lib/puma/server.rb CHANGED
@@ -2,20 +2,19 @@
2
2
 
3
3
  require 'stringio'
4
4
 
5
- require 'puma/thread_pool'
6
- require 'puma/const'
7
- require 'puma/events'
8
- require 'puma/null_io'
9
- require 'puma/reactor'
10
- require 'puma/client'
11
- require 'puma/binder'
12
- require 'puma/util'
13
- require 'puma/io_buffer'
14
- require 'puma/request'
5
+ require_relative 'thread_pool'
6
+ require_relative 'const'
7
+ require_relative 'log_writer'
8
+ require_relative 'events'
9
+ require_relative 'null_io'
10
+ require_relative 'reactor'
11
+ require_relative 'client'
12
+ require_relative 'binder'
13
+ require_relative 'util'
14
+ require_relative 'request'
15
15
 
16
16
  require 'socket'
17
- require 'io/wait'
18
- require 'forwardable'
17
+ require 'io/wait' unless Puma::HAS_NATIVE_IO_WAIT
19
18
 
20
19
  module Puma
21
20
 
@@ -30,12 +29,11 @@ module Puma
30
29
  #
31
30
  # Each `Puma::Server` will have one reactor and one thread pool.
32
31
  class Server
33
-
34
32
  include Puma::Const
35
33
  include Request
36
- extend Forwardable
37
34
 
38
35
  attr_reader :thread
36
+ attr_reader :log_writer
39
37
  attr_reader :events
40
38
  attr_reader :min_threads, :max_threads # for #stats
41
39
  attr_reader :requests_count # @version 5.0.0
@@ -45,23 +43,16 @@ module Puma
45
43
  :leak_stack_on_error,
46
44
  :persistent_timeout, :reaping_time
47
45
 
48
- # @deprecated v6.0.0
49
- attr_writer :auto_trim_time, :early_hints, :first_data_timeout,
50
- :leak_stack_on_error, :min_threads, :max_threads,
51
- :persistent_timeout, :reaping_time
52
-
53
46
  attr_accessor :app
54
47
  attr_accessor :binder
55
48
 
56
- def_delegators :@binder, :add_tcp_listener, :add_ssl_listener,
57
- :add_unix_listener, :connected_ports
58
-
59
- ThreadLocalKey = :puma_server
49
+ THREAD_LOCAL_KEY = :puma_server
60
50
 
61
51
  # Create a server for the rack app +app+.
62
52
  #
63
- # +events+ is an object which will be called when certain error events occur
64
- # to be handled. See Puma::Events for the list of current methods to implement.
53
+ # +log_writer+ is a Puma::LogWriter object used to log info and error messages.
54
+ #
55
+ # +events+ is a Puma::Events object used to notify application status events.
65
56
  #
66
57
  # Server#run returns a thread that you can join on to wait for the server
67
58
  # to do its work.
@@ -70,34 +61,56 @@ module Puma
70
61
  # and have default values set via +fetch+. Normally the values are set via
71
62
  # `::Puma::Configuration.puma_default_options`.
72
63
  #
73
- def initialize(app, events=Events.stdio, options={})
64
+ # @note The `events` parameter is set to nil, and set to `Events.new` in code.
65
+ # Often `options` needs to be passed, but `events` does not. Using nil allows
66
+ # calling code to not require events.rb.
67
+ #
68
+ def initialize(app, events = nil, options = {})
74
69
  @app = app
75
- @events = events
70
+ @events = events || Events.new
76
71
 
77
72
  @check, @notify = nil
78
73
  @status = :stop
79
74
 
80
- @auto_trim_time = 30
81
- @reaping_time = 1
82
-
83
75
  @thread = nil
84
76
  @thread_pool = nil
85
77
 
86
- @options = options
78
+ @options = if options.is_a?(UserFileDefaultOptions)
79
+ options
80
+ else
81
+ UserFileDefaultOptions.new(options, Configuration::DEFAULTS)
82
+ end
87
83
 
88
- @early_hints = options.fetch :early_hints, nil
89
- @first_data_timeout = options.fetch :first_data_timeout, FIRST_DATA_TIMEOUT
90
- @min_threads = options.fetch :min_threads, 0
91
- @max_threads = options.fetch :max_threads , (Puma.mri? ? 5 : 16)
92
- @persistent_timeout = options.fetch :persistent_timeout, PERSISTENT_TIMEOUT
93
- @queue_requests = options.fetch :queue_requests, true
94
- @max_fast_inline = options.fetch :max_fast_inline, MAX_FAST_INLINE
95
- @io_selector_backend = options.fetch :io_selector_backend, :auto
84
+ @clustered = (@options.fetch :workers, 0) > 0
85
+ @worker_write = @options[:worker_write]
86
+ @log_writer = @options.fetch :log_writer, LogWriter.stdio
87
+ @early_hints = @options[:early_hints]
88
+ @first_data_timeout = @options[:first_data_timeout]
89
+ @persistent_timeout = @options[:persistent_timeout]
90
+ @idle_timeout = @options[:idle_timeout]
91
+ @min_threads = @options[:min_threads]
92
+ @max_threads = @options[:max_threads]
93
+ @queue_requests = @options[:queue_requests]
94
+ @max_fast_inline = @options[:max_fast_inline]
95
+ @io_selector_backend = @options[:io_selector_backend]
96
+ @http_content_length_limit = @options[:http_content_length_limit]
97
+
98
+ # make this a hash, since we prefer `key?` over `include?`
99
+ @supported_http_methods =
100
+ if @options[:supported_http_methods] == :any
101
+ :any
102
+ else
103
+ if (ary = @options[:supported_http_methods])
104
+ ary
105
+ else
106
+ SUPPORTED_HTTP_METHODS
107
+ end.sort.product([nil]).to_h.freeze
108
+ end
96
109
 
97
110
  temp = !!(@options[:environment] =~ /\A(development|test)\z/)
98
111
  @leak_stack_on_error = @options[:environment] ? temp : true
99
112
 
100
- @binder = Binder.new(events)
113
+ @binder = Binder.new(log_writer)
101
114
 
102
115
  ENV['RACK_ENV'] ||= "development"
103
116
 
@@ -106,6 +119,8 @@ module Puma
106
119
  @precheck_closing = true
107
120
 
108
121
  @requests_count = 0
122
+
123
+ @idle_timeout_reached = false
109
124
  end
110
125
 
111
126
  def inherit_binder(bind)
@@ -115,7 +130,7 @@ module Puma
115
130
  class << self
116
131
  # @!attribute [r] current
117
132
  def current
118
- Thread.current[ThreadLocalKey]
133
+ Thread.current[THREAD_LOCAL_KEY]
119
134
  end
120
135
 
121
136
  # :nodoc:
@@ -193,12 +208,12 @@ module Puma
193
208
 
194
209
  # @!attribute [r] backlog
195
210
  def backlog
196
- @thread_pool and @thread_pool.backlog
211
+ @thread_pool&.backlog
197
212
  end
198
213
 
199
214
  # @!attribute [r] running
200
215
  def running
201
- @thread_pool and @thread_pool.spawned
216
+ @thread_pool&.spawned
202
217
  end
203
218
 
204
219
 
@@ -211,7 +226,7 @@ module Puma
211
226
  # value would be 4 until it finishes processing.
212
227
  # @!attribute [r] pool_capacity
213
228
  def pool_capacity
214
- @thread_pool and @thread_pool.pool_capacity
229
+ @thread_pool&.pool_capacity
215
230
  end
216
231
 
217
232
  # Runs the server.
@@ -227,29 +242,16 @@ module Puma
227
242
 
228
243
  @status = :run
229
244
 
230
- @thread_pool = ThreadPool.new(
231
- thread_name,
232
- @min_threads,
233
- @max_threads,
234
- ::Puma::IOBuffer,
235
- &method(:process_client)
236
- )
237
-
238
- @thread_pool.out_of_band_hook = @options[:out_of_band]
239
- @thread_pool.clean_thread_locals = @options[:clean_thread_locals]
245
+ @thread_pool = ThreadPool.new(thread_name, @options) { |client| process_client client }
240
246
 
241
247
  if @queue_requests
242
- @reactor = Reactor.new(@io_selector_backend, &method(:reactor_wakeup))
248
+ @reactor = Reactor.new(@io_selector_backend) { |c| reactor_wakeup c }
243
249
  @reactor.run
244
250
  end
245
251
 
246
- if @reaping_time
247
- @thread_pool.auto_reap!(@reaping_time)
248
- end
249
252
 
250
- if @auto_trim_time
251
- @thread_pool.auto_trim!(@auto_trim_time)
252
- end
253
+ @thread_pool.auto_reap! if @options[:reaping_time]
254
+ @thread_pool.auto_trim! if @options[:auto_trim_time]
253
255
 
254
256
  @check, @notify = Puma::Util.pipe unless @notify
255
257
 
@@ -328,8 +330,28 @@ module Puma
328
330
 
329
331
  while @status == :run || (drain && shutting_down?)
330
332
  begin
331
- ios = IO.select sockets, nil, nil, (shutting_down? ? 0 : nil)
332
- break unless ios
333
+ ios = IO.select sockets, nil, nil, (shutting_down? ? 0 : @idle_timeout)
334
+ unless ios
335
+ unless shutting_down?
336
+ @idle_timeout_reached = true
337
+
338
+ if @clustered
339
+ @worker_write << "i#{Process.pid}\n" rescue nil
340
+ next
341
+ else
342
+ @log_writer.log "- Idle timeout reached"
343
+ @status = :stop
344
+ end
345
+ end
346
+
347
+ break
348
+ end
349
+
350
+ if @idle_timeout_reached && @clustered
351
+ @idle_timeout_reached = false
352
+ @worker_write << "i#{Process.pid}\n" rescue nil
353
+ end
354
+
333
355
  ios.first.each do |sock|
334
356
  if sock == check
335
357
  break if handle_check
@@ -345,6 +367,7 @@ module Puma
345
367
  drain += 1 if shutting_down?
346
368
  pool << Client.new(io, @binder.env(sock)).tap { |c|
347
369
  c.listener = sock
370
+ c.http_content_length_limit = @http_content_length_limit
348
371
  c.send(addr_send_name, addr_value) if addr_value
349
372
  }
350
373
  end
@@ -353,27 +376,27 @@ module Puma
353
376
  # In the case that any of the sockets are unexpectedly close.
354
377
  raise
355
378
  rescue StandardError => e
356
- @events.unknown_error e, nil, "Listen loop"
379
+ @log_writer.unknown_error e, nil, "Listen loop"
357
380
  end
358
381
  end
359
382
 
360
- @events.debug "Drained #{drain} additional connections." if drain
383
+ @log_writer.debug "Drained #{drain} additional connections." if drain
361
384
  @events.fire :state, @status
362
385
 
363
386
  if queue_requests
364
387
  @queue_requests = false
365
388
  @reactor.shutdown
366
389
  end
390
+
367
391
  graceful_shutdown if @status == :stop || @status == :restart
368
392
  rescue Exception => e
369
- @events.unknown_error e, nil, "Exception handling servers"
393
+ @log_writer.unknown_error e, nil, "Exception handling servers"
370
394
  ensure
371
- # RuntimeError is Ruby 2.2 issue, can't modify frozen IOError
372
395
  # Errno::EBADF is infrequently raised
373
396
  [@check, @notify].each do |io|
374
397
  begin
375
398
  io.close unless io.closed?
376
- rescue Errno::EBADF, RuntimeError
399
+ rescue Errno::EBADF
377
400
  end
378
401
  end
379
402
  @notify = nil
@@ -412,9 +435,9 @@ module Puma
412
435
  # returning.
413
436
  #
414
437
  # Return true if one or more requests were processed.
415
- def process_client(client, buffer)
438
+ def process_client(client)
416
439
  # Advertise this server into the thread
417
- Thread.current[ThreadLocalKey] = self
440
+ Thread.current[THREAD_LOCAL_KEY] = self
418
441
 
419
442
  clean_thread_locals = @options[:clean_thread_locals]
420
443
  close_socket = true
@@ -438,15 +461,13 @@ module Puma
438
461
 
439
462
  while true
440
463
  @requests_count += 1
441
- case handle_request(client, buffer, requests + 1)
464
+ case handle_request(client, requests + 1)
442
465
  when false
443
466
  break
444
467
  when :async
445
468
  close_socket = false
446
469
  break
447
470
  when true
448
- buffer.reset
449
-
450
471
  ThreadPool.clean_thread_locals if clean_thread_locals
451
472
 
452
473
  requests += 1
@@ -476,11 +497,11 @@ module Puma
476
497
  end
477
498
  true
478
499
  rescue StandardError => e
479
- client_error(e, client)
500
+ client_error(e, client, requests)
480
501
  # The ensure tries to close +client+ down
481
502
  requests > 0
482
503
  ensure
483
- buffer.reset
504
+ client.io_buffer.reset
484
505
 
485
506
  begin
486
507
  client.close if close_socket
@@ -488,7 +509,7 @@ module Puma
488
509
  Puma::Util.purge_interrupt_queue
489
510
  # Already closed
490
511
  rescue StandardError => e
491
- @events.unknown_error e, nil, "Client"
512
+ @log_writer.unknown_error e, nil, "Client"
492
513
  end
493
514
  end
494
515
  end
@@ -504,23 +525,23 @@ module Puma
504
525
  # :nocov:
505
526
 
506
527
  # Handle various error types thrown by Client I/O operations.
507
- def client_error(e, client)
528
+ def client_error(e, client, requests = 1)
508
529
  # Swallow, do not log
509
530
  return if [ConnectionError, EOFError].include?(e.class)
510
531
 
511
- lowlevel_error(e, client.env)
512
532
  case e
513
533
  when MiniSSL::SSLError
514
- @events.ssl_error e, client.io
534
+ lowlevel_error(e, client.env)
535
+ @log_writer.ssl_error e, client.io
515
536
  when HttpParserError
516
- client.write_error(400)
517
- @events.parse_error e, client
537
+ response_to_error(client, requests, e, 400)
538
+ @log_writer.parse_error e, client
518
539
  when HttpParserError501
519
- client.write_error(501)
520
- @events.parse_error e, client
540
+ response_to_error(client, requests, e, 501)
541
+ @log_writer.parse_error e, client
521
542
  else
522
- client.write_error(500)
523
- @events.unknown_error e, nil, "Read"
543
+ response_to_error(client, requests, e, 500)
544
+ @log_writer.unknown_error e, nil, "Read"
524
545
  end
525
546
  end
526
547
 
@@ -541,10 +562,17 @@ module Puma
541
562
  backtrace = e.backtrace.nil? ? '<no backtrace available>' : e.backtrace.join("\n")
542
563
  [status, {}, ["Puma caught this error: #{e.message} (#{e.class})\n#{backtrace}"]]
543
564
  else
544
- [status, {}, ["An unhandled lowlevel error occurred. The application logs may have details.\n"]]
565
+ [status, {}, [""]]
545
566
  end
546
567
  end
547
568
 
569
+ def response_to_error(client, requests, err, status_code)
570
+ status, headers, res_body = lowlevel_error(err, client.env, status_code)
571
+ prepare_response(status, headers, res_body, requests, client)
572
+ client.write_error(status_code)
573
+ end
574
+ private :response_to_error
575
+
548
576
  # Wait for all outstanding requests to finish.
549
577
  #
550
578
  def graceful_shutdown
@@ -578,7 +606,7 @@ module Puma
578
606
 
579
607
  def notify_safely(message)
580
608
  @notify << message
581
- rescue IOError, NoMethodError, Errno::EPIPE
609
+ rescue IOError, NoMethodError, Errno::EPIPE, Errno::EBADF
582
610
  # The server, in another thread, is shutting down
583
611
  Puma::Util.purge_interrupt_queue
584
612
  rescue RuntimeError => e
@@ -623,5 +651,27 @@ module Puma
623
651
  def stats
624
652
  STAT_METHODS.map {|name| [name, send(name) || 0]}.to_h
625
653
  end
654
+
655
+ # below are 'delegations' to binder
656
+ # remove in Puma 7?
657
+
658
+
659
+ def add_tcp_listener(host, port, optimize_for_latency = true, backlog = 1024)
660
+ @binder.add_tcp_listener host, port, optimize_for_latency, backlog
661
+ end
662
+
663
+ def add_ssl_listener(host, port, ctx, optimize_for_latency = true,
664
+ backlog = 1024)
665
+ @binder.add_ssl_listener host, port, ctx, optimize_for_latency, backlog
666
+ end
667
+
668
+ def add_unix_listener(path, umask = nil, mode = nil, backlog = 1024)
669
+ @binder.add_unix_listener path, umask, mode, backlog
670
+ end
671
+
672
+ # @!attribute [r] connected_ports
673
+ def connected_ports
674
+ @binder.connected_ports
675
+ end
626
676
  end
627
677
  end
data/lib/puma/single.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'puma/runner'
4
- require 'puma/detect'
5
- require 'puma/plugin'
3
+ require_relative 'runner'
4
+ require_relative 'detect'
5
+ require_relative 'plugin'
6
6
 
7
7
  module Puma
8
8
  # This class is instantiated by the `Puma::Launcher` and used
@@ -16,26 +16,26 @@ module Puma
16
16
  # @!attribute [r] stats
17
17
  def stats
18
18
  {
19
- started_at: @started_at.utc.iso8601
20
- }.merge(@server.stats)
19
+ started_at: utc_iso8601(@started_at)
20
+ }.merge(@server.stats).merge(super)
21
21
  end
22
22
 
23
23
  def restart
24
- @server.begin_restart
24
+ @server&.begin_restart
25
25
  end
26
26
 
27
27
  def stop
28
- @server.stop(false) if @server
28
+ @server&.stop false
29
29
  end
30
30
 
31
31
  def halt
32
- @server.halt
32
+ @server&.halt
33
33
  end
34
34
 
35
35
  def stop_blocked
36
36
  log "- Gracefully stopping, waiting for requests to finish"
37
- @control.stop(true) if @control
38
- @server.stop(true) if @server
37
+ @control&.stop true
38
+ @server&.stop true
39
39
  end
40
40
 
41
41
  def run
@@ -55,7 +55,9 @@ module Puma
55
55
  log "Use Ctrl-C to stop"
56
56
  redirect_io
57
57
 
58
- @launcher.events.fire_on_booted!
58
+ @events.fire_on_booted!
59
+
60
+ debug_loaded_extensions("Loaded Extensions:") if @log_writer.debug?
59
61
 
60
62
  begin
61
63
  server_thread.join
@@ -15,15 +15,12 @@ module Puma
15
15
 
16
16
  ALLOWED_FIELDS = %w!control_url control_auth_token pid running_from!
17
17
 
18
- # @deprecated 6.0.0
19
- FIELDS = ALLOWED_FIELDS
20
-
21
18
  def initialize
22
19
  @options = {}
23
20
  end
24
21
 
25
22
  def save(path, permission = nil)
26
- contents = "---\n".dup
23
+ contents = +"---\n"
27
24
  @options.each do |k,v|
28
25
  next unless ALLOWED_FIELDS.include? k
29
26
  case v
@@ -50,6 +47,7 @@ module Puma
50
47
  v = v.strip
51
48
  @options[k] =
52
49
  case v
50
+ when '' then nil
53
51
  when /\A\d+\z/ then v.to_i
54
52
  when /\A\d+\.\d+\z/ then v.to_f
55
53
  else v.gsub(/\A"|"\z/, '')
@@ -58,11 +56,11 @@ module Puma
58
56
  end
59
57
 
60
58
  ALLOWED_FIELDS.each do |f|
61
- define_method f do
59
+ define_method f.to_sym do
62
60
  @options[f]
63
61
  end
64
62
 
65
- define_method "#{f}=" do |v|
63
+ define_method :"#{f}=" do |v|
66
64
  @options[f] = v
67
65
  end
68
66
  end