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,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../polyphony'
4
+ require 'pg'
5
+
6
+ # PG overrides
7
+ module ::PG
8
+ def self.connect(*args)
9
+ Connection.connect_start(*args).tap(&method(:connect_async))
10
+ end
11
+
12
+ def self.connect_async(conn)
13
+ socket_io = conn.socket_io
14
+ loop do
15
+ res = conn.connect_poll
16
+ case res
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)
20
+ when PGRES_POLLING_OK then return conn.setnonblocking(true)
21
+ end
22
+ end
23
+ end
24
+
25
+ def self.connect_sync(conn)
26
+ loop do
27
+ res = conn.connect_poll
28
+ case res
29
+ when PGRES_POLLING_FAILED
30
+ raise Error, conn.error_message
31
+ when PGRES_POLLING_OK
32
+ conn.setnonblocking(true)
33
+ return
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ # Overrides for PG connection
40
+ class ::PG::Connection
41
+ alias_method :orig_get_result, :get_result
42
+
43
+ def get_result(&block)
44
+ while is_busy
45
+ Thread.current.agent.wait_io(socket_io, false)
46
+ consume_input
47
+ end
48
+ orig_get_result(&block)
49
+ end
50
+
51
+ alias_method :orig_async_exec, :async_exec
52
+ def async_exec(*args, &block)
53
+ send_query(*args)
54
+ get_result(&block)
55
+ ensure
56
+ # cleanup result in order to allow next query
57
+ while get_result; end
58
+ end
59
+
60
+ def block(_timeout = 0)
61
+ while is_busy
62
+ Thread.current.agent.wait_io(socket_io, false)
63
+ consume_input
64
+ end
65
+ end
66
+
67
+ SQL_BEGIN = 'begin'
68
+ SQL_COMMIT = 'commit'
69
+ SQL_ROLLBACK = 'rollback'
70
+
71
+ # Starts a transaction, runs given block, and commits transaction. If an
72
+ # error is raised, the transaction is rolled back and the error is raised
73
+ # again.
74
+ # @return [void]
75
+ def transaction(&block)
76
+ return yield if @transaction # allow nesting of calls to #transactions
77
+
78
+ perform_transaction(&block)
79
+ end
80
+
81
+ def perform_transaction
82
+ query(SQL_BEGIN)
83
+ began = true
84
+ @transaction = true
85
+ yield
86
+ query(SQL_COMMIT)
87
+ rescue StandardError => e
88
+ query(SQL_ROLLBACK) if began
89
+ raise e
90
+ ensure
91
+ @transaction = false
92
+ end
93
+
94
+ self.async_api = true
95
+
96
+ def wait_for_notify(timeout = nil, &block)
97
+ return move_on_after(timeout) { wait_for_notify(&block) } if timeout
98
+
99
+ loop do
100
+ Thread.current.agent.wait_io(socket_io, false)
101
+ consume_input
102
+ notice = notifies
103
+ next unless notice
104
+
105
+ values = notice.values
106
+ block&.(*values)
107
+ return values.first
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polyphony
4
+ # Process patches
5
+ module Process
6
+ class << self
7
+ def watch(cmd = nil, &block)
8
+ terminated = nil
9
+ pid = cmd ? Kernel.spawn(cmd) : Polyphony.fork(&block)
10
+ Thread.current.agent.waitpid(pid)
11
+ terminated = true
12
+ ensure
13
+ kill_process(pid) unless terminated || pid.nil?
14
+ end
15
+
16
+ def kill_process(pid)
17
+ cancel_after(5) do
18
+ kill_and_await('TERM', pid)
19
+ end
20
+ rescue Polyphony::Cancel
21
+ kill_and_await(-9, pid)
22
+ end
23
+
24
+ def kill_and_await(sig, pid)
25
+ ::Process.kill(sig, pid)
26
+ Thread.current.agent.waitpid(pid)
27
+ rescue SystemCallError
28
+ # ignore
29
+ puts 'SystemCallError in kill_and_await'
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../polyphony'
4
+
5
+ require 'redis'
6
+ require 'hiredis/reader'
7
+
8
+ # Polyphony-based Redis driver
9
+ class Polyphony::RedisDriver
10
+ def self.connect(config)
11
+ raise 'unix sockets not supported' if config[:scheme] == 'unix'
12
+
13
+ # connection.connect_unix(config[:path], connect_timeout)
14
+
15
+ raise 'ssl not supported' if config[:scheme] == 'rediss' || config[:ssl]
16
+
17
+ # raise NotImplementedError, "SSL not supported by hiredis driver"
18
+
19
+ new(config[:host], config[:port])
20
+ # connection.connect(config[:host], config[:port], connect_timeout)
21
+ end
22
+
23
+ def initialize(host, port)
24
+ @connection = Polyphony::Net.tcp_connect(host, port)
25
+ @reader = ::Hiredis::Reader.new
26
+ end
27
+
28
+ def connected?
29
+ @connection && !@connection.closed?
30
+ end
31
+
32
+ def timeout=(timeout)
33
+ # ignore timeout for now
34
+ end
35
+
36
+ def disconnect
37
+ @connection.close
38
+ @connection = nil
39
+ end
40
+
41
+ def write(command)
42
+ @connection.write(format_command(command))
43
+ end
44
+
45
+ def format_command(args)
46
+ args = args.flatten
47
+ (+"*#{args.size}\r\n").tap do |s|
48
+ args.each do |a|
49
+ a = a.to_s
50
+ s << "$#{a.bytesize}\r\n#{a}\r\n"
51
+ end
52
+ end
53
+ end
54
+
55
+ def read
56
+ reply = @reader.gets
57
+ return reply if reply
58
+
59
+ while (data = @connection.readpartial(8192))
60
+ @reader.feed(data)
61
+ reply = @reader.gets
62
+ return reply unless reply == false
63
+ end
64
+ end
65
+ end
66
+
67
+ Redis::Connection.drivers << Polyphony::RedisDriver
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../polyphony'
4
+
5
+ STOCK_EVENTS = %i[line call return c_call c_return b_call b_return].freeze
6
+
7
+ module Polyphony
8
+ # Tracing functionality for Polyphony
9
+ module Trace
10
+ class << self
11
+ def new(*events)
12
+ start_stamp = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
13
+ events = STOCK_EVENTS if events.empty?
14
+ ::TracePoint.new(*events) { |tp| yield trace_record(tp, start_stamp) }
15
+ end
16
+
17
+ def trace_record(trp, start_stamp)
18
+ stamp = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start_stamp
19
+
20
+ { stamp: stamp, event: trp.event, location: "#{trp.path}:#{trp.lineno}",
21
+ self: trp.self, binding: trp.binding, fiber: tp_fiber(trp),
22
+ lineno: trp.lineno, method_id: trp.method_id,
23
+ path: trp.path, parameters: tp_params(trp),
24
+ return_value: tp_return_value(trp), schedule_value: tp_schedule_value(trp),
25
+ exception: tp_raised_exception(trp) }
26
+ end
27
+
28
+ def tp_fiber(trp)
29
+ trp.is_a?(FiberTracePoint) ? trp.fiber : Fiber.current
30
+ end
31
+
32
+ PARAMS_EVENTS = %i[call c_call b_call].freeze
33
+
34
+ def tp_params(trp)
35
+ PARAMS_EVENTS.include?(trp.event) ? trp.parameters : nil
36
+ end
37
+
38
+ RETURN_VALUE_EVENTS = %i[return c_return b_return].freeze
39
+
40
+ def tp_return_value(trp)
41
+ RETURN_VALUE_EVENTS.include?(trp.event) ? trp.return_value : nil
42
+ end
43
+
44
+ SCHEDULE_VALUE_EVENTS = %i[fiber_schedule fiber_run].freeze
45
+
46
+ def tp_schedule_value(trp)
47
+ SCHEDULE_VALUE_EVENTS.include?(trp.event) ? trp.value : nil
48
+ end
49
+
50
+ def tp_raised_exception(trp)
51
+ trp.event == :raise && trp.raised_exception
52
+ end
53
+
54
+ def analyze(records)
55
+ by_fiber = Hash.new { |h, f| h[f] = [] }
56
+ records.each_with_object(by_fiber) { |r, h| h[r[:fiber]] << r }
57
+ { by_fiber: by_fiber }
58
+ end
59
+
60
+ # Implements fake TracePoint instances for fiber-related events
61
+ class FiberTracePoint
62
+ attr_reader :event, :fiber, :value
63
+
64
+ def initialize(tpoint)
65
+ @tp = tpoint
66
+ @event = tpoint.return_value[0]
67
+ @fiber = tpoint.return_value[1]
68
+ @value = tpoint.return_value[2]
69
+ end
70
+
71
+ def lineno
72
+ @tp.lineno
73
+ end
74
+
75
+ def method_id
76
+ @tp.method_id
77
+ end
78
+
79
+ def path
80
+ @tp.path
81
+ end
82
+
83
+ def self
84
+ @tp.self
85
+ end
86
+
87
+ def binding
88
+ @tp.binding
89
+ end
90
+ end
91
+
92
+ class << ::TracePoint
93
+ POLYPHONY_FILE_REGEXP = /^#{::Exception::POLYPHONY_DIR}/.freeze
94
+
95
+ alias_method :orig_new, :new
96
+ def new(*args, &block)
97
+ events_mask, fiber_events_mask = event_masks(args)
98
+
99
+ orig_new(*events_mask) do |tp|
100
+ handle_tp_event(tp, fiber_events_mask, &block)
101
+ end
102
+ end
103
+
104
+ def handle_tp_event(tpoint, fiber_events_mask)
105
+ # next unless !$watched_fiber || Fiber.current == $watched_fiber
106
+
107
+ if tpoint.method_id == :__fiber_trace__
108
+ return if tpoint.event != :c_return
109
+ return unless fiber_events_mask.include?(tpoint.return_value[0])
110
+
111
+ tpoint = FiberTracePoint.new(tpoint)
112
+ elsif tpoint.path =~ POLYPHONY_FILE_REGEXP
113
+ return
114
+ end
115
+
116
+ yield tpoint
117
+ end
118
+
119
+ ALL_FIBER_EVENTS = %i[
120
+ fiber_create fiber_terminate fiber_schedule fiber_switchpoint fiber_run
121
+ fiber_ev_loop_enter fiber_ev_loop_leave
122
+ ].freeze
123
+
124
+ def event_masks(events)
125
+ events.each_with_object([[], []]) do |e, masks|
126
+ case e
127
+ when /fiber_/
128
+ masks[1] += e == :fiber_all ? ALL_FIBER_EVENTS : [e]
129
+ masks[0] << :c_return unless masks[0].include?(:c_return)
130
+ else
131
+ masks[0] << e
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './exceptions'
4
+
5
+ module Polyphony
6
+ # Implements a unidirectional communication channel along the lines of Go
7
+ # (buffered) channels.
8
+ class Channel
9
+ def initialize
10
+ @payload_queue = []
11
+ @waiting_queue = []
12
+ end
13
+
14
+ 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
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polyphony
4
+ # Common exception class for interrupting fibers. These exceptions allow
5
+ # control of fibers. BaseException exceptions can encapsulate a value and thus
6
+ # provide a way to interrupt long-running blocking operations while still
7
+ # passing a value back to the call site. BaseException exceptions can also
8
+ # references a cancel scope in order to allow correct bubbling of exceptions
9
+ # through nested cancel scopes.
10
+ class BaseException < ::Exception
11
+ attr_reader :value
12
+
13
+ def initialize(value = nil)
14
+ @caller_backtrace = caller
15
+ @value = value
16
+ end
17
+
18
+ def backtrace
19
+ sanitize(@caller_backtrace)
20
+ end
21
+ end
22
+
23
+ # MoveOn is used to interrupt a long-running blocking operation, while
24
+ # continuing the rest of the computation.
25
+ class MoveOn < BaseException; end
26
+
27
+ # Cancel is used to interrupt a long-running blocking operation, bubbling the
28
+ # exception up through cancel scopes and supervisors.
29
+ class Cancel < BaseException; end
30
+
31
+ # Terminate is used to interrupt a fiber once its parent fiber has terminated.
32
+ class Terminate < BaseException; end
33
+
34
+ # Restart is used to restart a fiber
35
+ class Restart < BaseException; end
36
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../extensions/core'
4
+ require_relative '../extensions/fiber'
5
+ require_relative './exceptions'
6
+ require_relative './throttler'
7
+
8
+ module Polyphony
9
+ # Global API methods to be included in ::Object
10
+ module GlobalAPI
11
+ def after(interval, &block)
12
+ spin do
13
+ sleep interval
14
+ block.()
15
+ end
16
+ end
17
+
18
+ def cancel_after(interval, with_exception: Polyphony::Cancel, &block)
19
+ fiber = ::Fiber.current
20
+ canceller = spin do
21
+ sleep interval
22
+ exception = with_exception.is_a?(Class) ?
23
+ with_exception.new : RuntimeError.new(with_exception)
24
+ fiber.schedule exception
25
+ end
26
+ block ? cancel_after_wrap_block(canceller, &block) : canceller
27
+ end
28
+
29
+ def cancel_after_wrap_block(canceller, &block)
30
+ block.call
31
+ ensure
32
+ canceller.stop
33
+ end
34
+
35
+ def spin(tag = nil, &block)
36
+ Fiber.current.spin(tag, caller, &block)
37
+ end
38
+
39
+ def spin_loop(tag = nil, rate: nil, &block)
40
+ if rate
41
+ Fiber.current.spin(tag, caller) do
42
+ throttled_loop(rate, &block)
43
+ end
44
+ else
45
+ Fiber.current.spin(tag, caller) { loop(&block) }
46
+ end
47
+ end
48
+
49
+ def every(interval)
50
+ next_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + interval
51
+ loop do
52
+ now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
53
+ Thread.current.agent.sleep(next_time - now)
54
+ yield
55
+ loop do
56
+ next_time += interval
57
+ break if next_time > now
58
+ end
59
+ end
60
+ end
61
+
62
+ def move_on_after(interval, with_value: nil, &block)
63
+ fiber = ::Fiber.current
64
+ unless block
65
+ return spin do
66
+ sleep interval
67
+ fiber.schedule with_value
68
+ end
69
+ end
70
+
71
+ move_on_after_with_block(fiber, interval, with_value, &block)
72
+ end
73
+
74
+ def move_on_after_with_block(fiber, interval, with_value, &block)
75
+ canceller = spin do
76
+ sleep interval
77
+ fiber.schedule Polyphony::MoveOn.new(with_value)
78
+ end
79
+ block.call
80
+ rescue Polyphony::MoveOn => e
81
+ e.value
82
+ ensure
83
+ canceller.stop
84
+ end
85
+
86
+ def receive
87
+ Fiber.current.receive
88
+ end
89
+
90
+ def receive_pending
91
+ Fiber.current.receive_pending
92
+ end
93
+
94
+ def supervise(*args, &block)
95
+ Fiber.current.supervise(*args, &block)
96
+ end
97
+
98
+ def sleep(duration = nil)
99
+ return sleep_forever unless duration
100
+
101
+ Thread.current.agent.sleep duration
102
+ end
103
+
104
+ def sleep_forever
105
+ Thread.current.agent.ref
106
+ loop { sleep 60 }
107
+ ensure
108
+ Thread.current.agent.unref
109
+ end
110
+
111
+ def throttled_loop(rate, count: nil, &block)
112
+ throttler = Polyphony::Throttler.new(rate)
113
+ if count
114
+ count.times { |_i| throttler.(&block) }
115
+ else
116
+ loop { throttler.(&block) }
117
+ end
118
+ ensure
119
+ throttler&.stop
120
+ end
121
+ end
122
+ end
123
+
124
+ Object.include Polyphony::GlobalAPI