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.
Files changed (221) hide show
  1. checksums.yaml +7 -0
  2. data/.gitbook.yaml +4 -0
  3. data/.github/workflows/test.yml +29 -0
  4. data/.gitignore +59 -0
  5. data/.rubocop.yml +175 -0
  6. data/CHANGELOG.md +393 -0
  7. data/Gemfile +3 -0
  8. data/Gemfile.lock +141 -0
  9. data/LICENSE +21 -0
  10. data/README.md +51 -0
  11. data/Rakefile +26 -0
  12. data/TODO.md +201 -0
  13. data/bin/polyphony-debug +87 -0
  14. data/docs/_config.yml +64 -0
  15. data/docs/_includes/head.html +40 -0
  16. data/docs/_includes/title.html +1 -0
  17. data/docs/_sass/custom/custom.scss +10 -0
  18. data/docs/_sass/overrides.scss +0 -0
  19. data/docs/_user-guide/all-about-timers.md +126 -0
  20. data/docs/_user-guide/index.md +9 -0
  21. data/docs/_user-guide/web-server.md +136 -0
  22. data/docs/api-reference/exception.md +27 -0
  23. data/docs/api-reference/fiber.md +425 -0
  24. data/docs/api-reference/index.md +9 -0
  25. data/docs/api-reference/io.md +36 -0
  26. data/docs/api-reference/object.md +99 -0
  27. data/docs/api-reference/polyphony-baseexception.md +33 -0
  28. data/docs/api-reference/polyphony-cancel.md +26 -0
  29. data/docs/api-reference/polyphony-moveon.md +24 -0
  30. data/docs/api-reference/polyphony-net.md +20 -0
  31. data/docs/api-reference/polyphony-process.md +28 -0
  32. data/docs/api-reference/polyphony-resourcepool.md +59 -0
  33. data/docs/api-reference/polyphony-restart.md +18 -0
  34. data/docs/api-reference/polyphony-terminate.md +18 -0
  35. data/docs/api-reference/polyphony-threadpool.md +67 -0
  36. data/docs/api-reference/polyphony-throttler.md +77 -0
  37. data/docs/api-reference/polyphony.md +36 -0
  38. data/docs/api-reference/thread.md +88 -0
  39. data/docs/assets/img/echo-fibers.svg +1 -0
  40. data/docs/assets/img/sleeping-fiber.svg +1 -0
  41. data/docs/faq.md +195 -0
  42. data/docs/favicon.ico +0 -0
  43. data/docs/getting-started/index.md +10 -0
  44. data/docs/getting-started/installing.md +34 -0
  45. data/docs/getting-started/overview.md +486 -0
  46. data/docs/getting-started/tutorial.md +359 -0
  47. data/docs/index.md +94 -0
  48. data/docs/main-concepts/concurrency.md +151 -0
  49. data/docs/main-concepts/design-principles.md +161 -0
  50. data/docs/main-concepts/exception-handling.md +291 -0
  51. data/docs/main-concepts/extending.md +89 -0
  52. data/docs/main-concepts/fiber-scheduling.md +197 -0
  53. data/docs/main-concepts/index.md +9 -0
  54. data/docs/polyphony-logo.png +0 -0
  55. data/examples/adapters/concurrent-ruby.rb +9 -0
  56. data/examples/adapters/pg_client.rb +36 -0
  57. data/examples/adapters/pg_notify.rb +35 -0
  58. data/examples/adapters/pg_pool.rb +43 -0
  59. data/examples/adapters/pg_transaction.rb +31 -0
  60. data/examples/adapters/redis_blpop.rb +12 -0
  61. data/examples/adapters/redis_channels.rb +122 -0
  62. data/examples/adapters/redis_client.rb +19 -0
  63. data/examples/adapters/redis_pubsub.rb +26 -0
  64. data/examples/adapters/redis_pubsub_perf.rb +68 -0
  65. data/examples/core/01-spinning-up-fibers.rb +18 -0
  66. data/examples/core/02-awaiting-fibers.rb +20 -0
  67. data/examples/core/03-interrupting.rb +39 -0
  68. data/examples/core/04-handling-signals.rb +19 -0
  69. data/examples/core/xx-agent.rb +102 -0
  70. data/examples/core/xx-at_exit.rb +29 -0
  71. data/examples/core/xx-caller.rb +12 -0
  72. data/examples/core/xx-channels.rb +45 -0
  73. data/examples/core/xx-daemon.rb +14 -0
  74. data/examples/core/xx-deadlock.rb +8 -0
  75. data/examples/core/xx-deferring-an-operation.rb +14 -0
  76. data/examples/core/xx-erlang-style-genserver.rb +81 -0
  77. data/examples/core/xx-exception-backtrace.rb +40 -0
  78. data/examples/core/xx-fork-cleanup.rb +22 -0
  79. data/examples/core/xx-fork-spin.rb +42 -0
  80. data/examples/core/xx-fork-terminate.rb +27 -0
  81. data/examples/core/xx-forking.rb +24 -0
  82. data/examples/core/xx-move_on.rb +23 -0
  83. data/examples/core/xx-pingpong.rb +18 -0
  84. data/examples/core/xx-queue-async.rb +120 -0
  85. data/examples/core/xx-readpartial.rb +18 -0
  86. data/examples/core/xx-recurrent-timer.rb +12 -0
  87. data/examples/core/xx-resource_delegate.rb +31 -0
  88. data/examples/core/xx-signals.rb +16 -0
  89. data/examples/core/xx-sleep-forever.rb +9 -0
  90. data/examples/core/xx-sleeping.rb +25 -0
  91. data/examples/core/xx-snooze-starve.rb +16 -0
  92. data/examples/core/xx-spin-fork.rb +49 -0
  93. data/examples/core/xx-spin_error_backtrace.rb +33 -0
  94. data/examples/core/xx-state-machine.rb +51 -0
  95. data/examples/core/xx-stop.rb +20 -0
  96. data/examples/core/xx-supervise-process.rb +30 -0
  97. data/examples/core/xx-supervisors.rb +21 -0
  98. data/examples/core/xx-thread-selector-sleep.rb +51 -0
  99. data/examples/core/xx-thread-selector-snooze.rb +46 -0
  100. data/examples/core/xx-thread-sleep.rb +17 -0
  101. data/examples/core/xx-thread-snooze.rb +34 -0
  102. data/examples/core/xx-thread_pool.rb +17 -0
  103. data/examples/core/xx-throttling.rb +18 -0
  104. data/examples/core/xx-timeout.rb +10 -0
  105. data/examples/core/xx-timer-gc.rb +17 -0
  106. data/examples/core/xx-trace.rb +79 -0
  107. data/examples/core/xx-using-a-mutex.rb +21 -0
  108. data/examples/core/xx-worker-thread.rb +30 -0
  109. data/examples/io/tunnel.rb +48 -0
  110. data/examples/io/xx-backticks.rb +11 -0
  111. data/examples/io/xx-echo_client.rb +25 -0
  112. data/examples/io/xx-echo_client_from_stdin.rb +21 -0
  113. data/examples/io/xx-echo_pipe.rb +16 -0
  114. data/examples/io/xx-echo_server.rb +17 -0
  115. data/examples/io/xx-echo_server_with_timeout.rb +34 -0
  116. data/examples/io/xx-echo_stdin.rb +14 -0
  117. data/examples/io/xx-happy-eyeballs.rb +36 -0
  118. data/examples/io/xx-httparty.rb +38 -0
  119. data/examples/io/xx-irb.rb +17 -0
  120. data/examples/io/xx-net-http.rb +15 -0
  121. data/examples/io/xx-open.rb +16 -0
  122. data/examples/io/xx-switch.rb +15 -0
  123. data/examples/io/xx-system.rb +11 -0
  124. data/examples/io/xx-tcpserver.rb +15 -0
  125. data/examples/io/xx-tcpsocket.rb +18 -0
  126. data/examples/io/xx-zip.rb +19 -0
  127. data/examples/performance/fiber_transfer.rb +47 -0
  128. data/examples/performance/fs_read.rb +38 -0
  129. data/examples/performance/mem-usage.rb +56 -0
  130. data/examples/performance/messaging.rb +29 -0
  131. data/examples/performance/multi_snooze.rb +33 -0
  132. data/examples/performance/snooze.rb +39 -0
  133. data/examples/performance/snooze_raw.rb +39 -0
  134. data/examples/performance/thread-vs-fiber/polyphony_mt_server.rb +74 -0
  135. data/examples/performance/thread-vs-fiber/polyphony_server.rb +45 -0
  136. data/examples/performance/thread-vs-fiber/polyphony_server_read_loop.rb +58 -0
  137. data/examples/performance/thread-vs-fiber/threaded_server.rb +27 -0
  138. data/examples/performance/thread-vs-fiber/xx-httparty_multi.rb +36 -0
  139. data/examples/performance/thread-vs-fiber/xx-httparty_threaded.rb +29 -0
  140. data/examples/performance/thread_pool_perf.rb +63 -0
  141. data/examples/performance/xx-array.rb +11 -0
  142. data/examples/performance/xx-fiber-switch.rb +9 -0
  143. data/examples/performance/xx-snooze.rb +15 -0
  144. data/examples/xx-spin.rb +32 -0
  145. data/ext/libev/Changes +548 -0
  146. data/ext/libev/LICENSE +37 -0
  147. data/ext/libev/README +59 -0
  148. data/ext/libev/README.embed +3 -0
  149. data/ext/libev/ev.c +5279 -0
  150. data/ext/libev/ev.h +856 -0
  151. data/ext/libev/ev_epoll.c +296 -0
  152. data/ext/libev/ev_kqueue.c +224 -0
  153. data/ext/libev/ev_linuxaio.c +642 -0
  154. data/ext/libev/ev_poll.c +156 -0
  155. data/ext/libev/ev_port.c +192 -0
  156. data/ext/libev/ev_select.c +316 -0
  157. data/ext/libev/ev_vars.h +215 -0
  158. data/ext/libev/ev_win32.c +162 -0
  159. data/ext/libev/ev_wrap.h +216 -0
  160. data/ext/libev/test_libev_win32.c +123 -0
  161. data/ext/polyphony/extconf.rb +20 -0
  162. data/ext/polyphony/fiber.c +109 -0
  163. data/ext/polyphony/libev.c +2 -0
  164. data/ext/polyphony/libev.h +9 -0
  165. data/ext/polyphony/libev_agent.c +882 -0
  166. data/ext/polyphony/polyphony.c +71 -0
  167. data/ext/polyphony/polyphony.h +97 -0
  168. data/ext/polyphony/polyphony_ext.c +21 -0
  169. data/ext/polyphony/queue.c +168 -0
  170. data/ext/polyphony/ring_buffer.c +96 -0
  171. data/ext/polyphony/ring_buffer.h +28 -0
  172. data/ext/polyphony/thread.c +208 -0
  173. data/ext/polyphony/tracing.c +11 -0
  174. data/lib/polyphony.rb +136 -0
  175. data/lib/polyphony/adapters/fs.rb +19 -0
  176. data/lib/polyphony/adapters/irb.rb +52 -0
  177. data/lib/polyphony/adapters/postgres.rb +110 -0
  178. data/lib/polyphony/adapters/process.rb +33 -0
  179. data/lib/polyphony/adapters/redis.rb +67 -0
  180. data/lib/polyphony/adapters/trace.rb +138 -0
  181. data/lib/polyphony/core/channel.rb +46 -0
  182. data/lib/polyphony/core/exceptions.rb +36 -0
  183. data/lib/polyphony/core/global_api.rb +124 -0
  184. data/lib/polyphony/core/resource_pool.rb +117 -0
  185. data/lib/polyphony/core/sync.rb +21 -0
  186. data/lib/polyphony/core/thread_pool.rb +64 -0
  187. data/lib/polyphony/core/throttler.rb +41 -0
  188. data/lib/polyphony/event.rb +17 -0
  189. data/lib/polyphony/extensions/core.rb +174 -0
  190. data/lib/polyphony/extensions/fiber.rb +379 -0
  191. data/lib/polyphony/extensions/io.rb +221 -0
  192. data/lib/polyphony/extensions/openssl.rb +81 -0
  193. data/lib/polyphony/extensions/socket.rb +150 -0
  194. data/lib/polyphony/extensions/thread.rb +108 -0
  195. data/lib/polyphony/net.rb +77 -0
  196. data/lib/polyphony/version.rb +5 -0
  197. data/polyphony.gemspec +40 -0
  198. data/test/coverage.rb +54 -0
  199. data/test/eg.rb +27 -0
  200. data/test/helper.rb +56 -0
  201. data/test/q.rb +24 -0
  202. data/test/run.rb +5 -0
  203. data/test/stress.rb +25 -0
  204. data/test/test_agent.rb +130 -0
  205. data/test/test_event.rb +59 -0
  206. data/test/test_ext.rb +196 -0
  207. data/test/test_fiber.rb +988 -0
  208. data/test/test_global_api.rb +352 -0
  209. data/test/test_io.rb +249 -0
  210. data/test/test_kernel.rb +57 -0
  211. data/test/test_process_supervision.rb +46 -0
  212. data/test/test_queue.rb +112 -0
  213. data/test/test_resource_pool.rb +138 -0
  214. data/test/test_signal.rb +100 -0
  215. data/test/test_socket.rb +34 -0
  216. data/test/test_supervise.rb +103 -0
  217. data/test/test_thread.rb +170 -0
  218. data/test/test_thread_pool.rb +101 -0
  219. data/test/test_throttler.rb +50 -0
  220. data/test/test_trace.rb +68 -0
  221. 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