polyphony 0.47.2 → 0.48.0

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +317 -293
  3. data/Gemfile.lock +1 -1
  4. data/TODO.md +46 -3
  5. data/examples/core/supervisor.rb +3 -3
  6. data/examples/core/worker-thread.rb +3 -4
  7. data/examples/io/tcp_proxy.rb +32 -0
  8. data/examples/io/unix_socket.rb +26 -0
  9. data/examples/performance/line_splitting.rb +34 -0
  10. data/examples/performance/loop.rb +32 -0
  11. data/examples/performance/thread-vs-fiber/polyphony_server.rb +6 -0
  12. data/ext/polyphony/backend_common.h +2 -2
  13. data/ext/polyphony/backend_io_uring.c +42 -83
  14. data/ext/polyphony/backend_libev.c +32 -77
  15. data/ext/polyphony/event.c +1 -1
  16. data/ext/polyphony/polyphony.c +0 -2
  17. data/ext/polyphony/polyphony.h +5 -4
  18. data/ext/polyphony/queue.c +2 -2
  19. data/ext/polyphony/thread.c +9 -28
  20. data/lib/polyphony/adapters/postgres.rb +3 -3
  21. data/lib/polyphony/core/global_api.rb +19 -16
  22. data/lib/polyphony/core/resource_pool.rb +1 -12
  23. data/lib/polyphony/core/thread_pool.rb +3 -1
  24. data/lib/polyphony/core/throttler.rb +1 -1
  25. data/lib/polyphony/extensions/fiber.rb +34 -8
  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 +68 -16
  29. data/lib/polyphony/version.rb +1 -1
  30. data/polyphony.gemspec +1 -1
  31. data/test/helper.rb +1 -0
  32. data/test/test_backend.rb +1 -1
  33. data/test/test_fiber.rb +64 -0
  34. data/test/test_global_api.rb +41 -1
  35. data/test/test_io.rb +26 -0
  36. data/test/test_resource_pool.rb +0 -21
  37. data/test/test_signal.rb +18 -0
  38. data/test/test_socket.rb +27 -2
  39. data/test/test_supervise.rb +2 -1
  40. metadata +7 -4
  41. data/ext/polyphony/backend.h +0 -26
@@ -58,18 +58,7 @@ module Polyphony
58
58
  # Discards the currently-acquired resource
59
59
  # instead of returning it to the pool when done.
60
60
  def discard!
61
- if block_given?
62
- @size.times do
63
- acquire do |r|
64
- next if yield(r)
65
-
66
- @size -= 1
67
- @acquired_resources.delete(Fiber.current)
68
- end
69
- end
70
- else
71
- @size -= 1 if @acquired_resources.delete(Fiber.current)
72
- end
61
+ @size -= 1 if @acquired_resources.delete(Fiber.current)
73
62
  end
74
63
 
75
64
  def preheat!
@@ -45,7 +45,9 @@ module Polyphony
45
45
  end
46
46
 
47
47
  def thread_loop
48
- loop { run_queued_task }
48
+ while true
49
+ run_queued_task
50
+ end
49
51
  end
50
52
 
51
53
  def run_queued_task
@@ -15,7 +15,7 @@ module Polyphony
15
15
  Thread.current.backend.sleep(delta) if delta > 0
16
16
  yield self
17
17
 
18
- loop do
18
+ while true
19
19
  @next_time += @min_dt
20
20
  break if @next_time > now
21
21
  end
@@ -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,20 +75,32 @@ 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
81
+ rescue Polyphony::MoveOn
82
+ # generated in #supervise_perform to stop supervisor
70
83
  ensure
71
84
  @on_child_done = nil
72
85
  end
73
86
 
74
87
  def supervise_perform(opts)
75
88
  fiber = receive
76
- restart_fiber(fiber, opts) if fiber
89
+ if fiber && opts[:restart]
90
+ restart_fiber(fiber, opts)
91
+ elsif Fiber.current.children.empty?
92
+ Fiber.current.stop
93
+ end
77
94
  rescue Polyphony::Restart
78
95
  restart_all_children
79
96
  rescue Exception => e
80
97
  Kernel.raise e if e.source_fiber.nil? || e.source_fiber == self
81
98
 
82
- restart_fiber(e.source_fiber, opts)
99
+ if opts[:restart]
100
+ restart_fiber(e.source_fiber, opts)
101
+ elsif Fiber.current.children.empty?
102
+ Fiber.current.stop
103
+ end
83
104
  end
84
105
 
85
106
  def restart_fiber(fiber, opts)
@@ -173,6 +194,7 @@ module Polyphony
173
194
  # signals (see also the patched `Kernel#trap`)
174
195
  def schedule_priority_oob_fiber(&block)
175
196
  f = Fiber.new do
