polyphony 0.78 → 0.81

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +19 -0
  3. data/Gemfile.lock +2 -1
  4. data/examples/core/pingpong.rb +7 -4
  5. data/examples/core/zlib_stream.rb +15 -0
  6. data/ext/polyphony/backend_common.c +16 -8
  7. data/ext/polyphony/backend_common.h +9 -3
  8. data/ext/polyphony/backend_io_uring.c +85 -31
  9. data/ext/polyphony/backend_libev.c +33 -17
  10. data/ext/polyphony/fiber.c +27 -27
  11. data/ext/polyphony/polyphony.c +9 -8
  12. data/ext/polyphony/polyphony.h +21 -7
  13. data/ext/polyphony/thread.c +6 -2
  14. data/lib/polyphony/adapters/fs.rb +4 -0
  15. data/lib/polyphony/adapters/process.rb +14 -1
  16. data/lib/polyphony/adapters/redis.rb +28 -0
  17. data/lib/polyphony/adapters/sequel.rb +19 -1
  18. data/lib/polyphony/core/debug.rb +201 -0
  19. data/lib/polyphony/core/exceptions.rb +21 -6
  20. data/lib/polyphony/core/global_api.rb +228 -73
  21. data/lib/polyphony/core/resource_pool.rb +65 -20
  22. data/lib/polyphony/core/sync.rb +57 -12
  23. data/lib/polyphony/core/thread_pool.rb +42 -5
  24. data/lib/polyphony/core/throttler.rb +21 -5
  25. data/lib/polyphony/core/timer.rb +125 -1
  26. data/lib/polyphony/extensions/exception.rb +36 -6
  27. data/lib/polyphony/extensions/fiber.rb +244 -61
  28. data/lib/polyphony/extensions/io.rb +4 -2
  29. data/lib/polyphony/extensions/kernel.rb +9 -4
  30. data/lib/polyphony/extensions/object.rb +8 -0
  31. data/lib/polyphony/extensions/openssl.rb +3 -1
  32. data/lib/polyphony/extensions/socket.rb +458 -39
  33. data/lib/polyphony/extensions/thread.rb +108 -43
  34. data/lib/polyphony/extensions/timeout.rb +12 -1
  35. data/lib/polyphony/extensions.rb +1 -0
  36. data/lib/polyphony/net.rb +66 -7
  37. data/lib/polyphony/version.rb +1 -1
  38. data/lib/polyphony.rb +0 -2
  39. data/test/test_backend.rb +6 -2
  40. data/test/test_global_api.rb +0 -23
  41. data/test/test_io.rb +7 -7
  42. data/test/test_resource_pool.rb +1 -1
  43. data/test/test_signal.rb +15 -15
  44. data/test/test_thread.rb +1 -1
  45. data/test/test_throttler.rb +0 -6
  46. data/test/test_trace.rb +189 -24
  47. metadata +9 -8
  48. data/lib/polyphony/core/channel.rb +0 -15
@@ -5,8 +5,11 @@ require_relative '../core/exceptions'
5
5
  # Thread extensions
6
6
  class ::Thread
7
7
  attr_reader :main_fiber, :result
8
+ attr_accessor :backend
8
9
 
9
10
  alias_method :orig_initialize, :initialize
11
+
12
+ # Initializes the thread.
10
13
  def initialize(*args, &block)
11
14
  @join_wait_queue = []
12
15
  @finalization_mutex = Mutex.new
@@ -15,53 +18,30 @@ class ::Thread
15
18
  orig_initialize { execute }
16
19
  end
17
20
 
18
- def execute
19
- # backend must be created in the context of the new thread, therefore it
20
- # cannot be created in Thread#initialize
21
- raise_error = false
22
- begin
23
- @backend = Polyphony::Backend.new
24
- rescue Exception => e
25
- raise_error = true
26
- raise e
27
- end
28
- setup
29
- @ready = true
30
- result = @block.(*@args)
31
- rescue Polyphony::MoveOn, Polyphony::Terminate => e
32
- result = e.value
33
- rescue Exception => e
34
- raise_error ? (raise e) : (result = e)
35
- ensure
36
- @ready = true
37
- finalize(result)
38
- end
39
-
40
- attr_accessor :backend
41
-
21
+ # Sets up the thread and its main fiber.
22
+ #
23
+ # @return [void]
42
24
  def setup
