polyphony 0.36 → 0.42

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 (118) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +11 -2
  3. data/.gitignore +2 -2
  4. data/.rubocop.yml +30 -0
  5. data/CHANGELOG.md +28 -2
  6. data/Gemfile +0 -11
  7. data/Gemfile.lock +15 -14
  8. data/README.md +2 -1
  9. data/Rakefile +7 -3
  10. data/TODO.md +28 -95
  11. data/docs/_config.yml +56 -7
  12. data/docs/_sass/custom/custom.scss +0 -30
  13. data/docs/_sass/overrides.scss +0 -46
  14. data/docs/{user-guide → _user-guide}/all-about-timers.md +0 -0
  15. data/docs/_user-guide/index.md +9 -0
  16. data/docs/{user-guide → _user-guide}/web-server.md +0 -0
  17. data/docs/api-reference/fiber.md +2 -2
  18. data/docs/api-reference/index.md +9 -0
  19. data/docs/api-reference/polyphony-process.md +1 -1
  20. data/docs/api-reference/thread.md +1 -1
  21. data/docs/faq.md +21 -11
  22. data/docs/getting-started/index.md +10 -0
  23. data/docs/getting-started/installing.md +2 -6
  24. data/docs/getting-started/overview.md +507 -0
  25. data/docs/getting-started/tutorial.md +27 -19
  26. data/docs/index.md +3 -2
  27. data/docs/main-concepts/concurrency.md +0 -5
  28. data/docs/main-concepts/design-principles.md +69 -21
  29. data/docs/main-concepts/extending.md +1 -1
  30. data/docs/main-concepts/index.md +9 -0
  31. data/examples/core/01-spinning-up-fibers.rb +1 -0
  32. data/examples/core/03-interrupting.rb +4 -1
  33. data/examples/core/04-handling-signals.rb +19 -0
  34. data/examples/core/xx-agent.rb +102 -0
  35. data/examples/core/xx-fork-cleanup.rb +22 -0
  36. data/examples/core/xx-sleeping.rb +14 -6
  37. data/examples/io/tunnel.rb +48 -0
  38. data/examples/io/xx-irb.rb +1 -1
  39. data/examples/performance/thread-vs-fiber/polyphony_mt_server.rb +7 -6
  40. data/examples/performance/thread-vs-fiber/polyphony_server.rb +13 -36
  41. data/examples/performance/thread-vs-fiber/polyphony_server_read_loop.rb +58 -0
  42. data/examples/performance/xx-array.rb +11 -0
  43. data/examples/performance/xx-fiber-switch.rb +9 -0
  44. data/examples/performance/xx-snooze.rb +15 -0
  45. data/ext/{gyro → polyphony}/extconf.rb +2 -2
  46. data/ext/{gyro → polyphony}/fiber.c +18 -22
  47. data/ext/{gyro → polyphony}/libev.c +0 -0
  48. data/ext/{gyro → polyphony}/libev.h +0 -0
  49. data/ext/polyphony/libev_agent.c +718 -0
  50. data/ext/polyphony/libev_queue.c +216 -0
  51. data/ext/{gyro/gyro.c → polyphony/polyphony.c} +16 -46
  52. data/ext/{gyro/gyro.h → polyphony/polyphony.h} +25 -39
  53. data/ext/polyphony/polyphony_ext.c +23 -0
  54. data/ext/{gyro → polyphony}/socket.c +21 -18
  55. data/ext/polyphony/thread.c +206 -0
  56. data/ext/{gyro → polyphony}/tracing.c +1 -1
  57. data/lib/polyphony.rb +40 -44
  58. data/lib/polyphony/adapters/fs.rb +1 -4
  59. data/lib/polyphony/adapters/irb.rb +1 -1
  60. data/lib/polyphony/adapters/postgres.rb +6 -5
  61. data/lib/polyphony/adapters/process.rb +27 -23
  62. data/lib/polyphony/adapters/trace.rb +110 -105
  63. data/lib/polyphony/core/channel.rb +35 -35
  64. data/lib/polyphony/core/exceptions.rb +29 -29
  65. data/lib/polyphony/core/global_api.rb +94 -91
  66. data/lib/polyphony/core/resource_pool.rb +83 -83
  67. data/lib/polyphony/core/sync.rb +16 -16
  68. data/lib/polyphony/core/thread_pool.rb +49 -37
  69. data/lib/polyphony/core/throttler.rb +30 -23
  70. data/lib/polyphony/event.rb +27 -0
  71. data/lib/polyphony/extensions/core.rb +25 -17
  72. data/lib/polyphony/extensions/fiber.rb +269 -267
  73. data/lib/polyphony/extensions/io.rb +56 -26
  74. data/lib/polyphony/extensions/openssl.rb +5 -9
  75. data/lib/polyphony/extensions/socket.rb +29 -10
  76. data/lib/polyphony/extensions/thread.rb +19 -12
  77. data/lib/polyphony/net.rb +64 -60
  78. data/lib/polyphony/version.rb +1 -1
  79. data/polyphony.gemspec +4 -7
  80. data/test/helper.rb +14 -1
  81. data/test/stress.rb +17 -12
  82. data/test/test_agent.rb +124 -0
  83. data/test/{test_async.rb → test_event.rb} +15 -7
  84. data/test/test_ext.rb +25 -4
  85. data/test/test_fiber.rb +19 -10
  86. data/test/test_global_api.rb +4 -4
  87. data/test/test_io.rb +46 -24
  88. data/test/test_queue.rb +74 -0
  89. data/test/test_signal.rb +3 -40
  90. data/test/test_socket.rb +33 -0
  91. data/test/test_thread.rb +38 -16
  92. data/test/test_thread_pool.rb +2 -2
  93. data/test/test_throttler.rb +0 -1
  94. data/test/test_trace.rb +6 -5
  95. metadata +41 -57
  96. data/docs/_includes/nav.html +0 -51
  97. data/docs/_includes/prevnext.html +0 -17
  98. data/docs/_layouts/default.html +0 -106
  99. data/docs/api-reference.md +0 -11
  100. data/docs/api-reference/gyro-async.md +0 -57
  101. data/docs/api-reference/gyro-child.md +0 -29
  102. data/docs/api-reference/gyro-queue.md +0 -44
  103. data/docs/api-reference/gyro-timer.md +0 -51
  104. data/docs/api-reference/gyro.md +0 -25
  105. data/docs/getting-started.md +0 -10
  106. data/docs/main-concepts.md +0 -10
  107. data/docs/user-guide.md +0 -10
  108. data/examples/core/forever_sleep.rb +0 -19
  109. data/ext/gyro/async.c +0 -148
  110. data/ext/gyro/child.c +0 -127
  111. data/ext/gyro/gyro_ext.c +0 -33
  112. data/ext/gyro/io.c +0 -474
  113. data/ext/gyro/queue.c +0 -142
  114. data/ext/gyro/selector.c +0 -205
  115. data/ext/gyro/signal.c +0 -118
  116. data/ext/gyro/thread.c +0 -298
  117. data/ext/gyro/timer.c +0 -134
  118. data/test/test_timer.rb +0 -56
