polyphony 0.47.5 → 0.49.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/Gemfile.lock +1 -1
  4. data/LICENSE +1 -1
  5. data/TODO.md +34 -17
  6. data/examples/io/tcp_proxy.rb +32 -0
  7. data/examples/performance/line_splitting.rb +34 -0
  8. data/examples/performance/loop.rb +32 -0
  9. data/examples/performance/thread-vs-fiber/polyphony_server.rb +6 -2
  10. data/ext/polyphony/backend_common.h +2 -2
  11. data/ext/polyphony/backend_io_uring.c +29 -68
  12. data/ext/polyphony/backend_libev.c +18 -59
  13. data/ext/polyphony/event.c +1 -1
  14. data/ext/polyphony/polyphony.c +0 -2
  15. data/ext/polyphony/polyphony.h +5 -4
  16. data/ext/polyphony/queue.c +2 -2
  17. data/ext/polyphony/thread.c +9 -28
  18. data/lib/polyphony.rb +2 -1
  19. data/lib/polyphony/adapters/postgres.rb +3 -3
  20. data/lib/polyphony/adapters/process.rb +2 -0
  21. data/lib/polyphony/core/global_api.rb +14 -2
  22. data/lib/polyphony/core/thread_pool.rb +3 -1
  23. data/lib/polyphony/core/throttler.rb +1 -1
  24. data/lib/polyphony/core/timer.rb +72 -0
  25. data/lib/polyphony/extensions/fiber.rb +28 -13
  26. data/lib/polyphony/extensions/io.rb +8 -14
  27. data/lib/polyphony/extensions/openssl.rb +4 -4
  28. data/lib/polyphony/extensions/socket.rb +5 -1
  29. data/lib/polyphony/extensions/thread.rb +1 -2
  30. data/lib/polyphony/net.rb +3 -6
  31. data/lib/polyphony/version.rb +1 -1
  32. data/polyphony.gemspec +1 -1
  33. data/test/helper.rb +2 -2
  34. data/test/test_backend.rb +26 -1
  35. data/test/test_fiber.rb +79 -1
  36. data/test/test_global_api.rb +30 -0
  37. data/test/test_io.rb +26 -0
  38. data/test/test_signal.rb +1 -2
  39. data/test/test_socket.rb +5 -5
  40. data/test/test_supervise.rb +1 -1
  41. data/test/test_timer.rb +124 -0
  42. metadata +8 -4
  43. data/ext/polyphony/backend.h +0 -26
@@ -34,9 +34,18 @@ module Polyphony
34
34
  schedule Polyphony::Cancel.new
35
35
  end
36
36
 
37
- def terminate
37
+ def graceful_shutdown=(graceful)
38
+ @graceful_shutdown = graceful
39
+ end
40
+
41
+ def graceful_shutdown?
42
+ @graceful_shutdown
43
+ end
44
+
45
+ def terminate(graceful = false)
38
46
  return if @running == false
39
47
 
48
+ @graceful_shutdown = graceful
40
49
  schedule Polyphony::Terminate.new
41
50
  end
42
51
 
@@ -66,7 +75,9 @@ module Polyphony
66
75
  @on_child_done = proc do |fiber, result|
67
76
  self << fiber unless result.is_a?(Exception)
68
77
  end
69
- loop { supervise_perform(opts) }
78
+ while true
79
+ supervise_perform(opts)
80
+ end
70
81
  rescue Polyphony::MoveOn
71
82
  # generated in #supervise_perform to stop supervisor
72
83
  ensure
@@ -210,11 +221,14 @@ module Polyphony
210
221
  @on_child_done&.(child_fiber, result)
211
222
  end
212
223
 
213
- def terminate_all_children
224
+ def terminate_all_children(graceful = false)
214
225
  return unless @children
215
226
 
216
227
  e = Polyphony::Terminate.new
217
- @children.each_key { |c| c.raise e }
228
+ @children.each_key do |c|
229
+ c.graceful_shutdown = true if graceful
230
+ c.raise e
231
+ end
218
232
  end
219
233
 
220
234
  def await_all_children
@@ -230,9 +244,13 @@ module Polyphony
230
244
  results.values
231
245
  end
232
246
 
233
- def shutdown_all_children
234
- terminate_all_children
235
- await_all_children
247
+ def shutdown_all_children(graceful = false)
248
+ return unless @children
249
+
250
+ @children.keys.each do |c|
251
+ c.terminate(graceful)
252
+ c.await
253
+ end
236
254
  end
237
255
  end
238
256
 
