polyphony 0.47.3 → 0.49.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +318 -295
  3. data/Gemfile.lock +1 -1
  4. data/LICENSE +1 -1
  5. data/TODO.md +40 -27
  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/io/unix_socket.rb +26 -0
  10. data/examples/performance/line_splitting.rb +34 -0
  11. data/examples/performance/loop.rb +32 -0
  12. data/examples/performance/thread-vs-fiber/polyphony_server.rb +6 -0
  13. data/ext/polyphony/backend_common.h +2 -2
  14. data/ext/polyphony/backend_io_uring.c +42 -83
  15. data/ext/polyphony/backend_libev.c +32 -77
  16. data/ext/polyphony/event.c +1 -1
  17. data/ext/polyphony/polyphony.c +0 -2
  18. data/ext/polyphony/polyphony.h +5 -4
  19. data/ext/polyphony/queue.c +2 -2
  20. data/ext/polyphony/thread.c +9 -28
  21. data/lib/polyphony.rb +1 -0
  22. data/lib/polyphony/adapters/postgres.rb +3 -3
  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 +68 -16
  31. data/lib/polyphony/version.rb +1 -1
  32. data/polyphony.gemspec +1 -1
  33. data/test/helper.rb +1 -0
  34. data/test/test_backend.rb +1 -1
  35. data/test/test_fiber.rb +64 -1
  36. data/test/test_global_api.rb +30 -0
  37. data/test/test_io.rb +26 -0
  38. data/test/test_socket.rb +32 -6
  39. data/test/test_supervise.rb +2 -1
  40. data/test/test_timer.rb +122 -0
  41. metadata +9 -4
  42. data/ext/polyphony/backend.h +0 -26
@@ -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
@@ -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
@@ -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.3'
4
+ VERSION = '0.49.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)
@@ -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