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
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ export_default :HTTP2Adapter
4
+
5
+ require 'http/2'
6
+
7
+ Response = import './response'
8
+
9
+ # HTTP 2 adapter
10
+ class HTTP2Adapter
11
+ def initialize(socket)
12
+ @socket = socket
13
+ @client = HTTP2::Client.new
14
+ @client.on(:frame) { |bytes| socket << bytes }
15
+ # @client.on(:frame_received) do |frame|
16
+ # puts "Received frame: #{frame.inspect}"
17
+ # end
18
+ # @client.on(:frame_sent) do |frame|
19
+ # puts "Sent frame: #{frame.inspect}"
20
+ # end
21
+
22
+ @reader = spin do
23
+ while (data = socket.readpartial(8192))
24
+ @client << data
25
+ snooze
26
+ end
27
+ end
28
+ end
29
+
30
+ def allocate_stream_adapter
31
+ StreamAdapter.new(self)
32
+ end
33
+
34
+ def allocate_stream
35
+ @client.new_stream
36
+ end
37
+
38
+ def protocol
39
+ :http2
40
+ end
41
+
42
+ # Virtualizes adapter over HTTP2 stream
43
+ class StreamAdapter
44
+ def initialize(connection)
45
+ @connection = connection
46
+ end
47
+
48
+ def request(ctx)
49
+ stream = setup_stream # (ctx, stream)
50
+ send_request(ctx, stream)
51
+
52
+ stream.on(:headers, &method(:on_headers))
53
+ stream.on(:data, &method(:on_data))
54
+ stream.on(:close, &method(:on_close))
55
+
56
+ # stream.on(:active) { puts "* active" }
57
+ # stream.on(:half_close) { puts "* half_close" }
58
+
59
+ wait_for_response(ctx, stream)
60
+ rescue Exception => e
61
+ p e
62
+ puts e.backtrace.join("\n")
63
+ # ensure
64
+ # stream.close
65
+ end
66
+
67
+ def send_request(ctx, stream)
68
+ headers = prepare_headers(ctx)
69
+ if ctx[:opts][:payload]
70
+ stream.headers(headers, end_stream: false)
71
+ stream.data(ctx[:opts][:payload], end_stream: true)
72
+ else
73
+ stream.headers(headers, end_stream: true)
74
+ end
75
+ end
76
+
77
+ def on_headers(headers)
78
+ if @waiting_headers_fiber
79
+ @waiting_headers_fiber.schedule headers.to_h
80
+ else
81
+ @headers = headers.to_h
82
+ end
83
+ end
84
+
85
+ def on_data(chunk)
86
+ if @waiting_chunk_fiber
87
+ @waiting_chunk_fiber&.schedule chunk
88
+ else
89
+ @buffered_chunks << chunk
90
+ end
91
+ end
92
+
93
+ def on_close(_stream)
94
+ @done = true
95
+ @waiting_done_fiber&.schedule
96
+ end
97
+
98
+ def setup_stream
99
+ stream = @connection.allocate_stream
100
+
101
+ @headers = nil
102
+ @done = nil
103
+ @buffered_chunks = []
104
+
105
+ @waiting_headers_fiber = nil
106
+ @waiting_chunk_fiber = nil
107
+ @waiting_done_fiber = nil
108
+
109
+ stream
110
+ end
111
+
112
+ def wait_for_response(_ctx, _stream)
113
+ headers = wait_for_headers
114
+ Response.new(self, headers[':status'].to_i, headers)
115
+ end
116
+
117
+ def wait_for_headers
118
+ return @headers if @headers
119
+
120
+ @waiting_headers_fiber = Fiber.current
121
+ suspend
122
+ end
123
+
124
+ def protocol
125
+ :http2
126
+ end
127
+
128
+ def prepare_headers(ctx)
129
+ headers = {
130
+ ':method' => ctx[:method].to_s,
131
+ ':scheme' => ctx[:uri].scheme,
132
+ ':authority' => [ctx[:uri].host, ctx[:uri].port].join(':'),
133
+ ':path' => ctx[:uri].request_uri,
134
+ 'User-Agent' => 'curl/7.54.0'
135
+ }
136
+ headers.merge!(ctx[:opts][:headers]) if ctx[:opts][:headers]
137
+ headers
138
+ end
139
+
140
+ def body
141
+ @waiting_done_fiber = Fiber.current
142
+ suspend
143
+ @buffered_chunks.join
144
+ # body = +''
145
+ # while !@done
146
+ # p :body_suspend_pre
147
+ # chunk = suspend
148
+ # p :body_suspend_post
149
+ # body << chunk
150
+ # end
151
+ # puts ""
152
+ # body
153
+ rescue Exception => e
154
+ p e
155
+ puts e.backtrace.join("\n")
156
+ end
157
+
158
+ def each_chunk
159
+ yield @buffered_chunks.shift until @buffered_chunks.empty?
160
+
161
+ @waiting_chunk_fiber = Fiber.current
162
+ until @done
163
+ chunk = suspend
164
+ yield chunk
165
+ end
166
+ end
167
+
168
+ def next_body_chunk
169
+ return yield @buffered_chunks.shift unless @buffered_chunks.empty?
170
+
171
+ @waiting_chunk_fuber = Fiber.current
172
+ until @done
173
+ chunk = suspend
174
+ return yield chunk
175
+ end
176
+
177
+ nil
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ export_default :Response
4
+
5
+ require 'json'
6
+
7
+ # HTTP response
8
+ class Response
9
+ attr_reader :status_code, :headers
10
+
11
+ def initialize(adapter, status_code, headers)
12
+ @adapter = adapter
13
+ @status_code = status_code
14
+ @headers = headers
15
+ end
16
+
17
+ def body
18
+ @body ||= @adapter.body
19
+ end
20
+
21
+ def each_chunk(&block)
22
+ @adapter.each_chunk(&block)
23
+ end
24
+
25
+ def next_body_chunk
26
+ @adapter.next_body_chunk
27
+ end
28
+
29
+ def json
30
+ @json ||= ::JSON.parse(body)
31
+ end
32
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ export_default :SiteConnectionManager
4
+
5
+ ResourcePool = import '../../core/resource_pool'
6
+ HTTP1Adapter = import './http1'
7
+ HTTP2Adapter = import './http2'
8
+
9
+ # HTTP site connection pool
10
+ class SiteConnectionManager < ResourcePool
11
+ def initialize(uri_key)
12
+ @uri_key = uri_key
13
+ super(limit: 4)
14
+ end
15
+
16
+ # def method_missing(sym, *args)
17
+ # raise "Invalid method #{sym}"
18
+ # end
19
+
20
+ def acquire
21
+ Gyro.ref
22
+ prepare_first_connection if @size.zero?
23
+ super
24
+ ensure
25
+ Gyro.unref
26
+ # The size goes back to 0 only in case existing connections get into an
27
+ # error state and then get discarded
28
+ @state = nil if @size == 0
29
+ end
30
+
31
+ def prepare_first_connection
32
+ case @state
33
+ when nil
34
+ @state = :first_connection
35
+ create_first_connection
36
+ when :first_connection
37
+ @first_connection_queue << Fiber.current
38
+ suspend
39
+ end
40
+ end
41
+
42
+ def create_first_connection
43
+ @first_connection_queue = []
44
+ # @first_connection_queue << Fiber.current
45
+
46
+ adapter = connect
47
+ @state = adapter.protocol
48
+ send(:"setup_#{@state}_allocator", adapter)
49
+ dequeue_first_connection_waiters
50
+ end
51
+
52
+ def setup_http1_allocator(adapter)
53
+ @size += 1
54
+ adapter.extend ResourceExtensions
55
+ @stock << adapter
56
+ @allocator = proc { connect }
57
+ end
58
+
59
+ def setup_http2_allocator(adapter)
60
+ @adapter = adapter
61
+ @limit = 20
62
+ @size += 1
63
+ stream_adapter = adapter.allocate_stream_adapter
64
+ stream_adapter.extend ResourceExtensions
65
+ @stock << stream_adapter
66
+ @allocator = proc { adapter.allocate_stream_adapter }
67
+ end
68
+
69
+ def dequeue_first_connection_waiters
70
+ return unless @first_connection_queue
71
+
72
+ @first_connection_queue.each(&:schedule)
73
+ @first_connection_queue = nil
74
+ end
75
+
76
+ def connect
77
+ socket = create_socket
78
+ protocol = socket_protocol(socket)
79
+ case protocol
80
+ when :http1
81
+ HTTP1Adapter.new(socket)
82
+ when :http2
83
+ HTTP2Adapter.new(socket)
84
+ else
85
+ raise "Unknown protocol #{protocol.inspect}"
86
+ end
87
+ end
88
+
89
+ def socket_protocol(socket)
90
+ if socket.is_a?(OpenSSL::SSL::SSLSocket) && socket.alpn_protocol == 'h2'
91
+ :http2
92
+ else
93
+ :http1
94
+ end
95
+ end
96
+
97
+ SECURE_OPTS = { secure: true, alpn_protocols: ['h2', 'http/1.1'] }.freeze
98
+
99
+ def create_socket
100
+ case @uri_key[:scheme]
101
+ when 'http'
102
+ Polyphony::Net.tcp_connect(@uri_key[:host], @uri_key[:port])
103
+ when 'https'
104
+ Polyphony::Net.tcp_connect(@uri_key[:host], @uri_key[:port], SECURE_OPTS)
105
+ else
106
+ raise "Invalid scheme #{@uri_key[:scheme].inspect}"
107
+ end
108
+ end
109
+ end
@@ -58,7 +58,6 @@ class Request
58
58
 