@@ -305,13 +323,10 @@ module Polyphony
305
323
  # the children are shut down, it is returned along with the uncaught_exception
306
324
  # flag set. Otherwise, it returns the given arguments.
307
325
  def finalize_children(result, uncaught_exception)
308
- begin
309
- shutdown_all_children
310
- rescue Exception => e
311
- result = e
312
- uncaught_exception = true
313
- end
326
+ shutdown_all_children
314
327
  [result, uncaught_exception]
328
+ rescue Exception => e
329
+ [e, true]
315
330
  end
316
331
 
317
332
  def inform_dependants(result, uncaught_exception)
@@ -119,18 +119,11 @@ class ::IO
119
119
  end
120
120
 
121
121
  alias_method :orig_readpartial, :read
122
- def readpartial(len, str = nil)
123
- @read_buffer ||= +''
124
- result = Thread.current.backend.read(self, @read_buffer, len, false)
122
+ def readpartial(len, str = +'')
123
+ result = Thread.current.backend.read(self, str, len, false)
125
124
  raise EOFError unless result
126
125
 
127
- if str
128
- str << @read_buffer
129
- else
130
- str = @read_buffer
131
- end
132
- @read_buffer = +''
133
- str
126
+ result
134
127
  end
135
128
 
136
129
  alias_method :orig_write, :write
@@ -154,15 +147,16 @@ class ::IO
154
147
 
155
148
  @read_buffer ||= +''
156
149
 
157
- loop do
150
+ while true
158
151
  idx = @read_buffer.index(sep)
159
152
  return @read_buffer.slice!(0, idx + sep_size) if idx
160
153
 
161
- data = readpartial(8192)
154
+ data = readpartial(8192, +'')
155
+ return nil unless data
162
156
  @read_buffer << data
163
- rescue EOFError
164
- return nil
165
157
  end
158
+ rescue EOFError
159
+ return nil
166
160
  end
167
161
 
168
162
  # def print(*args)
@@ -25,7 +25,7 @@ class ::OpenSSL::SSL::SSLSocket
25
25
 
26
26
  alias_method :orig_accept, :accept
27
27
  def accept
28
- loop do
28
+ while true
29
29
  result = accept_nonblock(exception: false)
30
30
  case result
31
31
  when :wait_readable then Thread.current.backend.wait_io(io, false)
@@ -37,14 +37,14 @@ class ::OpenSSL::SSL::SSLSocket
37
37
  end
38
38
 
39
39
  def accept_loop
40
- loop do
40
+ while true
41
41
  yield accept
42
42
  end
43
43
  end
44
44
 
45
45
  alias_method :orig_sysread, :sysread
46
46
  def sysread(maxlen, buf = +'')
47
- loop do
47
+ while true
48
48
  case (result = read_nonblock(maxlen, buf, exception: false))
49
49
  when :wait_readable then Thread.current.backend.wait_io(io, false)
50
50
  when :wait_writable then Thread.current.backend.wait_io(io, true)
@@ -55,7 +55,7 @@ class ::OpenSSL::SSL::SSLSocket
55
55
 
56
56
  alias_method :orig_syswrite, :syswrite
57
57
  def syswrite(buf)
58
- loop do
58
+ while true
59
59
  case (result = write_nonblock(buf, exception: false))
60
60
  when :wait_readable then Thread.current.backend.wait_io(io, false)
61
61
  when :wait_writable then Thread.current.backend.wait_io(io, true)
@@ -11,6 +11,10 @@ class ::Socket
11
11
  Thread.current.backend.accept(self, TCPSocket)
12
12
  end
13
13
 
14
+ def accept_loop(&block)
15
+ Thread.current.backend.accept_loop(self, TCPSocket, &block)
16
+ end
17
+
14
18
  NO_EXCEPTION = { exception: false }.freeze
15
19
 
16
20
  def connect(addr)
@@ -29,7 +33,7 @@ class ::Socket
29
33
 
30
34
  def recvfrom(maxlen, flags = 0)
31
35
  @read_buffer ||= +''
32
- loop do
36
+ while true
33
37
  result = recvfrom_nonblock(maxlen, flags, @read_buffer, **NO_EXCEPTION)
34
38
  case result
35
39
  when nil then raise IOError
@@ -41,8 +41,7 @@ class ::Thread
41
41
 
42
42
  def finalize(result)
43
43
  unless Fiber.current.children.empty?
44
- Fiber.current.terminate_all_children
45
- Fiber.current.await_all_children
44
+ Fiber.current.shutdown_all_children
46
45
  end
