polyphony 0.24 → 0.25

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +6 -0
  3. data/Gemfile.lock +1 -1
  4. data/TODO.md +12 -8
  5. data/docs/README.md +2 -2
  6. data/docs/summary.md +3 -3
  7. data/docs/technical-overview/concurrency.md +4 -6
  8. data/docs/technical-overview/design-principles.md +8 -8
  9. data/docs/technical-overview/exception-handling.md +1 -1
  10. data/examples/core/{01-spinning-up-coprocesses.rb → 01-spinning-up-fibers.rb} +1 -1
  11. data/examples/core/{02-awaiting-coprocesses.rb → 02-awaiting-fibers.rb} +3 -3
  12. data/examples/core/xx-erlang-style-genserver.rb +10 -10
  13. data/examples/core/xx-extended_fibers.rb +150 -0
  14. data/examples/core/xx-sleeping.rb +9 -0
  15. data/examples/core/xx-supervisors.rb +1 -1
  16. data/examples/interfaces/pg_pool.rb +3 -3
  17. data/examples/performance/mem-usage.rb +19 -4
  18. data/examples/performance/thread-vs-fiber/polyphony_server.rb +1 -5
  19. data/ext/gyro/gyro.c +9 -15
  20. data/lib/polyphony/core/cancel_scope.rb +0 -2
  21. data/lib/polyphony/core/exceptions.rb +2 -2
  22. data/lib/polyphony/core/global_api.rb +7 -8
  23. data/lib/polyphony/core/supervisor.rb +25 -31
  24. data/lib/polyphony/extensions/core.rb +4 -78
  25. data/lib/polyphony/extensions/fiber.rb +166 -0
  26. data/lib/polyphony/extensions/io.rb +2 -1
  27. data/lib/polyphony/version.rb +1 -1
  28. data/lib/polyphony.rb +6 -8
  29. data/test/test_async.rb +2 -2
  30. data/test/test_cancel_scope.rb +6 -6
  31. data/test/test_fiber.rb +382 -0
  32. data/test/test_global_api.rb +49 -50
  33. data/test/test_gyro.rb +1 -1
  34. data/test/test_io.rb +30 -29
  35. data/test/test_kernel.rb +2 -2
  36. data/test/test_signal.rb +1 -1
  37. data/test/test_supervisor.rb +27 -27
  38. data/test/test_timer.rb +2 -2
  39. metadata +7 -7
  40. data/examples/core/04-no-auto-run.rb +0 -16
  41. data/lib/polyphony/core/coprocess.rb +0 -168
  42. data/test/test_coprocess.rb +0 -440
