polyphony 0.19 → 0.20

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 (186) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/.rubocop.yml +87 -1
  4. data/CHANGELOG.md +35 -0
  5. data/Gemfile.lock +17 -6
  6. data/README.md +200 -139
  7. data/Rakefile +4 -4
  8. data/TODO.md +35 -7
  9. data/bin/poly +11 -0
  10. data/docs/getting-started/getting-started.md +1 -1
  11. data/docs/summary.md +3 -0
  12. data/docs/technical-overview/exception-handling.md +94 -0
  13. data/docs/technical-overview/fiber-scheduling.md +99 -0
  14. data/examples/core/cancel.rb +8 -4
  15. data/examples/core/channel_echo.rb +18 -17
  16. data/examples/core/defer.rb +12 -0
  17. data/examples/core/enumerator.rb +4 -4
  18. data/examples/core/fiber_error.rb +9 -0
  19. data/examples/core/fiber_error_with_backtrace.rb +73 -0
  20. data/examples/core/fork.rb +6 -6
  21. data/examples/core/genserver.rb +16 -8
  22. data/examples/core/lock.rb +3 -3
  23. data/examples/core/move_on.rb +4 -3
  24. data/examples/core/move_on_twice.rb +5 -5
  25. data/examples/core/move_on_with_ensure.rb +8 -11
  26. data/examples/core/move_on_with_value.rb +14 -0
  27. data/examples/core/{multiple_spawn.rb → multiple_spin.rb} +5 -5
  28. data/examples/core/nested_cancel.rb +5 -5
  29. data/examples/core/{nested_multiple_spawn.rb → nested_multiple_spin.rb} +6 -6
  30. data/examples/core/nested_spin.rb +17 -0
  31. data/examples/core/pingpong.rb +21 -0
  32. data/examples/core/pulse.rb +4 -5
  33. data/examples/core/resource.rb +6 -4
  34. data/examples/core/resource_cancel.rb +6 -9
  35. data/examples/core/resource_delegate.rb +3 -3
  36. data/examples/core/sleep.rb +3 -3
  37. data/examples/core/sleep_spin.rb +19 -0
  38. data/examples/core/snooze.rb +32 -0
  39. data/examples/core/spin.rb +14 -0
  40. data/examples/core/{spawn_cancel.rb → spin_cancel.rb} +6 -7
  41. data/examples/core/spin_error.rb +17 -0
  42. data/examples/core/spin_error_backtrace.rb +30 -0
  43. data/examples/core/spin_uncaught_error.rb +15 -0
  44. data/examples/core/supervisor.rb +8 -8
  45. data/examples/core/supervisor_with_cancel_scope.rb +7 -7
  46. data/examples/core/supervisor_with_error.rb +8 -8
  47. data/examples/core/supervisor_with_manual_move_on.rb +6 -7
  48. data/examples/core/suspend.rb +13 -0
  49. data/examples/core/thread.rb +1 -1
  50. data/examples/core/thread_cancel.rb +9 -11
  51. data/examples/core/thread_pool.rb +18 -14
  52. data/examples/core/throttle.rb +7 -7
  53. data/examples/core/timeout.rb +3 -3
  54. data/examples/fs/read.rb +7 -9
  55. data/examples/http/config.ru +7 -3
  56. data/examples/http/cuba.ru +22 -0
  57. data/examples/http/happy_eyeballs.rb +6 -4
  58. data/examples/http/http_client.rb +1 -1
  59. data/examples/http/http_get.rb +1 -1
  60. data/examples/http/http_parse_experiment.rb +21 -16
  61. data/examples/http/http_proxy.rb +28 -26
  62. data/examples/http/http_server.rb +10 -10
  63. data/examples/http/http_server_forked.rb +6 -5
  64. data/examples/http/http_server_throttled.rb +3 -3
  65. data/examples/http/http_ws_server.rb +11 -11
  66. data/examples/http/https_raw_client.rb +1 -1
  67. data/examples/http/https_server.rb +8 -8
  68. data/examples/http/https_wss_server.rb +13 -11
  69. data/examples/http/rack_server.rb +2 -2
  70. data/examples/http/rack_server_https.rb +4 -4
  71. data/examples/http/rack_server_https_forked.rb +5 -5
  72. data/examples/http/websocket_secure_server.rb +6 -6
  73. data/examples/http/websocket_server.rb +5 -5
  74. data/examples/interfaces/pg_client.rb +4 -4
  75. data/examples/interfaces/pg_pool.rb +13 -6
  76. data/examples/interfaces/pg_transaction.rb +5 -4
  77. data/examples/interfaces/redis_channels.rb +15 -11
  78. data/examples/interfaces/redis_client.rb +2 -2
  79. data/examples/interfaces/redis_pubsub.rb +2 -1
  80. data/examples/interfaces/redis_pubsub_perf.rb +13 -9
  81. data/examples/io/backticks.rb +11 -0
  82. data/examples/io/cat.rb +4 -5
  83. data/examples/io/echo_client.rb +9 -4
  84. data/examples/io/echo_client_from_stdin.rb +20 -0
  85. data/examples/io/echo_pipe.rb +7 -8
  86. data/examples/io/echo_server.rb +8 -6
  87. data/examples/io/echo_server_with_timeout.rb +13 -10
  88. data/examples/io/echo_stdin.rb +3 -3
  89. data/examples/io/httparty.rb +2 -2
  90. data/examples/io/httparty_multi.rb +8 -4
  91. data/examples/io/httparty_threaded.rb +6 -2
  92. data/examples/io/io_read.rb +2 -2
  93. data/examples/io/irb.rb +16 -4
  94. data/examples/io/net-http.rb +3 -3
  95. data/examples/io/open.rb +17 -0
  96. data/examples/io/system.rb +3 -3
  97. data/examples/io/tcpserver.rb +15 -0
  98. data/examples/io/tcpsocket.rb +6 -5
  99. data/examples/performance/multi_snooze.rb +29 -0
  100. data/examples/performance/{perf_snooze.rb → snooze.rb} +7 -5
  101. data/examples/performance/snooze_raw.rb +39 -0
  102. data/ext/gyro/async.c +165 -0
  103. data/ext/gyro/child.c +167 -0
  104. data/ext/{ev → gyro}/extconf.rb +4 -3
  105. data/ext/gyro/gyro.c +316 -0
  106. data/ext/{ev/ev.h → gyro/gyro.h} +12 -7
  107. data/ext/gyro/gyro_ext.c +23 -0
  108. data/ext/{ev → gyro}/io.c +65 -57
  109. data/ext/{ev → gyro}/libev.h +0 -0
  110. data/ext/gyro/signal.c +117 -0
  111. data/ext/{ev → gyro}/socket.c +61 -6
  112. data/ext/gyro/timer.c +199 -0
  113. data/ext/libev/Changes +35 -0
  114. data/ext/libev/README +2 -1
  115. data/ext/libev/ev.c +213 -151
  116. data/ext/libev/ev.h +95 -88
  117. data/ext/libev/ev_epoll.c +26 -15
  118. data/ext/libev/ev_kqueue.c +11 -5
  119. data/ext/libev/ev_linuxaio.c +642 -0
  120. data/ext/libev/ev_poll.c +13 -8
  121. data/ext/libev/ev_port.c +5 -2
  122. data/ext/libev/ev_vars.h +14 -3
  123. data/ext/libev/ev_wrap.h +16 -0
  124. data/lib/ev_ext.bundle +0 -0
  125. data/lib/polyphony.rb +46 -50
  126. data/lib/polyphony/auto_run.rb +12 -0
  127. data/lib/polyphony/core/cancel_scope.rb +11 -7
  128. data/lib/polyphony/core/channel.rb +16 -9
  129. data/lib/polyphony/core/coprocess.rb +101 -51
  130. data/lib/polyphony/core/exceptions.rb +14 -12
  131. data/lib/polyphony/core/resource_pool.rb +21 -8
  132. data/lib/polyphony/core/supervisor.rb +10 -5
  133. data/lib/polyphony/core/sync.rb +7 -6
  134. data/lib/polyphony/core/thread.rb +4 -4
  135. data/lib/polyphony/core/thread_pool.rb +4 -4
  136. data/lib/polyphony/core/throttler.rb +6 -4
  137. data/lib/polyphony/extensions/core.rb +253 -0
  138. data/lib/polyphony/extensions/io.rb +28 -16
  139. data/lib/polyphony/extensions/openssl.rb +2 -1
  140. data/lib/polyphony/extensions/socket.rb +47 -52
  141. data/lib/polyphony/http.rb +4 -3
  142. data/lib/polyphony/http/agent.rb +68 -57
  143. data/lib/polyphony/http/server.rb +5 -5
  144. data/lib/polyphony/http/server/http1.rb +268 -0
  145. data/lib/polyphony/http/server/http2.rb +62 -0
  146. data/lib/polyphony/http/server/http2_stream.rb +104 -0
  147. data/lib/polyphony/http/server/rack.rb +64 -0
  148. data/lib/polyphony/http/server/request.rb +119 -0
  149. data/lib/polyphony/net.rb +26 -15
  150. data/lib/polyphony/postgres.rb +17 -13
  151. data/lib/polyphony/redis.rb +16 -15
  152. data/lib/polyphony/version.rb +1 -1
  153. data/lib/polyphony/websocket.rb +11 -4
  154. data/polyphony.gemspec +13 -9
  155. data/test/eg.rb +27 -0
  156. data/test/helper.rb +25 -0
  157. data/test/run.rb +5 -0
  158. data/test/test_async.rb +33 -0
  159. data/test/test_coprocess.rb +239 -77
  160. data/test/test_core.rb +95 -61
  161. data/test/test_gyro.rb +148 -0
  162. data/test/test_http_server.rb +313 -0
  163. data/test/test_io.rb +79 -27
  164. data/test/test_kernel.rb +22 -12
  165. data/test/test_signal.rb +36 -0
  166. data/test/test_timer.rb +24 -0
  167. metadata +89 -33
  168. data/examples/core/nested_async.rb +0 -17
  169. data/examples/core/next_tick.rb +0 -12
  170. data/examples/core/sleep_spawn.rb +0 -19
  171. data/examples/core/spawn.rb +0 -14
  172. data/examples/core/spawn_error.rb +0 -28
  173. data/examples/performance/perf_multi_snooze.rb +0 -21
  174. data/ext/ev/async.c +0 -168
  175. data/ext/ev/child.c +0 -169
  176. data/ext/ev/ev_ext.c +0 -23
  177. data/ext/ev/ev_module.c +0 -242
  178. data/ext/ev/signal.c +0 -119
  179. data/ext/ev/timer.c +0 -197
  180. data/lib/polyphony/core/fiber_pool.rb +0 -98
  181. data/lib/polyphony/extensions/kernel.rb +0 -169
  182. data/lib/polyphony/http/http1_adapter.rb +0 -254
  183. data/lib/polyphony/http/http2_adapter.rb +0 -157
  184. data/lib/polyphony/http/rack.rb +0 -25
  185. data/lib/polyphony/http/request.rb +0 -66
  186. data/test/test_ev.rb +0 -110
