puma 5.6.4 → 6.4.2

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 (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