@@ -0,0 +1,382 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+
5
+ class FiberTest < MiniTest::Test
6
+ def test_that_new_spun_fiber_starts_in_suspended_state
7
+ result = nil
8
+ f = Fiber.spin { result = 42 }
9
+ assert_nil result
10
+ f.await
11
+ assert_equal 42, result
12
+ ensure
13
+ f&.stop
14
+ end
15
+
16
+ def test_that_await_blocks_until_fiber_is_done
17
+ result = nil
18
+ f = Fiber.spin do
19
+ snooze
20
+ result = 42
21
+ end
22
+ f.await
23
+ assert_equal 42, result
24
+ ensure
25
+ f&.stop
26
+ end
27
+
28
+ def test_that_await_returns_the_fibers_return_value
29
+ f = Fiber.spin { %i[foo bar] }
30
+ assert_equal %i[foo bar], f.await
31
+ ensure
32
+ f&.stop
33
+ end
34
+
35
+ def test_that_await_raises_error_raised_by_fiber
36
+ result = nil
37
+ f = Fiber.spin { raise 'foo' }
38
+ begin
39
+ result = f.await
40
+ rescue Exception => e
41
+ result = { error: e }
42
+ end
43
+ assert_kind_of Hash, result
44
+ assert_kind_of RuntimeError, result[:error]
45
+ ensure
46
+ f&.stop
47
+ end
48
+
49
+ def test_that_running_fiber_can_be_cancelled
50
+ result = []
51
+ error = nil
52
+ f = Fiber.spin do
53
+ result << 1
54
+ 2.times { snooze }
55
+ result << 2
56
+ end
57
+ defer { f.cancel! }
58
+ assert_equal 0, result.size
59
+ begin
60
+ f.await
61
+ rescue Polyphony::Cancel => e
62
+ error = e
63
+ end
64
+ assert_equal 1, result.size
65
+ assert_equal 1, result[0]
66
+ assert_kind_of Polyphony::Cancel, error
67
+ ensure
68
+ f&.stop
69
+ end
70
+
71
+ def test_that_running_fiber_can_be_interrupted
72
+ # that is, stopped without exception
73
+ result = []
74
+ f = Fiber.spin do
75
+ result << 1
76
+ 2.times { snooze }
77
+ result << 2
78
+ 3
79
+ end
80
+ defer { f.stop(42) }
81
+
82
+ await_result = f.await
83
+ assert_equal 1, result.size
84
+ assert_equal 42, await_result
85
+ ensure
86
+ f&.stop
87
+ end
88
+
89
+ def test_that_fiber_can_be_awaited
90
+ result = nil
91
+ f2 = nil
92
+ f1 = spin do
93
+ f2 = Fiber.spin do
94
+ snooze
95
+ 42
96
+ end
97
+ result = f2.await
98
+ end
99
+ suspend
100
+ assert_equal 42, result
101
+ ensure
102
+ f1&.stop
103
+ f2&.stop
104
+ end
105
+
106
+ def test_that_fiber_can_be_stopped
107
+ result = nil
108
+ f = spin do
109
+ snooze
110
+ result = 42
111
+ end
112
+ defer { f.interrupt }
113
+ suspend
114
+ assert_nil result
115
+ ensure
116
+ f&.stop
117
+ end
118
+
119
+ def test_that_fiber_can_be_cancelled
120
+ result = nil
121
+ f = spin do
122
+ snooze
123
+ result = 42
124
+ rescue Polyphony::Cancel => e
125
+ result = e
126
+ end
127
+ defer { f.cancel! }
128
+
129
+ suspend
130
+
131
+ assert_kind_of Polyphony::Cancel, result
132
+ assert_kind_of Polyphony::Cancel, f.result
133
+ assert_equal :dead, f.state
134
+ ensure
135
+ f&.stop
136
+ end
137
+
138
+ def test_that_inner_fiber_can_be_interrupted
139
+ result = nil
140
+ f2 = nil
141
+ f1 = spin do
142
+ f2 = spin do
143
+ snooze
144
+ result = 42
145
+ end
146
+ f2.await
147
+ result && result += 1
148
+ end
149
+ defer { f2.interrupt }
150
+ suspend
151
+ assert_nil result
152
+ assert_equal :dead, f1.state
153
+ assert_equal :dead, f2.state
154
+ ensure
155
+ f1&.stop
156
+ f2&.stop
157
+ end
158
+
159
+ def test_state
160
+ counter = 0
161
+ f = spin do
162
+ 3.times do
163
+ snooze
164
+ counter += 1
165
+ end
166
+ end
167
+
168
+ assert_equal :scheduled, f.state
169
+ assert_equal :running, Fiber.current.state
170
+ snooze
171
+ assert_equal :scheduled, f.state
172
+ snooze while counter < 3
173
+ assert_equal :dead, f.state
174
+ ensure
175
+ f&.stop
176
+ end
177
+
178
+ def test_fiber_exception_propagation
179
+ # error is propagated to calling fiber
180
+ raised_error = nil
181
+ spin do
182
+ spin do
183
+ raise 'foo'
184
+ end
185
+ snooze # allow nested fiber to run before finishing
186
+ end
187
+ suspend
188
+ rescue Exception => e
189
+ raised_error = e
190
+ ensure
191
+ assert raised_error
192
+ assert_equal 'foo', raised_error.message
193
+ end
194
+
195
+ def test_that_fiber_can_be_interrupted_before_first_scheduling
196
+ buffer = []
197
+ f = spin { buffer << 1 }
198
+ f.stop
199
+
200
+ snooze
201
+ assert !f.running?
202
+ assert_equal [], buffer
203
+ end
204
+
205
+ def test_exception_propagation_for_orphan_fiber
206
+ raised_error = nil
207
+ spin do
208
+ spin do
209
+ snooze
210
+ raise 'bar'
211
+ end
212
+ end
213
+ suspend
214
+ rescue Exception => e
215
+ raised_error = e
216
+ ensure
217
+ assert raised_error
218
+ assert_equal 'bar', raised_error.message
219
+ end
220
+
221
+ def test_await_multiple_fibers
222
+ f1 = spin { sleep 0.01; :foo }
223
+ f2 = spin { sleep 0.01; :bar }
224
+ f3 = spin { sleep 0.01; :baz }
225
+
226
+ result = Fiber.await(f1, f2, f3)
227
+ assert_equal %i{foo bar baz}, result
228
+ end
229
+
230
+ def test_join_multiple_fibers
231
+ f1 = spin { sleep 0.01; :foo }
232
+ f2 = spin { sleep 0.01; :bar }
233
+ f3 = spin { sleep 0.01; :baz }
234
+
235
+ result = Fiber.join(f1, f2, f3)
236
+ assert_equal %i{foo bar baz}, result
237
+ end
238
+
239
+ def test_select_from_multiple_fibers
240
+ buffer = []
241
+ f1 = spin { sleep 0.01; buffer << :foo; :foo }
242
+ f2 = spin { sleep 0.02; buffer << :bar; :bar }
243
+ f3 = spin { sleep 0.03; buffer << :baz; :baz }
244
+
245
+ result, selected = Fiber.select(f1, f2, f3)
246
+ assert_equal :foo, result
247
+ assert_equal f1, selected
248
+ assert_equal [:foo], buffer
249
+ end
250
+
251
+ def test_caller
252
+ location = /^#{__FILE__}:#{__LINE__ + 1}/
253
+ f = spin do
254
+ sleep 0.01
255
+ end
256
+ snooze
257
+
258
+ caller = f.caller
259
+ assert_match location, caller[0]
260
+ end
261
+
262
+ def test_location
263
+ location = /^#{__FILE__}:#{__LINE__ + 1}/
264
+ f = spin do
265
+ sleep 0.01
266
+ end
267
+ snooze
268
+
269
+ assert f.location =~ location
270
+ end
271
+
272
+ def test_when_done
273
+ flag = nil
274
+ values = []
275
+ f = spin do
276
+ snooze until flag
277
+ end
278
+ f.when_done { values << 42 }
279
+
280
+ snooze
281
+ assert values.empty?
282
+ snooze
283
+ flag = true
284
+ assert values.empty?
285
+ assert f.alive?
286
+
287
+ snooze
288
+ assert_equal [42], values
289
+ assert !f.running?
290
+ end
291
+
292
+ def test_interrupt
293
+ f = spin do
294
+ sleep 1
295
+ :foo
296
+ end
297
+
298
+ snooze
299
+ assert f.alive?
300
+
301
+ f.interrupt :bar
302
+ assert !f.running?
303
+
304
+ assert_equal :bar, f.result
305
+ end
306
+
307
+ def test_cancel
308
+ error = nil
309
+ f = spin do
310
+ sleep 1
311
+ :foo
312
+ end
313
+
314
+ snooze
315
+ f.cancel!
316
+ rescue Polyphony::Cancel => e
317
+ # cancel error should bubble up
318
+ error = e
319
+ ensure
320
+ assert error
321
+ assert_equal :dead, f.state
322
+ end
323
+ end
324
+
325
+ class MailboxTest < MiniTest::Test
326
+ def test_that_fiber_can_receive_messages
327
+ msgs = []
328
+ f = spin { loop { msgs << receive } }
329
+
330
+ snooze # allow fiber to start
331
+
332
+ 3.times do |i|
333
+ f << i
334
+ snooze
335
+ end
336
+
337
+ assert_equal [0, 1, 2], msgs
338
+ ensure
339
+ f&.stop
340
+ end
341
+
342
+ def test_that_multiple_messages_sent_at_once_arrive_in_order
343
+ msgs = []
344
+ f = spin { loop { msgs << receive } }
345
+
346
+ snooze # allow coproc to start
347
+
348
+ 3.times { |i| f << i }
349
+
350
+ snooze
351
+
352
+ assert_equal [0, 1, 2], msgs
353
+ ensure
354
+ f&.stop
355
+ end
356
+
357
+ def test_that_sent_message_are_queued_before_calling_receive
358
+ buffer = []
359
+ receiver = spin { suspend; 3.times { buffer << receive } }
360
+ sender = spin { 3.times { |i| receiver << (i * 10) } }
361
+
362
+ sender.await
363
+ receiver.schedule
364
+ receiver.await
365
+
366
+ assert_equal [0, 10, 20], buffer
367
+ end
368
+
369
+ def test_list_and_count
370
+ assert_equal 1, Fiber.count
371
+ assert_equal [Fiber.current], Fiber.list
372
+
373
+ f = spin { sleep 1 }
374
+ snooze
375
+ assert_equal 2, Fiber.count
376
+ assert_equal f, Fiber.list.last
377
+
378
+ f.stop
379
+ snooze
380
+ assert_equal 1, Fiber.count
381
+ end
382
+ end
@@ -3,43 +3,42 @@
3
3
  require_relative 'helper'