59
59
  def each_chunk(&block)
60
60
  if @buffered_body_chunks
61
- puts 'serve buffered body_chunks'
62
61
  @buffered_body_chunks.each(&block)
63
62
  @buffered_body_chunks = nil
64
63
  end
@@ -6,7 +6,7 @@ module Polyphony
6
6
  # HTTP imports (loaded dynamically)
7
7
  module HTTP
8
8
  auto_import(
9
- Agent: './http/agent',
9
+ Agent: './http/client/agent',
10
10
  Rack: './http/server/rack',
11
11
  Server: './http/server'
12
12
  )
data/lib/polyphony/net.rb CHANGED
@@ -41,7 +41,8 @@ def socket_from_options(host, port, opts)
41
41
  end
42
42
 
43
43
  def secure_socket(socket, context, opts)
44
- setup_alpn(context, opts[:alpn_protocols]) if context && opts[:alpn_protocols]
44
+ context ||= OpenSSL::SSL::SSLContext.new
45
+ setup_alpn(context, opts[:alpn_protocols]) if opts[:alpn_protocols]
45
46
  socket = secure_socket_wrapper(socket, context)
46
47
 
47
48
  socket.tap do |s|
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polyphony
4
- VERSION = '0.21'
4
+ VERSION = '0.22'
5
5
  end
