polyphony 0.43.6 → 0.44.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -0
  3. data/Gemfile.lock +5 -1
  4. data/README.md +20 -5
  5. data/TODO.md +10 -14
  6. data/bin/stress.rb +28 -0
  7. data/docs/getting-started/overview.md +2 -2
  8. data/examples/adapters/sequel_mysql.rb +23 -0
  9. data/examples/adapters/sequel_mysql_pool.rb +33 -0
  10. data/examples/core/xx-channels.rb +4 -2
  11. data/examples/core/xx-using-a-mutex.rb +2 -1
  12. data/examples/performance/fiber_transfer.rb +47 -0
  13. data/ext/polyphony/agent.h +41 -0
  14. data/ext/polyphony/event.c +86 -0
  15. data/ext/polyphony/fiber.c +0 -5
  16. data/ext/polyphony/libev_agent.c +201 -128
  17. data/ext/polyphony/polyphony.c +4 -2
  18. data/ext/polyphony/polyphony.h +24 -24
  19. data/ext/polyphony/polyphony_ext.c +4 -2
  20. data/ext/polyphony/queue.c +208 -0
  21. data/ext/polyphony/ring_buffer.c +0 -24
  22. data/ext/polyphony/thread.c +53 -38
  23. data/lib/polyphony.rb +13 -31
  24. data/lib/polyphony/adapters/mysql2.rb +19 -0
  25. data/lib/polyphony/adapters/sequel.rb +45 -0
  26. data/lib/polyphony/core/channel.rb +3 -34
  27. data/lib/polyphony/core/exceptions.rb +11 -0
  28. data/lib/polyphony/core/resource_pool.rb +23 -72
  29. data/lib/polyphony/core/sync.rb +12 -9
  30. data/lib/polyphony/extensions/core.rb +15 -8
  31. data/lib/polyphony/extensions/fiber.rb +4 -0
  32. data/lib/polyphony/extensions/socket.rb +9 -9
  33. data/lib/polyphony/extensions/thread.rb +1 -1
  34. data/lib/polyphony/net.rb +2 -1
  35. data/lib/polyphony/version.rb +1 -1
  36. data/polyphony.gemspec +2 -0
  37. data/test/helper.rb +2 -2
  38. data/test/test_agent.rb +2 -2
  39. data/test/test_event.rb +12 -0
  40. data/test/test_fiber.rb +17 -1
  41. data/test/test_io.rb +14 -0
  42. data/test/test_queue.rb +33 -0
  43. data/test/test_resource_pool.rb +34 -43
  44. data/test/test_signal.rb +2 -26
  45. data/test/test_socket.rb +0 -43
  46. data/test/test_trace.rb +18 -17
  47. metadata +40 -5
  48. data/ext/polyphony/libev_queue.c +0 -288
  49. 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,37 +17,29 @@ 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
33
29
  class << self
34
- def wait_for_signal(sig)
35
- raise "should be reimplemented"
36
-
37
- fiber = Fiber.current
38
- # Polyphony.ref
39
- old_trap = trap(sig) do
40
- # Polyphony.unref
41
- fiber.schedule(sig)
42
- trap(sig, old_trap)
43
- end
44
- suspend
45
-
46
- end
47
-
48
30
  def fork(&block)
49
31
  Kernel.fork do
50
- # # Since the fiber doing the fork will become the main fiber of the
51
- # # forked process, we leave it behind by transferring to a new fiber
52
- # # created in the context of the forked process, which rescues *all*
53
- # # exceptions, including Interrupt and SystemExit.
32
+ # A race condition can arise if a TERM or INT signal is received before
33
+ # the forked process has finished initializing. To prevent this we restore
34
+ # the default signal handlers, and then reinstall the custom Polyphony
35
+ # handlers just before running the given block.
36
+ trap('SIGTERM', 'DEFAULT')
37
+ trap('SIGINT', 'DEFAULT')
38
+
39
+ # Since the fiber doing the fork will become the main fiber of the
40
+ # forked process, we leave it behind by transferring to a new fiber
41
+ # created in the context of the forked process, which rescues *all*
42
+ # exceptions, including Interrupt and SystemExit.
54
43
  spin_forked_block(&block).transfer
55
44
  end
56
45
  end
@@ -61,7 +50,7 @@ module Polyphony
61
50
  rescue SystemExit
62
51
  # fall through to ensure
63
52
  rescue Exception => e
