polyphony 0.43.2 → 0.43.8

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +2 -2
  3. data/CHANGELOG.md +43 -0
  4. data/Gemfile.lock +2 -2
  5. data/README.md +2 -0
  6. data/TODO.md +2 -3
  7. data/docs/_includes/head.html +40 -0
  8. data/docs/_includes/title.html +1 -0
  9. data/docs/_user-guide/web-server.md +11 -11
  10. data/docs/getting-started/overview.md +4 -4
  11. data/docs/index.md +4 -3
  12. data/docs/main-concepts/design-principles.md +23 -34
  13. data/docs/main-concepts/fiber-scheduling.md +1 -1
  14. data/docs/polyphony-logo.png +0 -0
  15. data/examples/adapters/concurrent-ruby.rb +9 -0
  16. data/examples/core/xx-daemon.rb +14 -0
  17. data/examples/io/xx-happy-eyeballs.rb +21 -22
  18. data/examples/io/xx-zip.rb +19 -0
  19. data/examples/performance/fiber_transfer.rb +47 -0
  20. data/examples/performance/mem-usage.rb +34 -28
  21. data/examples/performance/messaging.rb +29 -0
  22. data/examples/performance/multi_snooze.rb +11 -9
  23. data/examples/xx-spin.rb +32 -0
  24. data/ext/polyphony/libev_agent.c +181 -24
  25. data/ext/polyphony/polyphony.c +0 -2
  26. data/ext/polyphony/polyphony.h +14 -7
  27. data/ext/polyphony/polyphony_ext.c +2 -2
  28. data/ext/polyphony/queue.c +168 -0
  29. data/ext/polyphony/ring_buffer.c +96 -0
  30. data/ext/polyphony/ring_buffer.h +28 -0
  31. data/ext/polyphony/thread.c +16 -8
  32. data/lib/polyphony.rb +28 -12
  33. data/lib/polyphony/core/global_api.rb +5 -3
  34. data/lib/polyphony/core/resource_pool.rb +19 -9
  35. data/lib/polyphony/core/thread_pool.rb +1 -1
  36. data/lib/polyphony/event.rb +5 -15
  37. data/lib/polyphony/extensions/core.rb +40 -0
  38. data/lib/polyphony/extensions/fiber.rb +9 -14
  39. data/lib/polyphony/extensions/io.rb +17 -16
  40. data/lib/polyphony/extensions/openssl.rb +8 -0
  41. data/lib/polyphony/extensions/socket.rb +12 -0
  42. data/lib/polyphony/version.rb +1 -1
  43. data/test/helper.rb +1 -1
  44. data/test/q.rb +24 -0
  45. data/test/test_agent.rb +3 -3
  46. data/test/test_event.rb +11 -0
  47. data/test/test_fiber.rb +3 -3
  48. data/test/test_global_api.rb +48 -15
  49. data/test/test_io.rb +24 -2
  50. data/test/test_queue.rb +39 -1
  51. data/test/test_resource_pool.rb +12 -0
  52. data/test/test_throttler.rb +6 -5
  53. data/test/test_trace.rb +18 -17
  54. metadata +15 -4
  55. data/ext/polyphony/libev_queue.c +0 -217
@@ -4,8 +4,11 @@ require 'fiber'
4
4
  require_relative './polyphony_ext'
5
5
 
6
6
  module Polyphony
7
- # Map Queue to Libev queue implementation
8
- Queue = LibevQueue
7
+ # replace core Queue class with our own
8
+ verbose = $VERBOSE
9
+ $VERBOSE = nil
10
+ Object.const_set(:Queue, Polyphony::Queue)
11
+ $VERBOSE = verbose
9
12
  end
10
13
 
11
14
  require_relative './polyphony/extensions/core'
@@ -94,17 +97,12 @@ module Polyphony
94
97
  Polyphony::Process.watch(cmd, &block)
95
98
  end
96
99
 
