polyphony 0.24 → 0.25

Sign up to get free protection for your applications and to get access to all the features.
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