polyphony 0.21 → 0.22
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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +11 -0
- data/Gemfile.lock +5 -5
- data/TODO.md +83 -20
- data/docs/technical-overview/design-principles.md +3 -3
- data/docs/technical-overview/faq.md +11 -0
- data/docs/technical-overview/fiber-scheduling.md +46 -33
- data/examples/core/sleep_spin.rb +2 -2
- data/examples/http/http2_raw.rb +135 -0
- data/examples/http/http_client.rb +14 -3
- data/examples/http/http_get.rb +28 -2
- data/examples/http/http_server.rb +3 -1
- data/examples/http/http_server_forked.rb +3 -1
- data/examples/interfaces/pg_pool.rb +1 -0
- data/examples/io/echo_server.rb +1 -0
- data/ext/gyro/async.c +7 -9
- data/ext/gyro/child.c +5 -8
- data/ext/gyro/extconf.rb +2 -0
- data/ext/gyro/gyro.c +159 -204
- data/ext/gyro/gyro.h +16 -6
- data/ext/gyro/io.c +7 -10
- data/ext/gyro/signal.c +3 -0
- data/ext/gyro/timer.c +9 -18
- data/lib/polyphony/auto_run.rb +12 -5
- data/lib/polyphony/core/coprocess.rb +1 -1
- data/lib/polyphony/core/resource_pool.rb +49 -15
- data/lib/polyphony/core/supervisor.rb +3 -2
- data/lib/polyphony/extensions/core.rb +16 -3
- data/lib/polyphony/extensions/io.rb +2 -0
- data/lib/polyphony/extensions/openssl.rb +60 -0
- data/lib/polyphony/extensions/socket.rb +0 -4
- data/lib/polyphony/http/client/agent.rb +127 -0
- data/lib/polyphony/http/client/http1.rb +129 -0
- data/lib/polyphony/http/client/http2.rb +180 -0
- data/lib/polyphony/http/client/response.rb +32 -0
- data/lib/polyphony/http/client/site_connection_manager.rb +109 -0
- data/lib/polyphony/http/server/request.rb +0 -1
- data/lib/polyphony/http.rb +1 -1
- data/lib/polyphony/net.rb +2 -1
- data/lib/polyphony/version.rb +1 -1
- data/lib/polyphony.rb +4 -4
- data/polyphony.gemspec +1 -0
- data/test/test_gyro.rb +42 -10
- data/test/test_resource_pool.rb +107 -0
- metadata +10 -4
- data/lib/polyphony/http/agent.rb +0 -250
data/ext/gyro/timer.c
CHANGED
@@ -95,29 +95,23 @@ static VALUE Gyro_Timer_initialize(VALUE self, VALUE after, VALUE repeat) {
|
|
95
95
|
}
|
96
96
|
|
97
97
|
void Gyro_Timer_callback(struct ev_loop *ev_loop, struct ev_timer *ev_timer, int revents) {
|
98
|
-
VALUE fiber;
|
99
|
-
VALUE resume_value;
|
100
98
|
struct Gyro_Timer *timer = (struct Gyro_Timer*)ev_timer;
|
101
99
|
|
102
|
-
// if (!timer->active) {
|
103
|
-
// return;
|
104
|
-
// }
|
105
|
-
|
106
100
|
if (!timer->repeat) {
|
107
101
|
timer->active = 0;
|
108
|
-
Gyro_del_watcher_ref(timer->self);
|
109
102
|
}
|
110
103
|
|
111
104
|
if (timer->fiber != Qnil) {
|
105
|
+
VALUE fiber = timer->fiber;
|
106
|
+
VALUE resume_value = DBL2NUM(timer->after);
|
107
|
+
|
112
108
|
ev_timer_stop(EV_DEFAULT, ev_timer);
|
113
|
-
Gyro_del_watcher_ref(timer->self);
|
114
109
|
timer->active = 0;
|
115
|
-
fiber = timer->fiber;
|
116
110
|
timer->fiber = Qnil;
|
117
|
-
resume_value
|
118
|
-
SCHEDULE_FIBER(fiber, 1, resume_value);
|
111
|
+
Gyro_schedule_fiber(fiber, resume_value);
|
119
112
|
}
|
120
113
|
else if (timer->callback != Qnil) {
|
114
|
+
Gyro_ref_count_decr();
|
121
115
|
rb_funcall(timer->callback, ID_call, 1, Qtrue);
|
122
116
|
}
|
123
117
|
}
|
@@ -127,13 +121,13 @@ static VALUE Gyro_Timer_start(VALUE self) {
|
|
127
121
|
GetGyro_Timer(self, timer);
|
128
122
|
|
129
123
|
if (rb_block_given_p()) {
|
124
|
+
Gyro_ref_count_incr();
|
130
125
|
timer->callback = rb_block_proc();
|
131
126
|
}
|
132
127
|
|
133
128
|
if (!timer->active) {
|
134
129
|
ev_timer_start(EV_DEFAULT, &timer->ev_timer);
|
135
130
|
timer->active = 1;
|
136
|
-
Gyro_add_watcher_ref(self);
|
137
131
|
}
|
138
132
|
|
139
133
|
return self;
|
@@ -146,7 +140,6 @@ static VALUE Gyro_Timer_stop(VALUE self) {
|
|
146
140
|
if (timer->active) {
|
147
141
|
ev_timer_stop(EV_DEFAULT, &timer->ev_timer);
|
148
142
|
timer->active = 0;
|
149
|
-
Gyro_del_watcher_ref(self);
|
150
143
|
}
|
151
144
|
|
152
145
|
return self;
|
@@ -166,7 +159,6 @@ static VALUE Gyro_Timer_reset(VALUE self) {
|
|
166
159
|
ev_timer_start(EV_DEFAULT, &timer->ev_timer);
|
167
160
|
if (!prev_active) {
|
168
161
|
timer->active = 1;
|
169
|
-
Gyro_add_watcher_ref(self);
|
170
162
|
}
|
171
163
|
|
172
164
|
return self;
|
@@ -181,11 +173,11 @@ static VALUE Gyro_Timer_await(VALUE self) {
|
|
181
173
|
timer->fiber = rb_fiber_current();
|
182
174
|
timer->active = 1;
|
183
175
|
ev_timer_start(EV_DEFAULT, &timer->ev_timer);
|
184
|
-
Gyro_add_watcher_ref(self);
|
185
176
|
|
186
|
-
ret =
|
177
|
+
ret = Gyro_yield();
|
187
178
|
|
188
179
|
// fiber is resumed, check if resumed value is an exception
|
180
|
+
timer->fiber = Qnil;
|
189
181
|
if (RTEST(rb_obj_is_kind_of(ret, rb_eException))) {
|
190
182
|
if (timer->active) {
|
191
183
|
timer->active = 0;
|
@@ -193,7 +185,6 @@ static VALUE Gyro_Timer_await(VALUE self) {
|
|
193
185
|
}
|
194
186
|
return rb_funcall(ret, ID_raise, 1, ret);
|
195
187
|
}
|
196
|
-
else
|
188
|
+
else
|
197
189
|
return ret;
|
198
|
-
}
|
199
190
|
}
|
data/lib/polyphony/auto_run.rb
CHANGED
@@ -3,10 +3,17 @@
|
|
3
3
|
require_relative '../polyphony'
|
4
4
|
|
5
5
|
at_exit do
|
6
|
-
|
6
|
+
unless Gyro.break?
|
7
|
+
repl = (Pry.current rescue nil) || (IRB.CurrentContext rescue nil)
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
# in most cases, once the root fiber is done there are still pending
|
10
|
+
# operations going on. If the reactor loop is not done, we suspend the root
|
11
|
+
# fiber until it is done
|
12
|
+
begin
|
13
|
+
suspend unless repl
|
14
|
+
rescue Exception => e
|
15
|
+
p e
|
16
|
+
puts e.backtrace.join("\n")
|
17
|
+
end
|
18
|
+
end
|
12
19
|
end
|
@@ -4,52 +4,68 @@ export_default :ResourcePool
|
|
4
4
|
|
5
5
|
# Implements a limited resource pool
|
6
6
|
class ResourcePool
|
7
|
+
attr_reader :limit, :size
|
8
|
+
|
7
9
|
# Initializes a new resource pool
|
8
10
|
# @param opts [Hash] options
|
9
11
|
# @param &block [Proc] allocator block
|
10
12
|
def initialize(opts, &block)
|
11
13
|
@allocator = block
|
12
14
|
|
13
|
-
@
|
14
|
-
@
|
15
|
+
@stock = []
|
16
|
+
@queue = []
|
15
17
|
|
16
18
|
@limit = opts[:limit] || 4
|
17
|
-
@
|
19
|
+
@size = 0
|
20
|
+
end
|
21
|
+
|
22
|
+
def available
|
23
|
+
@stock.size
|
18
24
|
end
|
19
25
|
|
20
26
|
def acquire
|
27
|
+
Gyro.ref
|
21
28
|
resource = wait_for_resource
|
22
29
|
return unless resource
|
23
30
|
|
24
31
|
yield resource
|
25
32
|
ensure
|
26
|
-
|
33
|
+
Gyro.unref
|
34
|
+
release(resource) if resource
|
27
35
|
end
|
28
36
|
|
29
37
|
def wait_for_resource
|
30
38
|
fiber = Fiber.current
|
31
|
-
@
|
39
|
+
@queue << fiber
|
32
40
|
ready_resource = from_stock
|
33
41
|
return ready_resource if ready_resource
|
34
42
|
|
35
43
|
suspend
|
36
44
|
ensure
|
37
|
-
@
|
45
|
+
@queue.delete(fiber)
|
38
46
|
end
|
39
47
|
|
40
|
-
def
|
41
|
-
|
48
|
+
def release(resource)
|
49
|
+
if resource.__discarded__
|
50
|
+
@size -= 1
|
51
|
+
elsif resource
|
52
|
+
return_to_stock(resource)
|
53
|
+
dequeue
|
54
|
+
end
|
55
|
+
end
|
42
56
|
|
43
|
-
|
44
|
-
|
57
|
+
def dequeue
|
58
|
+
return if @queue.empty? || @stock.empty?
|
59
|
+
|
60
|
+
@queue.shift.schedule(@stock.shift)
|
45
61
|
end
|
46
62
|
|
47
63
|
def return_to_stock(resource)
|
48
|
-
@
|
64
|
+
@stock << resource
|
49
65
|
end
|
50
66
|
|
51
67
|
def from_stock
|
52
|
-
@
|
68
|
+
@stock.shift || (@size < @limit && allocate)
|
53
69
|
end
|
54
70
|
|
55
71
|
def method_missing(sym, *args, &block)
|
@@ -60,14 +76,32 @@ class ResourcePool
|
|
60
76
|
true
|
61
77
|
end
|
62
78
|
|
79
|
+
# Extension to allow discarding of resources
|
80
|
+
module ResourceExtensions
|
81
|
+
def __discarded__
|
82
|
+
@__discarded__
|
83
|
+
end
|
84
|
+
|
85
|
+
def __discard__
|
86
|
+
@__discarded__ = true
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
63
90
|
# Allocates a resource
|
64
91
|
# @return [any] allocated resource
|
65
92
|
def allocate
|
66
|
-
@
|
67
|
-
@allocator.()
|
93
|
+
@size += 1
|
94
|
+
@allocator.().tap { |r| r.extend ResourceExtensions }
|
95
|
+
end
|
96
|
+
|
97
|
+
def <<(resource)
|
98
|
+
@size += 1
|
99
|
+
resource.extend ResourceExtensions
|
100
|
+
@stock << resource
|
101
|
+
dequeue
|
68
102
|
end
|
69
103
|
|
70
104
|
def preheat!
|
71
|
-
(@limit - @
|
105
|
+
(@limit - @size).times { @stock << from_stock }
|
72
106
|
end
|
73
107
|
end
|
@@ -16,7 +16,7 @@ class Supervisor
|
|
16
16
|
@supervisor_fiber = Fiber.current
|
17
17
|
block&.(self)
|
18
18
|
suspend
|
19
|
-
@coprocesses.map
|
19
|
+
@coprocesses.map(&:result)
|
20
20
|
rescue Exceptions::MoveOn => e
|
21
21
|
e.value
|
22
22
|
ensure
|
@@ -75,10 +75,11 @@ class Supervisor
|
|
75
75
|
end
|
76
76
|
end
|
77
77
|
|
78
|
+
# Extension for Coprocess class
|
78
79
|
class Coprocess
|
79
80
|
def self.await(*coprocs)
|
80
81
|
supervise do |s|
|
81
82
|
coprocs.each { |cp| s.add cp }
|
82
83
|
end
|
83
84
|
end
|
84
|
-
end
|
85
|
+
end
|
@@ -14,7 +14,7 @@ class ::Fiber
|
|
14
14
|
attr_accessor :__calling_fiber__
|
15
15
|
attr_accessor :__caller__
|
16
16
|
attr_writer :cancelled
|
17
|
-
attr_accessor :coprocess
|
17
|
+
attr_accessor :coprocess
|
18
18
|
|
19
19
|
class << self
|
20
20
|
alias_method :orig_new, :new
|
@@ -23,8 +23,12 @@ class ::Fiber
|
|
23
23
|
fiber_caller = caller
|
24
24
|
fiber = orig_new do |v|
|
25
25
|
block.call(v)
|
26
|
+
rescue Exception => e
|
27
|
+
puts "Uncaught exception #{calling_fiber.alive?}"
|
28
|
+
calling_fiber.transfer e if calling_fiber.alive?
|
26
29
|
ensure
|
27
|
-
|
30
|
+
fiber.mark_as_done!
|
31
|
+
suspend
|
28
32
|
end
|
29
33
|
fiber.__calling_fiber__ = calling_fiber
|
30
34
|
fiber.__caller__ = fiber_caller
|
@@ -38,7 +42,7 @@ class ::Fiber
|
|
38
42
|
def set_root_fiber
|
39
43
|
@root_fiber = current
|
40
44
|
end
|
41
|
-
|
45
|
+
end
|
42
46
|
|
43
47
|
def caller
|
44
48
|
@__caller__ ||= []
|
@@ -134,6 +138,10 @@ module ::Kernel
|
|
134
138
|
timer.stop
|
135
139
|
end
|
136
140
|
|
141
|
+
def defer(&block)
|
142
|
+
Fiber.new(&block).schedule
|
143
|
+
end
|
144
|
+
|
137
145
|
def spin(&block)
|
138
146
|
Coprocess.new(&block).run
|
139
147
|
end
|
@@ -251,3 +259,8 @@ module ::Timeout
|
|
251
259
|
raise error
|
252
260
|
end
|
253
261
|
end
|
262
|
+
|
263
|
+
trap('SIGINT') do
|
264
|
+
Gyro.break!
|
265
|
+
exit
|
266
|
+
end
|
@@ -157,11 +157,13 @@ class ::IO
|
|
157
157
|
# def readlines(sep = $/, limit = nil, chomp: nil)
|
158
158
|
# end
|
159
159
|
|
160
|
+
alias_method :orig_write_nonblock, :write_nonblock
|
160
161
|
def write_nonblock(string, _options = {})
|
161
162
|
# STDOUT << '>'
|
162
163
|
write(string, 0)
|
163
164
|
end
|
164
165
|
|
166
|
+
alias_method :orig_read_nonblock, :read_nonblock
|
165
167
|
def read_nonblock(maxlen, buf = nil, _options = nil)
|
166
168
|
# STDOUT << '<'
|
167
169
|
buf ? readpartial(maxlen, buf) : readpartial(maxlen)
|
@@ -17,4 +17,64 @@ class ::OpenSSL::SSL::SSLSocket
|
|
17
17
|
def reuse_addr
|
18
18
|
io.reuse_addr
|
19
19
|
end
|
20
|
+
|
21
|
+
def sysread(maxlen, buf)
|
22
|
+
loop do
|
23
|
+
read_watcher = nil
|
24
|
+
write_watcher = nil
|
25
|
+
result = read_nonblock(maxlen, buf, exception: false)
|
26
|
+
if result == :wait_readable
|
27
|
+
read_watcher ||= Gyro::IO.new(io, :r)
|
28
|
+
read_watcher.await
|
29
|
+
elsif result == :wait_writable
|
30
|
+
write_watcher ||= Gyro::IO.new(io, :w)
|
31
|
+
write_watcher.await
|
32
|
+
else
|
33
|
+
return result
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def flush
|
39
|
+
# osync = @sync
|
40
|
+
# @sync = true
|
41
|
+
# do_write ""
|
42
|
+
# return self
|
43
|
+
# ensure
|
44
|
+
# @sync = osync
|
45
|
+
end
|
46
|
+
|
47
|
+
# def do_write(s)
|
48
|
+
# @wbuffer = "" unless defined? @wbuffer
|
49
|
+
# @wbuffer << s
|
50
|
+
# @wbuffer.force_encoding(Encoding::BINARY)
|
51
|
+
# @sync ||= false
|
52
|
+
# if @sync or @wbuffer.size > BLOCK_SIZE
|
53
|
+
# until @wbuffer.empty?
|
54
|
+
# begin
|
55
|
+
# nwrote = syswrite(@wbuffer)
|
56
|
+
# rescue Errno::EAGAIN
|
57
|
+
# retry
|
58
|
+
# end
|
59
|
+
# @wbuffer[0, nwrote] = ""
|
60
|
+
# end
|
61
|
+
# end
|
62
|
+
# end
|
63
|
+
|
64
|
+
def syswrite(buf)
|
65
|
+
loop do
|
66
|
+
read_watcher = nil
|
67
|
+
write_watcher = nil
|
68
|
+
result = write_nonblock(buf, exception: false)
|
69
|
+
if result == :wait_readable
|
70
|
+
read_watcher ||= Gyro::IO.new(io, :r)
|
71
|
+
read_watcher.await
|
72
|
+
elsif result == :wait_writable
|
73
|
+
write_watcher ||= Gyro::IO.new(io, :w)
|
74
|
+
write_watcher.await
|
75
|
+
else
|
76
|
+
return result
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
20
80
|
end
|
@@ -15,8 +15,6 @@ class ::Socket
|
|
15
15
|
|
16
16
|
result == :wait_writable ? write_watcher.await : (raise IOError)
|
17
17
|
end
|
18
|
-
ensure
|
19
|
-
@write_watcher&.stop
|
20
18
|
end
|
21
19
|
|
22
20
|
def recv(maxlen, flags = 0, outbuf = nil)
|
@@ -37,8 +35,6 @@ class ::Socket
|
|
37
35
|
|
38
36
|
result == :wait_readable ? read_watcher.await : (return result)
|
39
37
|
end
|
40
|
-
ensure
|
41
|
-
@read_watcher&.stop
|
42
38
|
end
|
43
39
|
|
44
40
|
ZERO_LINGER = [0, 0].pack('ii').freeze
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export_default :Agent
|
4
|
+
|
5
|
+
require 'uri'
|
6
|
+
|
7
|
+
ResourcePool = import '../../core/resource_pool'
|
8
|
+
SiteConnectionManager = import './site_connection_manager'
|
9
|
+
|
10
|
+
# Implements an HTTP agent
|
11
|
+
class Agent
|
12
|
+
def self.get(*args, &block)
|
13
|
+
default.get(*args, &block)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.post(*args, &block)
|
17
|
+
default.post(*args, &block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.default
|
21
|
+
@default ||= new
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize
|
25
|
+
@pools = Hash.new do |h, k|
|
26
|
+
h[k] = SiteConnectionManager.new(k)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
OPTS_DEFAULT = {}.freeze
|
31
|
+
|
32
|
+
def get(url, opts = OPTS_DEFAULT, &block)
|
33
|
+
request(url, opts.merge(method: :GET), &block)
|
34
|
+
end
|
35
|
+
|
36
|
+
def post(url, opts = OPTS_DEFAULT, &block)
|
37
|
+
request(url, opts.merge(method: :POST), &block)
|
38
|
+
end
|
39
|
+
|
40
|
+
def request(url, opts = OPTS_DEFAULT, &block)
|
41
|
+
ctx = request_ctx(url, opts)
|
42
|
+
|
43
|
+
response = do_request(ctx, &block)
|
44
|
+
case response.status_code
|
45
|
+
when 301, 302
|
46
|
+
redirect(response.headers['Location'], ctx, opts, &block)
|
47
|
+
when 200, 204
|
48
|
+
response
|
49
|
+
else
|
50
|
+
raise "Error received from server: #{response.status_code}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def redirect(url, ctx, opts, &block)
|
55
|
+
url = redirect_url(url, ctx)
|
56
|
+
request(url, opts, &block)
|
57
|
+
end
|
58
|
+
|
59
|
+
def redirect_url(url, ctx)
|
60
|
+
case url
|
61
|
+
when /^http(?:s)?\:\/\//
|
62
|
+
url
|
63
|
+
when /^\/\/(.+)$/
|
64
|
+
ctx[:uri].scheme + url
|
65
|
+
when /^\//
|
66
|
+
format_uri(url, ctx)
|
67
|
+
else
|
68
|
+
ctx[:uri] + url
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def format_uri(url, ctx)
|
73
|
+
format(
|
74
|
+
'%<scheme>s://%<host>s%<url>s',
|
75
|
+
scheme: ctx[:uri].scheme,
|
76
|
+
host: ctx[:uri].host,
|
77
|
+
url: url
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
def request_ctx(url, opts)
|
82
|
+
{
|
83
|
+
method: opts[:method] || :GET,
|
84
|
+
uri: url_to_uri(url, opts),
|
85
|
+
opts: opts,
|
86
|
+
retry: 0
|
87
|
+
}
|
88
|
+
end
|
89
|
+
|
90
|
+
def url_to_uri(url, opts)
|
91
|
+
uri = URI(url)
|
92
|
+
if opts[:query]
|
93
|
+
query = opts[:query].map { |k, v| "#{k}=#{v}" }.join('&')
|
94
|
+
if uri.query
|
95
|
+
v.query = "#{uri.query}&#{query}"
|
96
|
+
else
|
97
|
+
uri.query = query
|
98
|
+
end
|
99
|
+
end
|
100
|
+
uri
|
101
|
+
end
|
102
|
+
|
103
|
+
def do_request(ctx, &block)
|
104
|
+
key = uri_key(ctx[:uri])
|
105
|
+
|
106
|
+
@pools[key].acquire do |adapter|
|
107
|
+
response = adapter.request(ctx)
|
108
|
+
case response.status_code
|
109
|
+
when 200, 204
|
110
|
+
if block
|
111
|
+
block.(response)
|
112
|
+
else
|
113
|
+
# read body
|
114
|
+
response.body
|
115
|
+
end
|
116
|
+
end
|
117
|
+
response
|
118
|
+
end
|
119
|
+
rescue Exception => e
|
120
|
+
p e
|
121
|
+
puts e.backtrace.join("\n")
|
122
|
+
end
|
123
|
+
|
124
|
+
def uri_key(uri)
|
125
|
+
{ scheme: uri.scheme, host: uri.host, port: uri.port }
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export_default :HTTP1Adapter
|
4
|
+
|
5
|
+
require 'http/parser'
|
6
|
+
|
7
|
+
Response = import './response'
|
8
|
+
|
9
|
+
# HTTP 1 adapter
|
10
|
+
class HTTP1Adapter
|
11
|
+
def initialize(socket)
|
12
|
+
@socket = socket
|
13
|
+
@parser = HTTP::Parser.new(self)
|
14
|
+
end
|
15
|
+
|
16
|
+
def on_headers_complete(headers)
|
17
|
+
@headers = headers
|
18
|
+
end
|
19
|
+
|
20
|
+
def on_body(chunk)
|
21
|
+
if @waiting_for_chunk
|
22
|
+
@buffered_chunks ||= []
|
23
|
+
@buffered_chunks << chunk
|
24
|
+
elsif @buffered_body
|
25
|
+
@buffered_body << chunk
|
26
|
+
else
|
27
|
+
@buffered_body = +chunk
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def on_message_complete
|
32
|
+
@done = true
|
33
|
+
end
|
34
|
+
|
35
|
+
def request(ctx)
|
36
|
+
# consume previous response if not finished
|
37
|
+
consume_response if @done == false
|
38
|
+
|
39
|
+
@socket << format_http1_request(ctx)
|
40
|
+
|
41
|
+
@buffered_body = nil
|
42
|
+
@done = false
|
43
|
+
|
44
|
+
read_headers
|
45
|
+
Response.new(self, @parser.status_code, @headers)
|
46
|
+
end
|
47
|
+
|
48
|
+
def read_headers
|
49
|
+
@headers = nil
|
50
|
+
while !@headers && (data = @socket.readpartial(8192))
|
51
|
+
@parser << data
|
52
|
+
end
|
53
|
+
|
54
|
+
raise 'Socket closed by host' unless @headers
|
55
|
+
end
|
56
|
+
|
57
|
+
def body
|
58
|
+
@waiting_for_chunk = nil
|
59
|
+
consume_response
|
60
|
+
@buffered_body
|
61
|
+
end
|
62
|
+
|
63
|
+
def each_chunk(&block)
|
64
|
+
if (body = @buffered_body)
|
65
|
+
@buffered_body = nil
|
66
|
+
@waiting_for_chunk = true
|
67
|
+
block.(body)
|
68
|
+
end
|
69
|
+
while !@done && (data = @socket.readpartial(8192))
|
70
|
+
@parser << data
|
71
|
+
end
|
72
|
+
raise 'Socket closed by host' unless @done
|
73
|
+
|
74
|
+
@buffered_chunks.each(&block)
|
75
|
+
end
|
76
|
+
|
77
|
+
def next_body_chunk
|
78
|
+
return nil if @done
|
79
|
+
if @buffered_chunks && !@buffered_chunks.empty?
|
80
|
+
return @buffered_chunks.shift
|
81
|
+
end
|
82
|
+
|
83
|
+
read_next_body_chunk
|
84
|
+
end
|
85
|
+
|
86
|
+
def read_next_body_chunk
|
87
|
+
@waiting_for_chunk = true
|
88
|
+
while !@done && (data = @socket.readpartial(8192))
|
89
|
+
@parser << data
|
90
|
+
break unless @buffered_chunks.empty?
|
91
|
+
end
|
92
|
+
@buffered_chunks.shift
|
93
|
+
end
|
94
|
+
|
95
|
+
def consume_response
|
96
|
+
while !@done && (data = @socket.readpartial(8192))
|
97
|
+
@parser << data
|
98
|
+
end
|
99
|
+
|
100
|
+
raise 'Socket closed by host' unless @done
|
101
|
+
end
|
102
|
+
|
103
|
+
HTTP1_REQUEST = <<~HTTP.gsub("\n", "\r\n")
|
104
|
+
%<method>s %<request>s HTTP/1.1
|
105
|
+
Host: %<host>s
|
106
|
+
%<headers>s
|
107
|
+
|
108
|
+
HTTP
|
109
|
+
|
110
|
+
def format_http1_request(ctx)
|
111
|
+
headers = format_headers(ctx)
|
112
|
+
|
113
|
+
format(
|
114
|
+
HTTP1_REQUEST,
|
115
|
+
method: ctx[:method],
|
116
|
+
request: ctx[:uri].request_uri,
|
117
|
+
host: ctx[:uri].host,
|
118
|
+
headers: headers
|
119
|
+
)
|
120
|
+
end
|
121
|
+
|
122
|
+
def format_headers(headers)
|
123
|
+
headers.map { |k, v| "#{k}: #{v}\r\n" }.join
|
124
|
+
end
|
125
|
+
|
126
|
+
def protocol
|
127
|
+
:http1
|
128
|
+
end
|
129
|
+
end
|