97
- def emit_signal_exception(exception, fiber = Thread.main.main_fiber)
98
- Thread.current.break_out_of_ev_loop(fiber, exception)
99
- end
100
-
101
- def install_terminating_signal_handler(signal, exception_class)
102
- trap(signal) { emit_signal_exception(exception_class.new) }
103
- end
104
-
105
100
  def install_terminating_signal_handlers
106
- install_terminating_signal_handler('SIGTERM', ::SystemExit)
107
- install_terminating_signal_handler('SIGINT', ::Interrupt)
101
+ trap('SIGTERM', SystemExit)
102
+ orig_trap('SIGINT') do
103
+ orig_trap('SIGINT') { exit! }
104
+ Thread.current.break_out_of_ev_loop(Thread.main.main_fiber, Interrupt.new)
105
+ end
108
106
  end
109
107
 
110
108
  def terminate_threads
@@ -114,7 +112,25 @@ module Polyphony
114
112
  threads.each(&:kill)
115
113
  threads.each(&:join)
116
114
  end
115
+
116
+ attr_accessor :original_pid
117
+
118
+ def install_at_exit_handler
119
+ @original_pid = ::Process.pid
120
+
121
+ # This at_exit handler is needed only when the original process exits. Due to
122
+ # the behaviour of fibers on fork (and especially on exit from forked
123
+ # processes,) we use a separate mechanism to terminate fibers in forked
124
+ # processes (see Polyphony.fork).
125
+ at_exit do
126
+ next unless @original_pid == ::Process.pid
127
+
128
+ Polyphony.terminate_threads
129
+ Fiber.current.shutdown_all_children
130
+ end
131
+ end
117
132
  end
118
133
  end
119
134
 
120
135
  Polyphony.install_terminating_signal_handlers
136
+ Polyphony.install_at_exit_handler
@@ -15,11 +15,13 @@ module Polyphony
15
15
  end
16
16
  end
17
17
 
18
- def cancel_after(interval, &block)
18
+ def cancel_after(interval, with_exception: Polyphony::Cancel, &block)
19
19
  fiber = ::Fiber.current
20
20
  canceller = spin do
21
21
  sleep interval
22
- fiber.schedule Polyphony::Cancel.new
22
+ exception = with_exception.is_a?(Class) ?
23
+ with_exception.new : RuntimeError.new(with_exception)
24
+ fiber.schedule exception
23
25
  end
24
26
  block ? cancel_after_wrap_block(canceller, &block) : canceller
25
27
  end
@@ -101,7 +103,7 @@ module Polyphony
101
103
 
102
104
  def sleep_forever
103
105
  Thread.current.agent.ref
104
- suspend
106
+ loop { sleep 60 }
105
107
  ensure
106
108
  Thread.current.agent.unref
107
109
  end
@@ -13,6 +13,7 @@ module Polyphony
13
13
 
14
14
  @stock = []
15
15
  @queue = []
16
+ @acquired_resources = {}
16
17
 
17
18
  @limit = opts[:limit] || 4
18
19
  @size = 0
@@ -23,16 +24,25 @@ module Polyphony
23
24
  end
24
25
 
25
26
  def acquire
26
- Thread.current.agent.ref
27
- resource = wait_for_resource
28
- return unless resource
29
-
30
- yield resource
31
- ensure
32
- Thread.current.agent.unref
33
- release(resource) if resource
27
+ fiber = Fiber.current
28
+ if @acquired_resources[fiber]
29
+ yield @acquired_resources[fiber]
30
+ else
31
+ begin
32
+ Thread.current.agent.ref
33
+ resource = wait_for_resource
34
+ return unless resource
35
+
36
+ @acquired_resources[fiber] = resource
37
+ yield resource
38
+ ensure
39
+ @acquired_resources.delete fiber
40
+ Thread.current.agent.unref
41
+ release(resource) if resource
42
+ end
43
+ end
34
44
  end
35
-
45
+
36
46
  def wait_for_resource
