polyphony 0.43.3 → 0.43.9

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +1 -1
  3. data/CHANGELOG.md +44 -0
  4. data/Gemfile.lock +1 -1
  5. data/README.md +21 -4
  6. data/TODO.md +1 -2
  7. data/bin/stress.rb +28 -0
  8. data/docs/_includes/head.html +40 -0
  9. data/docs/_includes/title.html +1 -0
  10. data/docs/_user-guide/web-server.md +11 -11
  11. data/docs/getting-started/overview.md +4 -4
  12. data/docs/index.md +4 -3
  13. data/docs/main-concepts/design-principles.md +23 -34
  14. data/docs/main-concepts/fiber-scheduling.md +1 -1
  15. data/docs/polyphony-logo.png +0 -0
  16. data/examples/core/xx-channels.rb +4 -2
  17. data/examples/core/xx-using-a-mutex.rb +2 -1
  18. data/examples/io/xx-happy-eyeballs.rb +21 -22
  19. data/examples/io/xx-zip.rb +19 -0
  20. data/examples/performance/fiber_transfer.rb +47 -0
  21. data/examples/performance/mem-usage.rb +34 -28
  22. data/examples/performance/messaging.rb +29 -0
  23. data/examples/performance/multi_snooze.rb +11 -9
  24. data/examples/xx-spin.rb +32 -0
  25. data/ext/polyphony/event.c +86 -0
  26. data/ext/polyphony/fiber.c +0 -5
  27. data/ext/polyphony/libev_agent.c +181 -24
  28. data/ext/polyphony/polyphony.c +0 -2
  29. data/ext/polyphony/polyphony.h +14 -7
  30. data/ext/polyphony/polyphony_ext.c +4 -2
  31. data/ext/polyphony/queue.c +187 -0
  32. data/ext/polyphony/ring_buffer.c +96 -0
  33. data/ext/polyphony/ring_buffer.h +28 -0
  34. data/ext/polyphony/thread.c +18 -12
  35. data/lib/polyphony.rb +5 -14
  36. data/lib/polyphony/core/channel.rb +3 -34
  37. data/lib/polyphony/core/global_api.rb +1 -1
  38. data/lib/polyphony/core/resource_pool.rb +13 -75
  39. data/lib/polyphony/core/sync.rb +12 -9
  40. data/lib/polyphony/core/thread_pool.rb +1 -1
  41. data/lib/polyphony/extensions/core.rb +34 -0
  42. data/lib/polyphony/extensions/fiber.rb +9 -2
  43. data/lib/polyphony/extensions/io.rb +17 -16
  44. data/lib/polyphony/extensions/openssl.rb +8 -0
  45. data/lib/polyphony/extensions/socket.rb +12 -0
  46. data/lib/polyphony/version.rb +1 -1
  47. data/test/helper.rb +1 -1
  48. data/test/q.rb +24 -0
  49. data/test/test_agent.rb +1 -1
  50. data/test/test_event.rb +12 -0
  51. data/test/test_global_api.rb +2 -2
  52. data/test/test_io.rb +24 -2
  53. data/test/test_queue.rb +59 -1
  54. data/test/test_resource_pool.rb +0 -43
  55. data/test/test_trace.rb +18 -17
  56. metadata +15 -5
  57. data/ext/polyphony/libev_queue.c +0 -217
  58. data/lib/polyphony/event.rb +0 -27
@@ -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
@@ -107,6 +107,15 @@ module ::Kernel
107
107
  $stdin.gets
108
108
  end
109
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
+
110
119
  alias_method :orig_system, :system
111
120
  def system(*args)
112
121
  Open3.popen2(*args) do |i, o, _t|
@@ -126,6 +135,31 @@ module ::Kernel
126
135
  break
127
136
  end
128
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
129
163
  end
130
164
 
131
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
@@ -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.3'
4
+ VERSION = '0.43.9'
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
@@ -34,6 +34,7 @@ class EventTest < MiniTest::Test
34
34
  }
35
35
  }
36
36
  snooze
37
+
37
38
  t = Thread.new do
38
39
  orig_sleep 0.001
39
40
  3.times { a.signal }
@@ -45,4 +46,15 @@ class EventTest < MiniTest::Test
45
46
  t&.kill
46
47
  t&.join
47
48
  end
49
+
50
+ def test_exception_while_waiting_for_event
51
+ e = Polyphony::Event.new
52
+
53
+ f = spin { e.await }
54
+ g = spin { f.raise 'foo' }
55
+
56
+ assert_raises(RuntimeError) do
57
+ f.await
58
+ end
59
+ end
48
60
  end
@@ -211,13 +211,13 @@ class SpinLoopTest < MiniTest::Test
211
211
 
212
212
  def test_spin_loop_location
213
213
  location = /^#{__FILE__}:#{__LINE__ + 1}/
214
- f = spin_loop {}
214
+ f = spin_loop { snooze }
215
215
 
216
216
  assert_match location, f.location
217
217
  end
218
218
 
219
219
  def test_spin_loop_tag
220
- f = spin_loop(:my_loop) {}
220
+ f = spin_loop(:my_loop) { snooze }
221
221
 
222
222
  assert_equal :my_loop, f.tag
223
223
  end
@@ -30,6 +30,14 @@ class IOTest < MiniTest::Test
30
30
  assert_equal 'hello', msg
31
31
  end
32
32
 
33
+ def test_write_multiple_arguments
34
+ i, o = IO.pipe
35
+ count = o.write('a', 'b', "\n", 'c')
36
+ assert_equal 4, count
37
+ o.close
38
+ assert_equal "ab\nc", i.read
39
+ end
40
+
33
41
  def test_that_double_chevron_method_returns_io
