polyphony 0.19 → 0.20

Sign up to get free protection for your applications and to get access to all the features.
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