37
47
  fiber = Fiber.current
38
48
  @queue << fiber
@@ -49,7 +49,7 @@ module Polyphony
49
49
  end
50
50
 
51
51
  def run_queued_task
52
- (block, watcher) = @task_queue.pop
52
+ (block, watcher) = @task_queue.shift
53
53
  result = block.()
54
54
  watcher&.signal(result)
55
55
  rescue Exception => e
@@ -3,25 +3,15 @@
3
3
  module Polyphony
4
4
  # Event watcher for thread-safe synchronisation
5
5
  class Event
6
- def initialize
7
- @i, @o = IO.pipe
8
- end
9
-
10
6
  def await
11
- Thread.current.agent.read(@i, +'', 8192, false)
12
- raise @value if @value.is_a?(Exception)
13
-
14
- @value
15
- end
16
-
17
- def await_no_raise
18
- Thread.current.agent.read(@i, +'', 8192, false)
19
- @value
7
+ @fiber = Fiber.current
8
+ Thread.current.agent.wait_event(true)
20
9
  end
21
10
 
22
11
  def signal(value = nil)
23
- @value = value
24
- Thread.current.agent.write(@o, '1')
12
+ @fiber&.schedule(value)
13
+ ensure
14
+ @fiber = nil
25
15
  end
26
16
  end
27
17
  end
@@ -57,6 +57,12 @@ module ::Process
57
57
  fiber.define_singleton_method(:pid) { pid }
58
58
  fiber
59
59
  end
60
+
61
+ alias_method :orig_daemon, :daemon
62
+ def daemon(*args)
63
+ orig_daemon(*args)
64
+ Polyphony.original_pid = Process.pid
65
+ end
60
66
  end
61
67
  end
62
68
 
@@ -101,6 +107,15 @@ module ::Kernel
101
107
  $stdin.gets
102
108
  end
103
109
 
110
+ alias_method :orig_p, :p
111
+ def p(*args)
112
+ strs = args.inject([]) do |m, a|
113
+ m << a.inspect << "\n"
114
+ end
115
+ STDOUT.write *strs
116
+ args.size == 1 ? args.first : args
117
+ end
118
+
104
119
  alias_method :orig_system, :system
105
120
  def system(*args)
106
121
  Open3.popen2(*args) do |i, o, _t|
@@ -120,6 +135,31 @@ module ::Kernel
120
135
  break
121
136
  end
122
137
  end
138
+
139
+ alias_method :orig_trap, :trap
140
+ def trap(sig, command = nil, &block)
141
+ return orig_trap(sig, command) if command.is_a? String
142
+
143
+ block = command if command.respond_to?(:call) && !block
144
+ exception = command.is_a?(Class) && command.new
145
+
146
+ # The signal trap can be invoked at any time, including while the system
147
+ # agent is blocking while polling for events. In order to deal with this
148
+ # correctly, we spin a fiber that will run the signal handler code, then
149
+ # call break_out_of_ev_loop, which will put the fiber at the front of the
150
+ # run queue, then wake up the system agent.
151
+ #
152
+ # If the command argument is an exception class however, it will be raised
153
+ # directly in the context of the main fiber.
154
+ orig_trap(sig) do
155
+ if exception
156
+ Thread.current.break_out_of_ev_loop(Thread.main.main_fiber, exception)
157
+ else
158
+ fiber = spin { snooze; block.call }
159
+ Thread.current.break_out_of_ev_loop(fiber, nil)
160
+ end
161
+ end
162
+ end
123
163
  end
124
164
 
125
165
  # Override Timeout to use cancel scope
@@ -189,7 +189,7 @@ module Polyphony
189
189
  end
190
190
 
191
191
  def receive_pending
192
- @mailbox.shift_each
192
+ @mailbox.shift_all
193
193
  end
194
194
  end
195
195
 
@@ -221,7 +221,14 @@ module Polyphony
221
221
  def await_all_children
