polyphony 0.21 → 0.22

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/CHANGELOG.md +11 -0
  4. data/Gemfile.lock +5 -5
  5. data/TODO.md +83 -20
  6. data/docs/technical-overview/design-principles.md +3 -3
  7. data/docs/technical-overview/faq.md +11 -0
  8. data/docs/technical-overview/fiber-scheduling.md +46 -33
  9. data/examples/core/sleep_spin.rb +2 -2
  10. data/examples/http/http2_raw.rb +135 -0
  11. data/examples/http/http_client.rb +14 -3
  12. data/examples/http/http_get.rb +28 -2
  13. data/examples/http/http_server.rb +3 -1
  14. data/examples/http/http_server_forked.rb +3 -1
  15. data/examples/interfaces/pg_pool.rb +1 -0
  16. data/examples/io/echo_server.rb +1 -0
  17. data/ext/gyro/async.c +7 -9
  18. data/ext/gyro/child.c +5 -8
  19. data/ext/gyro/extconf.rb +2 -0
  20. data/ext/gyro/gyro.c +159 -204
  21. data/ext/gyro/gyro.h +16 -6
  22. data/ext/gyro/io.c +7 -10
  23. data/ext/gyro/signal.c +3 -0
  24. data/ext/gyro/timer.c +9 -18
  25. data/lib/polyphony/auto_run.rb +12 -5
  26. data/lib/polyphony/core/coprocess.rb +1 -1
  27. data/lib/polyphony/core/resource_pool.rb +49 -15
  28. data/lib/polyphony/core/supervisor.rb +3 -2
  29. data/lib/polyphony/extensions/core.rb +16 -3
  30. data/lib/polyphony/extensions/io.rb +2 -0
  31. data/lib/polyphony/extensions/openssl.rb +60 -0
  32. data/lib/polyphony/extensions/socket.rb +0 -4
  33. data/lib/polyphony/http/client/agent.rb +127 -0
  34. data/lib/polyphony/http/client/http1.rb +129 -0
  35. data/lib/polyphony/http/client/http2.rb +180 -0
  36. data/lib/polyphony/http/client/response.rb +32 -0
  37. data/lib/polyphony/http/client/site_connection_manager.rb +109 -0
  38. data/lib/polyphony/http/server/request.rb +0 -1
  39. data/lib/polyphony/http.rb +1 -1
  40. data/lib/polyphony/net.rb +2 -1
  41. data/lib/polyphony/version.rb +1 -1
  42. data/lib/polyphony.rb +4 -4
  43. data/polyphony.gemspec +1 -0
  44. data/test/test_gyro.rb +42 -10
  45. data/test/test_resource_pool.rb +107 -0
  46. metadata +10 -4
  47. 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 = DBL2NUM(timer->after);
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 = YIELD_TO_REACTOR();
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
  }
@@ -3,10 +3,17 @@
3
3
  require_relative '../polyphony'
4
4
 
5
5
  at_exit do
6
- repl = (Pry.current rescue nil) || (IRB.CurrentContext rescue nil)
6
+ unless Gyro.break?
7
+ repl = (Pry.current rescue nil) || (IRB.CurrentContext rescue nil)
7
8
 
8
- # in most cases, once the root fiber is done there are still pending
9
- # operations going on. If the reactor loop is not done, we suspend the root
10
- # fiber until it is done
11
- suspend if $__reactor_fiber__&.alive? && !repl
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
@@ -101,7 +101,7 @@ class Coprocess
101
101
  end
102
102
 
103
103
  def caller
104
- @fiber&.__caller__[2..]
104
+ @fiber && @fiber.__caller__[2..-1]
105
105
  end
106
106
 
107
107
  def location
@@ -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
- @available = []
14
- @waiting = []
15
+ @stock = []
16
+ @queue = []
15
17
 
16
18
  @limit = opts[:limit] || 4
17
- @count = 0
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
- dequeue(resource) || return_to_stock(resource) if resource
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
- @waiting << fiber
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
- @waiting.delete(fiber)
45
+ @queue.delete(fiber)
38
46
  end
39
47
 
40
- def dequeue(resource)
41
- return nil if @waiting.empty?
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
- @waiting[0]&.schedule(resource)
44
- true
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
- @available << resource
64
+ @stock << resource
49
65
  end
50
66
 
51
67
  def from_stock
52
- @available.shift || (@count < @limit && allocate)
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
- @count += 1
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 - @count).times { @available << from_stock }
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 { |cp| cp.result }
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, :scheduled_value
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
- $__reactor_fiber__.safe_transfer if $__reactor_fiber__.alive?
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
- end
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