64
- e.full_message
53
+ warn e.full_message
65
54
  exit!
66
55
  ensure
67
56
  exit_forked_process
@@ -69,13 +58,6 @@ module Polyphony
69
58
  end
70
59
 
71
60
  def run_forked_block(&block)
72
- # A race condition can arise if a TERM or INT signal is received before
73
- # the forked process has finished initializing. To prevent this we restore
74
- # the default signal handlers, and then reinstall the custom Polyphony
75
- # handlers just before running the given block.
76
- trap('SIGTERM', 'DEFAULT')
77
- trap('SIGINT', 'DEFAULT')
78
-
79
61
  Thread.current.setup
80
62
  Fiber.current.setup_main_fiber
81
63
  Thread.current.agent.post_fork
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../polyphony'
4
+ require 'mysql2/client'
5
+
6
+ # Mysql2::Client overrides
7
+ Mysql2::Client.prepend(Module.new do
8
+ def initialize(config)
9
+ config[:async] = true
10
+ super
11
+ @io = ::IO.for_fd(socket)
12
+ end
13
+
14
+ def query(sql, **options)
15
+ super
16
+ Thread.current.agent.wait_io(@io, false)
17
+ async_result
18
+ end
19
+ end)
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../polyphony'
4
+ require 'sequel'
5
+
6
+ module Polyphony
7
+ # Sequel ConnectionPool that delegates to Polyphony::ResourcePool.
8
+ class FiberConnectionPool < Sequel::ConnectionPool
9
+ def initialize(db, opts = OPTS)
10
+ super
11
+ max_size = Integer(opts[:max_connections] || 4)
12
+ @pool = Polyphony::ResourcePool.new(limit: max_size) { make_new(:default) }
13
+ end
14
+
15
+ def hold(_server = nil)
16
+ @pool.acquire do |conn|
17
+ yield conn
18
+ rescue Polyphony::BaseException
19
+ # The connection may be in an unrecoverable state if interrupted,
20
+ # discard the connection from the pool so it isn't reused.
21
+ @pool.discard!
22
+ raise
23
+ end
24
+ end
25
+
26
+ def size
27
+ @pool.size
28
+ end
29
+
30
+ def max_size
31
+ @pool.limit
32
+ end
33
+
34
+ def preconnect(_concurrent = false)
35
+ @pool.preheat!
36
+ end
37
+ end
38
+
39
+ # Override Sequel::Database to use FiberConnectionPool by default.
40
+ Sequel::Database.prepend(Module.new do
41
+ def connection_pool_default_options
42
+ {pool_class: FiberConnectionPool}
43
+ end
44
+ end)
45
+ end
@@ -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
@@ -33,4 +33,15 @@ module Polyphony
33
33
 
34
34
  # Restart is used to restart a fiber
35
35
  class Restart < BaseException; end
36
+
37
+ # Interjection is used to run arbitrary code on arbitrary fibers at any point
38
+ class Interjection < BaseException
39
+ def initialize(proc)
40
+ @proc = proc
41
+ end
42
+
43
+ def invoke
44
+ @proc.call
45
+ end
46
+ end
36
47
  end
@@ -10,73 +10,35 @@ 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
23
20
  @stock.size
24
21
  end
25
22
 
26
- def acquire
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[fiber] = nil
40
- Thread.current.agent.unref
41
- release(resource) if resource
42
- end
43
- end
44
- end
45
-
46
- def wait_for_resource
23
+ def acquire(&block)
47
24
  fiber = Fiber.current
48
- @queue << fiber
49
- ready_resource = from_stock
50
- return ready_resource if ready_resource
25
+ return yield @acquired_resources[fiber] if @acquired_resources[fiber]
51
26
 
52
- suspend
53
- ensure
54
- @queue.delete(fiber)
27
+ acquire_from_stock(fiber, &block)
55
28
  end
56
29
 
57
- def release(resource)
58
- if resource.__discarded__
59
- @size -= 1
60
- elsif resource
61
- return_to_stock(resource)
62
- dequeue
30
+ def acquire_from_stock(fiber)
31
+ add_to_stock if (@stock.empty? || @stock.pending?) && @size < @limit
32
+ resource = @stock.shift
33
+ @acquired_resources[fiber] = resource
34
+ yield resource
35
+ ensure
36
+ if resource && @acquired_resources[fiber] == resource
37
+ @acquired_resources.delete(fiber)
38
+ @stock.push resource
63
39
  end
64
40
  end
