polyphony 0.47.3 → 0.49.0

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