polyphony 0.47.4 → 0.49.1

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 +321 -296
  3. data/Gemfile.lock +1 -1
  4. data/LICENSE +1 -1
  5. data/TODO.md +38 -29
  6. data/examples/core/supervisor.rb +3 -3
  7. data/examples/core/worker-thread.rb +3 -4
  8. data/examples/io/tcp_proxy.rb +32 -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 -22
  13. data/ext/polyphony/backend_io_uring.c +38 -78
  14. data/ext/polyphony/backend_libev.c +22 -67
  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.rb +2 -1
  21. data/lib/polyphony/adapters/postgres.rb +3 -3
  22. data/lib/polyphony/adapters/process.rb +2 -0
  23. data/lib/polyphony/core/global_api.rb +14 -2
  24. data/lib/polyphony/core/thread_pool.rb +3 -1
  25. data/lib/polyphony/core/throttler.rb +1 -1
  26. data/lib/polyphony/core/timer.rb +72 -0
  27. data/lib/polyphony/extensions/fiber.rb +32 -8
  28. data/lib/polyphony/extensions/io.rb +8 -14
  29. data/lib/polyphony/extensions/openssl.rb +4 -4
  30. data/lib/polyphony/extensions/socket.rb +13 -10
  31. data/lib/polyphony/net.rb +3 -6
  32. data/lib/polyphony/version.rb +1 -1
  33. data/polyphony.gemspec +1 -1
  34. data/test/helper.rb +1 -0
  35. data/test/test_backend.rb +1 -1
  36. data/test/test_fiber.rb +64 -1
  37. data/test/test_global_api.rb +30 -0
  38. data/test/test_io.rb +26 -0
  39. data/test/test_socket.rb +32 -6
  40. data/test/test_supervise.rb +2 -1
  41. data/test/test_timer.rb +124 -0
  42. metadata +8 -4
  43. data/ext/polyphony/backend.h +0 -26
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polyphony
4
+ # Implements a common timer for running multiple timeouts
5
+ class Timer
6
+ def initialize(resolution:)
7
+ @fiber = spin_loop(interval: resolution) { update }
8
+ @timeouts = {}
9
+ end
10
+
11
+ def stop
12
+ @fiber.stop
13
+ end
14
+
15
+ def cancel_after(duration, with_exception: Polyphony::Cancel)
16
+ fiber = Fiber.current
17
+ @timeouts[fiber] = {
18
+ duration: duration,
19
+ target_stamp: Time.now + duration,
20
+ exception: with_exception
21
+ }
22
+ yield
23
+ ensure
24
+ @timeouts.delete(fiber)
25
+ end
26
+
27
+ def move_on_after(duration, with_value: nil)
28
+ fiber = Fiber.current
29
+ @timeouts[fiber] = {
30
+ duration: duration,
31
+ target_stamp: Time.now + duration,
32
+ value: with_value
33
+ }
34
+ yield
35
+ rescue Polyphony::MoveOn => e
36
+ e.value
37
+ ensure
38
+ @timeouts.delete(fiber)
39
+ end
40
+
41
+ def reset
42
+ record = @timeouts[Fiber.current]
43
+ return unless record
44
+
45
+ record[:target_stamp] = Time.now + record[:duration]
46
+ end
47
+
48
+ private
49
+
50
+ def timeout_exception(record)
51
+ case (exception = record[:exception])
52
+ when Class then exception.new
53
+ when Array then exception[0].new(exception[1])
54
+ when nil then Polyphony::MoveOn.new(record[:value])
55
+ else RuntimeError.new(exception)
56
+ end
57
+ end
58
+
59
+ def update
60
+ now = Time.now
61
+ # elapsed = nil
62
+ @timeouts.each do |fiber, record|
63
+ next if record[:target_stamp] > now
64
+
65
+ exception = timeout_exception(record)
66
+ # (elapsed ||= []) << fiber
67
+ fiber.schedule exception
68
+ end
69
+ # elapsed&.each { |f| @timeouts.delete(f) }
70
+ end
71
+ end
72
+ 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)
@@ -200,11 +221,14 @@ module Polyphony
200
221
  @on_child_done&.(child_fiber, result)
201
222
  end
202
223
 
203
- def terminate_all_children
224
+ def terminate_all_children(graceful = false)
204
225
  return unless @children
205
226
 
206
227
  e = Polyphony::Terminate.new
