polyphony 0.43.4 → 0.43.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +1 -1
  3. data/CHANGELOG.md +45 -0
  4. data/Gemfile.lock +1 -1
  5. data/README.md +21 -4
  6. data/TODO.md +1 -6
  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 +2 -2
  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/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/agent.h +39 -0
  25. data/ext/polyphony/event.c +86 -0
  26. data/ext/polyphony/fiber.c +0 -5
  27. data/ext/polyphony/libev_agent.c +231 -79
  28. data/ext/polyphony/polyphony.c +2 -2
  29. data/ext/polyphony/polyphony.h +19 -16
  30. data/ext/polyphony/polyphony_ext.c +4 -2
  31. data/ext/polyphony/queue.c +194 -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 +48 -31
  35. data/lib/polyphony.rb +5 -6
  36. data/lib/polyphony/core/channel.rb +3 -34
  37. data/lib/polyphony/core/resource_pool.rb +13 -75
  38. data/lib/polyphony/core/sync.rb +12 -9
  39. data/lib/polyphony/core/thread_pool.rb +1 -1
  40. data/lib/polyphony/extensions/core.rb +9 -0
  41. data/lib/polyphony/extensions/fiber.rb +9 -2
  42. data/lib/polyphony/extensions/io.rb +16 -15
  43. data/lib/polyphony/extensions/openssl.rb +8 -0
  44. data/lib/polyphony/extensions/socket.rb +13 -9
  45. data/lib/polyphony/extensions/thread.rb +1 -1
  46. data/lib/polyphony/version.rb +1 -1
  47. data/test/helper.rb +2 -2
  48. data/test/q.rb +24 -0
  49. data/test/test_agent.rb +2 -2
  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 +16 -5
  57. data/ext/polyphony/libev_queue.c +0 -217
  58. data/lib/polyphony/event.rb +0 -27
@@ -4,9 +4,6 @@ require 'fiber'
4
4
  require_relative './polyphony_ext'
5
5
 
6
6
  module Polyphony
7
- # Map Queue to Libev queue implementation
8
- Queue = LibevQueue
9
-
10
7
  # replace core Queue class with our own
11
8
  verbose = $VERBOSE
12
9
  $VERBOSE = nil
@@ -20,13 +17,12 @@ require_relative './polyphony/extensions/fiber'
20
17
  require_relative './polyphony/extensions/io'
21
18
 
22
19
  Thread.current.setup_fiber_scheduling
23
- Thread.current.agent = Polyphony::LibevAgent.new
20
+ Thread.current.agent = Polyphony::Agent.new
24
21
 
25
22
  require_relative './polyphony/core/global_api'
26
23
  require_relative './polyphony/core/resource_pool'
27
24
  require_relative './polyphony/net'
28
25
  require_relative './polyphony/adapters/process'
29
- require_relative './polyphony/event'
30
26
 
31
27
  # Main Polyphony API
32
28
  module Polyphony
@@ -102,7 +98,10 @@ module Polyphony
102
98
 
103
99
  def install_terminating_signal_handlers
104
100
  trap('SIGTERM', SystemExit)
105
- trap('SIGINT', Interrupt)
101
+ orig_trap('SIGINT') do
102
+ orig_trap('SIGINT') { exit! }
103
+ Thread.current.break_out_of_ev_loop(Thread.main.main_fiber, Interrupt.new)
104
+ end
106
105
  end
107
106
 
108
107
  def terminate_threads
@@ -5,42 +5,11 @@ require_relative './exceptions'
5
5
  module Polyphony
6
6
  # Implements a unidirectional communication channel along the lines of Go
7
7
  # (buffered) channels.
8
- class Channel
9
- def initialize
10
- @payload_queue = []
11
- @waiting_queue = []
12
- end
8
+ class Channel < Polyphony::Queue
9
+ alias_method :receive, :shift
13
10
 
14
11
  def close
15
- stop = Polyphony::MoveOn.new
16
- @waiting_queue.slice(0..-1).each { |f| f.schedule(stop) }
17
- end
18
-
19
- def <<(value)
20
- if @waiting_queue.empty?
21
- @payload_queue << value
22
- else
23
- @waiting_queue.shift&.schedule(value)
24
- end
25
- snooze
26
- end
27
-
28
- def receive
29
- Thread.current.agent.ref
30
- if @payload_queue.empty?
31
- @waiting_queue << Fiber.current
32
- suspend
33
- else
34
- receive_from_queue
35
- end
36
- ensure
37
- Thread.current.agent.unref
38
- end
39
-
40
- def receive_from_queue
41
- payload = @payload_queue.shift
42
- snooze
43
- payload
12
+ flush_waiters(Polyphony::MoveOn.new)
44
13
  end
45
14
  end
46
15
  end
@@ -10,13 +10,10 @@ module Polyphony
10
10
  # @param &block [Proc] allocator block
11
11
  def initialize(opts, &block)
12
12
  @allocator = block
13
-
14
- @stock = []
15
- @queue = []
16
- @acquired_resources = {}
17
-
18
13
  @limit = opts[:limit] || 4
19
14
  @size = 0
15
+ @stock = Polyphony::Queue.new
16
+ @acquired_resources = {}
20
17
  end
21
18
 
22
19
  def available
@@ -25,58 +22,17 @@ module Polyphony
25
22
 
26
23
  def acquire
27
24
  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[fiber] = nil