47
46
  @finalization_mutex.synchronize do
48
47
  @terminated = true
@@ -8,10 +8,7 @@ module Polyphony
8
8
  module Net
9
9
  class << self
10
10
  def tcp_connect(host, port, opts = {})
11
- socket = ::Socket.new(:INET, :STREAM).tap do |s|
12
- addr = ::Socket.sockaddr_in(port, host)
13
- s.connect(addr)
14
- end
11
+ socket = TCPSocket.new(host, port)
15
12
  if opts[:secure_context] || opts[:secure]
16
13
  secure_socket(socket, opts[:secure_context], opts.merge(host: host))
17
14
  else
@@ -23,7 +20,7 @@ module Polyphony
23
20
  host ||= '0.0.0.0'
24
21
  raise 'Port number not specified' unless port
25
22
 
26
- socket = socket_from_options(host, port, opts)
23
+ socket = listening_socket_from_options(host, port, opts)
27
24
  if opts[:secure_context] || opts[:secure]
28
25
  secure_server(socket, opts[:secure_context], opts)
29
26
  else
@@ -31,7 +28,7 @@ module Polyphony
31
28
  end
32
29
  end
33
30
 
34
- def socket_from_options(host, port, opts)
31
+ def listening_socket_from_options(host, port, opts)
35
32
  ::Socket.new(:INET, :STREAM).tap do |s|
36
33
  s.reuse_addr if opts[:reuse_addr]
37
34
  s.dont_linger if opts[:dont_linger]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polyphony
4
- VERSION = '0.47.5'
4
+ VERSION = '0.49.2'
5
5
  end
@@ -6,7 +6,7 @@ Gem::Specification.new do |s|
6
6
  s.licenses = ['MIT']
7
7
  s.summary = 'Fine grained concurrency for Ruby'
8
8
  s.author = 'Sharon Rosner'
9
- s.email = 'ciconia@gmail.com'
9
+ s.email = 'sharon@noteflakes.com'
10
10
  s.files = `git ls-files`.split
11
11
  s.homepage = 'https://digital-fabric.github.io/polyphony'