data/lib/polyphony.rb CHANGED
@@ -52,7 +52,7 @@ module Polyphony
52
52
  end
53
53
 
54
54
  def fork(&block)
55
- Gyro.break
55
+ Gyro.break!
56
56
  pid = Kernel.fork do
57
57
  setup_forked_process
58
58
  block.()
@@ -64,13 +64,13 @@ module Polyphony
64
64
  # exiting.
65
65
  suspend
66
66
  end
67
- Gyro.restart
67
+ Gyro.reset!
68
68
  pid
69
69
  end
70
70
 
71
71
  def reset!
72
- Fiber.root.scheduled_value = nil
73
- Gyro.restart
72
+ # Fiber.root.scheduled_value = nil
73
+ Gyro.reset!
74
74
  end
75
75
 
76
76
  private
data/polyphony.gemspec CHANGED
@@ -16,6 +16,7 @@ Gem::Specification.new do |s|
16
16
  s.extra_rdoc_files = ["README.md"]
17
17
  s.extensions = ["ext/gyro/extconf.rb"]
18
18
  s.require_paths = ["lib"]
19
+ s.required_ruby_version = '>= 2.6'
19
20
 
20
21
  s.executables = ['poly']
21
22
 
data/test/test_gyro.rb CHANGED
@@ -2,7 +2,42 @@
2
2
 
