polyphony 0.43.8
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.
- checksums.yaml +7 -0
- data/.gitbook.yaml +4 -0
- data/.github/workflows/test.yml +29 -0
- data/.gitignore +59 -0
- data/.rubocop.yml +175 -0
- data/CHANGELOG.md +393 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +141 -0
- data/LICENSE +21 -0
- data/README.md +51 -0
- data/Rakefile +26 -0
- data/TODO.md +201 -0
- data/bin/polyphony-debug +87 -0
- data/docs/_config.yml +64 -0
- data/docs/_includes/head.html +40 -0
- data/docs/_includes/title.html +1 -0
- data/docs/_sass/custom/custom.scss +10 -0
- data/docs/_sass/overrides.scss +0 -0
- data/docs/_user-guide/all-about-timers.md +126 -0
- data/docs/_user-guide/index.md +9 -0
- data/docs/_user-guide/web-server.md +136 -0
- data/docs/api-reference/exception.md +27 -0
- data/docs/api-reference/fiber.md +425 -0
- data/docs/api-reference/index.md +9 -0
- data/docs/api-reference/io.md +36 -0
- data/docs/api-reference/object.md +99 -0
- data/docs/api-reference/polyphony-baseexception.md +33 -0
- data/docs/api-reference/polyphony-cancel.md +26 -0
- data/docs/api-reference/polyphony-moveon.md +24 -0
- data/docs/api-reference/polyphony-net.md +20 -0
- data/docs/api-reference/polyphony-process.md +28 -0
- data/docs/api-reference/polyphony-resourcepool.md +59 -0
- data/docs/api-reference/polyphony-restart.md +18 -0
- data/docs/api-reference/polyphony-terminate.md +18 -0
- data/docs/api-reference/polyphony-threadpool.md +67 -0
- data/docs/api-reference/polyphony-throttler.md +77 -0
- data/docs/api-reference/polyphony.md +36 -0
- data/docs/api-reference/thread.md +88 -0
- data/docs/assets/img/echo-fibers.svg +1 -0
- data/docs/assets/img/sleeping-fiber.svg +1 -0
- data/docs/faq.md +195 -0
- data/docs/favicon.ico +0 -0
- data/docs/getting-started/index.md +10 -0
- data/docs/getting-started/installing.md +34 -0
- data/docs/getting-started/overview.md +486 -0
- data/docs/getting-started/tutorial.md +359 -0
- data/docs/index.md +94 -0
- data/docs/main-concepts/concurrency.md +151 -0
- data/docs/main-concepts/design-principles.md +161 -0
- data/docs/main-concepts/exception-handling.md +291 -0
- data/docs/main-concepts/extending.md +89 -0
- data/docs/main-concepts/fiber-scheduling.md +197 -0
- data/docs/main-concepts/index.md +9 -0
- data/docs/polyphony-logo.png +0 -0
- data/examples/adapters/concurrent-ruby.rb +9 -0
- data/examples/adapters/pg_client.rb +36 -0
- data/examples/adapters/pg_notify.rb +35 -0
- data/examples/adapters/pg_pool.rb +43 -0
- data/examples/adapters/pg_transaction.rb +31 -0
- data/examples/adapters/redis_blpop.rb +12 -0
- data/examples/adapters/redis_channels.rb +122 -0
- data/examples/adapters/redis_client.rb +19 -0
- data/examples/adapters/redis_pubsub.rb +26 -0
- data/examples/adapters/redis_pubsub_perf.rb +68 -0
- data/examples/core/01-spinning-up-fibers.rb +18 -0
- data/examples/core/02-awaiting-fibers.rb +20 -0
- data/examples/core/03-interrupting.rb +39 -0
- data/examples/core/04-handling-signals.rb +19 -0
- data/examples/core/xx-agent.rb +102 -0
- data/examples/core/xx-at_exit.rb +29 -0
- data/examples/core/xx-caller.rb +12 -0
- data/examples/core/xx-channels.rb +45 -0
- data/examples/core/xx-daemon.rb +14 -0
- data/examples/core/xx-deadlock.rb +8 -0
- data/examples/core/xx-deferring-an-operation.rb +14 -0
- data/examples/core/xx-erlang-style-genserver.rb +81 -0
- data/examples/core/xx-exception-backtrace.rb +40 -0
- data/examples/core/xx-fork-cleanup.rb +22 -0
- data/examples/core/xx-fork-spin.rb +42 -0
- data/examples/core/xx-fork-terminate.rb +27 -0
- data/examples/core/xx-forking.rb +24 -0
- data/examples/core/xx-move_on.rb +23 -0
- data/examples/core/xx-pingpong.rb +18 -0
- data/examples/core/xx-queue-async.rb +120 -0
- data/examples/core/xx-readpartial.rb +18 -0
- data/examples/core/xx-recurrent-timer.rb +12 -0
- data/examples/core/xx-resource_delegate.rb +31 -0
- data/examples/core/xx-signals.rb +16 -0
- data/examples/core/xx-sleep-forever.rb +9 -0
- data/examples/core/xx-sleeping.rb +25 -0
- data/examples/core/xx-snooze-starve.rb +16 -0
- data/examples/core/xx-spin-fork.rb +49 -0
- data/examples/core/xx-spin_error_backtrace.rb +33 -0
- data/examples/core/xx-state-machine.rb +51 -0
- data/examples/core/xx-stop.rb +20 -0
- data/examples/core/xx-supervise-process.rb +30 -0
- data/examples/core/xx-supervisors.rb +21 -0
- data/examples/core/xx-thread-selector-sleep.rb +51 -0
- data/examples/core/xx-thread-selector-snooze.rb +46 -0
- data/examples/core/xx-thread-sleep.rb +17 -0
- data/examples/core/xx-thread-snooze.rb +34 -0
- data/examples/core/xx-thread_pool.rb +17 -0
- data/examples/core/xx-throttling.rb +18 -0
- data/examples/core/xx-timeout.rb +10 -0
- data/examples/core/xx-timer-gc.rb +17 -0
- data/examples/core/xx-trace.rb +79 -0
- data/examples/core/xx-using-a-mutex.rb +21 -0
- data/examples/core/xx-worker-thread.rb +30 -0
- data/examples/io/tunnel.rb +48 -0
- data/examples/io/xx-backticks.rb +11 -0
- data/examples/io/xx-echo_client.rb +25 -0
- data/examples/io/xx-echo_client_from_stdin.rb +21 -0
- data/examples/io/xx-echo_pipe.rb +16 -0
- data/examples/io/xx-echo_server.rb +17 -0
- data/examples/io/xx-echo_server_with_timeout.rb +34 -0
- data/examples/io/xx-echo_stdin.rb +14 -0
- data/examples/io/xx-happy-eyeballs.rb +36 -0
- data/examples/io/xx-httparty.rb +38 -0
- data/examples/io/xx-irb.rb +17 -0
- data/examples/io/xx-net-http.rb +15 -0
- data/examples/io/xx-open.rb +16 -0
- data/examples/io/xx-switch.rb +15 -0
- data/examples/io/xx-system.rb +11 -0
- data/examples/io/xx-tcpserver.rb +15 -0
- data/examples/io/xx-tcpsocket.rb +18 -0
- data/examples/io/xx-zip.rb +19 -0
- data/examples/performance/fiber_transfer.rb +47 -0
- data/examples/performance/fs_read.rb +38 -0
- data/examples/performance/mem-usage.rb +56 -0
- data/examples/performance/messaging.rb +29 -0
- data/examples/performance/multi_snooze.rb +33 -0
- data/examples/performance/snooze.rb +39 -0
- data/examples/performance/snooze_raw.rb +39 -0
- data/examples/performance/thread-vs-fiber/polyphony_mt_server.rb +74 -0
- data/examples/performance/thread-vs-fiber/polyphony_server.rb +45 -0
- data/examples/performance/thread-vs-fiber/polyphony_server_read_loop.rb +58 -0
- data/examples/performance/thread-vs-fiber/threaded_server.rb +27 -0
- data/examples/performance/thread-vs-fiber/xx-httparty_multi.rb +36 -0
- data/examples/performance/thread-vs-fiber/xx-httparty_threaded.rb +29 -0
- data/examples/performance/thread_pool_perf.rb +63 -0
- data/examples/performance/xx-array.rb +11 -0
- data/examples/performance/xx-fiber-switch.rb +9 -0
- data/examples/performance/xx-snooze.rb +15 -0
- data/examples/xx-spin.rb +32 -0
- data/ext/libev/Changes +548 -0
- data/ext/libev/LICENSE +37 -0
- data/ext/libev/README +59 -0
- data/ext/libev/README.embed +3 -0
- data/ext/libev/ev.c +5279 -0
- data/ext/libev/ev.h +856 -0
- data/ext/libev/ev_epoll.c +296 -0
- data/ext/libev/ev_kqueue.c +224 -0
- data/ext/libev/ev_linuxaio.c +642 -0
- data/ext/libev/ev_poll.c +156 -0
- data/ext/libev/ev_port.c +192 -0
- data/ext/libev/ev_select.c +316 -0
- data/ext/libev/ev_vars.h +215 -0
- data/ext/libev/ev_win32.c +162 -0
- data/ext/libev/ev_wrap.h +216 -0
- data/ext/libev/test_libev_win32.c +123 -0
- data/ext/polyphony/extconf.rb +20 -0
- data/ext/polyphony/fiber.c +109 -0
- data/ext/polyphony/libev.c +2 -0
- data/ext/polyphony/libev.h +9 -0
- data/ext/polyphony/libev_agent.c +882 -0
- data/ext/polyphony/polyphony.c +71 -0
- data/ext/polyphony/polyphony.h +97 -0
- data/ext/polyphony/polyphony_ext.c +21 -0
- data/ext/polyphony/queue.c +168 -0
- data/ext/polyphony/ring_buffer.c +96 -0
- data/ext/polyphony/ring_buffer.h +28 -0
- data/ext/polyphony/thread.c +208 -0
- data/ext/polyphony/tracing.c +11 -0
- data/lib/polyphony.rb +136 -0
- data/lib/polyphony/adapters/fs.rb +19 -0
- data/lib/polyphony/adapters/irb.rb +52 -0
- data/lib/polyphony/adapters/postgres.rb +110 -0
- data/lib/polyphony/adapters/process.rb +33 -0
- data/lib/polyphony/adapters/redis.rb +67 -0
- data/lib/polyphony/adapters/trace.rb +138 -0
- data/lib/polyphony/core/channel.rb +46 -0
- data/lib/polyphony/core/exceptions.rb +36 -0
- data/lib/polyphony/core/global_api.rb +124 -0
- data/lib/polyphony/core/resource_pool.rb +117 -0
- data/lib/polyphony/core/sync.rb +21 -0
- data/lib/polyphony/core/thread_pool.rb +64 -0
- data/lib/polyphony/core/throttler.rb +41 -0
- data/lib/polyphony/event.rb +17 -0
- data/lib/polyphony/extensions/core.rb +174 -0
- data/lib/polyphony/extensions/fiber.rb +379 -0
- data/lib/polyphony/extensions/io.rb +221 -0
- data/lib/polyphony/extensions/openssl.rb +81 -0
- data/lib/polyphony/extensions/socket.rb +150 -0
- data/lib/polyphony/extensions/thread.rb +108 -0
- data/lib/polyphony/net.rb +77 -0
- data/lib/polyphony/version.rb +5 -0
- data/polyphony.gemspec +40 -0
- data/test/coverage.rb +54 -0
- data/test/eg.rb +27 -0
- data/test/helper.rb +56 -0
- data/test/q.rb +24 -0
- data/test/run.rb +5 -0
- data/test/stress.rb +25 -0
- data/test/test_agent.rb +130 -0
- data/test/test_event.rb +59 -0
- data/test/test_ext.rb +196 -0
- data/test/test_fiber.rb +988 -0
- data/test/test_global_api.rb +352 -0
- data/test/test_io.rb +249 -0
- data/test/test_kernel.rb +57 -0
- data/test/test_process_supervision.rb +46 -0
- data/test/test_queue.rb +112 -0
- data/test/test_resource_pool.rb +138 -0
- data/test/test_signal.rb +100 -0
- data/test/test_socket.rb +34 -0
- data/test/test_supervise.rb +103 -0
- data/test/test_thread.rb +170 -0
- data/test/test_thread_pool.rb +101 -0
- data/test/test_throttler.rb +50 -0
- data/test/test_trace.rb +68 -0
- metadata +482 -0
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Polyphony
|
4
|
+
# Implements a limited resource pool
|
5
|
+
class ResourcePool
|
6
|
+
attr_reader :limit, :size
|
7
|
+
|
8
|
+
# Initializes a new resource pool
|
9
|
+
# @param opts [Hash] options
|
10
|
+
# @param &block [Proc] allocator block
|
11
|
+
def initialize(opts, &block)
|
12
|
+
@allocator = block
|
13
|
+
|
14
|
+
@stock = []
|
15
|
+
@queue = []
|
16
|
+
@acquired_resources = {}
|
17
|
+
|
18
|
+
@limit = opts[:limit] || 4
|
19
|
+
@size = 0
|
20
|
+
end
|
21
|
+
|
22
|
+
def available
|
23
|
+
@stock.size
|
24
|
+
end
|
25
|
+
|
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
|
47
|
+
fiber = Fiber.current
|
48
|
+
@queue << fiber
|
49
|
+
ready_resource = from_stock
|
50
|
+
return ready_resource if ready_resource
|
51
|
+
|
52
|
+
suspend
|
53
|
+
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)
|
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
|
+
def method_missing(sym, *args, &block)
|
81
|
+
acquire { |r| r.send(sym, *args, &block) }
|
82
|
+
end
|
83
|
+
|
84
|
+
def respond_to_missing?(*_args)
|
85
|
+
true
|
86
|
+
end
|
87
|
+
|
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
|
+
# Allocates a resource
|
100
|
+
# @return [any] allocated resource
|
101
|
+
def allocate
|
102
|
+
@size += 1
|
103
|
+
@allocator.().tap { |r| r.extend ResourceExtensions }
|
104
|
+
end
|
105
|
+
|
106
|
+
def <<(resource)
|
107
|
+
@size += 1
|
108
|
+
resource.extend ResourceExtensions
|
109
|
+
@stock << resource
|
110
|
+
dequeue
|
111
|
+
end
|
112
|
+
|
113
|
+
def preheat!
|
114
|
+
(@limit - @size).times { @stock << allocate }
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Polyphony
|
4
|
+
# Implements mutex lock for synchronizing access to a shared resource
|
5
|
+
class Mutex
|
6
|
+
def initialize
|
7
|
+
@waiting_fibers = Polyphony::Queue.new
|
8
|
+
end
|
9
|
+
|
10
|
+
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
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'etc'
|
4
|
+
|
5
|
+
module Polyphony
|
6
|
+
# Implements a pool of threads
|
7
|
+
class ThreadPool
|
8
|
+
attr_reader :size
|
9
|
+
|
10
|
+
def self.process(&block)
|
11
|
+
@default_pool ||= new
|
12
|
+
@default_pool.process(&block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.reset
|
16
|
+
return unless @default_pool
|
17
|
+
|
18
|
+
@default_pool.stop
|
19
|
+
@default_pool = nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(size = Etc.nprocessors)
|
23
|
+
@size = size
|
24
|
+
@task_queue = Polyphony::Queue.new
|
25
|
+
@threads = (1..@size).map { Thread.new { thread_loop } }
|
26
|
+
end
|
27
|
+
|
28
|
+
def process(&block)
|
29
|
+
setup unless @task_queue
|
30
|
+
|
31
|
+
watcher = Fiber.current.auto_watcher
|
32
|
+
@task_queue << [block, watcher]
|
33
|
+
watcher.await
|
34
|
+
end
|
35
|
+
|
36
|
+
def cast(&block)
|
37
|
+
setup unless @task_queue
|
38
|
+
|
39
|
+
@task_queue << [block, nil]
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
def busy?
|
44
|
+
!@task_queue.empty?
|
45
|
+
end
|
46
|
+
|
47
|
+
def thread_loop
|
48
|
+
loop { run_queued_task }
|
49
|
+
end
|
50
|
+
|
51
|
+
def run_queued_task
|
52
|
+
(block, watcher) = @task_queue.shift
|
53
|
+
result = block.()
|
54
|
+
watcher&.signal(result)
|
55
|
+
rescue Exception => e
|
56
|
+
watcher ? watcher.signal(e) : raise(e)
|
57
|
+
end
|
58
|
+
|
59
|
+
def stop
|
60
|
+
@threads.each(&:kill)
|
61
|
+
@threads.each(&:join)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Polyphony
|
4
|
+
# Implements general-purpose throttling
|
5
|
+
class Throttler
|
6
|
+
def initialize(rate)
|
7
|
+
@rate = rate_from_argument(rate)
|
8
|
+
@min_dt = 1.0 / @rate
|
9
|
+
@next_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
10
|
+
end
|
11
|
+
|
12
|
+
def call
|
13
|
+
now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
14
|
+
delta = @next_time - now
|
15
|
+
Thread.current.agent.sleep(delta) if delta > 0
|
16
|
+
yield self
|
17
|
+
|
18
|
+
loop do
|
19
|
+
@next_time += @min_dt
|
20
|
+
break if @next_time > now
|
21
|
+
end
|
22
|
+
end
|
23
|
+
alias_method :process, :call
|
24
|
+
|
25
|
+
def stop
|
26
|
+
@stop = true
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def rate_from_argument(arg)
|
32
|
+
return arg if arg.is_a?(Numeric)
|
33
|
+
|
34
|
+
if arg.is_a?(Hash)
|
35
|
+
return 1.0 / arg[:interval] if arg[:interval]
|
36
|
+
return arg[:rate] if arg[:rate]
|
37
|
+
end
|
38
|
+
raise "Invalid rate argument #{arg.inspect}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Polyphony
|
4
|
+
# Event watcher for thread-safe synchronisation
|
5
|
+
class Event
|
6
|
+
def await
|
7
|
+
@fiber = Fiber.current
|
8
|
+
Thread.current.agent.wait_event(true)
|
9
|
+
end
|
10
|
+
|
11
|
+
def signal(value = nil)
|
12
|
+
@fiber&.schedule(value)
|
13
|
+
ensure
|
14
|
+
@fiber = nil
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fiber'
|
4
|
+
require 'timeout'
|
5
|
+
require 'open3'
|
6
|
+
|
7
|
+
require_relative '../core/exceptions'
|
8
|
+
|
9
|
+
# Exeption overrides
|
10
|
+
class ::Exception
|
11
|
+
class << self
|
12
|
+
attr_accessor :__disable_sanitized_backtrace__
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_accessor :source_fiber
|
16
|
+
|
17
|
+
alias_method :orig_initialize, :initialize
|
18
|
+
def initialize(*args)
|
19
|
+
@__raising_fiber__ = Fiber.current
|
20
|
+
orig_initialize(*args)
|
21
|
+
end
|
22
|
+
|
23
|
+
alias_method :orig_backtrace, :backtrace
|
24
|
+
def backtrace
|
25
|
+
unless @first_backtrace_call
|
26
|
+
@first_backtrace_call = true
|
27
|
+
return orig_backtrace
|
28
|
+
end
|
29
|
+
|
30
|
+
sanitized_backtrace
|
31
|
+
end
|
32
|
+
|
33
|
+
def sanitized_backtrace
|
34
|
+
if @__raising_fiber__
|
35
|
+
backtrace = orig_backtrace || []
|
36
|
+
sanitize(backtrace + @__raising_fiber__.caller)
|
37
|
+
else
|
38
|
+
sanitize(orig_backtrace)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
POLYPHONY_DIR = File.expand_path(File.join(__dir__, '..'))
|
43
|
+
|
44
|
+
def sanitize(backtrace)
|
45
|
+
return backtrace if ::Exception.__disable_sanitized_backtrace__
|
46
|
+
|
47
|
+
backtrace.reject { |l| l[POLYPHONY_DIR] }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Overrides for Process
|
52
|
+
module ::Process
|
53
|
+
class << self
|
54
|
+
alias_method :orig_detach, :detach
|
55
|
+
def detach(pid)
|
56
|
+
fiber = spin { Thread.current.agent.waitpid(pid) }
|
57
|
+
fiber.define_singleton_method(:pid) { pid }
|
58
|
+
fiber
|
59
|
+
end
|
60
|
+
|
61
|
+
alias_method :orig_daemon, :daemon
|
62
|
+
def daemon(*args)
|
63
|
+
orig_daemon(*args)
|
64
|
+
Polyphony.original_pid = Process.pid
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Kernel extensions (methods available to all objects / call sites)
|
70
|
+
module ::Kernel
|
71
|
+
alias_method :orig_sleep, :sleep
|
72
|
+
|
73
|
+
alias_method :orig_backtick, :`
|
74
|
+
def `(cmd)
|
75
|
+
Open3.popen3(cmd) do |i, o, e, _t|
|
76
|
+
i.close
|
77
|
+
err = e.read
|
78
|
+
$stderr << err if err
|
79
|
+
o.read || ''
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
ARGV_GETS_LOOP = proc do |calling_fiber|
|
84
|
+
while (fn = ARGV.shift)
|
85
|
+
File.open(fn, 'r') do |f|
|
86
|
+
while (line = f.gets)
|
87
|
+
calling_fiber = calling_fiber.transfer(line)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
nil
|
92
|
+
rescue Exception => e
|
93
|
+
calling_fiber.transfer(e)
|
94
|
+
end
|
95
|
+
|
96
|
+
alias_method :orig_gets, :gets
|
97
|
+
def gets(*_args)
|
98
|
+
if !ARGV.empty? || @gets_fiber
|
99
|
+
@gets_fiber ||= Fiber.new(&ARGV_GETS_LOOP)
|
100
|
+
@gets_fiber.thread = Thread.current
|
101
|
+
result = @gets_fiber.alive? && @gets_fiber.safe_transfer(Fiber.current)
|
102
|
+
return result if result
|
103
|
+
|
104
|
+
@gets_fiber = nil
|
105
|
+
end
|
106
|
+
|
107
|
+
$stdin.gets
|
108
|
+
end
|
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
|
+
|
119
|
+
alias_method :orig_system, :system
|
120
|
+
def system(*args)
|
121
|
+
Open3.popen2(*args) do |i, o, _t|
|
122
|
+
i.close
|
123
|
+
pipe_to_eof(o, $stdout)
|
124
|
+
end
|
125
|
+
true
|
126
|
+
rescue SystemCallError
|
127
|
+
nil
|
128
|
+
end
|
129
|
+
|
130
|
+
def pipe_to_eof(src, dest)
|
131
|
+
loop do
|
132
|
+
data = src.readpartial(8192)
|
133
|
+
dest << data
|
134
|
+
rescue EOFError
|
135
|
+
break
|
136
|
+
end
|
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
|
163
|
+
end
|
164
|
+
|
165
|
+
# Override Timeout to use cancel scope
|
166
|
+
module ::Timeout
|
167
|
+
def self.timeout(sec, klass = nil, message = nil, &block)
|
168
|
+
cancel_after(sec, &block)
|
169
|
+
rescue Polyphony::Cancel => e
|
170
|
+
error = klass ? klass.new(message) : ::Timeout::Error.new
|
171
|
+
error.set_backtrace(e.backtrace)
|
172
|
+
raise error
|
173
|
+
end
|
174
|
+
end
|
@@ -0,0 +1,379 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fiber'
|
4
|
+
|
5
|
+
require_relative '../core/exceptions'
|
6
|
+
|
7
|
+
module Polyphony
|
8
|
+
# Fiber control API
|
9
|
+
module FiberControl
|
10
|
+
def await
|
11
|
+
if @running == false
|
12
|
+
return @result.is_a?(Exception) ? (Kernel.raise @result) : @result
|
13
|
+
end
|
14
|
+
|
15
|
+
fiber = Fiber.current
|
16
|
+
@waiting_fibers ||= {}
|
17
|
+
@waiting_fibers[fiber] = true
|
18
|
+
suspend
|
19
|
+
ensure
|
20
|
+
@waiting_fibers&.delete(fiber)
|
21
|
+
end
|
22
|
+
alias_method :join, :await
|
23
|
+
|
24
|
+
def interrupt(value = nil)
|
25
|
+
return if @running == false
|
26
|
+
|
27
|
+
schedule Polyphony::MoveOn.new(value)
|
28
|
+
end
|
29
|
+
alias_method :stop, :interrupt
|
30
|
+
|
31
|
+
def restart(value = nil)
|
32
|
+
raise "Can''t restart main fiber" if @main
|
33
|
+
|
34
|
+
if @running
|
35
|
+
schedule Polyphony::Restart.new(value)
|
36
|
+
return self
|
37
|
+
end
|
38
|
+
|
39
|
+
parent.spin(@tag, @caller, &@block).tap do |f|
|
40
|
+
f.schedule(value) unless value.nil?
|
41
|
+
end
|
42
|
+
end
|
43
|
+
alias_method :reset, :restart
|
44
|
+
|
45
|
+
def cancel
|
46
|
+
return if @running == false
|
47
|
+
|
48
|
+
schedule Polyphony::Cancel.new
|
49
|
+
end
|
50
|
+
|
51
|
+
def terminate
|
52
|
+
return if @running == false
|
53
|
+
|
54
|
+
schedule Polyphony::Terminate.new
|
55
|
+
end
|
56
|
+
|
57
|
+
def raise(*args)
|
58
|
+
error = error_from_raise_args(args)
|
59
|
+
schedule(error)
|
60
|
+
end
|
61
|
+
|
62
|
+
def error_from_raise_args(args)
|
63
|
+
case (arg = args.shift)
|
64
|
+
when String then RuntimeError.new(arg)
|
65
|
+
when Class then arg.new(args.shift)
|
66
|
+
when Exception then arg
|
67
|
+
else RuntimeError.new
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Fiber supervision
|
73
|
+
module FiberSupervision
|
74
|
+
def supervise(opts = {})
|
75
|
+
@counter = 0
|
76
|
+
@on_child_done = proc do |fiber, result|
|
77
|
+
self << fiber unless result.is_a?(Exception)
|
78
|
+
end
|
79
|
+
loop { supervise_perform(opts) }
|
80
|
+
ensure
|
81
|
+
@on_child_done = nil
|
82
|
+
end
|
83
|
+
|
84
|
+
def supervise_perform(opts)
|
85
|
+
fiber = receive
|
86
|
+
restart_fiber(fiber, opts) if fiber
|
87
|
+
rescue Polyphony::Restart
|
88
|
+
restart_all_children
|
89
|
+
rescue Exception => e
|
90
|
+
Kernel.raise e if e.source_fiber.nil? || e.source_fiber == self
|
91
|
+
|
92
|
+
restart_fiber(e.source_fiber, opts)
|
93
|
+
end
|
94
|
+
|
95
|
+
def restart_fiber(fiber, opts)
|
96
|
+
opts[:watcher]&.send [:restart, fiber]
|
97
|
+
case opts[:restart]
|
98
|
+
when true
|
99
|
+
fiber.restart
|
100
|
+
when :one_for_all
|
101
|
+
@children.keys.each(&:restart)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Class methods for controlling fibers (namely await and select)
|
107
|
+
module FiberControlClassMethods
|
108
|
+
def await(*fibers)
|
109
|
+
return [] if fibers.empty?
|
110
|
+
|
111
|
+
state = setup_await_select_state(fibers)
|
112
|
+
await_setup_monitoring(fibers, state)
|
113
|
+
suspend
|
114
|
+
fibers.map(&:result)
|
115
|
+
ensure
|
116
|
+
await_select_cleanup(state)
|
117
|
+
end
|
118
|
+
alias_method :join, :await
|
119
|
+
|
120
|
+
def setup_await_select_state(fibers)
|
121
|
+
{
|
122
|
+
awaiter: Fiber.current,
|
123
|
+
pending: fibers.each_with_object({}) { |f, h| h[f] = true }
|
124
|
+
}
|
125
|
+
end
|
126
|
+
|
127
|
+
def await_setup_monitoring(fibers, state)
|
128
|
+
fibers.each do |f|
|
129
|
+
f.when_done { |r| await_fiber_done(f, r, state) }
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def await_fiber_done(fiber, result, state)
|
134
|
+
state[:pending].delete(fiber)
|
135
|
+
|
136
|
+
if state[:cleanup]
|
137
|
+
state[:awaiter].schedule if state[:pending].empty?
|
138
|
+
elsif !state[:done] && (result.is_a?(Exception) || state[:pending].empty?)
|
139
|
+
state[:awaiter].schedule(result)
|
140
|
+
state[:done] = true
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def await_select_cleanup(state)
|
145
|
+
return if state[:pending].empty?
|
146
|
+
|
147
|
+
terminate = Polyphony::Terminate.new
|
148
|
+
state[:cleanup] = true
|
149
|
+
state[:pending].each_key { |f| f.schedule(terminate) }
|
150
|
+
suspend
|
151
|
+
end
|
152
|
+
|
153
|
+
def select(*fibers)
|
154
|
+
state = setup_await_select_state(fibers)
|
155
|
+
select_setup_monitoring(fibers, state)
|
156
|
+
suspend
|
157
|
+
ensure
|
158
|
+
await_select_cleanup(state)
|
159
|
+
end
|
160
|
+
|
161
|
+
def select_setup_monitoring(fibers, state)
|
162
|
+
fibers.each do |f|
|
163
|
+
f.when_done { |r| select_fiber_done(f, r, state) }
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def select_fiber_done(fiber, result, state)
|
168
|
+
state[:pending].delete(fiber)
|
169
|
+
if state[:cleanup]
|
170
|
+
# in cleanup mode the selector is resumed if no more pending fibers
|
171
|
+
state[:awaiter].schedule if state[:pending].empty?
|
172
|
+
elsif !state[:selected]
|
173
|
+
# first fiber to complete, we schedule the result
|
174
|
+
state[:awaiter].schedule([fiber, result])
|
175
|
+
state[:selected] = true
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Messaging functionality
|
181
|
+
module FiberMessaging
|
182
|
+
def <<(value)
|
183
|
+
@mailbox << value
|
184
|
+
end
|
185
|
+
alias_method :send, :<<
|
186
|
+
|
187
|
+
def receive
|
188
|
+
@mailbox.shift
|
189
|
+
end
|
190
|
+
|
191
|
+
def receive_pending
|
192
|
+
@mailbox.shift_all
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# Methods for controlling child fibers
|
197
|
+
module ChildFiberControl
|
198
|
+
def children
|
199
|
+
(@children ||= {}).keys
|
200
|
+
end
|
201
|
+
|
202
|
+
def spin(tag = nil, orig_caller = Kernel.caller, &block)
|
203
|
+
f = Fiber.new { |v| f.run(v) }
|
204
|
+
f.prepare(tag, block, orig_caller, self)
|
205
|
+
(@children ||= {})[f] = true
|
206
|
+
f
|
207
|
+
end
|
208
|
+
|
209
|
+
def child_done(child_fiber, result)
|
210
|
+
@children.delete(child_fiber)
|
211
|
+
@on_child_done&.(child_fiber, result)
|
212
|
+
end
|
213
|
+
|
214
|
+
def terminate_all_children
|
215
|
+
return unless @children
|
216
|
+
|
217
|
+
e = Polyphony::Terminate.new
|
218
|
+
@children.each_key { |c| c.raise e }
|
219
|
+
end
|
220
|
+
|
221
|
+
def await_all_children
|
222
|
+
return unless @children && !@children.empty?
|
223
|
+
|
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
|
232
|
+
end
|
233
|
+
|
234
|
+
def shutdown_all_children
|
235
|
+
terminate_all_children
|
236
|
+
await_all_children
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
# Fiber life cycle methods
|
241
|
+
module FiberLifeCycle
|
242
|
+
def prepare(tag, block, caller, parent)
|
243
|
+
@thread = Thread.current
|
244
|
+
@tag = tag
|
245
|
+
@parent = parent
|
246
|
+
@caller = caller
|
247
|
+
@block = block
|
248
|
+
@mailbox = Polyphony::Queue.new
|
249
|
+
__fiber_trace__(:fiber_create, self)
|
250
|
+
schedule
|
251
|
+
end
|
252
|
+
|
253
|
+
def run(first_value)
|
254
|
+
setup first_value
|
255
|
+
result = @block.(first_value)
|
256
|
+
finalize result
|
257
|
+
rescue Polyphony::Restart => e
|
258
|
+
restart_self(e.value)
|
259
|
+
rescue Polyphony::MoveOn, Polyphony::Terminate => e
|
260
|
+
finalize e.value
|
261
|
+
rescue Exception => e
|
262
|
+
e.source_fiber = self
|
263
|
+
finalize e, true
|
264
|
+
end
|
265
|
+
|
266
|
+
def setup(first_value)
|
267
|
+
Kernel.raise first_value if first_value.is_a?(Exception)
|
268
|
+
|
269
|
+
@running = true
|
270
|
+
end
|
271
|
+
|
272
|
+
# Performs setup for a "raw" Fiber created using Fiber.new. Note that this
|
273
|
+
# fiber is an orphan fiber (has no parent), since we cannot control how the
|
274
|
+
# fiber terminates after it has already been created. Calling #setup_raw
|
275
|
+
# allows the fiber to be scheduled and to receive messages.
|
276
|
+
def setup_raw
|
277
|
+
@thread = Thread.current
|
278
|
+
@mailbox = Polyphony::Queue.new
|
279
|
+
end
|
280
|
+
|
281
|
+
def setup_main_fiber
|
282
|
+
@main = true
|
283
|
+
@tag = :main
|
284
|
+
@thread = Thread.current
|
285
|
+
@running = true
|
286
|
+
@children&.clear
|
287
|
+
@mailbox = Polyphony::Queue.new
|
288
|
+
end
|
289
|
+
|
290
|
+
def restart_self(first_value)
|
291
|
+
@mailbox = Polyphony::Queue.new
|
292
|
+
@when_done_procs = nil
|
293
|
+
@waiting_fibers = nil
|
294
|
+
run(first_value)
|
295
|
+
end
|
296
|
+
|
297
|
+
def finalize(result, uncaught_exception = false)
|
298
|
+
result, uncaught_exception = finalize_children(result, uncaught_exception)
|
299
|
+
__fiber_trace__(:fiber_terminate, self, result)
|
300
|
+
@result = result
|
301
|
+
@running = false
|
302
|
+
inform_dependants(result, uncaught_exception)
|
303
|
+
ensure
|
304
|
+
Thread.current.switch_fiber
|
305
|
+
end
|
306
|
+
|
307
|
+
# Shuts down all children of the current fiber. If any exception occurs while
|
308
|
+
# the children are shut down, it is returned along with the uncaught_exception
|
309
|
+
# flag set. Otherwise, it returns the given arguments.
|
310
|
+
def finalize_children(result, uncaught_exception)
|
311
|
+
begin
|
312
|
+
shutdown_all_children
|
313
|
+
rescue Exception => e
|
314
|
+
result = e
|
315
|
+
uncaught_exception = true
|
316
|
+
end
|
317
|
+
[result, uncaught_exception]
|
318
|
+
end
|
319
|
+
|
320
|
+
def inform_dependants(result, uncaught_exception)
|
321
|
+
@parent&.child_done(self, result)
|
322
|
+
@when_done_procs&.each { |p| p.(result) }
|
323
|
+
@waiting_fibers&.each_key do |f|
|
324
|
+
f.schedule(result)
|
325
|
+
end
|
326
|
+
return unless uncaught_exception && !@waiting_fibers
|
327
|
+
|
328
|
+
# propagate unaught exception to parent
|
329
|
+
@parent&.schedule(result)
|
330
|
+
end
|
331
|
+
|
332
|
+
def when_done(&block)
|
333
|
+
@when_done_procs ||= []
|
334
|
+
@when_done_procs << block
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
# Fiber extensions
|
340
|
+
class ::Fiber
|
341
|
+
prepend Polyphony::FiberControl
|
342
|
+
include Polyphony::FiberSupervision
|
343
|
+
include Polyphony::FiberMessaging
|
344
|
+
include Polyphony::ChildFiberControl
|
345
|
+
include Polyphony::FiberLifeCycle
|
346
|
+
|
347
|
+
extend Polyphony::FiberControlClassMethods
|
348
|
+
|
349
|
+
attr_accessor :tag, :thread, :parent
|
350
|
+
attr_reader :result
|
351
|
+
|
352
|
+
def running?
|
353
|
+
@running
|
354
|
+
end
|
355
|
+
|
356
|
+
def inspect
|
357
|
+
"#<Fiber:#{object_id} #{location} (#{state})>"
|
358
|
+
end
|
359
|
+
alias_method :to_s, :inspect
|
360
|
+
|
361
|
+
def location
|
362
|
+
@caller ? @caller[0] : '(root)'
|
363
|
+
end
|
364
|
+
|
365
|
+
def caller
|
366
|
+
spin_caller = @caller || []
|
367
|
+
if @parent
|
368
|
+
spin_caller + @parent.caller
|
369
|
+
else
|
370
|
+
spin_caller
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
def main?
|
375
|
+
@main
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
Fiber.current.setup_main_fiber
|