43
25
  @main_fiber = Fiber.current
44
26
  @main_fiber.setup_main_fiber
45
27
  setup_fiber_scheduling
46
28
  end
47
29
 
48
- def finalize(result)
49
- unless Fiber.current.children.empty?
50
- Fiber.current.shutdown_all_children
51
- end
52
- @finalization_mutex.synchronize do
53
- @terminated = true
54
- @result = result
55
- signal_waiters(result)
56
- end
57
- @backend&.finalize
58
- end
59
-
60
- def signal_waiters(result)
61
- @join_wait_queue.each { |w| w.signal(result) }
62
- end
63
-
64
30
  alias_method :orig_join, :join
31
+
32
+ # call-seq:
33
+ # thread.join -> result
34
+ # thread.join(timeout) -> result
35
+ # thread.await -> result
36
+ # thread.await(timeout) -> result
37
+ #
38
+ # Waits for the thread to terminate and returns its return value. If the
39
+ # thread terminated with an uncaught exception, it is propagated to the
40
+ # waiting fiber. If a timeout interval is specified, the thread will be
41
+ # terminated without propagating the timeout exception.
42
+ #
43
+ # @param timeout [Number] timeout interval
44
+ # @return [any] thread's return value
65
45
  def join(timeout = nil)
66
46
  watcher = Fiber.current.auto_watcher
67
47
 
@@ -77,6 +57,16 @@ class ::Thread
77
57
  alias_method :await, :join
78
58
 
79
59
  alias_method :orig_raise, :raise
60
+
61
+ # call-seq:
62
+ # thread.raise
63
+ # thread.raise(exception_class)
64
+ # thread.raise(exception_instance)
65
+ #
66
+ # Raises an exception in the context of the thread. If no exception is given,
67
+ # a `RuntimeError` is raised.
68
+ #
69
+ # @param error [Exception, Class, nil] exception spec
80
70
  def raise(error = nil)
81
71
  Thread.pass until @main_fiber
82
72
  error = RuntimeError.new if error.nil?
@@ -88,13 +78,22 @@ class ::Thread
88
78
  end
89
79
 
90
80
  alias_method :orig_kill, :kill
81
+
82
+ # Terminates the thread.
83
+ #
84
+ # @return [Thread] self
91
85
  def kill
92
- return if @terminated
86
+ return self if @terminated
93
87
 
94
88
  raise Polyphony::Terminate
89
+ self
95
90
  end
96
91
 
97
92
  alias_method :orig_inspect, :inspect
93
+
94
+ # Returns a string representation of the thread for debugging purposes.
95
+ #
96
+ # @return [String] string representation
98
97
  def inspect
99
98
  return orig_inspect if self == Thread.main
100
99
 
@@ -103,20 +102,86 @@ class ::Thread
103
102
  end
104
103
  alias_method :to_s, :inspect
105
104
 
105
+ # Returns the source location of the thread's block.
106
+ #
107
+ # @return [String] source location
106
108
  def location
107
109
  @block.source_location.join(':')
108
110
  end
109
111
 
110
- def <<(value)
111
- main_fiber << value
112
+ # Sends a message to the thread's main fiber.
113
+ #
114
+ # @param msg [any] message
115
+ # @return [Fiber] main fiber
116
+ def <<(msg)
117
+ main_fiber << msg
112
118
  end
113
119
  alias_method :send, :<<
114
120
 
121
+ # Sets the idle GC period for the thread's backend.
122
+ #
123
+ # @param period [Number] GC period in seconds
124
+ # @return [Number] GC period
115
125
  def idle_gc_period=(period)
116
126
  backend.idle_gc_period = period
117
127
  end
118
128
 
