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