222
222
  return unless @children && !@children.empty?
223
223
 
224
- Fiber.await(*@children.keys)
224
+ @results = @children.dup
225
+ @on_child_done = proc do |c, r|
226
+ @results[c] = r
227
+ self.schedule if @children.empty?
228
+ end
229
+ suspend
230
+ @on_child_done = nil
231
+ @results.values
225
232
  end
226
233
 
227
234
  def shutdown_all_children
@@ -370,15 +377,3 @@ class ::Fiber
370
377
  end
371
378
 
372
379
  Fiber.current.setup_main_fiber
373
-
374
- # This at_exit handler is needed only when the original process exits. Due to
375
- # the behaviour of fibers on fork (and especially on exit from forked
376
- # processes,) we use a separate mechanism to terminate fibers in forked
377
- # processes (see Polyphony.fork).
378
- orig_pid = Process.pid
379
- at_exit do
380
- next unless orig_pid == Process.pid
381
-
382
- Polyphony.terminate_threads
383
- Fiber.current.shutdown_all_children
384
- end
@@ -97,7 +97,7 @@ class ::IO
97
97
  # end
98
98
 
99
99
  alias_method :orig_read, :read
100
- def read(len = 1 << 30)
100
+ def read(len = nil)
101
101
  @read_buffer ||= +''
102
102
  result = Thread.current.agent.read(self, @read_buffer, len, true)
103
103
  return nil unless result
@@ -108,19 +108,23 @@ class ::IO
108
108
  end
109
109
 
110
110
  alias_method :orig_readpartial, :read
111
- def readpartial(len)
111
+ def readpartial(len, str = nil)
112
112
  @read_buffer ||= +''
113
113
  result = Thread.current.agent.read(self, @read_buffer, len, false)
114
114
  raise EOFError unless result
115
115
 
116
- already_read = @read_buffer
116
+ if str
117
+ str << @read_buffer
118
+ else
119
+ str = @read_buffer
120
+ end
117
121
  @read_buffer = +''
118
- already_read
122
+ str
119
123
  end
120
124
 
121
125
  alias_method :orig_write, :write
122
- def write(str)
123
- Thread.current.agent.write(self, str)
126
+ def write(str, *args)
127
+ Thread.current.agent.write(self, str, *args)
124
128
  end
125
129
 
126
130
  alias_method :orig_write_chevron, :<<
@@ -166,16 +170,13 @@ class ::IO
166
170
  return
167
171
  end
168
172
 
169
- s = args.each_with_object(+'') do |a, str|
170
- if a.is_a?(Array)
171
- a.each { |a2| str << a2.to_s << "\n" }
172
- else
173
- a = a.to_s
174
- str << a
175
- str << "\n" unless a =~ /\n$/
176
- end
173
+ strs = args.inject([]) do |m, a|
174
+ a = a.to_s
175
+ m << a
176
+ m << "\n" unless a =~ /\n$/
177
+ m
177
178
  end
178
- write s
179
+ write *strs
179
180
  nil
180
181
  end
181
182
 
@@ -193,7 +194,7 @@ class ::IO
193
194
 
194
195
  alias_method :orig_write_nonblock, :write_nonblock
195
196
  def write_nonblock(string, _options = {})
196
- write(string, 0)
197
+ write(string)
197
198
  end
198
199
 
199
200
  alias_method :orig_read_nonblock, :read_nonblock
@@ -5,6 +5,12 @@ require_relative './socket'
5
5
 
6
6
  # Open ssl socket helper methods (to make it compatible with Socket API)
7
7
  class ::OpenSSL::SSL::SSLSocket
8
+ alias_method :orig_initialize, :initialize
9
+ def initialize(socket, context = nil)
10
+ socket = socket.respond_to?(:io) ? socket.io || socket : socket
11
+ context ? orig_initialize(socket, context) : orig_initialize(socket)
12
+ end
13
+
8
14
  def dont_linger
9
15
  io.dont_linger