@@ -1,13 +1,20 @@
1
1
  ---
2
2
  layout: page
3
3
  title: Tutorial
4
- nav_order: 2
5
4
  parent: Getting Started
6
- permalink: /getting-started/tutorial/
7
- prev_title: Installing Polyphony
8
- next_title: Concurrency the Easy Way
5
+ nav_order: 3
6
+ ---
7
+
8
+ # Tutorial
9
+ {: .no_toc }
10
+
11
+ ## Table of contents
12
+ {: .no_toc .text-delta }
13
+
14
+ - TOC
15
+ {:toc}
16
+
9
17
  ---
10
- # Polyphony: a Tutorial
11
18
 
12
19
  Polyphony is a new Ruby library aimed at making writing concurrent Ruby apps
13
20
  easy and fun. In this article, we'll introduce Polyphony's fiber-based
@@ -116,7 +123,7 @@ suspend # The main fiber suspends, waiting for all other work to finish
116
123
  sleep 1 # The sleeper fiber goes to sleep
117
124
  Gyro::Timer.new(1, 0).await # A timer event watcher is setup and yields
118
125
  Thread.current.switch_fiber # Polyphony looks for other runnable fibers
119
- Thread.current.selector.run # With no work left, the event loop is ran
126
+ Thread.current.agent.poll # With no work left, the event loop is ran
120
127
  fiber.schedule # The timer event fires, scheduling the sleeper fiber
121
128
  # <= The sleep method returns
122
129
  puts "Woke up"
