polyphony 0.47.2 → 0.48.0

Sign up to get free protection for your applications and to get access to all the features.
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