65
-
66
- def dequeue
67
- return if @queue.empty? || @stock.empty?
68
-
69
- @queue.shift.schedule(@stock.shift)
70
- 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
-
41
+
80
42
  def method_missing(sym, *args, &block)
81
43
  acquire { |r| r.send(sym, *args, &block) }
82
44
  end
@@ -85,33 +47,22 @@ module Polyphony
85
47
  true
86
48
  end
87
49
 
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
50
  # Allocates a resource
100
51
  # @return [any] allocated resource
101
- def allocate
52
+ def add_to_stock
102
53
  @size += 1
103
- @allocator.().tap { |r| r.extend ResourceExtensions }
54
+ resource = @allocator.call
55
+ @stock << resource
104
56
  end
105
57
 
106
- def <<(resource)
107
- @size += 1
108
- resource.extend ResourceExtensions
109
- @stock << resource
110
- dequeue
58
+ # Discards the currently-acquired resource
59
+ # instead of returning it to the pool when done.
60
+ def discard!
61
+ @size -= 1 if @acquired_resources.delete(Fiber.current)
111
62
  end
112
63
 
113
64
  def preheat!
114
- (@limit - @size).times { @stock << allocate }
65
+ add_to_stock while @size < @limit
115
66
  end
116
67
  end
117
68
  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
@@ -46,6 +46,10 @@ class ::Exception
46
46
 
47
47
  backtrace.reject { |l| l[POLYPHONY_DIR] }
48
48
  end
49
+
50
+ def invoke
51
+ Kernel.raise(self)
52
+ end
49
53
  end
50
54
 
51
55
  # Overrides for Process
@@ -140,8 +144,16 @@ module ::Kernel
140
144
  def trap(sig, command = nil, &block)
141
145
  return orig_trap(sig, command) if command.is_a? String
142
146
 
143
- block = command if command.respond_to?(:call) && !block
144
- exception = command.is_a?(Class) && command.new
147
+ block = command if !block && command.respond_to?(:call)
148
+ if block
149
+ exception = Polyphony::Interjection.new(block)
150
+ else
151
+ exception = command.is_a?(Class) && command.new
152
+ end
153
+
154
+ unless exception
155
+ raise ArgumentError, "Must supply block or exception or callable object"
156
+ end
145
157
 
146
158
  # The signal trap can be invoked at any time, including while the system
147
159
  # agent is blocking while polling for events. In order to deal with this
@@ -152,12 +164,7 @@ module ::Kernel
152
164
  # If the command argument is an exception class however, it will be raised
153
165
  # directly in the context of the main fiber.
154
166
  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
167
+ Thread.current.break_out_of_ev_loop(Thread.main.main_fiber, exception)
161
168
  end
162
169
  end
163
170
  end
@@ -67,6 +67,10 @@ module Polyphony
67
67
  else RuntimeError.new
68
68
  end
69
69
  end
70
+
71
+ def interject(&block)
72
+ raise Polyphony::Interjection.new(block)
73
+ end
70
74
  end
71
75
 
72
76
  # Fiber supervision
@@ -24,15 +24,7 @@ class ::Socket
24
24
  NO_EXCEPTION = { exception: false }.freeze
25
25
 
26
26
  def connect(remotesockaddr)
27
- loop do
28
- result = connect_nonblock(remotesockaddr, **NO_EXCEPTION)
29
- case result
30
- when 0 then return
31
- when :wait_writable then Thread.current.agent.wait_io(self, true)
32
- else
33
- raise IOError
34
- end
35
- end
27
+ Thread.current.agent.connect(self, remotesockaddr.ip_address, remotesockaddr.ip_port)
36
28
  end
37
29
 
38
30
  def recv(maxlen, flags = 0, outbuf = nil)
@@ -75,6 +67,10 @@ class ::Socket
75
67
  setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)
76
68
  end
77
69
 
70
+ def reuse_port
71
+ setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEPORT, 1)
72
+ end
73
+
78
74
  class << self
79
75
  alias_method :orig_getaddrinfo, :getaddrinfo
80
76
  def getaddrinfo(*args)
@@ -128,6 +124,10 @@ class ::TCPSocket
128
124
  def reuse_addr
129
125
  setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEADDR, 1)
130
126
  end
127
+
128
+ def reuse_port
129
+ setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEPORT, 1)
130
+ end
131
131
  end
132
132
 
133
133
  # Override stock TCPServer code by encapsulating a Socket instance.