197
+ Fiber.current.setup_raw
176
198
  block.call
177
199
  rescue Exception => e
178
200
  Thread.current.schedule_and_wakeup(Thread.main.main_fiber, e)
@@ -199,11 +221,14 @@ module Polyphony
199
221
  @on_child_done&.(child_fiber, result)
200
222
  end
201
223
 
202
- def terminate_all_children
224
+ def terminate_all_children(graceful = false)
203
225
  return unless @children
204
226
 
205
227
  e = Polyphony::Terminate.new
206
- @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
207
232
  end
208
233
 
209
234
  def await_all_children
@@ -219,8 +244,8 @@ module Polyphony
219
244
  results.values
220
245
  end
221
246
 
222
- def shutdown_all_children
223
- terminate_all_children
247
+ def shutdown_all_children(graceful = false)
248
+ terminate_all_children(graceful)
224
249
  await_all_children
225
250
  end
226
251
  end
@@ -262,6 +287,7 @@ module Polyphony
262
287
  # allows the fiber to be scheduled and to receive messages.
263
288
  def setup_raw
264
289
  @thread = Thread.current
290
+ @running = true
265
291
  end
266
292
 
267
293
  def setup_main_fiber
@@ -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)
@@ -8,7 +8,11 @@ require_relative '../core/thread_pool'
8
8
  # Socket overrides (eventually rewritten in C)
9
9
  class ::Socket
10
10
  def accept
11
- Thread.current.backend.accept(self)
11
+ Thread.current.backend.accept(self, TCPSocket)
12
+ end
13
+
14
+ def accept_loop(&block)
15
+ Thread.current.backend.accept_loop(self, TCPSocket, &block)
12
16
  end
13
17
 
14
18
  NO_EXCEPTION = { exception: false }.freeze
@@ -19,17 +23,7 @@ class ::Socket
19
23
  end
20
24
 
21
25
  def recv(maxlen, flags = 0, outbuf = nil)
22
- Thread.current.backend.recv(self, buf || +'', maxlen)
23
- # outbuf ||= +''
24
- # loop do
25
- # result = recv_nonblock(maxlen, flags, outbuf, **NO_EXCEPTION)
26
- # case result
27
- # when nil then raise IOError
28
- # when :wait_readable then Thread.current.backend.wait_io(self, false)
29
- # else
30
- # return result
31
- # end
32
- # end
26
+ Thread.current.backend.recv(self, outbuf || +'', maxlen)
33
27
  end
34
28
 
35
29
  def recv_loop(&block)
@@ -39,7 +33,7 @@ class ::Socket
39
33
 
40
34
  def recvfrom(maxlen, flags = 0)
41
35
  @read_buffer ||= +''
42
- loop do
36
+ while true
43
37
  result = recvfrom_nonblock(maxlen, flags, @read_buffer, **NO_EXCEPTION)
44
38
  case result
45
39
  when nil then raise IOError
@@ -144,7 +138,7 @@ class ::TCPSocket
144
138
  end
145
139
 
146
140
  def recv(maxlen, flags = 0, outbuf = nil)
147
- Thread.current.backend.recv(self, buf || +'', maxlen)
141
+ Thread.current.backend.recv(self, outbuf || +'', maxlen)
148
142
  end
149
143
 
150
144
  def recv_loop(&block)
@@ -198,11 +192,12 @@ class ::TCPServer
198
192
 
199
193
  alias_method :orig_accept, :accept
200
194
  def accept
201
- @io.accept
195
+ Thread.current.backend.accept(@io, TCPSocket)
196
+ # @io.accept
202
197
  end
203
198
 
204
199
  def accept_loop(&block)
205
- Thread.current.backend.accept_loop(@io, &block)
200
+ Thread.current.backend.accept_loop(@io, TCPSocket, &block)
206
201
  end
207
202
 
208
203
  alias_method :orig_close, :close
@@ -210,3 +205,60 @@ class ::TCPServer
210
205
  @io.close
211
206
  end
212
207
  end