4
4
 
5
5
  class SpinTest < MiniTest::Test
6
- def test_that_spin_returns_a_coprocess
6
+ def test_that_spin_returns_a_fiber
7
7
  result = nil
8
- coprocess = spin { result = 42 }
8
+ fiber = spin { result = 42 }
9
9
 
10
- assert_kind_of(Polyphony::Coprocess, coprocess)
11
- assert_nil(result)
10
+ assert_kind_of Fiber, fiber
11
+ assert_nil result
12
12
  suspend
13
- assert_equal(42, result)
13
+ assert_equal 42, result
14
14
  end
15
15
 
16
- def test_that_spin_accepts_coprocess_argument
16
+ def test_that_spin_accepts_fiber_argument
17
17
  result = nil
18
- coprocess = Polyphony::Coprocess.new { result = 42 }
19
- coprocess.run
18
+ fiber = Fiber.spin { result = 42 }
20
19
 
21
- assert_nil(result)
20
+ assert_nil result
22
21
  suspend
23
- assert_equal(42, result)
22
+ assert_equal 42, result
24
23
  end
25
24
 
26
- def test_that_spined_coprocess_saves_result
27
- coprocess = spin { 42 }
25
+ def test_that_spined_fiber_saves_result
26
+ fiber = spin { 42 }
28
27
 