129
+ # Sets the idle handler for the thread's backend.
130
+ #
131
+ # @param &block [Proc] idle handler
132
+ # @return [Proc] idle handler
119
133
  def on_idle(&block)
120
134
  backend.idle_proc = block
121
135
  end
136
+
137
+ private
138
+
139
+ # Runs the thread's block, handling any uncaught exceptions.
140
+ #
141
+ # @return [void]
142
+ def execute
143
+ # backend must be created in the context of the new thread, therefore it
144
+ # cannot be created in Thread#initialize
145
+ raise_error = false
146
+ begin
147
+ @backend = Polyphony::Backend.new
148
+ rescue Exception => e
149
+ raise_error = true
150
+ raise e
151
+ end
152
+ setup
153
+ @ready = true
154
+ result = @block.(*@args)
155
+ rescue Polyphony::MoveOn, Polyphony::Terminate => e
156
+ result = e.value
157
+ rescue Exception => e
158
+ raise_error ? (raise e) : (result = e)
159
+ ensure
160
+ @ready = true
161
+ finalize(result)
162
+ end
163
+
164
+ # Finalizes the thread.
165
+ #
166
+ # @param result [any] thread's return value
167
+ # @return [void]
168
+ def finalize(result)
169
+ unless Fiber.current.children.empty?
170
+ Fiber.current.shutdown_all_children
171
+ end
172
+ @finalization_mutex.synchronize do
173
+ @terminated = true
174
+ @result = result
175
+ signal_waiters(result)
176
+ end
177
+ @backend&.finalize
178
+ end
179
+
180
+ # Signals all fibers waiting for the thread to terminate.
181
+ #
182
+ # @param result [any] thread's return value
183
+ # @return [void]
184
+ def signal_waiters(result)
185
+ @join_wait_queue.each { |w| w.signal(result) }
186
+ end
122
187
  end
@@ -2,8 +2,19 @@
2
2
 
3
3
  require 'timeout'
4
4
 
5
- # Override Timeout to use cancel scope
5
+ # Timeout extensions
6
6
  module ::Timeout
7
+
8
+ # Sets a timeout for the given block. This method provides an equivalent API
9
+ # to the stock Timeout API provided by Ruby. In case of a timeout, the block
10
+ # will be interrupted and an exception will be raised according to the given
11
+ # arguments.
12
+ #
13
+ # @param sec [Number] timeout period in seconds
14
+ # @param klass [Class] exception class
15
+ # @param message [String] exception message
16
+ # @param &block [Proc] code to run
17
+ # @return [any] block's return value
7
18
  def self.timeout(sec, klass = Timeout::Error, message = 'execution expired', &block)
8
19
  cancel_after(sec, with_exception: [klass, message], &block)
9
20
  end
@@ -3,6 +3,7 @@
3
3
  require_relative './extensions/exception'
4
4
  require_relative './extensions/fiber'
5
5
  require_relative './extensions/io'
6
+ require_relative './extensions/object'
6
7
  require_relative './extensions/kernel'
7
8
  require_relative './extensions/process'
8
9
  require_relative './extensions/thread'
data/lib/polyphony/net.rb CHANGED
@@ -4,9 +4,25 @@ require_relative './extensions/socket'
4
4
  require_relative './extensions/openssl'
5
5
 
6
6
  module Polyphony
7
+
7
8
  # A more elegant networking API
8
9
  module Net
9
10
  class << self
11
+
12
+ # call-seq:
13
+ # Polyphony::Net.tcp_connect(host, port) -> TCPSocket
14
+ # Polyphony::Net.tcp_connect(host, port, secure: true) -> SSLSocket
15
+ # Polyphony::Net.tcp_connect(host, port, secure_context: ctx) -> SSLSocket
16
+ #
17
+ # Create a TCP connection to the given host and port, returning the new
18
+ # socket. If `opts[:secure]` is true, or if an SSL context is given in
19
+ # `opts[:secure_context]`, a TLS handshake is performed, and an SSLSocket
20
+ # is returned.
21
+ #
22
+ # @param host [String] hostname
23
+ # @param port [Integer] port number
24
+ # @param opts [Hash] connection options
25
+ # @return [TCPSocket, SSLSocket] connected socket
10
26
  def tcp_connect(host, port, opts = {})
