polyphony 0.43.8 → 0.45.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -1
  3. data/CHANGELOG.md +38 -0
  4. data/Gemfile.lock +13 -11
  5. data/README.md +20 -5
  6. data/Rakefile +1 -1
  7. data/TODO.md +16 -14
  8. data/bin/stress.rb +28 -0
  9. data/docs/_posts/2020-07-26-polyphony-0.44.md +77 -0
  10. data/docs/api-reference/thread.md +1 -1
  11. data/docs/getting-started/overview.md +14 -14
  12. data/docs/getting-started/tutorial.md +1 -1
  13. data/examples/adapters/sequel_mysql.rb +23 -0
  14. data/examples/adapters/sequel_mysql_pool.rb +33 -0
  15. data/examples/core/{xx-agent.rb → xx-backend.rb} +5 -5
  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-pry.rb +18 -0
  19. data/examples/io/xx-rack_server.rb +71 -0
  20. data/examples/performance/thread-vs-fiber/polyphony_server_read_loop.rb +1 -1
  21. data/ext/polyphony/backend.h +41 -0
  22. data/ext/polyphony/event.c +86 -0
  23. data/ext/polyphony/extconf.rb +1 -1
  24. data/ext/polyphony/fiber.c +0 -5
  25. data/ext/polyphony/{libev_agent.c → libev_backend.c} +234 -228
  26. data/ext/polyphony/polyphony.c +4 -0
  27. data/ext/polyphony/polyphony.h +16 -16
  28. data/ext/polyphony/polyphony_ext.c +4 -2
  29. data/ext/polyphony/queue.c +52 -12
  30. data/ext/polyphony/thread.c +55 -42
  31. data/lib/polyphony.rb +25 -39
  32. data/lib/polyphony/adapters/irb.rb +2 -17
  33. data/lib/polyphony/adapters/mysql2.rb +19 -0
  34. data/lib/polyphony/adapters/postgres.rb +5 -5
  35. data/lib/polyphony/adapters/process.rb +2 -2
  36. data/lib/polyphony/adapters/readline.rb +17 -0
  37. data/lib/polyphony/adapters/sequel.rb +45 -0
  38. data/lib/polyphony/core/channel.rb +3 -34
  39. data/lib/polyphony/core/exceptions.rb +11 -0
  40. data/lib/polyphony/core/global_api.rb +11 -6
  41. data/lib/polyphony/core/resource_pool.rb +22 -71
  42. data/lib/polyphony/core/sync.rb +48 -9
  43. data/lib/polyphony/core/throttler.rb +1 -1
  44. data/lib/polyphony/extensions/core.rb +37 -19
  45. data/lib/polyphony/extensions/fiber.rb +5 -1
  46. data/lib/polyphony/extensions/io.rb +7 -8
  47. data/lib/polyphony/extensions/openssl.rb +6 -6
  48. data/lib/polyphony/extensions/socket.rb +12 -22
  49. data/lib/polyphony/extensions/thread.rb +6 -5
  50. data/lib/polyphony/net.rb +2 -1
  51. data/lib/polyphony/version.rb +1 -1
  52. data/polyphony.gemspec +6 -3
  53. data/test/helper.rb +1 -1
  54. data/test/{test_agent.rb → test_backend.rb} +22 -22
  55. data/test/test_event.rb +1 -0
  56. data/test/test_fiber.rb +21 -5
  57. data/test/test_io.rb +1 -1
  58. data/test/test_kernel.rb +5 -0
  59. data/test/test_queue.rb +20 -0
  60. data/test/test_resource_pool.rb +34 -43
  61. data/test/test_signal.rb +5 -29
  62. data/test/test_sync.rb +52 -0
  63. metadata +74 -30
  64. data/.gitbook.yaml +0 -4
  65. data/lib/polyphony/event.rb +0 -17
@@ -16,7 +16,7 @@ if Object.constants.include?(:Reline)
16
16
  fiber.cancel
17
17
  end
18
18
  read_ios.each do |io|
19
- Thread.current.agent.wait_io(io, false)
19
+ Thread.current.backend.wait_io(io, false)
20
20
  return [io]
21
21
  end
22
22
  rescue Polyphony::Cancel
@@ -26,22 +26,7 @@ if Object.constants.include?(:Reline)
26
26
  end
27
27
  end
28
28
  else
29
- # readline blocks the current thread, so we offload it to the blocking-ops
30
- # thread pool. That way, the reactor loop can keep running while waiting for
31
- # readline to return
32
- module ::Readline
33
- alias_method :orig_readline, :readline
34
-
35
- Workers = Polyphony::ThreadPool.new
36
-
37
- def readline(*args)
38
- p :readline
39
- # caller.each do |l|
40
- # STDOUT.orig_puts l
41
- # end
42
- Workers.process { orig_readline(*args) }
43
- end
44
- end
29
+ require_relative './readline'
45
30
 
46
31
  # RubyLex patches
47
32
  class ::RubyLex
@@ -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.backend.wait_io(@io, false)
17
+ async_result
18
+ end
19
+ end)
@@ -15,8 +15,8 @@ module ::PG
15
15
  res = conn.connect_poll
16
16
  case res
17
17
  when PGRES_POLLING_FAILED then raise Error, conn.error_message
18
- when PGRES_POLLING_READING then Thread.current.agent.wait_io(socket_io, false)
19
- when PGRES_POLLING_WRITING then Thread.current.agent.wait_io(socket_io, true)
18
+ when PGRES_POLLING_READING then Thread.current.backend.wait_io(socket_io, false)
19
+ when PGRES_POLLING_WRITING then Thread.current.backend.wait_io(socket_io, true)
20
20
  when PGRES_POLLING_OK then return conn.setnonblocking(true)