29
- assert_kind_of(Polyphony::Coprocess, coprocess)
30
- assert_nil(coprocess.result)
28
+ assert_kind_of Fiber, fiber
29
+ assert_nil fiber.result
31
30
  suspend
32
- assert_equal(42, coprocess.result)
31
+ assert_equal 42, fiber.result
33
32
  end
34
33
 
35
- def test_that_spined_coprocess_can_be_interrupted
36
- coprocess = spin do
34
+ def test_that_spined_fiber_can_be_interrupted
35
+ fiber = spin do
37
36
  sleep(1)
38
37
  42
39
38
  end
40
- defer { coprocess.interrupt }
39
+ defer { fiber.interrupt }
41
40
  suspend
42
- assert_nil(coprocess.result)
41
+ assert_nil fiber.result
43
42
  end
44
43
  end
45
44
 
@@ -51,7 +50,7 @@ class CancelScopeTest < Minitest::Test
51
50
  end
52
51
  end
53
52
 
54
- def test_that_cancel_scope_cancels_coprocess
53
+ def test_that_cancel_scope_cancels_fiber
55
54
  ctx = {}
56
55
  spin do
57
56
  after(0.005) { ctx[:cancel_scope].cancel! }
@@ -60,13 +59,13 @@ class CancelScopeTest < Minitest::Test
60
59
  ctx[:result] = e
61
60
  nil
62
61
  end
63
- assert_nil(ctx[:result])
62
+ assert_nil ctx[:result]
64
63
  # async operation will only begin on next iteration of event loop
65
- assert_nil(ctx[:cancel_scope])
64
+ assert_nil ctx[:cancel_scope]
66
65
 
67
66
  Gyro.run
68
- assert_kind_of(Polyphony::CancelScope, ctx[:cancel_scope])
69
- assert_kind_of(Polyphony::Cancel, ctx[:result])
67
+ assert_kind_of Polyphony::CancelScope, ctx[:cancel_scope]
68
+ assert_kind_of Polyphony::Cancel, ctx[:result]
70
69
  end
71
70
 
72
71
  def test_that_cancel_scope_cancels_async_op_with_stop
@@ -77,8 +76,8 @@ class CancelScopeTest < Minitest::Test
77
76
  end
78
77
 
79
78
  Gyro.run
80
- assert(ctx[:cancel_scope])
81
- assert_nil(ctx[:result])
79
+ assert ctx[:cancel_scope]
80
+ assert_nil ctx[:result]
82
81
  end
83
82
 
84
83
  def test_that_cancel_after_raises_cancelled_exception
@@ -92,7 +91,7 @@ class CancelScopeTest < Minitest::Test
92
91
  result = :cancelled
93
92
  end
94
93
  suspend
95
- assert_equal(:cancelled, result)
94
+ assert_equal :cancelled, result
96
95
  end
97
96
 
98
97
  # def test_that_cancel_scopes_can_be_nested
@@ -108,8 +107,8 @@ class CancelScopeTest < Minitest::Test
108
107
  # outer_result = 42
109
108
  # end
110
109
  # suspend
111
- # assert_nil(inner_result)
112
- # assert_equal(42, outer_result)
110
+ # assert_nil inner_result
111
+ # assert_equal 42, outer_result
113
112
 
114
113
  # Polyphony.reset!
115
114
 
@@ -124,8 +123,8 @@ class CancelScopeTest < Minitest::Test
124
123
  # outer_result = 42
125
124
  # end
126
125
  # suspend
