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,151 @@
1
+ ---
2
+ layout: page
3
+ title: Concurrency the Easy Way
4
+ nav_order: 1
5
+ parent: Main Concepts
6
+ permalink: /main-concepts/concurrency/
7
+ prev_title: Tutorial
8
+ next_title: How Fibers are Scheduled
9
+ ---
10
+ # Concurrency the Easy Way
11
+
12
+ Concurrency is a major consideration for modern programmers. Applications and
13
+ digital platforms are nowadays expected to do multiple things at once: serve
14
+ multiple clients, process multiple background jobs, talk to multiple external
15
+ services. Concurrency is the property of our programming environment allowing us
16
+ to schedule and control multiple ongoing operations.
17
+
18
+ Traditionally, concurrency has been achieved by using multiple processes or
19
+ threads. Both approaches have proven problematic. Processes consume relatively a
20
+ lot of memory, and are relatively difficult to coordinate. Threads consume less
21
+ memory than processes and make it difficult to synchronize access to shared
22
+ resources, often leading to race conditions and memory corruption. Using threads
23
+ often necessitates either using special-purpose thread-safe data structures, or
24
+ otherwise protecting shared resource access using mutexes and critical sections.
25
+ In addition, dynamic languages such as Ruby and Python will synchronize multiple
26
+ threads using a global interpreter lock, which means thread execution cannot be
27
+ parallelized. Furthermore, the amount of threads and processes on a single
28
+ system is relatively limited, to the order of several hundreds or a few thousand
29
+ at most.
30
+
31
+ Polyphony offers a third way to write concurrent programs, by using a Ruby
32
+ construct called [fibers](https://ruby-doc.org/core-2.6.5/Fiber.html). Fibers,
33
+ based on the idea of [coroutines](https://en.wikipedia.org/wiki/Coroutine),
34
+ provide a way to run a computation that can be suspended and resumed at any
35
+ moment. For example, a computation waiting for a reply from a database can
36
+ suspend itself, transferring control to another ongoing computation, and be
37
+ resumed once the database has sent back its reply. Meanwhile, another
38
+ computation is started that opens a socket to a remote service, and then
39
+ suspends itself, waiting for the connection to be established.
40
+
41
+ This form of concurrency, called cooperative concurrency (in contrast to
42
+ pre-emptive concurrency, like in threads and processes), offers many advantages,
43
+ especially for applications that are [I/O
44
+ bound](https://en.wikipedia.org/wiki/I/O_bound). Fibers are very lightweight
45
+ (starting at about 10KB), can be context-switched faster than threads or
46
+ processes, and literally millions of them can be created on a single system -
47
+ the only limiting factor is available memory.
48
+
49
+ Polyphony takes Ruby's fibers and adds a way to schedule and switch between them
50
+ automatically whenever a blocking operation is started, such as waiting for a
51
+ TCP connection to be established, for incoming data on an HTTP conection, or for
52
+ a timer to elapse. In addition, Polyphony patches the stock Ruby classes to
53
+ support its concurrency model, letting developers use all of Ruby's stdlib, for
54
+ example `Net::HTTP` and `Mail` while reaping the benefits of lightweight,
55
+ fine-grained, performant, fiber-based concurrency.
56
+
57
+ Writing concurrent applications using Polyphony's fiber-based concurrency model
58
+ offers a significant performance advantage. Complex concurrent tasks can be
59
+ broken down into many fine-grained concurrent operations with very low overhead.
60
+ More importantly, this concurrency model lets developers express their ideas in
61
+ a sequential fashion, leading to source code that is much easier to read and
62
+ understand, compared to callback-style programming.
63
+
64
+ ## Fibers - Polyphony's basic unit of concurrency
65
+
66
+ Polyphony extends the core `Fiber` class with additional functionality that
67
+ allows scheduling, synchronizing, interrupting and otherwise controlling running
68
+ fibers. Starting a concurrent operation inside a fiber is as simple as a `spin`
69
+ method call:
70
+
71
+ ```ruby
72
+ while (connection = server.accept)
73
+ spin { handle_connection(connection) }
74
+ end
75
+ ```
76
+
77
+ In order to facilitate developing applications that employ complex concurrent
78
+ patterns and can scale easily, Polyphony employs a [structured
79
+ approach](https://en.wikipedia.org/wiki/Structured_concurrency) to controlling
80
+ fiber lifetime. A spun fiber is considered the *child* of the fiber from which
81
+ it was spun, and is always limited to the life time of its parent:
82
+
83
+ ```ruby
84
+ parent = spin do
85
+ do_something
86
+ child = spin do
87
+ do_some_other_stuff
88
+ end
89
+ # the child fiber is guaranteed to stop executing before the parent fiber
90
+ # terminates
91
+ end
92
+ ```
93
+
94
+ Any uncaught exception raised in a fiber will be
95
+ [propagated]((exception-handling.md)) to its parent, and potentially further up
96
+ the fiber hierarchy, all the way to the main fiber:
97
+
98
+ ```ruby
99
+ parent = spin do
100
+ child = spin do
101
+ raise 'foo'
102
+ end
103
+ sleep
104
+ end
105
+
106
+ sleep
107
+ # the exception will be propagated from the child fiber to the parent fiber,
108
+ # and from the parent fiber to the main fiber, which will cause the program to
109
+ # abort.
110
+ ```
111
+
112
+ In addition, fibers can communicate with each other using message passing,
113
+ turning them into autonomous actors in a highly concurrent environment. Message
114
+ passing is in many ways a superior way to pass data between concurrent entities,
115
+ obviating the need to synchronize access to shared resources:
116
+
117
+ ```ruby
118
+ writer = spin do
119
+ while (write_request = receive)
120
+ do_write(write_request)
121
+ end
122
+ end
123
+ ...
124
+ writer << { stamp: Time.now, value: rand }
125
+ ```
126
+
127
+ ## Higher-Order Concurrency Constructs
128
+
129
+ Polyphony also provides several methods and constructs for controlling multiple
130
+ fibers. Methods like `cancel_after` and `move_on_after` allow interrupting a
131
+ fiber that's blocking on any arbitrary operation.
132
+
133
+ Some other constructs offered by Polyphony:
134
+
135
+ * `Mutex` - a mutex used to synchronize access to a single shared resource.
136
+ * `ResourcePool` - used for synchronizing access to a limited amount of shared
137
+ resources, for example a pool of database connections.
138
+ * `Throttler` - used for throttling repeating operations, for example throttling
139
+ access to a shared resource, or throttling incoming requests.
140
+
141
+ ## A Compelling Concurrency Solution for Ruby
142
+
143
+ > The goal of Ruby is to make programmers happy.
144
+
145
+ — Yukihiro “Matz” Matsumoto
146
+
147
+ Polyphony's goal is to make programmers even happier by offering them an easy
148
+ way to write concurrent applications in Ruby. Polyphony aims to show that Ruby
149
+ can be used for developing sufficiently high-performance applications, while
150
+ offering all the advantages of Ruby, with source code that is easy to read and
151
+ understand.
@@ -0,0 +1,161 @@
1
+ ---
2
+ layout: page
3
+ title: The Design of Polyphony
4
+ nav_order: 5
5
+ parent: Main Concepts
6
+ permalink: /main-concepts/design-principles/
7
+ prev_title: Extending Polyphony
8
+ ---
9
+ # The Design of Polyphony
10
+
11
+ Polyphony is a new gem that aims to enable developing high-performance
12
+ concurrent applications in Ruby using a fluent, compact syntax and API.
13
+ Polyphony enables fine-grained concurrency - the splitting up of operations into
14
+ a large number of concurrent tasks, each concerned with small part of the whole
15
+ and advancing at its own pace. Polyphony aims to solve some of the problems
16
+ associated with concurrent Ruby programs using a novel design that sets it apart
17
+ from other approaches currently being used in Ruby.
18
+
19
+ ## Origins
20
+
21
+ The Ruby core language (at least in its MRI implementation) currently provides
22
+ two main constructs for performing concurrent work: threads and fibers. While
23
+ Ruby threads are basically wrappers for OS threads, fibers are essentially
24
+ continuations, allowing pausing and resuming distinct computations. Fibers have
25
+ been traditionally used mostly for implementing enumerators and generators.
26
+
27
+ In addition to the core Ruby concurrency primitives, some Ruby gems have been
28
+ offering an alternative solution to writing concurrent Ruby apps, most notably
29
+ [EventMachine](https://github.com/eventmachine/eventmachine/), which implements
30
+ an event reactor and offers an asynchronous callback-based API for writing
31
+ concurrent code.
32
+
33
+ In the last couple of years, however, fibers have been receiving more attention
34
+ as a possible constructs for writing concurrent programs. In particular, the
35
+ [Async](https://github.com/socketry/async) framework, created by [Samuel
36
+ Williams](https://github.com/ioquatix), offering a comprehensive set of
37
+ libraries, employs fibers in conjunction with an event reactor provided by the
38
+ [nio4r](https://github.com/socketry/nio4r) gem, which wraps the C
39
+ library [libev](http://software.schmorp.de/pkg/libev.html).
40
+
41
+ In addition, recently some effort was undertaken to provide a way to
42
+ [automatically switch between fibers](https://bugs.ruby-lang.org/issues/13618)
43
+ whenever a blocking operation is performed, or to [integrate a fiber
44
+ scheduler](https://bugs.ruby-lang.org/issues/16786) into the core Ruby code.
45
+
46
+ Nevertheless, while work is being done to harness fibers for providing a better
47
+ way to do concurrency in Ruby, fibers remain a mistery for most Ruby
48
+ programmers, a perplexing unfamiliar corner right at the heart of Ruby.
49
+
50
+ ## The History of Polyphony
51
+
52
+ Polyphony started as an experiment, but over about two years of slow, jerky
53
+ evolution turned into something I'm really excited to share with the Ruby
54
+ community. Polyphony's design is both similar and different than the projects
55
+ mentioned above.
56
+
57
+ Polyphony today as nothing like the way it began. A careful examination of the
58
+ [CHANGELOG](https://github.com/digital-fabric/polyphony/blob/master/CHANGELOG.md)
59
+ would show how Polyphony explored not only different event reactor designs, but
60
+ also different API designs incorporating various concurrent paradigms such as
61
+ promises, async/await, fibers, and finally structured concurrency.
62
+
63
+ Throughout the development process, it was my intention to create a programming
64
+ interface that would make it easy to author highly-concurrent Ruby programs.
65
+
66
+ ## Design Principles
67
+
68
+ While Polyphony, like nio4r or EventMachine, uses an event reactor to turn
69
+ blocking operations into non-blocking ones, it completely embraces fibers and in
70
+ fact does not provide any callback-based APIs.
71
+
72
+ Furthermore, Polyphony provides fullblown fiber-aware implementations of
73
+ blocking operations, such as `read/write`, `sleep` or `waitpid`, instead of just
74
+ event watching primitives.
75
+
76
+ Polyphony's design is based on the following principles:
77
+
78
+ - The concurrency model should feel "baked-in". The API should allow
79
+ concurrency with minimal effort. Polyphony should facilitate writing both
80
+ large apps and small scripts with as little boilerplate code as possible.
81
+ There should be no calls to initialize the event reactor, or other ceremonial
82
+ code:
83
+
84
+ ```ruby
85
+ require 'polyphony'
86
+
87
+ # start 10 fibers, each sleeping for 3 seconds
88
+ 10.times { spin { sleep 3 } }
89
+
90
+ puts 'going to sleep now'
91
+ # wait for other fibers to terminate
92
+ suspend
93
+ ```
94
+
95
+ - Blocking operations should yield to other concurrent tasks without any
96
+ decoration or wrapper APIs. This means no `async/await` notation, and no
97
+ async callback-style APIs.
98
+
99
+ ```ruby
100
+ # in Polyphony, I/O ops might block the current fiber, but implicitly yield to
101
+ # other concurrent fibers:
102
+ clients.each do |client|
103
+ spin { client.puts 'Elvis has left the chatroom' }
104
+ end
105
+ ```
106
+
107
+ - Concurrency primitives should be accessible using idiomatic Ruby techniques
108
+ (blocks, method chaining...) and should feel as much as possible "part of the
109
+ language". The resulting API is fundamentally based on methods rather than classes,
110
+ for example `spin` or `move_on_after`, leading to a coding style that is both
111
+ more compact and more legible:
112
+
113
+ ```ruby
114
+ fiber = spin {
115
+ move_on_after(3) {
116
+ do_something_slow
117
+ }
118
+ }
119
+ ```
120
+
121
+ - Polyphony should embrace Ruby's standard `raise/rescue/ensure` exception
122
+ handling mechanism. Exception handling in a highly concurrent environment
123
+ should be robust and foolproof:
124
+
125
+ ```ruby
126
+ cancel_after(0.5) do
127
+ puts 'going to sleep'
128
+ sleep 1
129
+ # this will not be printed
130
+ puts 'wokeup'
131
+ ensure
132
+ # this will be printed
133
+ puts 'done sleeping'
134
+ end
135
+ ```
136
+
137
+ - Concurrency primitives should allow creating higher-order concurrent
138
+ constructs through composition.
139
+
140
+ - The entire design should embrace fibers. There should be no callback-based
141
+ asynchronous APIs. The library and its ecosystem will foster the development
142
+ of techniques and tools for converting callback-based APIs to fiber-based ones.
143
+
144
+ - Use of extensive monkey patching of Ruby core modules and classes such as
145
+ `Kernel`, `Fiber`, `IO` and `Timeout`. This allows porting over non-Polyphony
146
+ code, as well as using a larger part of stdlib in a concurrent manner, without
147
+ having to use custom non-standard network classes or other glue code.
148
+
149
+ ```ruby
150
+ require 'polyphony'
151
+
152
+ # use TCPServer from Ruby's stdlib
153
+ server = TCPServer.open('127.0.0.1', 1234)
154
+ while (client = server.accept)
155
+ spin {
156
+ while (data = client.gets)
157
+ client.write("you said: #{ data.chomp }\n")
158
+ end
159
+ }
160
+ end
161
+ ```
@@ -0,0 +1,291 @@
1
+ ---
2
+ layout: page
3
+ title: Exception Handling
4
+ nav_order: 3
5
+ parent: Main Concepts
6
+ permalink: /main-concepts/exception-handling/
7
+ prev_title: How Fibers are Scheduled
8
+ next_title: Extending Polyphony
9
+ ---
10
+ # Exception Handling
11
+
12
+ Ruby employs a pretty robust exception handling mechanism. An raised exception
13
+ will propagate up the fiber tree until a suitable exception handler is found,
14
+ based on the exception's class. In addition, the exception will include a stack
15
+ trace showing the execution path from the exception's locus back to the
16
+ program's entry point. Unfortunately, when exceptions are raised while switching
17
+ between fibers, stack traces will only include partial information. Here's a
18
+ simple demonstration:
19
+
20
+ _fiber\_exception.rb_
21
+
22
+ ```ruby
23
+ require 'fiber'
24
+
25
+ def fail!
26
+ raise 'foobar'
27
+ end
28
+
29
+ f = Fiber.new do
30
+ Fiber.new do
31
+ fail!
32
+ end.transfer
33
+ end
34
+
35
+ f.transfer
36
+ ```
37
+
38
+ Running the above program will give us:
39
+
40
+ ```text
41
+ Traceback (most recent call last):
42
+ 1: from fiber_exception.rb:9:in `block (2 levels) in <main>'
43
+ fiber_exception.rb:4:in `fail!': foobar (RuntimeError)
44
+ ```
45
+
46
+ So, the stack trace includes two frames: the exception's locus on line 4 and the
47
+ call site at line 9. But we have no information on how we got to line 9. Let's
48
+ imagine if we had more complete information about the sequence of execution. In
49
+ fact, what is missing is information about how the different fibers were
50
+ created. If we had that, our stack trace would have looked something like this:
51
+
52
+ ```text
53
+ Traceback (most recent call last):
54
+ 4: from fiber_exception.rb:13:in `<main>'
55
+ 3: from fiber_exception.rb:7:in `Fiber.new'
56
+ 2: from fiber_exception.rb:8:in `Fiber.new'
57
+ 1: from fiber_exception.rb:9:in `block (2 levels) in <main>'
58
+ fiber_exception.rb:4:in `fail!': foobar (RuntimeError)
59
+ ```
60
+
61
+ In order to achieve this, Polyphony patches `Fiber.new` to keep track of the
62
+ call stack at the moment the fiber was created, as well as the fiber from which
63
+ the call happened. In addition, Polyphony patches `Exception#backtrace` in order
64
+ to synthesize a complete stack trace based on the call stack information stored
65
+ for the current fiber. This is done recursively through the chain of fibers
66
+ leading up to the current location. What we end up with is a record of the
67
+ entire sequence of \(possibly intermittent\) execution leading up to the point
68
+ where the exception was raised.
69
+
70
+ In addition, the backtrace is sanitized to remove stack frames originating from
71
+ the Polyphony code itself, which hides away the Polyphony plumbing and lets
72
+ developers concentrate on their own code. The sanitizing of exception backtraces
73
+ can be disabled by setting the `Exception.__disable_sanitized_backtrace__` flag:
74
+
75
+ ```ruby
76
+ Exception.__disable_sanitized_backtrace__ = true
77
+ ...
78
+ ```
79
+
80
+ ## Exceptions and Fiber Scheduling
81
+
82
+ Polyphony takes advantages of Ruby's `Fiber#transfer` API to allow interrupting
83
+ fiber execution and raise cross-fiber exceptions. This is done by inspecting the
84
+ return value of `Fiber#transfer`, which returns when the fiber resumes, at every
85
+ [switchpoint](../fiber-scheduling/#switchpoints). If the return value is an
86
+ exception, it is raised in the context of the resumed fiber, and is then subject
87
+ to any `rescue` statements in the context of that fiber.
88
+
89
+ Exceptions can be passed to arbitrary fibers by using `Fiber#raise`. They can also be manually raised in fibers by using `Fiber#schedule`:
90
+
91
+ ```ruby
92
+ f = spin do
93
+ suspend
94
+ rescue => e
95
+ puts e.message
96
+ end
97
+
98
+ f.schedule(RuntimeError.new('foo')) #=> will print 'foo'
99
+ ```
100
+
101
+ ## Cleaning Up After Exceptions - Using Ensure
102
+
103
+ A major issue when handling exceptions is cleaning up - freeing up resources
104
+ that have been allocated, cancelling ongoing operations, etc. Polyphony allows
105
+ using the normal `ensure` statement for cleaning up. Have a look at Polyphony's
106
+ implementation of `Kernel#sleep`:
107
+
108
+ ```ruby
109
+ def sleep(duration)
110
+ timer = Gyro::Timer.new(duration, 0)
111
+ timer.await
112
+ ensure
113
+ timer.stop
114
+ end
115
+ ```
116
+
117
+ This method creates a one-shot timer with the given duration and then suspends
118
+ the current fiber, waiting for the timer to fire and then resume the fiber.
119
+ While the awaiting fiber is suspended, other operations might be going on, which
120
+ might interrupt the `sleep` operation by scheduling the awaiting fiber with an
121
+ exception, for example a `MoveOn` or a `Cancel` exception. For this reason, we
122
+ need to _ensure_ that the timer will be stopped, regardless of whether it has
123
+ fired or not. We call `timer.stop` inside an ensure block, thus ensuring that
124
+ the timer will have stopped once the awaiting fiber has resumed, even if it has
125
+ not fired.
126
+
127
+ ## Exception Propagation
128
+
129
+ One of the "annoying" things about exceptions is that for them to be useful, you
130
+ have to intercept them \(using `rescue`\). If you forget to do that, you'll end
131
+ up with uncaught exceptions that can wreak havoc. For example, by default a Ruby
132
+ `Thread` in which an exception was raised without being caught, will simply
133
+ terminate with the exception silently swallowed.
134
+
135
+ To prevent the same from happening with fibers, Polyphony provides a robust
136
+ mechanism that propagates uncaught exceptions up through the chain of parent
137
+ fibers. Let's discuss the following example:
138
+
139
+ ```ruby
140
+ require 'polyphony'
141
+
142
+ spin do
143
+ spin do
144
+ spin do
145
+ spin do
146
+ raise 'foo'
147
+ end
148
+ sleep
149
+ end
150
+ sleep
151
+ end
152
+ sleep
153
+ end
154
+
155
+ sleep
156
+ ```
157
+
158
+ In the above example, four nested fibers are created, and each of them, except
159
+ for the innermost fiber, goes to sleep for an unlimited duration. An exception
160
+ is raised in the innermost fiber, and having no corresponding exception handler,
161
+ will propagate up through the enclosing fibers, until reaching the
162
+ top-most level, that of the root fiber, at which point the exception will cause
163
+ the program to abort and print an error message.
164
+
165
+ ## MoveOn and Cancel - Interrupting Fiber Execution
166
+
167
+ In addition to enhancing Ruby's normal exception-handling mechanism, Polyphony
168
+ provides two exception classes that used exclusively to interrupt fiber
169
+ execution: `MoveOn` and `Cancel`. Both of these classes are used in various
170
+ fiber-control APIs, and `MoveOn` exceptions in particular are handled in a
171
+ particular manner by Polyphony. The difference between `MoveOn` and `Cancel` is
172
+ that `MoveOn` stops fiber execution without the exception propagating. It can
173
+ optionally provide an arbitrary return value for the fiber. `Cancel` will propagate
174
+ up like all exceptions.
175
+
176
+ The `MoveOn` and `Cancel` classes are normally used indirectly, through the
177
+ `Fiber#interrupt` and `Fiber#cancel` APIs, and also through the use of [cancel
178
+ scopes](#):
179
+
180
+ ```ruby
181
+ f1 = spin { sleep 100; return 'foo' }
182
+ f2 = spin { f1.await }
183
+ ...
184
+ f1.interrupt('bar')
185
+ f2.result #=> 'bar'
186
+
187
+ f3 = spin { sleep 100 }
188
+ ...
189
+ f3.cancel #=> will raise a Cancel exception
190
+ ```
191
+
192
+ In addition to `MoveOn` and `Cancel`, Polyphony employs internally another
193
+ exception class, `Terminate` for terminating a fiber once its parent has
194
+ finished executing.
195
+
196
+ ## The Special Problem of Signal Handling
197
+
198
+ Ruby by default handles process signals by generating exceptions, allowing the
199
+ handling of signals in a structured manner. However, process signals may arrive
200
+ at any moment, and may be trapped while any arbitrary fiber is running, and even
201
+ while an event loop is running.
202
+
203
+ Two signals in particular require special care as they involve the stopping of
204
+ the entire process: `TERM` and `INT`. The `TERM` signal should be handled
205
+ gracefully, i.e. with proper cleanup, which also means terminating all fibers.
206
+ The `INT` signal requires halting the process and printing a correct stack
207
+ trace.
208
+
209
+ To ensure correct behaviour for these two signals, polyphony installs signal
210
+ handlers that ensure that the main thread's event loop stops if it's currently
211
+ running, and that the corresponding exceptions (namely `SystemExit` and
212
+ `Interrupt`) are handled correctly by passing them to the main fiber.
213
+
214
+ ### Graceful process termination
215
+
216
+ In order to ensure your application terminates gracefully upon receiving an
217
+ `INT` or `TERM` signal, you'll need to:
218
+
219
+ 1. Rescue the corresponding exceptions in the main fiber.
220
+ 2. Rescue `Polyphony::Terminate` exceptions in each fiber that needs to perform
221
+ operations such as handling any pending requests, etc.
222
+
223
+ ```ruby
224
+ # In a worker fiber
225
+ def do_work
226
+ loop do
227
+ req = receive
228
+ handle_req(req)
229
+ end
230
+ rescue Polyphony::Terminate
231
+ # We still need to handle any pending request
232
+ receive_pending.each { handle_req(req) }
233
+ end
234
+
235
+ # on the main fiber
236
+ begin
237
+ spin_up_lots_fibers
238
+ rescue Interrupt, SystemExit
239
+ Fiber.current.terminate_all_children
240
+ Fiber.current.await_all_children
241
+ end
242
+ ```
243
+
244
+ ### Handling other signals
245
+
246
+ Care should be taken when handling other signals. There are two options for
247
+ correctly handling the signals: using Ruby's stock `trap` method, and using
248
+ Polyphony's signal watchers. The stock method involves trapping signals as
249
+ usual, but making sure we're not inside the event loop:
250
+
251
+ ```ruby
252
+ trap('SIGHUP') do
253
+ Thread.current.break_out_of_ev_loop(Thread.current.main_fiber, nil)
254
+ handle_hup_signal
255
+ end
256
+ ```
257
+
258
+ A second technique that might be useful is to use a `Gyro::Async` watcher and
259
+ signal it when the process signal is trapped:
260
+
261
+ ```ruby
262
+ sighup_async = Gyro::Async.new
263
+ sighup_handler = spin_loop do
264
+ sighup_async.await
265
+ handle_sighup
266
+ end
267
+
268
+ trap('SIGHUP') { sighup_async.signal }
269
+ ```
270
+
271
+ Another alternative is to use `Polyphony.wait_for_signal`, which uses a
272
+ `Gyro::Signal` watcher under the hood:
273
+
274
+ ```ruby
275
+ hup_handler = spin_loop do
276
+ Polyphony.wait_for_signal('SIGHUP')
277
+ handle_hup_signal
278
+ end
279
+ ```
280
+
281
+ ## The Special Problem of Thread Termination
282
+
283
+ Thread termination using `Thread#kill` or `Thread#raise` also presents the same
284
+ problems as signal handling in a multi-fiber environment. The termination can
285
+ occur while any fiber is running, and even while running the thread's event
286
+ loop.
287
+
288
+ To ensure proper thread termination, including the termination of all the
289
+ thread's fibers, Polyphony patches the `Thread#kill` and `Thread#raise` methods
290
+ to schedule the thread's main fiber with the corresponding exceptions, thus
291
+ ensuring an orderly termination or exception handling.