11
27
  socket = TCPSocket.new(host, port)
12
28
  if opts[:secure_context] || opts[:secure]
@@ -16,6 +32,15 @@ module Polyphony
16
32
  end
17
33
  end
18
34
 
35
+ # Creates a server socket for accepting incoming connection on the given
36
+ # host and port. If `opts[:secure]` is true, or if an SSL context is given
37
+ # in `opts[:secure_context]`, a TLS handshake is performed, and an
38
+ # SSLSocket is returned.
39
+ #
40
+ # @param host [String] hostname
41
+ # @param port [Integer] port number
42
+ # @param opts [Hash] connection options
43
+ # @return [TCPServer, SSLServer] listening socket
19
44
  def tcp_listen(host = nil, port = nil, opts = {})
20
45
  host ||= '0.0.0.0'
21
46
  raise 'Port number not specified' unless port
@@ -28,6 +53,29 @@ module Polyphony
28
53
  end
29
54
  end
30
55
 
56
+ # Sets up ALPN negotiation for the given context. The ALPN handler for the
57
+ # context will select the first protocol from the list given by the client
58
+ # that appears in the list of given protocols, according to the specified
59
+ # order.
60
+ #
61
+ # @param context [SSLContext] SSL context
62
+ # @param protocols [Array] array of supported protocols
63
+ # @return [void]
64
+ def setup_alpn(context, protocols)
65
+ context.alpn_protocols = protocols
66
+ context.alpn_select_cb = lambda do |peer_protocols|
67
+ (protocols & peer_protocols).first
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ # Creates a listening `Socket` instance.
74
+ #
75
+ # @param host [String] hostname
76
+ # @param port [Integer] port number
77
+ # @param opts [Hash] connection options
78
+ # @return [Socket] listening socket
31
79
  def listening_socket_from_options(host, port, opts)
32
80
  ::Socket.new(:INET, :STREAM).tap do |s|
33
81
  s.reuse_addr if opts[:reuse_addr]
@@ -39,6 +87,12 @@ module Polyphony
39
87
  end
40
88
  end
41
89
 
90
+ # Wraps the given socket with a SSLSocket and performs a TLS handshake.
91
+ #
92
+ # @param socket [Socket] plain socket
93
+ # @param context [SSLContext, nil] SSL context
94
+ # @param opts [Hash] connection options
95
+ # @return [SSLSocket] SSL socket
42
96
  def secure_socket(socket, context, opts)
43
97
  context ||= OpenSSL::SSL::SSLContext.new
44
98
  setup_alpn(context, opts[:alpn_protocols]) if opts[:alpn_protocols]
@@ -51,6 +105,11 @@ module Polyphony
51
105
  end
52
106
  end
53
107
 
108
+ # Wraps the given socket with an SSLSocket.
109
+ #
110
+ # @param socket [Socket] plain socket
111
+ # @param context [SSLContext] SSL context
112
+ # @return [SSLSocket] SSL socket
54
113
  def secure_socket_wrapper(socket, context)
55
114
  if context
56
115
  OpenSSL::SSL::SSLSocket.new(socket, context)
@@ -59,17 +118,17 @@ module Polyphony
59
118
  end
60
119
  end
61
120
 
121
+ # Wraps the given socket with an SSLServer, setting up ALPN from the given
122
+ # options.
123
+ #
124
+ # @param socket [Socket] plain socket
125
+ # @param context [SSLContext] SSL context
126
+ # @param opts [Hash] options
127
+ # @return [SSLServer] SSL socket
62
128
  def secure_server(socket, context, opts)
63
129
  setup_alpn(context, opts[:alpn_protocols]) if opts[:alpn_protocols]
64
130
  OpenSSL::SSL::SSLServer.new(socket, context)
65
131
  end
66
-
67
- def setup_alpn(context, protocols)
68
- context.alpn_protocols = protocols
69
- context.alpn_select_cb = lambda do |peer_protocols|
70
- (protocols & peer_protocols).first
71
- end
72
- end
73
132
  end