@@ -218,7 +225,7 @@ this by setting up a timeout fiber that cancels the fiber dealing with the conne
218
225
  def handle_client(client)
219
226
  timeout = cancel_after(10)
220
227
  while (data = client.gets)
221
- timeout.reset
228
+ timeout.restart
222
229
  client << data
223
230
  end
224
231
  rescue Polyphony::Cancel
@@ -230,18 +237,19 @@ ensure
230
237
  end
231
238
  ```
232
239
 
233
- The cancel scope is initialized with a timeout of 10 seconds. Any blocking
234
- operation ocurring within the cancel scope will be interrupted once 10 seconds
235
- have elapsed. In order to keep the connection alive while the client is active,
236
- we call `scope.reset_timeout` each time data is received from the client, and
237
- thus reset the cancel scope timer.
238
-
239
- In addition, we use an `ensure` block to make sure the client connection is
240
- closed, whether or not it was interrupted by the cancel scope timer. The habit
241
- of always cleaning up using `ensure` in the face of potential interruptions is a
242
- fundamental element of using Polyphony correctly. This makes your code robust,
243
- even in a highly chaotic concurrent execution environment where tasks can be
244
- interrupted at any time.
240
+ The call to `#cancel_after` spins up a new fiber that will sleep for 10 seconds,
241
+ then cancel its parent. The call to `client.gets` blocks until new data is
242
+ available. If no new data is available, the `timeout` fiber will finish
243
+ sleeping, and then cancel the client handling fiber by raising a
244
+ `Polyphony::Cancel` exception. However, if new data is received, the `timeout`
245
+ fiber is restarted, causing to begin sleeping again for 10 seconds. If the
246
+ client has closed the connection, or some other exception occurs, the `timeout`
247
+ fiber is automatically stopped as it is a child of the client handling fiber.
248
+
249
+ The habit of always cleaning up using `ensure` in the face of potential
250
+ interruptions is a fundamental element of using Polyphony correctly. This makes
251
+ your code robust, even in a highly chaotic concurrent execution environment
252
+ where tasks can be started, restarted and interrupted at any time.
245
253
 
246
254
  ## Implementing graceful shutdown
247
255
 
