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
@@ -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
  - - ">="