74
133
  end
75
134
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polyphony
4
- VERSION = '0.78'
4
+ VERSION = '0.81'
5
5
  end
data/lib/polyphony.rb CHANGED
@@ -9,7 +9,6 @@ Thread.current.backend = Polyphony::Backend.new
9
9
 
10
10
  require_relative './polyphony/extensions'
11
11
  require_relative './polyphony/core/exceptions'
12
- require_relative './polyphony/core/global_api'
13
12
  require_relative './polyphony/core/resource_pool'
14
13
  require_relative './polyphony/core/sync'
15
14
  require_relative './polyphony/core/timer'
@@ -53,7 +52,6 @@ module Polyphony
53
52
 
54
53
  def run_forked_block(&block)
55
54
  Thread.current.setup
56
- Fiber.current.setup_main_fiber
57
55
  Thread.current.backend.post_fork
58
56
 
59
57
  install_terminating_signal_handlers
data/test/test_backend.rb CHANGED
@@ -191,7 +191,9 @@ class BackendTest < MiniTest::Test
191
191
  Net = Polyphony::Net
192
192
 
193
193
  def test_accept
194
- server = Net.listening_socket_from_options('127.0.0.1', 1234, reuse_addr: true)
194
+ server = Net.send(
195
+ :listening_socket_from_options, '127.0.0.1', 1234, reuse_addr: true
196
+ )
195
197
 
196
198
  clients = []
197
199
  server_fiber = spin_loop do
@@ -218,7 +220,9 @@ class BackendTest < MiniTest::Test
218
220
  end
219
221
 
220
222
  def test_accept_loop
221
- server = Net.listening_socket_from_options('127.0.0.1', 1235, reuse_addr: true)
223
+ server = Net.send(
224
+ :listening_socket_from_options, '127.0.0.1', 1235, reuse_addr: true
225
+ )
222
226
 
223
227
  clients = []
224
228
  server_fiber = spin do
@@ -140,17 +140,6 @@ class MoveOnAfterTest < MiniTest::Test
140
140
  assert_in_range 0.014..0.02, t1 - t0 if IS_LINUX
141
141
  end
142
142
 
143
- def test_move_on_after_without_block
144
- t0 = Time.now
145
- f = move_on_after(0.01, with_value: 'foo')
146
- assert_kind_of Fiber, f
147
- assert_equal Fiber.current, f.parent
148
- v = sleep 1
149
- t1 = Time.now
150
- assert t1 - t0 < 0.1
151
- assert_equal 'foo', v
152
- end
153
-
154
143
  def test_nested_move_on_after
155
144
  skip unless IS_LINUX
156
145
 
@@ -190,18 +179,6 @@ class CancelAfterTest < MiniTest::Test
190
179
  assert t1 - t0 < 0.1
191
180
  end
192
181
 
193
- def test_cancel_after_without_block
194
- t0 = Time.now
195
- f = cancel_after(0.01)
196
- assert_kind_of Fiber, f
197
- assert_equal Fiber.current, f.parent
198
- assert_raises Polyphony::Cancel do
199
- sleep 1
200
- end
201
- t1 = Time.now
202
- assert t1 - t0 < 0.1
203
- end
204
-
205
182
  def test_cancel_after_with_reset
206
183
  t0 = Time.now
207
184
  cancel_after(0.01) do |f|
data/test/test_io.rb CHANGED
@@ -309,13 +309,13 @@ class IOClassMethodsTest < MiniTest::Test
309
309
  assert_equal BIN_DATA, s
310
310
  end
311
311
 
312
- def test_foreach
313
- skip 'IO.foreach is not yet implemented'
314
- lines = []
315
- IO.foreach(__FILE__) { |l| lines << l }
316
- assert_equal "# frozen_string_literal: true\n", lines[0]
317
- assert_equal "end\n", lines[-1]
318
- end
312
+ # def test_foreach
313
+ # skip 'IO.foreach is not yet implemented'
314
+ # lines = []
315
+ # IO.foreach(__FILE__) { |l| lines << l }
316
+ # assert_equal "# frozen_string_literal: true\n", lines[0]
317
+ # assert_equal "end\n", lines[-1]
318
+ # end
319
319
 