3
3
  require_relative 'helper'
4
4
 
5
- class RunTest < Minitest::Test
5
+ class SchedulingTest < MiniTest::Test
6
+ def test_fiber_state
7
+ assert_equal :running, Fiber.current.state
8
+
9
+ f = Fiber.new {}
10
+
11
+ assert_equal :paused, f.state
12
+ f.resume
13
+ assert_equal :dead, f.state
14
+
15
+ f = Fiber.new { }
16
+ f.schedule
17
+ assert_equal :scheduled, f.state
18
+ snooze
19
+ assert_equal :dead, f.state
20
+ end
21
+
22
+ def test_schedule
23
+ values = []
24
+ fibers = 3.times.map { |i| Fiber.new { values << i } }
25
+ fibers[0].schedule
26
+
27
+ assert_equal [], values
28
+ snooze
29
+ assert_equal [0], values
30
+
31
+ fibers[1].schedule
32
+ fibers[2].schedule
33
+
34
+ assert_equal [0], values
35
+ snooze
36
+ assert_equal [0, 1, 2], values
37
+ end
38
+ end
39
+
40
+ class RunTest < MiniTest::Test
6
41
  def test_that_run_loop_returns_immediately_if_no_watchers
7
42
  t0 = Time.now
8
43
  suspend
@@ -88,7 +123,7 @@ class IdleTest < MiniTest::Test
88
123
  end.schedule
89
124
 
90
125
  Fiber.new do
91
- Gyro.break
126
+ Gyro.break!
92
127
  end.schedule
93
128
 
94
129
  suspend
@@ -96,7 +131,7 @@ class IdleTest < MiniTest::Test
96
131
  assert_equal [:foo], values
97
132
  end
98
133
 
99
- def test_start
134
+ def test_reset
100
135
  values = []
101
136
  f1 = Fiber.new do
102
137
  values << :foo
@@ -106,7 +141,7 @@ class IdleTest < MiniTest::Test
106
141
  end.schedule
107
142
 
108
143
  f2 = Fiber.new do
109
- Gyro.break
144
+ Gyro.reset!
110
145
  values << :restarted
111
146
  snooze
112
147
  values << :baz
@@ -114,12 +149,9 @@ class IdleTest < MiniTest::Test
114
149
 
115
150
  suspend
116
151
 
117
- Gyro.start
118
- f2.schedule
119
152
  f1.schedule
120
153
  suspend
121
-
122
- assert_equal %i[foo restarted bar baz], values
154
+ assert_equal %i[foo restarted baz], values
123
155
  end
124
156
 
125
157
  def test_restart
@@ -127,13 +159,13 @@ class IdleTest < MiniTest::Test
127
159
  Fiber.new do
128
160
  values << :foo
129
161
  snooze
130
- # this part will not be reached, as f
162
+ # this part will not be reached, as Gyro state is reset
131
163
  values << :bar
132
164
  suspend
133
165
  end.schedule
134
166
 
135
167
  Fiber.new do
136
- Gyro.restart
168
+ Gyro.reset!
137
169
 
138
170
  # control is transfer to the fiber that called Gyro.restart