@@ -14,8 +14,9 @@ implements a comprehensive
14
14
  using [libev](https://github.com/enki/libev) as a high-performance event reactor
15
15
  for I/O, timers, and other asynchronous events.
16
16
 
17
- [FAQ](faq){: .btn .btn-green .text-gamma }
18
17
  [Take the tutorial](getting-started/tutorial){: .btn .btn-blue .text-gamma }
18
+ [Main Concepts](main-concepts/concurrency/){: .btn .btn-green .text-gamma }
19
+ [FAQ](faq){: .btn .btn-green .text-gamma }
19
20
  [Source code](https://github.com/digital-fabric/polyphony){: .btn .btn-purple .text-gamma target="_blank" }
20
21
  {: .mt-6 .h-align-center }
21
22
 
@@ -52,7 +53,7 @@ adapters are being developed.
52
53
  * Natural, sequential programming style that makes it easy to reason about
53
54
  concurrent code.
54
55
  * Abstractions and constructs for controlling the execution of concurrent code:
55
- supervisors, cancel scopes, throttling, resource pools etc.
56
+ supervisors, throttling, resource pools etc.
56
57
  * Code can use native networking classes and libraries, growing support for
57
58
  third-party gems such as `pg` and `redis`.
58
59
  * Use stdlib classes such as `TCPServer` and `TCPSocket` and `Net::HTTP`.
@@ -130,16 +130,11 @@ Polyphony also provides several methods and constructs for controlling multiple
130
130
  fibers. Methods like `cancel_after` and `move_on_after` allow interrupting a
131
131
  fiber that's blocking on any arbitrary operation.
132
132
 
133
- Cancel scopes \(borrowed from the brilliant Python library
134
- [Trio](https://trio.readthedocs.io/en/stable/)\) allows cancelling ongoing
135
- operations for any reason with more control over cancelling behaviour.
136
-
137
133
  Some other constructs offered by Polyphony:
138
134
 
139
135
  * `Mutex` - a mutex used to synchronize access to a single shared resource.
140
136
  * `ResourcePool` - used for synchronizing access to a limited amount of shared
141
137
  resources, for example a pool of database connections.
142
-
143
138
  * `Throttler` - used for throttling repeating operations, for example throttling
144
139
  access to a shared resource, or throttling incoming requests.
145
140
 
@@ -1,18 +1,76 @@
1
1
  ---
2
2
  layout: page
3
- title: Design Principles
3
+ title: The Design of Polyphony
4
4
  nav_order: 5
5
5
  parent: Main Concepts
6
6
  permalink: /main-concepts/design-principles/
7
7
  prev_title: Extending Polyphony
8
8
  ---
9
- # Design Principles
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
+ ## Design Principles
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
+ While Polyphony, like nio4r or EventMachine, uses an event reactor to turn
64
+ blocking operations into non-blocking ones, it completely embraces fibers and in
65
+ fact does not provide any callback-based APIs. Furthermore, Polyphony provides
66
+ fullblown fiber-aware implementations of blocking operations, such as
67
+ `read/write`, `sleep` or `waitpid`, instead of just event watching primitives.
68
+
69
+ Throughout the development process, it was my intention to create a programming
70
+ interface that would make highly-concurrent
71
+
72
+
10
73
 
11
- Polyphony was created in order to enable developing high-performance concurrent
12
- applications in Ruby using a fluent, compact syntax and API. Polyphony enables
13
- fine-grained concurrency - the splitting up of operations into a large number of
14
- concurrent tasks, each concerned with small part of the whole and advancing at
15
- its own pace.
16
74
 
17
75
 
18
76
 
@@ -46,8 +104,8 @@ library. Polyphony's design is based on the following principles:
46
104
  async callback-style APIs.
47
105
 
48
106
  ```ruby
49
- # in Polyphony, I/O ops block the current fiber, but implicitly yield to other
50
- # concurrent fibers:
107
+ # in Polyphony, I/O ops might block the current fiber, but implicitly yield to
108
+ # other concurrent fibers:
51
109
  clients.each { |client|
52
110
  spin { client.puts 'Elvis has left the chatroom' }
53
111
  }
@@ -85,18 +143,8 @@ library. Polyphony's design is based on the following principles:
85
143
  end
86
144
  ```
87
145
 
88
- - Concurrency primitives should allow creating higher-order concurrent
89
- constructs through composition. This is done primarily through supervisors and
90
- cancel scopes:
91
-
92
- ```ruby
93
- # wait for multiple fibers
94
- supervise { |s|
95
- clients.each { |client|
96
- s.spin { client.puts 'Elvis has left the chatroom' }
97
- }
98
- }
99
- ```
146
+ - Concurrency primitives should allow creating higher-order concurrent
147
+ constructs through composition.
100
148
 
101
149
  - The entire design should embrace fibers. There should be no callback-based
102
150
  asynchronous APIs.
@@ -5,7 +5,7 @@ nav_order: 4
5
5
  parent: Main Concepts
6
6
  permalink: /main-concepts/extending/
7
7
  prev_title: Exception Handling
8
- next_title: Design Principles
8
+ next_title: The Design of Polyphony
9
9
  ---
10
10
  # Extending Polyphony
11
11
 
@@ -0,0 +1,9 @@
1
+ ---
2
+ layout: page
3
+ title: Main Concepts
4
+ has_children: true
5
+ nav_order: 3
6
+ ---
7
+
8
+ # Main Concepts
9
+ {: .no_toc }
@@ -14,4 +14,5 @@ end
14
14
  spin { nap(:a, 1) }
15
15
  spin { nap(:b, 2) }
16
16
 
17
+ # Calling suspend will block until all child fibers have terminated
17
18
  suspend
@@ -18,7 +18,8 @@ ensure
18
18
  end
19
19
 
20
20
  # The Kernel#cancel_after interrupts a blocking operation by raising a
21
- # Polyphony::Cancel exception after the given timeout
21
+ # Polyphony::Cancel exception after the given timeout. If not rescued, the
22
+ # exception is propagated up the fiber hierarchy
22
23
  spin do
23
24
  # cancel after 1 second
24
25
  cancel_after(1) { nap(:cancel, 2) }
@@ -26,6 +27,8 @@ rescue Polyphony::Cancel => e
26
27
  puts "got exception: #{e}"
27
28
  end
28
29
 
30
+ # The Kernel#move_on_after interrupts a blocking operation by raising a
31
+ # Polyphony::MoveOn exception, which is silently swallowed by the fiber
29
32
  spin do
30
33
  # move on after 1 second
31
34
  move_on_after(1) do
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'polyphony'
5
+
6
+ # trap('TERM') do
7
+ # Polyphony.emit_signal_exception(::SystemExit)
8
+ # end
9
+
10
+ # trap('INT') do
11
+ # Polyphony.emit_signal_exception(::Interrupt)
12
+ # end
13
+
14
+ puts "go to sleep"
15
+ begin
16
+ sleep
17
+ ensure
18
+ puts "done sleeping"
19
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'polyphony'
5
+
6
+ Exception.__disable_sanitized_backtrace__ = true
7
+
8
+ class Test
9
+ def test_sleep
10
+ puts "going to sleep"
11
+ sleep 1
12
+ puts "done sleeping"
13
+ end
14
+
15
+ def test_spin
16
+ spin {
17
+ 10.times {
18
+ STDOUT << '.'
19
+ sleep 0.1
20
+ }
21
+ }
22
+
23
+ puts "going to sleep\n"
24
+ sleep 1
25
+ puts 'woke up'
26
+ end
27
+
28
+ def test_file
29
+ f = File.open(__FILE__, 'r')
30
+ puts Thread.current.agent.read(f, +'', 10000, true)
31
+
32
+ Thread.current.agent.write(STDOUT, "Write something: ")
33
+ str = +''
34
+ Thread.current.agent.read(STDIN, str, 5, false)
35
+ puts str
36
+ end
37
+
38
+ def test_fork
39
+ pid = fork do
40
+ Thread.current.agent.post_fork
41
+ puts 'child going to sleep'
42
+ sleep 1
43
+ puts 'child done sleeping'
44
+ exit(42)
45
+ end
46
+
47
+ puts "Waiting for pid #{pid}"
48
+ result = Thread.current.agent.waitpid(pid)
49
+ puts "Done waiting"
50
+ p result
51
+ end
52
+
53
+ def test_async
54
+ async = Polyphony::Event.new
55
+
56
+ spin {
57
+ puts "signaller starting"
58
+ sleep 1
59
+ puts "signal"
60
+ async.signal(:foo)
61
+ }
62
+
63
+ puts "awaiting event"
64
+ p async.await
65
+ end
66
+
67
+ def test_queue
68
+ q = Gyro::Queue.new
69
+ spin {
70
+ 10.times {
71
+ q << Time.now.to_f
72
+ sleep 0.2
73
+ }
74
+ q << :STOP
75
+ }
76
+
77
+ loop do
78
+ value = q.shift
79
+ break if value == :STOP
80
+
81
+ p value
82
+ end
83
+ end
84
+
85
+ def test_thread
86
+ t = Thread.new do
87
+ puts "thread going to sleep"
88
+ sleep 0.2
89
+ puts "thread done sleeping"
90
+ end
91
+
92
+ t.await
93
+ end
94
+ end
95
+
96
+ t = Test.new
97
+
98
+ t.methods.select { |m| m =~ /^test_/ }.each do |m|
99
+ puts '*' * 40
100
+ puts m
101
+ t.send(m)
102
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'polyphony'
5
+
6
+ Exception.__disable_sanitized_backtrace__ = true
7
+
8
+ t = Thread.new do
9
+ async = Gyro::Async.new
10
+ spin { async.await }
11
+ sleep 100
12
+ end
13
+
14
+ sleep 0.5
15
+
16
+ Polyphony.fork do
17
+ puts "forked #{Process.pid}"
18
+ sleep 1
19
+ puts "done sleeping"
20
+ end
21
+
22
+ sleep 50
@@ -5,13 +5,21 @@ require 'polyphony'
5
5
 
6
6
  Exception.__disable_sanitized_backtrace__ = true
7
7
 
8
- spin {
9
- 10.times {
10
- STDOUT << '.'
11
- sleep 0.1
12
- }
13
- }
8
+ # spin {
9
+ # 10.times {
10
+ # STDOUT << '.'
11
+ # sleep 0.1
12
+ # }
13
+ # }
14
14
 
15
15
  puts 'going to sleep...'
16
16
  sleep 1
17
17
  puts 'woke up'
18
+
19
+ counter = 0
20
+ t = Polyphony::Throttler.new(5)
21
+ t.process do
22
+ p counter
23
+ counter += 1
24
+ t.stop if counter > 5
25
+ end