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.
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