208
+
209
+ class ::UNIXServer
210
+ alias_method :orig_accept, :accept
211
+ def accept
212
+ Thread.current.backend.accept(self, UNIXSocket)
213
+ end
214
+
215
+ def accept_loop(&block)
216
+ Thread.current.backend.accept_loop(self, UNIXSocket, &block)
217
+ end
218
+ end
219
+
220
+ class ::UNIXSocket
221
+ def recv(maxlen, flags = 0, outbuf = nil)
222
+ Thread.current.backend.recv(self, outbuf || +'', maxlen)
223
+ end
224
+
225
+ def recv_loop(&block)
226
+ Thread.current.backend.recv_loop(self, &block)
227
+ end
228
+ alias_method :read_loop, :recv_loop
229
+
230
+ def send(mesg, flags = 0)
231
+ Thread.current.backend.send(self, mesg)
232
+ end
233
+
234
+ def write(str, *args)
235
+ if args.empty?
236
+ Thread.current.backend.send(self, str)
237
+ else
238
+ Thread.current.backend.send(self, str + args.join)
239
+ end
240
+ end
241
+ alias_method :<<, :write
242
+
243
+ def readpartial(maxlen, str = nil)
244
+ @read_buffer ||= +''
245
+ result = Thread.current.backend.recv(self, @read_buffer, maxlen)
246
+ raise EOFError unless result
247
+
248
+ if str
249
+ str << @read_buffer
250
+ else
251
+ str = @read_buffer
252
+ end
253
+ @read_buffer = +''
254
+ str
255
+ end
256
+
257
+ def read_nonblock(len, str = nil, exception: true)
258
+ @io.read_nonblock(len, str, exception: exception)
259
+ end
260
+
261
+ def write_nonblock(buf, exception: true)
262
+ @io.write_nonblock(buf, exception: exception)
263
+ end
264
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polyphony
4
- VERSION = '0.47.2'
4
+ VERSION = '0.48.0'
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'
@@ -105,7 +105,7 @@ class BackendTest < MiniTest::Test
105
105
 
106
106
  clients = []
107
107
  server_fiber = spin do
108
- @backend.accept_loop(server) { |c| clients << c }
108
+ @backend.accept_loop(server, TCPSocket) { |c| clients << c }
109
109
  end
110
110
 
111
111
  c1 = TCPSocket.new('127.0.0.1', 1234)
@@ -1038,3 +1038,67 @@ class RestartTest < MiniTest::Test
1038
1038
  assert_equal [f, 'foo', 'bar', :done, f2, 'baz', 42, :done], buffer
1039
1039
  end
1040
1040
  end
1041
+
1042
+ class GracefulTerminationTest < MiniTest::Test
1043
+ def test_graceful_termination
1044
+ buffer = []
1045
+ f = spin do
1046
+ buffer << 1
1047
+ snooze
1048
+ buffer << 2
1049
+ sleep 3
1050
+ buffer << 3
1051
+ ensure
1052
+ buffer << 4 if Fiber.current.graceful_shutdown?
1053
+ end
1054
+
1055
+ 3.times { snooze }
1056
+ f.terminate(false)
1057
+ f.await
1058
+ assert_equal [1, 2], buffer
1059
+
1060
+ buffer = []
1061
+ f = spin do
1062
+ buffer << 1
1063
+ snooze
1064
+ buffer << 2
1065
+ sleep 3
1066
+ buffer << 3
1067
+ ensure
1068
+ buffer << 4 if Fiber.current.graceful_shutdown?
1069
+ end
1070
+
1071
+ 3.times { snooze }
1072
+ f.terminate(true)
1073
+ f.await
1074
+ assert_equal [1, 2, 4], buffer
1075
+ end
1076
+
1077
+ def test_graceful_child_shutdown
1078
+ buffer = []
1079
+ f0 = spin do
1080
+ f1 = spin do
1081
+ sleep
1082
+ ensure
1083
+ buffer << 1 if Fiber.current.graceful_shutdown?
1084
+ end
1085
+
1086
+ f2 = spin do
1087
+ sleep
1088
+ ensure
1089
+ buffer << 2 if Fiber.current.graceful_shutdown?
1090
+ end
1091
+
1092
+ sleep
1093
+ ensure
1094
+ Fiber.current.terminate_all_children(true) if Fiber.current.graceful_shutdown?
1095
+ Fiber.current.await_all_children
1096
+ end
1097
+
1098
+ 3.times { snooze }
1099
+ f0.terminate(true)
1100
+ f0.await
1101
+
1102
+ assert_equal [1, 2], buffer
1103
+ end
1104
+ end
@@ -210,7 +210,7 @@ class CancelAfterTest < MiniTest::Test
210
210
  sleep 0.007
211
211
  end
212
212
  t1 = Time.now
213
- assert_in_range 0.014..0.02, t1 - t0
213
+ assert_in_range 0.014..0.024, t1 - t0
214
214
  end
215
215
 
216
216
  class CustomException < Exception
@@ -297,6 +297,46 @@ class SpinLoopTest < MiniTest::Test
297
297
  f.stop
298
298
  assert_in_range 1..3, counter
299
299
  end
300
+
301
+ def test_spin_loop_with_interval
302
+ buffer = []
303
+ counter = 0
304
+ t0 = Time.now
305
+ f = spin_loop(interval: 0.01) { buffer << (counter += 1) }
306
+ sleep 0.02
307
+ f.stop
308
+ assert_in_range 1..3, counter
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
300
340
  end
301
341
 
302
342
  class SpinScopeTest < MiniTest::Test