139
171
  values << :restarted
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+
5
+ class ResourcePoolTest < MiniTest::Test
6
+ def test_resource_pool_limit
7
+ resources = [+'a', +'b']
8
+ pool = Polyphony::ResourcePool.new(limit: 2) { resources.shift }
9
+
10
+ assert_equal 2, pool.limit
11
+ assert_equal 0, pool.available
12
+ assert_equal 0, pool.size
13
+
14
+ results = []
15
+ 4.times {
16
+ spin {
17
+ snooze
18
+ pool.acquire { |resource|
19
+ results << resource
20
+ snooze
21
+ }
22
+ }
23
+ }
24
+ 2.times { snooze }
25
+ assert_equal 2, pool.limit
26
+ assert_equal 0, pool.available
27
+ assert_equal 2, pool.size
28
+
29
+ 2.times { snooze }
30
+
31
+ assert_equal ['a', 'b', 'a', 'b'], results
32
+
33
+ 2.times { snooze }
34
+
35
+ assert_equal 2, pool.limit
36
+ assert_equal 2, pool.available
37
+ assert_equal 2, pool.size
38
+ end
39
+
40
+ def test_discard
41
+ resources = [+'a', +'b']
42
+ pool = Polyphony::ResourcePool.new(limit: 2) { resources.shift }
43
+
44
+ results = []
45
+ 4.times {
46
+ spin {
47
+ snooze
48
+ pool.acquire { |resource|
49
+ results << resource
50
+ resource.__discard__ if resource == 'b'
51
+ snooze
52
+ }
53
+ }
54
+ }
55
+ 6.times { snooze }
56
+
57
+ assert_equal ['a', 'b', 'a', 'a'], results
58
+ assert_equal 1, pool.size
59
+ end
60
+
61
+ def test_add
62
+ resources = [+'a', +'b']
63
+ pool = Polyphony::ResourcePool.new(limit: 2) { resources.shift }
64
+
65
+ pool << +'c'
66
+
67
+ results = []
68
+ 4.times {
69
+ spin {
70
+ snooze
71
+ pool.acquire { |resource|
72
+ results << resource
73
+ resource.__discard__ if resource == 'b'
74
+ snooze
75
+ }
76
+ }
77
+ }
78
+ 6.times { snooze }
79
+
80
+ assert_equal ['c', 'a', 'c', 'a'], results
81
+ end
82
+
83
+ def test_single_resource_limit
84
+ resources = [+'a', +'b']
85
+ pool = Polyphony::ResourcePool.new(limit: 1) { resources.shift }
86
+
87
+ results = []
88
+ 10.times {
89
+ spin {
90
+ snooze
91
+ pool.acquire { |resource|
92
+ results << resource
93
+ snooze
94
+ }
95
+ }
96
+ }
97
+ 20.times { snooze }
98
+
99
+ assert_equal ['a'] * 10, results
100
+ end
101
+
102
+ def test_failing_allocator
103
+ pool = Polyphony::ResourcePool.new(limit: 4) { raise }
104
+
105
+ assert_raises { pool.acquire { } }
106
+ end
107
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: polyphony
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.21'
4
+ version: '0.22'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-12-12 00:00:00.000000000 Z
11
+ date: 2020-01-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: modulation
@@ -272,6 +272,7 @@ files:
272
272
  - examples/http/config.ru
273
273
  - examples/http/cuba.ru
274
274
  - examples/http/happy_eyeballs.rb
275
+ - examples/http/http2_raw.rb
275
276
  - examples/http/http_client.rb
276
277
  - examples/http/http_get.rb
277
278
  - examples/http/http_parse_experiment.rb
@@ -369,7 +370,11 @@ files:
369
370
  - lib/polyphony/extensions/socket.rb
370
371
  - lib/polyphony/fs.rb
371
372
  - lib/polyphony/http.rb
372
- - lib/polyphony/http/agent.rb
373
+ - lib/polyphony/http/client/agent.rb
374
+ - lib/polyphony/http/client/http1.rb
375
+ - lib/polyphony/http/client/http2.rb
376
+ - lib/polyphony/http/client/response.rb
377
+ - lib/polyphony/http/client/site_connection_manager.rb
373
378
  - lib/polyphony/http/server.rb
374
379
  - lib/polyphony/http/server/http1.rb
375
380
  - lib/polyphony/http/server/http2.rb
@@ -393,6 +398,7 @@ files:
393
398
  - test/test_http_server.rb
394
399
  - test/test_io.rb
395
400
  - test/test_kernel.rb
401
+ - test/test_resource_pool.rb
396
402
  - test/test_signal.rb
397
403
  - test/test_timer.rb
398
404
  homepage: http://github.com/digital-fabric/polyphony
@@ -412,7 +418,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
412
418
  requirements:
413
419
  - - ">="
414
420
  - !ruby/object:Gem::Version
415
- version: '0'
421
+ version: '2.6'
416
422
  required_rubygems_version: !ruby/object:Gem::Requirement
417
423
  requirements:
418
424
  - - ">="