@@ -1,98 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- export :available,
4
- :checked_out,
5
- :reset!,
6
- :size,
7
- :run
8
-
9
- require 'fiber'
10
-
11
- # Array of available fibers
12
- @pool = []
13
-
14
- # Array of fibers in use
15
- @checked_out = {}
16
-
17
- # Fiber count
18
- @count = 0
19
-
20
- # Returns number of available fibers in pool
21
- # @return [Integer] available fibers count
22
- def available
23
- @pool.size
24
- end
25
-
26
- def checked_out
27
- @checked_out.size
28
- end
29
-
30
- # Returns size of fiber pool (including currently used fiber)
31
- # @return [Integer] fiber pool size
32
- def size
33
- @count
34
- end
35
-
36
- def downsize
37
- return if @count < 5
38
- max_available = @count >= 5 ? @count / 5 : 2
39
- if @pool.count > max_available
40
- @pool.slice!(max_available, 50).each { |f| f.transfer :stop }
41
- end
42
- end
43
-
44
- @downsize_timer = EV::Timer.new(5, 5)
45
- @downsize_timer.start { downsize }
46
- EV.unref
47
-
48
- # Invokes the given block using a fiber taken from the fiber pool. If the pool
49
- # is exhausted, a new fiber will be created.
50
- # @return [Fiber]
51
- def run(&block)
52
- fiber = @pool.empty? ? new_fiber : @pool.shift
53
- fiber.next_job = block
54
- fiber
55
- end
56
-
57
- def reset!
58
- @count = 0
59
- @pool = []
60
- @checked_out = {}
61
- end
62
-
63
- # Creates a new fiber to be added to the pool
64
- # @return [Fiber] new fiber
65
- def new_fiber
66
- Fiber.new { fiber_loop }
67
- end
68
-
69
- # Runs a job-processing loop inside the current fiber
70
- # @return [void]
71
- def fiber_loop
72
- fiber = Fiber.current
73
- @count += 1
74
- error = nil
75
- loop do
76
- job, fiber.next_job = fiber.next_job, nil
77
- @checked_out[fiber] = true
78
- fiber.cancelled = nil
79
-
80
- job&.(fiber)
81
-
82
- @pool << fiber
83
- @checked_out.delete(fiber)
84
- break if suspend == :stop
85
- end
86
- rescue => e
87
- # uncaught error
88
- error = e
89
- ensure
90
- @pool.delete(self)
91
- @checked_out.delete(fiber)
92
- @count -= 1
93
-
94
- # We need to explicitly transfer control to reactor fiber, otherwise it will
95
- # be transferred to the main fiber, which would normally be blocking on
96
- # something
97
- $__reactor_fiber__.transfer unless error
98
- end
@@ -1,169 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'fiber'
4
- require 'timeout'
5
-
6
- CancelScope = import('../core/cancel_scope')
7
- Coprocess = import('../core/coprocess')
8
- Exceptions = import('../core/exceptions')
9
- Supervisor = import('../core/supervisor')
10
- Throttler = import('../core/throttler')
11
-
12
- # Fiber extensions
13
- class ::Fiber
14
- attr_writer :cancelled
15
- attr_accessor :next_job, :coprocess
16
-
17
- def cancelled?
18
- @cancelled
19
- end
20
-
21
- def schedule(value = nil)
22
- EV.schedule_fiber(self, value)
23
- end
24
-
25
- # Associate a (pseudo-)coprocess with the main fiber
26
- current.coprocess = Coprocess.new(current)
27
- end
28
-
29
- class ::Exception
30
- SANITIZE_RE = /lib\/polyphony/.freeze
31
- SANITIZE_PROC = proc { |l| l !~ SANITIZE_RE }
32
-
33
- def cleanup_backtrace(caller = nil)
34
- combined = caller ? backtrace + caller : backtrace
35
- set_backtrace(combined.select(&SANITIZE_PROC))
36
- end
37
- end
38
-
39
- class Pulser
40
- def initialize(freq)
41
- fiber = Fiber.current
42
- @timer = EV::Timer.new(freq, freq)
43
- @timer.start { fiber.transfer freq }
44
- end
45
-
46
- def await
47
- suspend
48
- rescue Exception => e
49
- @timer.stop
50
- raise e
51
- end
52
-
53
- def stop
54
- @timer.stop
55
- end
56
- end
57
-
58
- module ::Process
59
- def self.detach(pid)
60
- spin {
61
- EV::Child.new(pid).await
62
- }
63
- end
64
- end
65
-
66
- require 'open3'
67
-
68
- # Kernel extensions (methods available to all objects)
69
- module ::Kernel
70
- def after(duration, &block)
71
- EV::Timer.new(freq, 0).start(&block)
72
- end
73
-
74
- def async(sym = nil, &block)
75
- if sym
76
- async_decorate(is_a?(Class) ? self : singleton_class, sym)
77
- else
78
- Coprocess.new(&block)
79
- end
80
- end
81
-
82
- # Converts a regular method into an async method, i.e. a method that returns a
83
- # proc that eventually executes the original code.
84
- # @param receiver [Object] object receiving the method call
85
- # @param sym [Symbol] method name
86
- # @return [void]
87
- def async_decorate(receiver, sym)
88
- sync_sym = :"sync_#{sym}"
89
- receiver.alias_method(sync_sym, sym)
90
- receiver.class_eval("def #{sym}(*args, &block); Coprocess.new { send(#{sync_sym.inspect}, *args, &block) }; end")
91
- end
92
-
93
- def cancel_after(duration, &block)
94
- CancelScope.new(timeout: duration, mode: :cancel).(&block)
95
- end
96
-
97
- def spin(proc = nil, &block)
98
- if proc.is_a?(Coprocess)
99
- proc.run
100
- else
101
- Coprocess.new(&(block || proc)).run
102
- end
103
- end
104
-
105
- def every(freq, &block)
106
- EV::Timer.new(freq, freq).start(&block)
107
- end
108
-
109
- def move_on_after(duration, &block)
110
- CancelScope.new(timeout: duration).(&block)
111
- end
112
-
113
- def pulse(freq)
114
- Pulser.new(freq)
115
- end
116
-
117
- def receive
118
- Fiber.current.coprocess.receive
119
- end
120
-
121
- alias_method :sync_sleep, :sleep
122
- def sleep(duration)
123
- timer = EV::Timer.new(duration, 0)
124
- timer.await
125
- ensure
126
- timer.stop
127
- end
128
-
129
- def supervise(&block)
130
- Supervisor.new.await(&block)
131
- end
132
-
133
- alias_method :orig_system, :system
134
- def system(*args)
135
- Open3.popen2(*args) do |i, o, t|
136
- i.close
137
- o.read
138
- end
139
- rescue SystemCallError => e
140
- nil
141
- end
142
-
143
- def throttled_loop(rate, count: nil, &block)
144
- throttler = Throttler.new(rate)
145
- if count
146
- count.times { throttler.(&block) }
147
- else
148
- loop { throttler.(&block) }
149
- end
150
- end
151
-
152
- def throttle(rate)
153
- Throttler.new(rate)
154
- end
155
-
156
- def timeout(duration, opts = {}, &block)
157
- CancelScope.new(**opts, timeout: duration).(&block)
158
- end
159
- end
160
-
161
- module ::Timeout
162
- def self.timeout(sec, klass = nil, message = nil, &block)
163
- cancel_after(sec, &block)
164
- rescue Exceptions::Cancel => e
165
- error = klass ? klass.new(message) : ::Timeout::Error.new
166
- error.set_backtrace(e.backtrace)
167
- raise error
168
- end
169
- end
@@ -1,254 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- export_default :HTTP1Adapter
4
-
5
- require 'http/parser'
6
-
7
- Request = import('./request')
8
- HTTP2 = import('./http2_adapter')
9
-
10
- # HTTP1 protocol implementation
11
- class HTTP1Adapter
12
- # Initializes a protocol adapter instance
13
- def initialize(conn, opts)
14
- @conn = conn
15
- @opts = opts
16
- @parser = HTTP::Parser.new(self)
17
- @parse_fiber = Fiber.new { parse_loop }
18
- end
19
-
20
- def protocol
21
- 'http/1.1'
22
- end
23
-
24
- # Parses incoming data, potentially firing parser callbacks. This loop runs on
25
- # a separate fiber and is resumed only when the handler (client) loop asks for
26
- # headers, or the request body, or waits for the request to be completed. The
27
- # control flow is as follows (arrows represent control transfer between
28
- # fibers):
29
- #
30
- # handler parse_loop
31
- # get_headers --> ...
32
- # @parser << @conn.readpartial(8192)
33
- # ... <-- on_headers
34
- #
35
- # get_body --> ...
36
- # ... <-- on_body
37
- #
38
- # consume_request --> ...
39
- # @parser << @conn.readpartial(8192)
40
- # ... <-- on_message_complete
41
- #
42
- def parse_loop
43
- while (data = @conn.readpartial(8192))
44
- break unless data
45
- @parser << data
46
- snooze
47
- end
48
- @calling_fiber.transfer nil
49
- rescue SystemCallError, IOError => error
50
- # ignore IO/system call errors
51
- @calling_fiber.transfer nil
52
- rescue Exception => error
53
- # an error return value will be raised by the receiving fiber
54
- @calling_fiber.transfer error
55
- end
56
-
57
- # request API
58
-
59
- # Iterates over incoming requests. Requests are yielded once all headers have
60
- # been received. It is left to the application to read the request body or
61
- # diesregard it.
62
- def each(&block)
63
- can_upgrade = true
64
- while @parse_fiber.alive? && (headers = get_headers)
65
- if can_upgrade
66
- # The connection can be upgraded only on the first request
67
- return if upgrade_connection(headers, &block)
68
- can_upgrade = false
69
- end
70
-
71
- @headers_sent = nil
72
- block.(Request.new(headers, self))
73
-
74
- if @parser.keep_alive?
75
- @parsing = false
76
- else
77
- break
78
- end
79
- end
80
- ensure
81
- @conn.close rescue nil
82
- end
83
-
84
- # Reads headers for the next request. Transfers control to the parse loop,
85
- # and resumes once the parse_loop has fired the on_headers callback
86
- def get_headers
87
- @parsing = true
88
- @calling_fiber = Fiber.current
89
- @parse_fiber.safe_transfer
90
- end
91
-
92
- # Reads a body chunk for the current request. Transfers control to the parse
93
- # loop, and resumes once the parse_loop has fired the on_body callback
94
- def get_body_chunk
95
- @calling_fiber = Fiber.current
96
- @read_body = true
97
- @parse_fiber.safe_transfer
98
- end
99
-
100
- # Waits for the current request to complete. Transfers control to the parse
101
- # loop, and resumes once the parse_loop has fired the on_message_complete
102
- # callback
103
- def consume_request
104
- return unless @parsing
105
-
106
- @calling_fiber = Fiber.current
107
- @read_body = false
108
- @parse_fiber.safe_transfer while @parsing
109
- end
110
-
111
- # Upgrades the connection to a different protocol, if the 'Upgrade' header is
112
- # given. By default the only supported upgrade protocol is HTTP2. Additional
113
- # protocols, notably WebSocket, can be specified by passing a hash to the
114
- # :upgrade option when starting a server:
115
- #
116
- # opts = {
117
- # upgrade: {
118
- # websocket: Polyphony::Websocket.handler(&method(:ws_handler))
119
- # }
120
- # }
121
- # Polyphony::HTTP::Server.serve('0.0.0.0', 1234, opts) { |req| ... }
122
- #
123
- # @param headers [Hash] request headers
124
- # @return [boolean] truthy if the connection has been upgraded
125
- def upgrade_connection(headers, &block)
126
- upgrade_protocol = headers['Upgrade']
127
- return nil unless upgrade_protocol
128
-
129
- if @opts[:upgrade] && @opts[:upgrade][upgrade_protocol.to_sym]
130
- @opts[:upgrade][upgrade_protocol.to_sym].(@conn, headers)
131
- return true
132
- end
133
-
134
- return nil unless upgrade_protocol == 'h2c'
135
-
136
- # upgrade to HTTP/2
137
- HTTP2.upgrade_each(@conn, @opts, http2_upgraded_headers(headers), &block)
138
- true
139
- end
140
-
141
- # Returns headers for HTTP2 upgrade
142
- # @param headers [Hash] request headers
143
- # @return [Hash] headers for HTTP2 upgrade
144
- def http2_upgraded_headers(headers)
145
- headers.merge(
146
- ':scheme' => 'http',
147
- ':authority' => headers['Host'],
148
- )
149
- end
150
-
151
- # HTTP parser callbacks, called in the context of @parse_fiber
152
-
153
- # Resumes client fiber on receipt of all headers
154
- # @param headers [Hash] request headers
155
- # @return [void]
156
- def on_headers_complete(headers)
157
- headers[':path'] = @parser.request_url
158
- headers[':method'] = @parser.http_method
159
- @calling_fiber.transfer(headers)
160
- end
161
-
162
- # Resumes client fiber on receipt of body chunk
163
- # @param chunk [String] body chunk
164
- # @return [void]
165
- def on_body(chunk)
166
- @calling_fiber.transfer(chunk) if @read_body
167
- end
168
-
169
- # Resumes client fiber on request completion
170
- # @return [void]
171
- def on_message_complete
172
- @parsing = false
173
- @calling_fiber.transfer nil
174
- end
175
-
176
- # response API
177
-
178
- # Sends response including headers and body. Waits for the request to complete
179
- # if not yet completed. The body is sent using chunked transfer encoding.
180
- # @param chunk [String] response body
181
- # @param headers
182
- def respond(chunk, headers)
183
- consume_request if @parsing
184
- data = format_headers(headers, empty_response: !chunk)
185
- if chunk
186
- data << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n0\r\n\r\n"
187
- end
188
- @conn << data
189
- @headers_sent = true
190
- end
191
-
192
- DEFAULT_HEADERS_OPTS = {
193
- empty_response: false,
194
- consume_request: true
195
- }
196
-
197
- # Sends response headers. Waits for the request to complete if not yet
198
- # completed. If empty_response is true(thy), the response status code will
199
- # default to 204, otherwise to 200.
200
- # @param headers [Hash] response headers
201
- # @param empty_response [boolean] whether a response body will be sent
202
- # @return [void]
203
- def send_headers(headers, opts = DEFAULT_HEADERS_OPTS)
204
- return if @headers_sent
205
-
206
- consume_request if @parsing && opts[:consume_request]
207
- @conn << format_headers(headers, opts[:empty_response])
208
- @headers_sent = true
209
- end
210
-
211
- # Sends a response body chunk. If no headers were sent, default headers are
212
- # sent using #send_headers. if the done option is true(thy), an empty chunk
213
- # will be sent to signal response completion to the client.
214
- # @param chunk [String] response body chunk
215
- # @param done [boolean] whether the response is completed
216
- # @return [void]
217
- def send_body_chunk(chunk, done: false)
218
- send_headers({}) unless @headers_sent
219
-
220
- data = +"#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
221
- data << "0\r\n\r\n" if done
222
- @conn << data
223
- end
224
-
225
- # Finishes the response to the current request. If no headers were sent,
226
- # default headers are sent using #send_headers.
227
- # @return [void]
228
- def finish
229
- send_headers({}, true) unless @headers_sent
230
-
231
- @conn << "0\r\n\r\n" if @body_sent
232
- end
233
-
234
- private
235
-
236
- # Formats response headers. If empty_response is true(thy), the response
237
- # status code will default to 204, otherwise to 200.
238
- # @param headers [Hash] response headers
239
- # @param empty_response [boolean] whether a response body will be sent
240
- # @return [String] formatted response headers
241
- def format_headers(headers, empty_response)
242
- status = headers[':status'] || (empty_response ? 204 : 200)
243
- data = empty_response ?
244
- +"HTTP/1.1 #{status}\r\n" :
245
- +"HTTP/1.1 #{status}\r\nTransfer-Encoding: chunked\r\n"
246
-
247
- headers.each do |k, v|
248
- next if k =~ /^:/
249
- v.is_a?(Array) ?
250
- v.each { |o| data << "#{k}: #{o}\r\n" } : data << "#{k}: #{v}\r\n"
251
- end
252
- data << "\r\n"
253
- end
254
- end