320
320
  def test_read_class_method
321
321
  s = IO.read(__FILE__)
@@ -88,7 +88,7 @@ class ResourcePoolTest < MiniTest::Test
88
88
  assert_equal 2, pool.limit
89
89
  assert_equal 0, pool.size
90
90
 
91
- pool.preheat!
91
+ pool.fill!
92
92
  assert_equal 2, pool.size
93
93
  end
94
94
 
data/test/test_signal.rb CHANGED
@@ -33,24 +33,24 @@ class SignalTrapTest < Minitest::Test
33
33
  Fiber.current.tag = :main
34
34
 
35
35
  expected = [
36
- [:fiber_switchpoint, :main],
37
- [:fiber_event_poll_enter, :main],
38
- [:fiber_create, :oob],
39
- [:fiber_schedule, :oob],
40
- [:fiber_event_poll_leave, :main],
41
- [:fiber_run, :oob],
42
- [:fiber_terminate, :oob],
43
- [:fiber_switchpoint, :oob],
44
- [:fiber_event_poll_enter, :oob],
45
- [:fiber_schedule, :main],
46
- [:fiber_event_poll_leave, :oob],
47
- [:fiber_run, :main]
36
+ [:block, :main],
37
+ [:enter_poll, :main],
38
+ [:spin, :oob],
39
+ [:schedule, :oob],
40
+ [:leave_poll, :main],
41
+ [:unblock, :oob],
42
+ [:terminate, :oob],
43
+ [:block, :oob],
44
+ [:enter_poll, :oob],
45
+ [:schedule, :main],
46
+ [:leave_poll, :oob],
47
+ [:unblock, :main]
48
48
  ]
49
49
  if Thread.backend.kind == :libev
50
50
  expected += [
51
- [:fiber_schedule, :main],
52
- [:fiber_switchpoint, :main],
53
- [:fiber_run, :main]
51
+ [:schedule, :main],
52
+ [:block, :main],
53
+ [:unblock, :main]
54
54
  ]
55
55
  end
56
56
 
data/test/test_thread.rb CHANGED
@@ -132,7 +132,7 @@ class ThreadTest < MiniTest::Test
132
132
  Thread.backend.trace_proc = proc {|*r| records << r }
133
133
  suspend
134
134
  assert_equal [
135
- [:fiber_switchpoint, Fiber.current, ["#{__FILE__}:#{__LINE__ - 2}:in `test_that_suspend_returns_immediately_if_no_watchers'"] + caller]
135
+ [:block, Fiber.current, ["#{__FILE__}:#{__LINE__ - 2}:in `test_that_suspend_returns_immediately_if_no_watchers'"] + caller]
136
136
  ], records
137
137
  ensure
138
138
  Thread.backend.trace_proc = nil
@@ -11,8 +11,6 @@ class ThrottlerTest < MiniTest::Test
11
11
  sleep 0.2
12
12
  f.stop
13
13
  assert_in_range 1..4, buffer.size if IS_LINUX
14
- ensure
15
- t.stop
16
14
  end
17
15
 
18
16
  def test_throttler_with_hash_of_rate
@@ -24,8 +22,6 @@ class ThrottlerTest < MiniTest::Test
24
22
  sleep 0.25
25
23
  f.stop
26
24
  assert_in_range 4..6, buffer.size if IS_LINUX
27
- ensure
28
- t.stop
29
25
  end
30
26
 
31
27
  def test_throttler_with_hash_of_interval
@@ -35,8 +31,6 @@ class ThrottlerTest < MiniTest::Test
35
31
  sleep 0.02
36
32
  f.stop
37
33
  assert_in_range 2..4, buffer.size if IS_LINUX
38
- ensure
39
- t.stop
40
34
  end
41
35
 
42
36
  def test_throttler_with_invalid_argument