12
12
  s.metadata = {
@@ -4,6 +4,7 @@ require 'bundler/setup'
4
4
 
5
5
  require_relative './coverage' if ENV['COVERAGE']
6
6
 
7
+ require 'httparty'
7
8
  require 'polyphony'
8
9
 
9
10
  require 'fileutils'
@@ -56,8 +57,7 @@ class MiniTest::Test
56
57
 
57
58
  def teardown
58
59
  # trace "* teardown #{self.name}"
59
- Fiber.current.terminate_all_children
60
- Fiber.current.await_all_children
60
+ Fiber.current.shutdown_all_children
61
61
  Fiber.current.instance_variable_set(:@auto_watcher, nil)
62
62
  rescue => e
63
63
  puts e
@@ -100,12 +100,37 @@ class BackendTest < MiniTest::Test
100
100
  assert_equal [:ready, 'foo', 'bar', :done], buf
101
101
  end
102
102
 
103
+ def test_read_loop_terminate
104
+ i, o = IO.pipe
105
+
106
+ buf = []
107
+ parent = spin do
108
+ f = spin do
109
+ buf << :ready
110
+ @backend.read_loop(i) { |d| buf << d }
111
+ buf << :done
112
+ end
113
+ suspend
114
+ end
115
+
116
+ # writing always causes snoozing
117
+ o << 'foo'
118
+ sleep 0.01
119
+ o << 'bar'
120
+ sleep 0.01
121
+
122
+ parent.stop
123
+
124
+ parent.await
125
+ assert_equal [:ready, 'foo', 'bar'], buf
126
+ end
127
+
103
128
  def test_accept_loop
104
129
  server = TCPServer.new('127.0.0.1', 1234)
105
130
 
106
131
  clients = []
107
132
  server_fiber = spin do
108
- @backend.accept_loop(server) { |c| clients << c }
133
+ @backend.accept_loop(server, TCPSocket) { |c| clients << c }
109
134
  end
110
135
 
111
136
  c1 = TCPSocket.new('127.0.0.1', 1234)
@@ -639,7 +639,6 @@ class FiberTest < MiniTest::Test
639
639
  i.close
640
640
  f.await
641
641
  rescue Exception => e
642
- trace e
643
642
  o << e.class.name
644
643
  o.close
645
644
  end
@@ -1038,3 +1037,82 @@ class RestartTest < MiniTest::Test
1038
1037
  assert_equal [f, 'foo', 'bar', :done, f2, 'baz', 42, :done], buffer
1039
1038
  end
1040
1039
  end
1040
+
1041
+ class ChildrenTerminationTest < MiniTest::Test
1042
+ def test_shutdown_all_children
1043
+ f = spin do
1044
+ 1000.times { spin { suspend } }
1045
+ suspend
1046
+ end
1047
+
1048
+ snooze
1049
+ assert_equal 1000, f.children.size
1050
+
1051
+ f.shutdown_all_children
1052
+ assert_equal 0, f.children.size
1053
+ end
1054
+ end
1055
+
1056
+ class GracefulTerminationTest < MiniTest::Test
1057
+ def test_graceful_termination
1058
+ buffer = []
1059
+ f = spin do
1060
+ buffer << 1
1061
+ snooze
1062
+ buffer << 2
1063
+ sleep 3
1064
+ buffer << 3
1065
+ ensure
1066
+ buffer << 4 if Fiber.current.graceful_shutdown?
1067
+ end
1068
+
1069
+ 3.times { snooze }
1070
+ f.terminate(false)
1071
+ f.await
1072
+ assert_equal [1, 2], buffer
1073
+
1074
+ buffer = []
1075
+ f = spin do
1076
+ buffer << 1
1077
+ snooze
1078
+ buffer << 2
1079
+ sleep 3
1080
+ buffer << 3
1081
+ ensure
1082
+ buffer << 4 if Fiber.current.graceful_shutdown?
1083
+ end
1084
+
1085
+ 3.times { snooze }
1086
+ f.terminate(true)
1087
+ f.await
1088
+ assert_equal [1, 2, 4], buffer
1089
+ end
1090
+
1091
+ def test_graceful_child_shutdown
1092
+ buffer = []
1093
+ f0 = spin do
1094
+ f1 = spin do
1095
+ sleep
1096
+ ensure
1097
+ buffer << 1 if Fiber.current.graceful_shutdown?
1098
+ end
1099
+
1100
+ f2 = spin do
1101
+ sleep
1102
+ ensure
1103
+ buffer << 2 if Fiber.current.graceful_shutdown?
1104
+ end
1105
+
1106
+ sleep
1107
+ ensure
1108
+ Fiber.current.terminate_all_children(true) if Fiber.current.graceful_shutdown?
1109
+ Fiber.current.await_all_children
1110
+ end
1111
+
1112
+ 3.times { snooze }
1113
+ f0.terminate(true)
1114
+ f0.await
1115
+
1116
+ assert_equal [1, 2], buffer
1117
+ end
1118
+ end
@@ -307,6 +307,36 @@ class SpinLoopTest < MiniTest::Test
307
307
  f.stop
308
308
  assert_in_range 1..3, counter
309
309
  end
310
+
311
+ def test_spin_loop_break
312
+ i = 0
313
+ f = spin_loop do
314
+ i += 1
315
+ snooze
316
+ break if i >= 5
317
+ end
318
+ f.await
319
+ assert_equal 5, i
320
+
321
+ i = 0
322
+ f = spin_loop do
323
+ i += 1
324
+ snooze
325
+ raise StopIteration if i >= 5
326
+ end
327
+ f.await
328
+ assert_equal 5, i
329
+ end
330
+
331
+ def test_throttled_spin_loop_break
332
+ i = 0
333
+ f = spin_loop(rate: 100) do
334
+ i += 1
335
+ break if i >= 5
336
+ end
337
+ f.await
338
+ assert_equal 5, i
339
+ end
310
340
  end
311
341
 
312
342
  class SpinScopeTest < MiniTest::Test
@@ -92,6 +92,32 @@ class IOTest < MiniTest::Test
92
92
  assert_raises(EOFError) { i.readpartial(1) }
93
93
  end
94
94
 
95
+ def test_gets
96
+ i, o = IO.pipe
97
+
98
+ buf = []
99
+ f = spin do
100
+ while (l = i.gets)
101
+ buf << l
102
+ end
103
+ end
104
+
105
+ snooze
106
+ assert_equal [], buf
107
+
108
+ o << 'fab'
109
+ snooze
110
+ assert_equal [], buf
111
+
112
+ o << "ulous\n"
113
+ 10.times { snooze }
114
+ assert_equal ["fabulous\n"], buf
115
+
116
+ o.close
117
+ f.await
118
+ assert_equal ["fabulous\n"], buf
119
+ end
120
+
95
121
  def test_getc
96
122
  i, o = IO.pipe
97
123