polyphony 0.43.6 → 0.44.0

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