10
16
  end
@@ -35,6 +41,7 @@ class ::OpenSSL::SSL::SSLSocket
35
41
  loop do
36
42
  case (result = read_nonblock(maxlen, buf, exception: false))
37
43
  when :wait_readable then Thread.current.agent.wait_io(io, false)
44
+ when :wait_writable then Thread.current.agent.wait_io(io, true)
38
45
  else return result
39
46
  end
40
47
  end
@@ -44,6 +51,7 @@ class ::OpenSSL::SSL::SSLSocket
44
51
  def syswrite(buf)
45
52
  loop do
46
53
  case (result = write_nonblock(buf, exception: false))
54
+ when :wait_readable then Thread.current.agent.wait_io(io, false)
47
55
  when :wait_writable then Thread.current.agent.wait_io(io, true)
48
56
  else
49
57
  return result
@@ -5,6 +5,16 @@ require 'socket'
5
5
  require_relative './io'
6
6
  require_relative '../core/thread_pool'
7
7
 
8
+ class ::BasicSocket
9
+ def write_nonblock(string, _options = {})
10
+ write(string)
11
+ end
12
+
13
+ def read_nonblock(maxlen, str = nil, _options = {})
14
+ readpartial(maxlen, str)
15
+ end
16
+ end
17
+
8
18
  # Socket overrides (eventually rewritten in C)
9
19
  class ::Socket
10
20
  def accept
@@ -77,6 +87,8 @@ end
77
87
  class ::TCPSocket
78
88
  NO_EXCEPTION = { exception: false }.freeze
79
89
 
90
+ attr_reader :io
91
+
80
92
  def initialize(remote_host, remote_port, local_host = nil, local_port = nil)
81
93
  @io = Socket.new Socket::AF_INET, Socket::SOCK_STREAM
82
94
  if local_host && local_port
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polyphony
4
- VERSION = '0.43.2'
4
+ VERSION = '0.43.8'
5
5
  end
@@ -32,7 +32,7 @@ class MiniTest::Test
32
32
  Fiber.current.setup_main_fiber
33
33
  Fiber.current.instance_variable_set(:@auto_watcher, nil)
34
34
  Thread.current.agent = Polyphony::LibevAgent.new
35
- sleep 0
35
+ sleep 0 # apparently this helps with timer accuracy
36
36
  end
37
37
 
38
38
  def teardown
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'fiber'
5
+ require_relative '../lib/polyphony_ext'
6
+
7
+ queue = Polyphony::LibevQueue.new
8
+
9
+ queue.push :a
10
+ queue.push :b
11
+ queue.push :c
12
+ p [queue.shift_no_wait]
13
+ queue.push :d
14
+ p [queue.shift_no_wait]
15
+ p [queue.shift_no_wait]
16
+ p [queue.shift_no_wait]
17
+ p [queue.shift_no_wait]
18
+
19
+ queue.unshift :e
20
+ p [queue.shift_no_wait]
21
+
22
+ queue.push :f
23
+ queue.push :g
24
+ p [queue.shift_no_wait]
@@ -97,7 +97,7 @@ class AgentTest < MiniTest::Test
97
97
  o.close
98
98
 
99
99
  # read_loop will snooze after every read
100
- 4.times { snooze }
100
+ 6.times { snooze }
101
101
 
102
102
  assert_equal [:ready, 'foo', 'bar', :done], buf
103
103
  end
@@ -111,12 +111,12 @@ class AgentTest < MiniTest::Test
111
111
  end
112
112
 
113
113
  c1 = TCPSocket.new('127.0.0.1', 1234)
114
- snooze
114
+ 10.times { snooze }
115
115
 
116
116
  assert_equal 1, clients.size
117
117
 
118
118
  c2 = TCPSocket.new('127.0.0.1', 1234)
119
- 2.times { snooze }
119
+ 10.times { snooze }
120
120
 
121
121
  assert_equal 2, clients.size
122
122