21
21
  end
22
22
  end
@@ -42,7 +42,7 @@ class ::PG::Connection
42
42
 
43
43
  def get_result(&block)
44
44
  while is_busy
45
- Thread.current.agent.wait_io(socket_io, false)
45
+ Thread.current.backend.wait_io(socket_io, false)
46
46
  consume_input
47
47
  end
48
48
  orig_get_result(&block)
@@ -59,7 +59,7 @@ class ::PG::Connection
59
59
 
60
60
  def block(_timeout = 0)
61
61
  while is_busy
62
- Thread.current.agent.wait_io(socket_io, false)
62
+ Thread.current.backend.wait_io(socket_io, false)
63
63
  consume_input
64
64
  end
65
65
  end
@@ -97,7 +97,7 @@ class ::PG::Connection
97
97
  return move_on_after(timeout) { wait_for_notify(&block) } if timeout
98
98
 
99
99
  loop do
100
- Thread.current.agent.wait_io(socket_io, false)
100
+ Thread.current.backend.wait_io(socket_io, false)
101
101
  consume_input
102
102
  notice = notifies
103
103
  next unless notice
@@ -7,7 +7,7 @@ module Polyphony
7
7
  def watch(cmd = nil, &block)
8
8
  terminated = nil
9
9
  pid = cmd ? Kernel.spawn(cmd) : Polyphony.fork(&block)
10
- Thread.current.agent.waitpid(pid)
10
+ Thread.current.backend.waitpid(pid)
11
11
  terminated = true
12
12
  ensure
13
13
  kill_process(pid) unless terminated || pid.nil?
@@ -23,7 +23,7 @@ module Polyphony
23
23
 
24
24
  def kill_and_await(sig, pid)
25
25
  ::Process.kill(sig, pid)
26
- Thread.current.agent.waitpid(pid)
26
+ Thread.current.backend.waitpid(pid)
27
27
  rescue SystemCallError
28
28
  # ignore
29
29
  puts 'SystemCallError in kill_and_await'
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'polyphony'
4
+ require 'readline'
5
+
6
+ # readline blocks the current thread, so we offload it to the blocking-ops
7
+ # thread pool. That way, the reactor loop can keep running while waiting for
8
+ # readline to return
9
+ module ::Readline
10
+ alias_method :orig_readline, :readline
11
+
12
+ Worker = Polyphony::ThreadPool.new(1)
13
+
14
+ def readline(*args)
15
+ Worker.process { orig_readline(*args) }
16
+ end
17
+ 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
@@ -19,13 +19,18 @@ module Polyphony
19
19
  fiber = ::Fiber.current
20
20
  canceller = spin do
21
21
  sleep interval
22
- exception = with_exception.is_a?(Class) ?
23
- with_exception.new : RuntimeError.new(with_exception)
22
+ exception = cancel_exception(with_exception)
24
23
  fiber.schedule exception
25
24
  end
26
25
  block ? cancel_after_wrap_block(canceller, &block) : canceller
27
26
  end
28
27
 
28
+ def cancel_exception(exception)
29
+ return exception.new if exception.is_a?(Class)
30
+
31
+ RuntimeError.new(exception)
32
+ end
33
+
29
34
  def cancel_after_wrap_block(canceller, &block)
30
35
  block.call
31
36
  ensure
@@ -50,7 +55,7 @@ module Polyphony
50
55
  next_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + interval
51
56
  loop do
52
57
  now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
53
- Thread.current.agent.sleep(next_time - now)
58
+ Thread.current.backend.sleep(next_time - now)
54
59
  yield
55
60
  loop do
56
61
  next_time += interval
@@ -98,14 +103,14 @@ module Polyphony
98
103
  def sleep(duration = nil)
99
104
  return sleep_forever unless duration
100
105
 
101
- Thread.current.agent.sleep duration
106
+ Thread.current.backend.sleep duration
102
107
  end
103
108
 
104
109
  def sleep_forever
105
- Thread.current.agent.ref
110
+ Thread.current.backend.ref
106
111
  loop { sleep 60 }
107
112
  ensure
108
- Thread.current.agent.unref
113
+ Thread.current.backend.unref
109
114
  end
110
115
 
111
116
  def throttled_loop(rate, count: nil, &block)
@@ -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.delete fiber
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
41
 
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
-
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,57 @@ 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
22
+ end
23
+
24
+ def conditional_release
25
+ @store << @token
26
+ @token = nil
27
+ @holding_fiber = nil
28
+ end
29
+
30
+ def conditional_reacquire
31
+ @token = @store.shift
32
+ @holding_fiber = Fiber.current
33
+ end
34
+ end
35
+
36
+ # Implements a fiber-aware ConditionVariable
37
+ class ConditionVariable
38
+ def initialize
39
+ @queue = Polyphony::Queue.new
40
+ end
41
+
42
+ def wait(mutex, _timeout = nil)
43
+ mutex.conditional_release
44
+ @queue << Fiber.current
45
+ Thread.current.backend.wait_event(true)
46
+ mutex.conditional_reacquire
47
+ end
48
+
49
+ def signal
50
+ fiber = @queue.shift
51
+ fiber.schedule
52
+ end
53
+
54
+ def broadcast
55
+ while (fiber = @queue.shift)
56
+ fiber.schedule
57
+ end
19
58
  end
20
59
  end
21
60
  end