jun-puma 1.0.0-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.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/History.md +2897 -0
  3. data/LICENSE +29 -0
  4. data/README.md +475 -0
  5. data/bin/puma +10 -0
  6. data/bin/puma-wild +25 -0
  7. data/bin/pumactl +12 -0
  8. data/docs/architecture.md +74 -0
  9. data/docs/compile_options.md +55 -0
  10. data/docs/deployment.md +102 -0
  11. data/docs/fork_worker.md +35 -0
  12. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  13. data/docs/images/puma-connection-flow.png +0 -0
  14. data/docs/images/puma-general-arch.png +0 -0
  15. data/docs/jungle/README.md +9 -0
  16. data/docs/jungle/rc.d/README.md +74 -0
  17. data/docs/jungle/rc.d/puma +61 -0
  18. data/docs/jungle/rc.d/puma.conf +10 -0
  19. data/docs/kubernetes.md +78 -0
  20. data/docs/nginx.md +80 -0
  21. data/docs/plugins.md +38 -0
  22. data/docs/rails_dev_mode.md +28 -0
  23. data/docs/restart.md +65 -0
  24. data/docs/signals.md +98 -0
  25. data/docs/stats.md +142 -0
  26. data/docs/systemd.md +253 -0
  27. data/docs/testing_benchmarks_local_files.md +150 -0
  28. data/docs/testing_test_rackup_ci_files.md +36 -0
  29. data/ext/puma_http11/PumaHttp11Service.java +17 -0
  30. data/ext/puma_http11/ext_help.h +15 -0
  31. data/ext/puma_http11/extconf.rb +80 -0
  32. data/ext/puma_http11/http11_parser.c +1057 -0
  33. data/ext/puma_http11/http11_parser.h +65 -0
  34. data/ext/puma_http11/http11_parser.java.rl +145 -0
  35. data/ext/puma_http11/http11_parser.rl +149 -0
  36. data/ext/puma_http11/http11_parser_common.rl +54 -0
  37. data/ext/puma_http11/mini_ssl.c +842 -0
  38. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  39. data/ext/puma_http11/org/jruby/puma/Http11.java +228 -0
  40. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +455 -0
  41. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +509 -0
  42. data/ext/puma_http11/puma_http11.c +495 -0
  43. data/lib/puma/app/status.rb +96 -0
  44. data/lib/puma/binder.rb +502 -0
  45. data/lib/puma/cli.rb +247 -0
  46. data/lib/puma/client.rb +682 -0
  47. data/lib/puma/cluster/worker.rb +180 -0
  48. data/lib/puma/cluster/worker_handle.rb +96 -0
  49. data/lib/puma/cluster.rb +616 -0
  50. data/lib/puma/commonlogger.rb +115 -0
  51. data/lib/puma/configuration.rb +390 -0
  52. data/lib/puma/const.rb +307 -0
  53. data/lib/puma/control_cli.rb +316 -0
  54. data/lib/puma/detect.rb +45 -0
  55. data/lib/puma/dsl.rb +1425 -0
  56. data/lib/puma/error_logger.rb +113 -0
  57. data/lib/puma/events.rb +57 -0
  58. data/lib/puma/io_buffer.rb +46 -0
  59. data/lib/puma/jruby_restart.rb +11 -0
  60. data/lib/puma/json_serialization.rb +96 -0
  61. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  62. data/lib/puma/launcher.rb +488 -0
  63. data/lib/puma/log_writer.rb +147 -0
  64. data/lib/puma/minissl/context_builder.rb +96 -0
  65. data/lib/puma/minissl.rb +459 -0
  66. data/lib/puma/null_io.rb +84 -0
  67. data/lib/puma/plugin/systemd.rb +90 -0
  68. data/lib/puma/plugin/tmp_restart.rb +36 -0
  69. data/lib/puma/plugin.rb +111 -0
  70. data/lib/puma/puma_http11.jar +0 -0
  71. data/lib/puma/rack/builder.rb +297 -0
  72. data/lib/puma/rack/urlmap.rb +93 -0
  73. data/lib/puma/rack_default.rb +24 -0
  74. data/lib/puma/reactor.rb +125 -0
  75. data/lib/puma/request.rb +688 -0
  76. data/lib/puma/runner.rb +213 -0
  77. data/lib/puma/sd_notify.rb +149 -0
  78. data/lib/puma/server.rb +680 -0
  79. data/lib/puma/single.rb +69 -0
  80. data/lib/puma/state_file.rb +68 -0
  81. data/lib/puma/thread_pool.rb +434 -0
  82. data/lib/puma/util.rb +141 -0
  83. data/lib/puma.rb +78 -0
  84. data/lib/rack/handler/puma.rb +144 -0
  85. data/tools/Dockerfile +16 -0
  86. data/tools/trickletest.rb +44 -0
  87. metadata +153 -0
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'runner'
4
+ require_relative 'detect'
5
+ require_relative 'plugin'
6
+
7
+ module Puma
8
+ # This class is instantiated by the `Puma::Launcher` and used
9
+ # to boot and serve a Ruby application when no puma "workers" are needed
10
+ # i.e. only using "threaded" mode. For example `$ puma -t 1:5`
11
+ #
12
+ # At the core of this class is running an instance of `Puma::Server` which
13
+ # gets created via the `start_server` method from the `Puma::Runner` class
14
+ # that this inherits from.
15
+ class Single < Runner
16
+ # @!attribute [r] stats
17
+ def stats
18
+ {
19
+ started_at: utc_iso8601(@started_at)
20
+ }.merge(@server.stats).merge(super)
21
+ end
22
+
23
+ def restart
24
+ @server&.begin_restart
25
+ end
26
+
27
+ def stop
28
+ @server&.stop false
29
+ end
30
+
31
+ def halt
32
+ @server&.halt
33
+ end
34
+
35
+ def stop_blocked
36
+ log "- Gracefully stopping, waiting for requests to finish"
37
+ @control&.stop true
38
+ @server&.stop true
39
+ end
40
+
41
+ def run
42
+ output_header "single"
43
+
44
+ load_and_bind
45
+
46
+ Plugins.fire_background
47
+
48
+ @launcher.write_state
49
+
50
+ start_control
51
+
52
+ @server = server = start_server
53
+ server_thread = server.run
54
+
55
+ log "Use Ctrl-C to stop"
56
+ redirect_io
57
+
58
+ @events.fire_on_booted!
59
+
60
+ debug_loaded_extensions("Loaded Extensions:") if @log_writer.debug?
61
+
62
+ begin
63
+ server_thread.join
64
+ rescue Interrupt
65
+ # Swallow it
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Puma
4
+
5
+ # Puma::Launcher uses StateFile to write a yaml file for use with Puma::ControlCLI.
6
+ #
7
+ # In previous versions of Puma, YAML was used to read/write the state file.
8
+ # Since Puma is similar to Bundler/RubyGems in that it may load before one's app
9
+ # does, minimizing the dependencies that may be shared with the app is desired.
10
+ #
11
+ # At present, it only works with numeric and string values. It is still a valid
12
+ # yaml file, and the CI tests parse it with Psych.
13
+ #
14
+ class StateFile
15
+
16
+ ALLOWED_FIELDS = %w!control_url control_auth_token pid running_from!
17
+
18
+ def initialize
19
+ @options = {}
20
+ end
21
+
22
+ def save(path, permission = nil)
23
+ contents = +"---\n"
24
+ @options.each do |k,v|
25
+ next unless ALLOWED_FIELDS.include? k
26
+ case v
27
+ when Numeric
28
+ contents << "#{k}: #{v}\n"
29
+ when String
30
+ next if v.strip.empty?
31
+ contents << (k == 'running_from' || v.to_s.include?(' ') ?
32
+ "#{k}: \"#{v}\"\n" : "#{k}: #{v}\n")
33
+ end
34
+ end
35
+ if permission
36
+ File.write path, contents, mode: 'wb:UTF-8'
37
+ else
38
+ File.write path, contents, mode: 'wb:UTF-8', perm: permission
39
+ end
40
+ end
41
+
42
+ def load(path)
43
+ File.read(path).lines.each do |line|
44
+ next if line.start_with? '#'
45
+ k,v = line.split ':', 2
46
+ next unless v && ALLOWED_FIELDS.include?(k)
47
+ v = v.strip
48
+ @options[k] =
49
+ case v
50
+ when '' then nil
51
+ when /\A\d+\z/ then v.to_i
52
+ when /\A\d+\.\d+\z/ then v.to_f
53
+ else v.gsub(/\A"|"\z/, '')
54
+ end
55
+ end
56
+ end
57
+
58
+ ALLOWED_FIELDS.each do |f|
59
+ define_method f.to_sym do
60
+ @options[f]
61
+ end
62
+
63
+ define_method :"#{f}=" do |v|
64
+ @options[f] = v
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,434 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thread'
4
+
5
+ require_relative 'io_buffer'
6
+
7
+ module Puma
8
+ # Internal Docs for A simple thread pool management object.
9
+ #
10
+ # Each Puma "worker" has a thread pool to process requests.
11
+ #
12
+ # First a connection to a client is made in `Puma::Server`. It is wrapped in a
13
+ # `Puma::Client` instance and then passed to the `Puma::Reactor` to ensure
14
+ # the whole request is buffered into memory. Once the request is ready, it is passed into
15
+ # a thread pool via the `Puma::ThreadPool#<<` operator where it is stored in a `@todo` array.
16
+ #
17
+ # Each thread in the pool has an internal loop where it pulls a request from the `@todo` array
18
+ # and processes it.
19
+ class ThreadPool
20
+ class ForceShutdown < RuntimeError
21
+ end
22
+
23
+ # How long, after raising the ForceShutdown of a thread during
24
+ # forced shutdown mode, to wait for the thread to try and finish
25
+ # up its work before leaving the thread to die on the vine.
26
+ SHUTDOWN_GRACE_TIME = 5 # seconds
27
+
28
+ # Maintain a minimum of +min+ and maximum of +max+ threads
29
+ # in the pool.
30
+ #
31
+ # The block passed is the work that will be performed in each
32
+ # thread.
33
+ #
34
+ def initialize(name, options = {}, &block)
35
+ @not_empty = ConditionVariable.new
36
+ @not_full = ConditionVariable.new
37
+ @mutex = Mutex.new
38
+
39
+ @todo = []
40
+
41
+ @spawned = 0
42
+ @waiting = 0
43
+
44
+ @name = name
45
+ @min = Integer(options[:min_threads])
46
+ @max = Integer(options[:max_threads])
47
+ # Not an 'exposed' option, options[:pool_shutdown_grace_time] is used in CI
48
+ # to shorten @shutdown_grace_time from SHUTDOWN_GRACE_TIME. Parallel CI
49
+ # makes stubbing constants difficult.
50
+ @shutdown_grace_time = Float(options[:pool_shutdown_grace_time] || SHUTDOWN_GRACE_TIME)
51
+ @block = block
52
+ @out_of_band = options[:out_of_band]
53
+ @clean_thread_locals = options[:clean_thread_locals]
54
+ @before_thread_start = options[:before_thread_start]
55
+ @before_thread_exit = options[:before_thread_exit]
56
+ @reaping_time = options[:reaping_time]
57
+ @auto_trim_time = options[:auto_trim_time]
58
+
59
+ @shutdown = false
60
+
61
+ @trim_requested = 0
62
+ @out_of_band_pending = false
63
+
64
+ @workers = []
65
+
66
+ @auto_trim = nil
67
+ @reaper = nil
68
+
69
+ @mutex.synchronize do
70
+ @min.times do
71
+ spawn_thread
72
+ @not_full.wait(@mutex)
73
+ end
74
+ end
75
+
76
+ @force_shutdown = false
77
+ @shutdown_mutex = Mutex.new
78
+ end
79
+
80
+ attr_reader :spawned, :trim_requested, :waiting
81
+
82
+ def self.clean_thread_locals
83
+ Thread.current.keys.each do |key| # rubocop: disable Style/HashEachMethods
84
+ Thread.current[key] = nil unless key == :__recursive_key__
85
+ end
86
+ end
87
+
88
+ # How many objects have yet to be processed by the pool?
89
+ #
90
+ def backlog
91
+ with_mutex { @todo.size }
92
+ end
93
+
94
+ # @!attribute [r] pool_capacity
95
+ def pool_capacity
96
+ waiting + (@max - spawned)
97
+ end
98
+
99
+ # @!attribute [r] busy_threads
100
+ # @version 5.0.0
101
+ def busy_threads
102
+ with_mutex { @spawned - @waiting + @todo.size }
103
+ end
104
+
105
+ # :nodoc:
106
+ #
107
+ # Must be called with @mutex held!
108
+ #
109
+ def spawn_thread
110
+ @spawned += 1
111
+
112
+ trigger_before_thread_start_hooks
113
+ th = Thread.new(@spawned) do |spawned|
114
+ Puma.set_thread_name '%s tp %03i' % [@name, spawned]
115
+ todo = @todo
116
+ block = @block
117
+ mutex = @mutex
118
+ not_empty = @not_empty
119
+ not_full = @not_full
120
+
121
+ while true
122
+ work = nil
123
+
124
+ mutex.synchronize do
125
+ while todo.empty?
126
+ if @trim_requested > 0
127
+ @trim_requested -= 1
128
+ @spawned -= 1
129
+ @workers.delete th
130
+ not_full.signal
131
+ trigger_before_thread_exit_hooks
132
+ Thread.exit
133
+ end
134
+
135
+ @waiting += 1
136
+ if @out_of_band_pending && trigger_out_of_band_hook
137
+ @out_of_band_pending = false
138
+ end
139
+ not_full.signal
140
+ begin
141
+ not_empty.wait mutex
142
+ ensure
143
+ @waiting -= 1
144
+ end
145
+ end
146
+
147
+ work = todo.shift
148
+ end
149
+
150
+ if @clean_thread_locals
151
+ ThreadPool.clean_thread_locals
152
+ end
153
+
154
+ begin
155
+ @out_of_band_pending = true if block.call(work)
156
+ rescue Exception => e
157
+ STDERR.puts "Error reached top of thread-pool: #{e.message} (#{e.class})"
158
+ end
159
+ end
160
+ end
161
+
162
+ @workers << th
163
+
164
+ th
165
+ end
166
+
167
+ private :spawn_thread
168
+
169
+ def trigger_before_thread_start_hooks
170
+ return unless @before_thread_start&.any?
171
+
172
+ @before_thread_start.each do |b|
173
+ begin
174
+ b.call
175
+ rescue Exception => e
176
+ STDERR.puts "WARNING before_thread_start hook failed with exception (#{e.class}) #{e.message}"
177
+ end
178
+ end
179
+ nil
180
+ end
181
+
182
+ private :trigger_before_thread_start_hooks
183
+
184
+ def trigger_before_thread_exit_hooks
185
+ return unless @before_thread_exit&.any?
186
+
187
+ @before_thread_exit.each do |b|
188
+ begin
189
+ b.call
190
+ rescue Exception => e
191
+ STDERR.puts "WARNING before_thread_exit hook failed with exception (#{e.class}) #{e.message}"
192
+ end
193
+ end
194
+ nil
195
+ end
196
+
197
+ private :trigger_before_thread_exit_hooks
198
+
199
+ # @version 5.0.0
200
+ def trigger_out_of_band_hook
201
+ return false unless @out_of_band&.any?
202
+
203
+ # we execute on idle hook when all threads are free
204
+ return false unless @spawned == @waiting
205
+
206
+ @out_of_band.each(&:call)
207
+ true
208
+ rescue Exception => e
209
+ STDERR.puts "Exception calling out_of_band_hook: #{e.message} (#{e.class})"
210
+ true
211
+ end
212
+
213
+ private :trigger_out_of_band_hook
214
+
215
+ # @version 5.0.0
216
+ def with_mutex(&block)
217
+ @mutex.owned? ?
218
+ yield :
219
+ @mutex.synchronize(&block)
220
+ end
221
+
222
+ # Add +work+ to the todo list for a Thread to pickup and process.
223
+ def <<(work)
224
+ with_mutex do
225
+ if @shutdown
226
+ raise "Unable to add work while shutting down"
227
+ end
228
+
229
+ @todo << work
230
+
231
+ if @waiting < @todo.size and @spawned < @max
232
+ spawn_thread
233
+ end
234
+
235
+ @not_empty.signal
236
+ end
237
+ end
238
+
239
+ # This method is used by `Puma::Server` to let the server know when
240
+ # the thread pool can pull more requests from the socket and
241
+ # pass to the reactor.
242
+ #
243
+ # The general idea is that the thread pool can only work on a fixed
244
+ # number of requests at the same time. If it is already processing that
245
+ # number of requests then it is at capacity. If another Puma process has
246
+ # spare capacity, then the request can be left on the socket so the other
247
+ # worker can pick it up and process it.
248
+ #
249
+ # For example: if there are 5 threads, but only 4 working on
250
+ # requests, this method will not wait and the `Puma::Server`
251
+ # can pull a request right away.
252
+ #
253
+ # If there are 5 threads and all 5 of them are busy, then it will
254
+ # pause here, and wait until the `not_full` condition variable is
255
+ # signaled, usually this indicates that a request has been processed.
256
+ #
257
+ # It's important to note that even though the server might accept another
258
+ # request, it might not be added to the `@todo` array right away.
259
+ # For example if a slow client has only sent a header, but not a body
260
+ # then the `@todo` array would stay the same size as the reactor works
261
+ # to try to buffer the request. In that scenario the next call to this
262
+ # method would not block and another request would be added into the reactor
263
+ # by the server. This would continue until a fully buffered request
264
+ # makes it through the reactor and can then be processed by the thread pool.
265
+ def wait_until_not_full
266
+ with_mutex do
267
+ while true
268
+ return if @shutdown
269
+
270
+ # If we can still spin up new threads and there
271
+ # is work queued that cannot be handled by waiting
272
+ # threads, then accept more work until we would
273
+ # spin up the max number of threads.
274
+ return if busy_threads < @max
275
+
276
+ @not_full.wait @mutex
277
+ end
278
+ end
279
+ end
280
+
281
+ # @version 5.0.0
282
+ def wait_for_less_busy_worker(delay_s)
283
+ return unless delay_s && delay_s > 0
284
+
285
+ # Ruby MRI does GVL, this can result
286
+ # in processing contention when multiple threads
287
+ # (requests) are running concurrently
288
+ return unless Puma.mri?
289
+
290
+ with_mutex do
291
+ return if @shutdown
292
+
293
+ # do not delay, if we are not busy
294
+ return unless busy_threads > 0
295
+
296
+ # this will be signaled once a request finishes,
297
+ # which can happen earlier than delay
298
+ @not_full.wait @mutex, delay_s
299
+ end
300
+ end
301
+
302
+ # If there are any free threads in the pool, tell one to go ahead
303
+ # and exit. If +force+ is true, then a trim request is requested
304
+ # even if all threads are being utilized.
305
+ #
306
+ def trim(force=false)
307
+ with_mutex do
308
+ free = @waiting - @todo.size
309
+ if (force or free > 0) and @spawned - @trim_requested > @min
310
+ @trim_requested += 1
311
+ @not_empty.signal
312
+ end
313
+ end
314
+ end
315
+
316
+ # If there are dead threads in the pool make them go away while decreasing
317
+ # spawned counter so that new healthy threads could be created again.
318
+ def reap
319
+ with_mutex do
320
+ dead_workers = @workers.reject(&:alive?)
321
+
322
+ dead_workers.each do |worker|
323
+ worker.kill
324
+ @spawned -= 1
325
+ end
326
+
327
+ @workers.delete_if do |w|
328
+ dead_workers.include?(w)
329
+ end
330
+ end
331
+ end
332
+
333
+ class Automaton
334
+ def initialize(pool, timeout, thread_name, message)
335
+ @pool = pool
336
+ @timeout = timeout
337
+ @thread_name = thread_name
338
+ @message = message
339
+ @running = false
340
+ end
341
+
342
+ def start!
343
+ @running = true
344
+
345
+ @thread = Thread.new do
346
+ Puma.set_thread_name @thread_name
347
+ while @running
348
+ @pool.public_send(@message)
349
+ sleep @timeout
350
+ end
351
+ end
352
+ end
353
+
354
+ def stop
355
+ @running = false
356
+ @thread.wakeup
357
+ end
358
+ end
359
+
360
+ def auto_trim!(timeout=@auto_trim_time)
361
+ @auto_trim = Automaton.new(self, timeout, "#{@name} threadpool trimmer", :trim)
362
+ @auto_trim.start!
363
+ end
364
+
365
+ def auto_reap!(timeout=@reaping_time)
366
+ @reaper = Automaton.new(self, timeout, "#{@name} threadpool reaper", :reap)
367
+ @reaper.start!
368
+ end
369
+
370
+ # Allows ThreadPool::ForceShutdown to be raised within the
371
+ # provided block if the thread is forced to shutdown during execution.
372
+ def with_force_shutdown
373
+ t = Thread.current
374
+ @shutdown_mutex.synchronize do
375
+ raise ForceShutdown if @force_shutdown
376
+ t[:with_force_shutdown] = true
377
+ end
378
+ yield
379
+ ensure
380
+ t[:with_force_shutdown] = false
381
+ end
382
+
383
+ # Tell all threads in the pool to exit and wait for them to finish.
384
+ # Wait +timeout+ seconds then raise +ForceShutdown+ in remaining threads.
385
+ # Next, wait an extra +@shutdown_grace_time+ seconds then force-kill remaining
386
+ # threads. Finally, wait 1 second for remaining threads to exit.
387
+ #
388
+ def shutdown(timeout=-1)
389
+ threads = with_mutex do
390
+ @shutdown = true
391
+ @trim_requested = @spawned
392
+ @not_empty.broadcast
393
+ @not_full.broadcast
394
+
395
+ @auto_trim&.stop
396
+ @reaper&.stop
397
+ # dup workers so that we join them all safely
398
+ @workers.dup
399
+ end
400
+
401
+ if timeout == -1
402
+ # Wait for threads to finish without force shutdown.
403
+ threads.each(&:join)
404
+ else
405
+ join = ->(inner_timeout) do
406
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
407
+ threads.reject! do |t|
408
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
409
+ t.join inner_timeout - elapsed
410
+ end
411
+ end
412
+
413
+ # Wait +timeout+ seconds for threads to finish.
414
+ join.call(timeout)
415
+
416
+ # If threads are still running, raise ForceShutdown and wait to finish.
417
+ @shutdown_mutex.synchronize do
418
+ @force_shutdown = true
419
+ threads.each do |t|
420
+ t.raise ForceShutdown if t[:with_force_shutdown]
421
+ end
422
+ end
423
+ join.call(@shutdown_grace_time)
424
+
425
+ # If threads are _still_ running, forcefully kill them and wait to finish.
426
+ threads.each(&:kill)
427
+ join.call(1)
428
+ end
429
+
430
+ @spawned = 0
431
+ @workers = []
432
+ end
433
+ end
434
+ end
data/lib/puma/util.rb ADDED
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri/common'
4
+
5
+ module Puma
6
+ module Util
7
+ module_function
8
+
9
+ def pipe
10
+ IO.pipe
11
+ end
12
+
13
+ # An instance method on Thread has been provided to address https://bugs.ruby-lang.org/issues/13632,
14
+ # which currently affects some older versions of Ruby: 2.2.7 2.2.8 2.2.9 2.2.10 2.3.4 2.4.1
15
+ # Additional context: https://github.com/puma/puma/pull/1345
16
+ def purge_interrupt_queue
17
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
18
+ end
19
+
20
+ # Escapes and unescapes a URI escaped string with
21
+ # +encoding+. +encoding+ will be the target encoding of the string
22
+ # returned, and it defaults to UTF-8
23
+ if defined?(::Encoding)
24
+ def escape(s, encoding = Encoding::UTF_8)
25
+ URI.encode_www_form_component(s, encoding)
26
+ end
27
+
28
+ def unescape(s, encoding = Encoding::UTF_8)
29
+ URI.decode_www_form_component(s, encoding)
30
+ end
31
+ else
32
+ def escape(s, encoding = nil)
33
+ URI.encode_www_form_component(s, encoding)
34
+ end
35
+
36
+ def unescape(s, encoding = nil)
37
+ URI.decode_www_form_component(s, encoding)
38
+ end
39
+ end
40
+ module_function :unescape, :escape
41
+
42
+ DEFAULT_SEP = /[&;] */n
43
+
44
+ # Stolen from Mongrel, with some small modifications:
45
+ # Parses a query string by breaking it up at the '&'
46
+ # and ';' characters. You can also use this to parse
47
+ # cookies by changing the characters used in the second
48
+ # parameter (which defaults to '&;').
49
+ def parse_query(qs, d = nil, &unescaper)
50
+ unescaper ||= method(:unescape)
51
+
52
+ params = {}
53
+
54
+ (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
55
+ next if p.empty?
56
+ k, v = p.split('=', 2).map(&unescaper)
57
+
58
+ if cur = params[k]
59
+ if cur.class == Array
60
+ params[k] << v
61
+ else
62
+ params[k] = [cur, v]
63
+ end
64
+ else
65
+ params[k] = v
66
+ end
67
+ end
68
+
69
+ params
70
+ end
71
+
72
+ # A case-insensitive Hash that preserves the original case of a
73
+ # header when set.
74
+ class HeaderHash < Hash
75
+ def self.new(hash={})
76
+ HeaderHash === hash ? hash : super(hash)
77
+ end
78
+
79
+ def initialize(hash={})
80
+ super()
81
+ @names = {}
82
+ hash.each { |k, v| self[k] = v }
83
+ end
84
+
85
+ def each
86
+ super do |k, v|
87
+ yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
88
+ end
89
+ end
90
+
91
+ # @!attribute [r] to_hash
92
+ def to_hash
93
+ hash = {}
94
+ each { |k,v| hash[k] = v }
95
+ hash
96
+ end
97
+
98
+ def [](k)
99
+ super(k) || super(@names[k.downcase])
100
+ end
101
+
102
+ def []=(k, v)
103
+ canonical = k.downcase
104
+ delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary
105
+ @names[k] = @names[canonical] = k
106
+ super k, v
107
+ end
108
+
109
+ def delete(k)
110
+ canonical = k.downcase
111
+ result = super @names.delete(canonical)
112
+ @names.delete_if { |name,| name.downcase == canonical }
113
+ result
114
+ end
115
+
116
+ def include?(k)
117
+ @names.include?(k) || @names.include?(k.downcase)
118
+ end
119
+
120
+ alias_method :has_key?, :include?
121
+ alias_method :member?, :include?
122
+ alias_method :key?, :include?
123
+
124
+ def merge!(other)
125
+ other.each { |k, v| self[k] = v }
126
+ self
127
+ end
128
+
129
+ def merge(other)
130
+ hash = dup
131
+ hash.merge! other
132
+ end
133
+
134
+ def replace(other)
135
+ clear
136
+ other.each { |k, v| self[k] = v }
137
+ self
138
+ end
139
+ end
140
+ end
141
+ end