127
- # assert_equal(42, inner_result)
128
- # assert_equal(42, outer_result)
126
+ # assert_equal 42, inner_result
127
+ # assert_equal 42, outer_result
129
128
  # end
130
129
  end
131
130
 
@@ -139,22 +138,22 @@ class SupervisorTest < MiniTest::Test
139
138
 
140
139
  def parallel_sleep(ctx)
141
140
  supervise do |s|
142
- (1..3).each { |idx| s.spin sleep_and_set(ctx, idx) }
141
+ (1..3).each { |idx| s.spin(&sleep_and_set(ctx, idx)) }
143
142
  end
144
143
  end
145
144
 
146
- def test_that_supervisor_waits_for_all_nested_coprocesses_to_complete
145
+ def test_that_supervisor_waits_for_all_nested_fibers_to_complete
147
146
  ctx = {}
148
147
  spin do
149
148
  parallel_sleep(ctx)
150
149
  end
151
150
  suspend
152
- assert(ctx[1])
153
- assert(ctx[2])
154
- assert(ctx[3])
151
+ assert ctx[1]
152
+ assert ctx[2]
153
+ assert ctx[3]
155
154
  end
156
155
 
157
- def test_that_supervisor_can_add_coprocesses_after_having_started
156
+ def test_that_supervisor_can_add_fibers_after_having_started
158
157
  result = []
159
158
  spin do
160
159
  supervisor = Polyphony::Supervisor.new
@@ -170,7 +169,7 @@ class SupervisorTest < MiniTest::Test
170
169
  supervisor.await
171
170
  end.await
172
171
 
173
- assert_equal([0, 1, 2], result.sort)
172
+ assert_equal [0, 1, 2], result.sort
174
173
  end
175
174
  end
176
175
 
@@ -193,7 +192,7 @@ class ExceptionTest < MiniTest::Test
193
192
  frames << 3
194
193
  raise e
195
194
  end
196
- 4.times { snooze }
195
+ 5.times { |i| snooze }
197
196
  rescue Exception => e
198
197
  error = e
199
198
  ensure
@@ -246,12 +245,12 @@ class MoveOnAfterTest < MiniTest::Test
246
245
  def test_spin_loop
247
246
  buffer = []
248
247
  counter = 0
249
- cp = spin_loop do
248
+ f = spin_loop do
250
249
  buffer << (counter += 1)
251
250
  snooze
252
251
  end
253
252
 
254
- assert_kind_of Polyphony::Coprocess, cp
253
+ assert_kind_of Fiber, f
255
254
  assert_equal [], buffer
256
255
  snooze
257
256
  assert_equal [1], buffer
@@ -259,40 +258,40 @@ class MoveOnAfterTest < MiniTest::Test
259
258
  assert_equal [1, 2], buffer
260
259
  snooze
261
260
  assert_equal [1, 2, 3], buffer
262
- cp.stop
261
+ f.stop
263
262
  snooze
264
- assert !cp.alive?
263
+ assert !f.running?
265
264
  assert_equal [1, 2, 3], buffer
266
265
  end
267
266
 
268
267
  def test_throttled_loop
269
268
  buffer = []
270
269
  counter = 0
271
- cp = spin do
270
+ f = spin do
272
271
  throttled_loop(50) { buffer << (counter += 1) }
273
272
  end
274
273
  sleep 0.1
275
- cp.stop
274
+ f.stop
276
275
  assert_equal [1, 2, 3, 4, 5], buffer
277
276
  end
278
277
 
279
278
  def test_throttled_loop_with_count
280
279
  buffer = []
281
280
  counter = 0
282
- cp = spin do
281
+ f = spin do
283
282
  throttled_loop(50, count: 5) { buffer << (counter += 1) }
284
283
  end
285
- cp.await
284
+ f.await
286
285
  assert_equal [1, 2, 3, 4, 5], buffer
287
286
  end
288
287
 
289
288
  def test_every
290
289
  buffer = []
291
- cp = spin do
290
+ f = spin do
292
291
  every(0.01) { buffer << 1 }
293
292
  end
294
293
  sleep 0.05
295
- cp.stop
296
- assert_equal 5, buffer.size
294
+ f.stop
295
+ assert (4..5).include?(buffer.size)
297
296
  end
298
297
  end
data/test/test_gyro.rb CHANGED
@@ -8,7 +8,7 @@ class GyroTest < MiniTest::Test
8
8
 
9
9
  f = Fiber.new {}
10
10
 
11
- assert_equal :paused, f.state
11
+ assert_equal :suspended, f.state
12
12
  f.resume
13
13
  assert_equal :dead, f.state
14
14