40
- Thread.current.agent.unref
41
- release(resource) if resource
42
- end
43
- end
44
- end
45
-
46
- def wait_for_resource
47
- fiber = Fiber.current
48
- @queue << fiber
49
- ready_resource = from_stock
50
- return ready_resource if ready_resource
25
+ return @acquired_resources[fiber] if @acquired_resources[fiber]
51
26
 
52
- suspend
27
+ add_to_stock if @size < @limit && @stock.empty?
28
+ resource = @stock.shift
29
+ @acquired_resources[fiber] = resource
30
+ yield resource
53
31
  ensure
54
- @queue.delete(fiber)
55
- end
56
-
57
- def release(resource)
58
- if resource.__discarded__
59
- @size -= 1
60
- elsif resource
61
- return_to_stock(resource)
62
- dequeue
63
- end
64
- end
65
-
66
- def dequeue
67
- return if @queue.empty? || @stock.empty?
68
-
69
- @queue.shift.schedule(@stock.shift)
32
+ @acquired_resources.delete(fiber)
33
+ @stock.push resource if resource
70
34
  end
71
-
72
- def return_to_stock(resource)
73
- @stock << resource
74
- end
75
-
76
- def from_stock
77
- @stock.shift || (@size < @limit && allocate)
78
- end
79
-
35
+
80
36
  def method_missing(sym, *args, &block)
81
37
  acquire { |r| r.send(sym, *args, &block) }
82
38
  end
@@ -85,33 +41,15 @@ module Polyphony
85
41
  true
86
42
  end
87
43
 
88
- # Extension to allow discarding of resources
89
- module ResourceExtensions
90
- def __discarded__
91
- @__discarded__
92
- end
93
-
94
- def __discard__
95
- @__discarded__ = true
96
- end
97
- end
98
-
99
44
  # Allocates a resource
100
45
  # @return [any] allocated resource
101
- def allocate
102
- @size += 1
103
- @allocator.().tap { |r| r.extend ResourceExtensions }
104
- end
105
-
106
- def <<(resource)
46
+ def add_to_stock
107
47
  @size += 1
108
- resource.extend ResourceExtensions
109
- @stock << resource
110
- dequeue
48
+ @stock << @allocator.call
111
49
  end
112
50
 
113
51
  def preheat!
114
- (@limit - @size).times { @stock << allocate }
52
+ add_to_stock while @size < @limit
115
53
  end
116
54
  end
117
55
  end
@@ -4,18 +4,21 @@ module Polyphony
4
4
  # Implements mutex lock for synchronizing access to a shared resource
5
5
  class Mutex
6
6
  def initialize
7
- @waiting_fibers = Polyphony::Queue.new
7
+ @store = Queue.new
8
+ @store << :token
8
9
  end
9
10
 
10
11
  def synchronize
11
- fiber = Fiber.current
12
- @waiting_fibers << fiber
13
- suspend if @waiting_fibers.size > 1
14
- yield
15
- ensure
16
- @waiting_fibers.delete(fiber)
17
- @waiting_fibers.first&.schedule
18
- snooze
12
+ return yield if @holding_fiber == Fiber.current
13
+
14
+ begin
15
+ token = @store.shift
16
+ @holding_fiber = Fiber.current
17
+ yield
18
+ ensure
19
+ @holding_fiber = nil
20
+ @store << token if token
21
+ end
19
22
  end
20
23
  end
21
24
  end
@@ -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|
@@ -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
@@ -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
@@ -14,15 +24,7 @@ class ::Socket
14
24
  NO_EXCEPTION = { exception: false }.freeze
15
25
 
16
26
  def connect(remotesockaddr)
17
- loop do
18
- result = connect_nonblock(remotesockaddr, **NO_EXCEPTION)
19
- case result
20
- when 0 then return
21
- when :wait_writable then Thread.current.agent.wait_io(self, true)
22
- else
23
- raise IOError
24
- end
25
- end
27
+ Thread.current.agent.connect(self, remotesockaddr.ip_address, remotesockaddr.ip_port)
26
28
  end
27
29
 
28
30
  def recv(maxlen, flags = 0, outbuf = nil)
@@ -77,6 +79,8 @@ end
77
79
  class ::TCPSocket
78
80
  NO_EXCEPTION = { exception: false }.freeze
79
81
 
82
+ attr_reader :io
83
+
80
84
  def initialize(remote_host, remote_port, local_host = nil, local_port = nil)
81
85
  @io = Socket.new Socket::AF_INET, Socket::SOCK_STREAM
82
86
  if local_host && local_port
@@ -18,7 +18,7 @@ class ::Thread
18
18
  def execute
19
19
  # agent must be created in the context of the new thread, therefore it
20
20
  # cannot be created in Thread#initialize
21
- @agent = Polyphony::LibevAgent.new
21
+ @agent = Polyphony::Agent.new
22
22
  setup
23
23
  @ready = true
24
24
  result = @block.(*@args)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polyphony
4
- VERSION = '0.43.4'
4
+ VERSION = '0.43.10'
5
5
  end
@@ -31,8 +31,8 @@ class MiniTest::Test
31
31
  end
32
32
  Fiber.current.setup_main_fiber
33
33
  Fiber.current.instance_variable_set(:@auto_watcher, nil)
34
- Thread.current.agent = Polyphony::LibevAgent.new
35
- sleep 0
34
+ Thread.current.agent = Polyphony::Agent.new
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]