207
- @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
208
232
  end
209
233
 
210
234
  def await_all_children
@@ -220,8 +244,8 @@ module Polyphony
220
244
  results.values
221
245
  end
222
246
 
223
- def shutdown_all_children
224
- terminate_all_children
247
+ def shutdown_all_children(graceful = false)
248
+ terminate_all_children(graceful)
225
249
  await_all_children
226
250
  end
227
251
  end
@@ -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
@@ -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
@@ -134,7 +138,7 @@ class ::TCPSocket
134
138
  end
135
139
 
136
140
  def recv(maxlen, flags = 0, outbuf = nil)
137
- Thread.current.backend.recv(self, buf || +'', maxlen)
141
+ Thread.current.backend.recv(self, outbuf || +'', maxlen)
138
142
  end
139
143
 
140
144
  def recv_loop(&block)
@@ -188,11 +192,12 @@ class ::TCPServer
188
192
 
189
193
  alias_method :orig_accept, :accept
190
194
  def accept
191
- @io.accept
195
+ Thread.current.backend.accept(@io, TCPSocket)
196
+ # @io.accept
192
197
  end
193
198
 
194
199
  def accept_loop(&block)
195
- Thread.current.backend.accept_loop(@io, &block)
200
+ Thread.current.backend.accept_loop(@io, TCPSocket, &block)
196
201
  end
197
202
 
198
203
  alias_method :orig_close, :close
@@ -204,19 +209,17 @@ end
204
209
  class ::UNIXServer
205
210
  alias_method :orig_accept, :accept
206
211
  def accept
207
- Thread.current.backend.accept(self)
212
+ Thread.current.backend.accept(self, UNIXSocket)
208
213
  end
209
214
 
210
215
  def accept_loop(&block)
211
- Thread.current.backend.accept_loop(self, &block)
216
+ Thread.current.backend.accept_loop(self, UNIXSocket, &block)
212
217
  end
213
-
214
-
215
218
  end
216
219
 
217
220
  class ::UNIXSocket
218
221
  def recv(maxlen, flags = 0, outbuf = nil)
219
- Thread.current.backend.recv(self, buf || +'', maxlen)
222
+ Thread.current.backend.recv(self, outbuf || +'', maxlen)
220
223
  end
221
224
 
222
225
  def recv_loop(&block)
@@ -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.4'
4
+ VERSION = '0.49.1'
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)
@@ -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,67 @@ 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 GracefulTerminationTest < MiniTest::Test
1042
+ def test_graceful_termination
1043
+ buffer = []
1044
+ f = spin do
1045
+ buffer << 1
1046
+ snooze
1047
+ buffer << 2
1048
+ sleep 3
1049
+ buffer << 3
1050
+ ensure
1051
+ buffer << 4 if Fiber.current.graceful_shutdown?
1052
+ end
1053
+
1054
+ 3.times { snooze }
1055
+ f.terminate(false)
1056
+ f.await
1057
+ assert_equal [1, 2], buffer
1058
+
1059
+ buffer = []
1060
+ f = spin do
1061
+ buffer << 1
1062
+ snooze
1063
+ buffer << 2
1064
+ sleep 3
1065
+ buffer << 3
1066
+ ensure
1067
+ buffer << 4 if Fiber.current.graceful_shutdown?
1068
+ end
1069
+
1070
+ 3.times { snooze }
1071
+ f.terminate(true)
1072
+ f.await
1073
+ assert_equal [1, 2, 4], buffer
1074
+ end
1075
+
1076
+ def test_graceful_child_shutdown
1077
+ buffer = []
1078
+ f0 = spin do
1079
+ f1 = spin do
1080
+ sleep
1081
+ ensure
1082
+ buffer << 1 if Fiber.current.graceful_shutdown?
1083
+ end
1084
+
1085
+ f2 = spin do
1086
+ sleep
1087
+ ensure
1088
+ buffer << 2 if Fiber.current.graceful_shutdown?
1089
+ end
1090
+
1091
+ sleep
1092
+ ensure
1093
+ Fiber.current.terminate_all_children(true) if Fiber.current.graceful_shutdown?
1094
+ Fiber.current.await_all_children
1095
+ end
1096
+
1097
+ 3.times { snooze }
1098
+ f0.terminate(true)
1099
+ f0.await
1100
+
1101
+ assert_equal [1, 2], buffer
1102
+ end
1103
+ 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