34
42
  assert_equal @o, @o << 'foo'
35
43
 
@@ -83,6 +91,20 @@ class IOTest < MiniTest::Test
83
91
 
84
92
  assert_raises(EOFError) { i.readpartial(1) }
85
93
  end
94
+
95
+ # see https://github.com/digital-fabric/polyphony/issues/30
96
+ def test_reopened_tempfile
97
+ file = Tempfile.new
98
+ file << 'hello: world'
99
+ file.close
100
+
101
+ buf = nil
102
+ File.open(file, 'r:bom|utf-8') do |f|
103
+ buf = f.read(16384)
104
+ end
105
+
106
+ assert_equal 'hello: world', buf
107
+ end
86
108
  end
87
109
 
88
110
  class IOClassMethodsTest < MiniTest::Test
@@ -121,7 +143,7 @@ class IOClassMethodsTest < MiniTest::Test
121
143
  assert_equal "end\n", lines[-1]
122
144
  end
123
145
 
124
- def test_read
146
+ def test_read_class_method
125
147
  s = IO.read(__FILE__)
126
148
  assert_kind_of String, s
127
149
  assert(!s.empty?)
@@ -144,7 +166,7 @@ class IOClassMethodsTest < MiniTest::Test
144
166
 
145
167
  WRITE_DATA = "foo\nbar קוקו"
146
168
 
147
- def test_write
169
+ def test_write_class_method
148
170
  fn = '/tmp/test_write'
149
171
  FileUtils.rm(fn) rescue nil
150
172
 
@@ -8,7 +8,7 @@ class QueueTest < MiniTest::Test
8
8
  @queue = Polyphony::Queue.new
9
9
  end
10
10
 
11
- def test_pop
11
+ def test_push_shift
12
12
  spin {
13
13
  @queue << 42
14
14
  }
@@ -21,6 +21,18 @@ class QueueTest < MiniTest::Test
21
21
  assert_equal [1, 2, 3, 4], buf
22
22
  end
23
23
 
24
+ def test_unshift
25
+ @queue.push 1
26
+ @queue.push 2
27
+ @queue.push 3
28
+ @queue.unshift 4
29
+
30
+ buf = []
31
+ buf << @queue.shift while !@queue.empty?
32
+
33
+ assert_equal [4, 1, 2, 3], buf
34
+ end
35
+
24
36
  def test_multiple_waiters
25
37
  a = spin { @queue.shift }
26
38
  b = spin { @queue.shift }
@@ -41,6 +53,19 @@ class QueueTest < MiniTest::Test
41
53
  buf = []
42
54
  @queue.shift_each { |i| buf << i }
43
55
  assert_equal [1, 2, 3, 4], buf
56
+
57
+ buf = []
58
+ @queue.shift_each { |i| buf << i }
59
+ assert_equal [], buf
60
+ end
61
+
62
+ def test_shift_all
63
+ (1..4).each { |i| @queue << i }
64
+ buf = @queue.shift_all
65
+ assert_equal [1, 2, 3, 4], buf
66
+
67
+ buf = @queue.shift_all
68
+ assert_equal [], buf
44
69
  end
45
70
 
46
71
  def test_empty?
@@ -71,4 +96,37 @@ class QueueTest < MiniTest::Test
71
96
  assert_nil f2.await
72
97
  assert_equal :bar, f3.await
73
98
  end
99
+
100
+ def test_fiber_removal_from_queue_simple
101
+ f1 = spin { @queue.shift }
102
+
103
+ # let fibers run
104
+ snooze
105
+
106
+ f1.stop
107
+ snooze
108
+
109
+ @queue << :foo
110
+ assert_nil f1.await
111
+ end
112
+
113
+ def test_queue_size
114
+ assert_equal 0, @queue.size
115
+
116
+ @queue.push 1
117
+
118
+ assert_equal 1, @queue.size
119
+
120
+ @queue.push 2
121
+
122
+ assert_equal 2, @queue.size
123
+
124
+ @queue.shift
125
+
126
+ assert_equal 1, @queue.size
127
+
128
+ @queue.shift
129
+
130
+ assert_equal 0, @queue.size
131
+ end
74
132
  end
@@ -37,49 +37,6 @@ class ResourcePoolTest < MiniTest::Test
37
37
  assert_equal 2, pool.size
38
38
  end
39
39
 
40
- def test_discard
41
- resources = [+'a', +'b']
42
- pool = Polyphony::ResourcePool.new(limit: 2) { resources.shift }
43
-
44
- results = []
45
- 4.times {
46
- spin {
47
- snooze
48
- pool.acquire { |resource|
49
- results << resource
50
- resource.__discard__ if resource == 'b'
51
- snooze
52
- }
53
- }
54
- }
55
- 6.times { snooze }
56
-
57
- assert_equal ['a', 'b', 'a', 'a'], results
58
- assert_equal 1, pool.size
59
- end
60
-
61
- def test_add
62
- resources = [+'a', +'b']
63
- pool = Polyphony::ResourcePool.new(limit: 2) { resources.shift }
64
-
65
- pool << +'c'
66
-
67
- results = []
68
- 4.times {
69
- spin {
70
- snooze
71
- pool.acquire { |resource|
72
- results << resource
73
- resource.__discard__ if resource == 'b'
74
- snooze
75
- }
76
- }
77
- }
78
- 6.times { snooze }
79
-
80
- assert_equal ['c', 'a', 'c', 'a'], results
81
- end
82
-
83
40
  def test_single_resource_limit
84
41
  resources = [+'a', +'b']
85
42
  pool = Polyphony::ResourcePool.new(limit: 1) { resources.shift }