polyphony 0.58 → 0.61

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/Gemfile.lock +15 -29
  4. data/examples/core/message_based_supervision.rb +51 -0
  5. data/examples/io/echo_server.rb +16 -7
  6. data/ext/polyphony/backend_common.c +160 -7
  7. data/ext/polyphony/backend_common.h +34 -2
  8. data/ext/polyphony/backend_io_uring.c +119 -40
  9. data/ext/polyphony/backend_io_uring_context.c +10 -1
  10. data/ext/polyphony/backend_io_uring_context.h +5 -3
  11. data/ext/polyphony/backend_libev.c +109 -31
  12. data/ext/polyphony/extconf.rb +2 -2
  13. data/ext/polyphony/fiber.c +1 -34
  14. data/ext/polyphony/polyphony.c +12 -19
  15. data/ext/polyphony/polyphony.h +9 -20
  16. data/ext/polyphony/polyphony_ext.c +0 -4
  17. data/ext/polyphony/queue.c +12 -12
  18. data/ext/polyphony/runqueue.c +21 -98
  19. data/ext/polyphony/runqueue.h +26 -0
  20. data/ext/polyphony/thread.c +6 -113
  21. data/lib/polyphony/core/timer.rb +2 -2
  22. data/lib/polyphony/extensions/fiber.rb +102 -82
  23. data/lib/polyphony/extensions/io.rb +10 -9
  24. data/lib/polyphony/extensions/openssl.rb +14 -4
  25. data/lib/polyphony/extensions/socket.rb +15 -15
  26. data/lib/polyphony/extensions/thread.rb +1 -1
  27. data/lib/polyphony/version.rb +1 -1
  28. data/polyphony.gemspec +0 -7
  29. data/test/test_backend.rb +46 -9
  30. data/test/test_ext.rb +1 -1
  31. data/test/test_fiber.rb +106 -18
  32. data/test/test_global_api.rb +1 -1
  33. data/test/test_io.rb +29 -0
  34. data/test/test_supervise.rb +100 -100
  35. data/test/test_thread.rb +5 -11
  36. data/test/test_thread_pool.rb +1 -1
  37. data/test/test_trace.rb +28 -49
  38. metadata +5 -109
  39. data/ext/polyphony/tracing.c +0 -11
  40. data/lib/polyphony/adapters/trace.rb +0 -138
@@ -0,0 +1,26 @@
1
+ #ifndef RUNQUEUE_H
2
+ #define RUNQUEUE_H
3
+
4
+ #include "polyphony.h"
5
+ #include "runqueue_ring_buffer.h"
6
+
7
+ typedef struct runqueue {
8
+ runqueue_ring_buffer entries;
9
+ unsigned int high_watermark;
10
+ } runqueue_t;
11
+
12
+ void runqueue_initialize(runqueue_t *runqueue);
13
+ void runqueue_finalize(runqueue_t *runqueue);
14
+ void runqueue_mark(runqueue_t *runqueue);
15
+
16
+ void runqueue_push(runqueue_t *runqueue, VALUE fiber, VALUE value, int reschedule);
17
+ void runqueue_unshift(runqueue_t *runqueue, VALUE fiber, VALUE value, int reschedule);
18
+ runqueue_entry runqueue_shift(runqueue_t *runqueue);
19
+ void runqueue_delete(runqueue_t *runqueue, VALUE fiber);
20
+ int runqueue_index_of(runqueue_t *runqueue, VALUE fiber);
21
+ void runqueue_clear(runqueue_t *runqueue);
22
+ long runqueue_len(runqueue_t *runqueue);
23
+ long runqueue_max_len(runqueue_t *runqueue);
24
+ int runqueue_empty_p(runqueue_t *runqueue);
25
+
26
+ #endif /* RUNQUEUE_H */
@@ -1,73 +1,24 @@
1
1
  #include "polyphony.h"
2
+ #include "backend_common.h"
2
3
 
3
4
  ID ID_deactivate_all_watchers_post_fork;
4
5
  ID ID_ivar_backend;
5
6
  ID ID_ivar_join_wait_queue;
6
7
  ID ID_ivar_main_fiber;
7
8
  ID ID_ivar_terminated;
8
- ID ID_ivar_runqueue;
9
9
  ID ID_stop;
10
10
 
11
11
  static VALUE Thread_setup_fiber_scheduling(VALUE self) {
12
- VALUE runqueue = rb_funcall(cRunqueue, ID_new, 0);
13
-
14
12
  rb_ivar_set(self, ID_ivar_main_fiber, rb_fiber_current());
15
- rb_ivar_set(self, ID_ivar_runqueue, runqueue);
16
-
17
13
  return self;
18
14
  }
19
15
 
20
- static VALUE SYM_scheduled_fibers;
21
- static VALUE SYM_pending_watchers;
22
-
23
- static VALUE Thread_fiber_scheduling_stats(VALUE self) {
24
- VALUE backend = rb_ivar_get(self, ID_ivar_backend);
25
- VALUE stats = rb_hash_new();
26
- VALUE runqueue = rb_ivar_get(self, ID_ivar_runqueue);
27
- long pending_count;
28
-
29
- long scheduled_count = Runqueue_len(runqueue);
30
- rb_hash_aset(stats, SYM_scheduled_fibers, INT2NUM(scheduled_count));
31
-
32
- pending_count = Backend_pending_count(backend);
33
- rb_hash_aset(stats, SYM_pending_watchers, INT2NUM(pending_count));
34
-
35
- return stats;
36
- }
37
-
38
- void schedule_fiber(VALUE self, VALUE fiber, VALUE value, int prioritize) {
39
- VALUE runqueue;
40
- int already_runnable;
41
-
42
- if (rb_fiber_alive_p(fiber) != Qtrue) return;
43
- already_runnable = rb_ivar_get(fiber, ID_ivar_runnable) != Qnil;
44
-
45
- COND_TRACE(3, SYM_fiber_schedule, fiber, value);
46
- runqueue = rb_ivar_get(self, ID_ivar_runqueue);
47
- (prioritize ? Runqueue_unshift : Runqueue_push)(runqueue, fiber, value, already_runnable);
48
- if (!already_runnable) {
49
- rb_ivar_set(fiber, ID_ivar_runnable, Qtrue);
50
- if (rb_thread_current() != self) {
51
- // If the fiber scheduling is done across threads, we need to make sure the
52
- // target thread is woken up in case it is in the middle of running its
53
- // event selector. Otherwise it's gonna be stuck waiting for an event to
54
- // happen, not knowing that it there's already a fiber ready to run in its
55
- // run queue.
56
- VALUE backend = rb_ivar_get(self, ID_ivar_backend);
57
- Backend_wakeup(backend);
58
- }
59
- }
60
- }
61
-
62
- VALUE Thread_fiber_scheduling_index(VALUE self, VALUE fiber) {
63
- VALUE runqueue = rb_ivar_get(self, ID_ivar_runqueue);
64
-
65
- return INT2NUM(Runqueue_index_of(runqueue, fiber));
16
+ inline void schedule_fiber(VALUE self, VALUE fiber, VALUE value, int prioritize) {
17
+ Backend_schedule_fiber(self, rb_ivar_get(self, ID_ivar_backend), fiber, value, prioritize);
66
18
  }
67
19
 
68
20
  VALUE Thread_fiber_unschedule(VALUE self, VALUE fiber) {
69
- VALUE runqueue = rb_ivar_get(self, ID_ivar_runqueue);
70
- Runqueue_delete(runqueue, fiber);
21
+ Backend_unschedule_fiber(rb_ivar_get(self, ID_ivar_backend), fiber);
71
22
  return self;
72
23
  }
73
24
 
@@ -82,65 +33,15 @@ VALUE Thread_schedule_fiber_with_priority(VALUE self, VALUE fiber, VALUE value)
82
33
  }
83
34
 
84
35
  VALUE Thread_switch_fiber(VALUE self) {
85
- VALUE current_fiber = rb_fiber_current();
86
- VALUE runqueue = rb_ivar_get(self, ID_ivar_runqueue);
87
- runqueue_entry next;
88
- VALUE backend = rb_ivar_get(self, ID_ivar_backend);
89
- unsigned int pending_ops_count = Backend_pending_count(backend);
90
- unsigned int backend_was_polled = 0;
91
- unsigned int idle_tasks_run_count = 0;
92
-
93
- if (__tracing_enabled__ && (rb_ivar_get(current_fiber, ID_ivar_running) != Qfalse))
94
- TRACE(2, SYM_fiber_switchpoint, current_fiber);
95
-
96
- while (1) {
97
- next = Runqueue_shift(runqueue);
98
- if (next.fiber != Qnil) {
99
- // Polling for I/O op completion is normally done when the run queue is
100
- // empty, but if the runqueue never empties, we'll never get to process
101
- // any event completions. In order to prevent this, an anti-starve
102
- // mechanism is employed, under the following conditions:
103
- // - a blocking poll was not yet performed
104
- // - there are pending blocking operations
105
- // - the runqueue has signalled that a non-blocking poll should be
106
- // performed
107
- // - the run queue length high watermark has reached its threshold (currently 128)
108
- // - the run queue switch counter has reached its threshold (currently 64)
109
- if (!backend_was_polled && pending_ops_count && Runqueue_should_poll_nonblocking(runqueue)) {
110
- // this prevents event starvation in case the run queue never empties
111
- Backend_poll(backend, Qtrue, current_fiber, runqueue);
112
- }
113
- break;
114
- }
115
-
116
- if (!idle_tasks_run_count) {
117
- idle_tasks_run_count++;
118
- Backend_run_idle_tasks(backend);
119
- }
120
- if (pending_ops_count == 0) break;
121
- Backend_poll(backend, Qnil, current_fiber, runqueue);
122
- backend_was_polled = 1;
123
- }
124
-
125
- if (next.fiber == Qnil) return Qnil;
126
-
127
- // run next fiber
128
- COND_TRACE(3, SYM_fiber_run, next.fiber, next.value);
129
-
130
- rb_ivar_set(next.fiber, ID_ivar_runnable, Qnil);
131
- RB_GC_GUARD(next.fiber);
132
- RB_GC_GUARD(next.value);
133
- return (next.fiber == current_fiber) ?
134
- next.value : FIBER_TRANSFER(next.fiber, next.value);
36
+ return Backend_switch_fiber(rb_ivar_get(self, ID_ivar_backend));
135
37
  }
136
38
 
137
39
  VALUE Thread_fiber_schedule_and_wakeup(VALUE self, VALUE fiber, VALUE resume_obj) {
138
- VALUE backend = rb_ivar_get(self, ID_ivar_backend);
139
40
  if (fiber != Qnil) {
140
41
  Thread_schedule_fiber_with_priority(self, fiber, resume_obj);
141
42
  }
142
43
 
143
- if (Backend_wakeup(backend) == Qnil) {
44
+ if (Backend_wakeup(rb_ivar_get(self, ID_ivar_backend)) == Qnil) {
144
45
  // we're not inside the ev_loop, so we just do a switchpoint
145
46
  Thread_switch_fiber(self);
146
47
  }
@@ -159,14 +60,12 @@ VALUE Thread_class_backend(VALUE _self) {
159
60
 
160
61
  void Init_Thread() {
161
62
  rb_define_method(rb_cThread, "setup_fiber_scheduling", Thread_setup_fiber_scheduling, 0);
162
- rb_define_method(rb_cThread, "fiber_scheduling_stats", Thread_fiber_scheduling_stats, 0);
163
63
  rb_define_method(rb_cThread, "schedule_and_wakeup", Thread_fiber_schedule_and_wakeup, 2);
164
64
 
165
65
  rb_define_method(rb_cThread, "schedule_fiber", Thread_schedule_fiber, 2);
166
66
  rb_define_method(rb_cThread, "schedule_fiber_with_priority",
167
67
  Thread_schedule_fiber_with_priority, 2);
168
68
  rb_define_method(rb_cThread, "switch_fiber", Thread_switch_fiber, 0);
169
- rb_define_method(rb_cThread, "fiber_scheduling_index", Thread_fiber_scheduling_index, 1);
170
69
  rb_define_method(rb_cThread, "fiber_unschedule", Thread_fiber_unschedule, 1);
171
70
 
172
71
  rb_define_singleton_method(rb_cThread, "backend", Thread_class_backend, 0);
@@ -178,11 +77,5 @@ void Init_Thread() {
178
77
  ID_ivar_join_wait_queue = rb_intern("@join_wait_queue");
179
78
  ID_ivar_main_fiber = rb_intern("@main_fiber");
180
79
  ID_ivar_terminated = rb_intern("@terminated");
181
- ID_ivar_runqueue = rb_intern("@runqueue");
182
80
  ID_stop = rb_intern("stop");
183
-
184
- SYM_scheduled_fibers = ID2SYM(rb_intern("scheduled_fibers"));
185
- SYM_pending_watchers = ID2SYM(rb_intern("pending_watchers"));
186
- rb_global_variable(&SYM_scheduled_fibers);
187
- rb_global_variable(&SYM_pending_watchers);
188
81
  }
@@ -3,8 +3,8 @@
3
3
  module Polyphony
4
4
  # Implements a common timer for running multiple timeouts
5
5
  class Timer
6
- def initialize(resolution:)
7
- @fiber = spin_loop(interval: resolution) { update }
6
+ def initialize(tag = nil, resolution:)
7
+ @fiber = spin_loop(tag, interval: resolution) { update }
8
8
  @timeouts = {}
9
9
  end
10
10
 
@@ -22,9 +22,9 @@ module Polyphony
22
22
  return self
23
23
  end
24
24
 
25
- parent.spin(@tag, @caller, &@block).tap do |f|
26
- f.schedule(value) unless value.nil?
27
- end
25
+ fiber = parent.spin(@tag, @caller, &@block)
26
+ fiber.schedule(value) unless value.nil?
27
+ fiber
28
28
  end
29
29
  alias_method :reset, :restart
30
30
 
@@ -66,6 +66,11 @@ module Polyphony
66
66
  def interject(&block)
67
67
  raise Polyphony::Interjection.new(block)
68
68
  end
69
+
70
+ def await
71
+ Fiber.await(self).first
72
+ end
73
+ alias_method :join, :await
69
74
  end
70
75
 
71
76
  # Fiber supervision
@@ -78,7 +83,7 @@ module Polyphony
78
83
  while true
79
84
  supervise_perform(opts)
80
85
  end
81
- rescue Polyphony::MoveOn
86
+ rescue Polyphony::MoveOn
82
87
  # generated in #supervise_perform to stop supervisor
83
88
  ensure
84
89
  @on_child_done = nil
@@ -119,72 +124,62 @@ module Polyphony
119
124
  def await(*fibers)
120
125
  return [] if fibers.empty?
121
126
 
122
- state = setup_await_select_state(fibers)
123
- await_setup_monitoring(fibers, state)
124
- suspend
125
- fibers.map(&:result)
126
- ensure
127
- await_select_cleanup(state) if state
128
- end
129
- alias_method :join, :await
130
-
131
- def setup_await_select_state(fibers)
132
- {
133
- awaiter: Fiber.current,
134
- pending: fibers.each_with_object({}) { |f, h| h[f] = true }
135
- }
136
- end
137
-
138
- def await_setup_monitoring(fibers, state)
127
+ Fiber.current.message_on_child_termination = true
128
+ results = {}
139
129
  fibers.each do |f|
140
- f.when_done { |r| await_fiber_done(f, r, state) }
130
+ results[f] = nil
131
+ if f.dead?
132
+ # fiber already terminated, so queue message
133
+ Fiber.current.send [f, f.result]
134
+ else
135
+ f.monitor
136
+ end
141
137
  end
142
- end
143
-
144
- def await_fiber_done(fiber, result, state)
145
- state[:pending].delete(fiber)
146
-
147
- if state[:cleanup]
148
- state[:awaiter].schedule if state[:pending].empty?
149
- elsif !state[:done] && (result.is_a?(Exception) || state[:pending].empty?)
150
- state[:awaiter].schedule(result)
151
- state[:done] = true
138
+ exception = nil
139
+ while !fibers.empty?
140
+ (fiber, result) = receive
141
+ next unless fibers.include?(fiber)
142
+
143
+ fibers.delete(fiber)
144
+ if result.is_a?(Exception)
145
+ exception ||= result
146
+ fibers.each { |f| f.terminate }
147
+ else
148
+ results[fiber] = result
149
+ end
152
150
  end
153
- end
154
-
155
- def await_select_cleanup(state)
156
- return if state[:pending].empty?
157
-
158
- terminate = Polyphony::Terminate.new
159
- state[:cleanup] = true
160
- state[:pending].each_key { |f| f.schedule(terminate) }
161
- suspend
162
- end
163
-
164
- def select(*fibers)
165
- state = setup_await_select_state(fibers)
166
- select_setup_monitoring(fibers, state)
167
- suspend
151
+ results.values
168
152
  ensure
169
- await_select_cleanup(state)
153
+ Fiber.current.message_on_child_termination = false
154
+ raise exception if exception
170
155
  end
156
+ alias_method :join, :await
171
157
 
172
- def select_setup_monitoring(fibers, state)
158
+ def select(*fibers)
159
+ return nil if fibers.empty?
160
+
173
161
  fibers.each do |f|
174
- f.when_done { |r| select_fiber_done(f, r, state) }
162
+ if f.dead?
163
+ result = f.result
164
+ result.is_a?(Exception) ? (raise result) : (return [f, result])
165
+ end
175
166
  end
176
- end
177
167
 
178
- def select_fiber_done(fiber, result, state)
179
- state[:pending].delete(fiber)
180
- if state[:cleanup]
181
- # in cleanup mode the selector is resumed if no more pending fibers
182
- state[:awaiter].schedule if state[:pending].empty?
183
- elsif !state[:selected]
184
- # first fiber to complete, we schedule the result
185
- state[:awaiter].schedule([fiber, result])
186
- state[:selected] = true
168
+ Fiber.current.message_on_child_termination = true
169
+ fibers.each { |f| f.monitor }
170
+ while true
171
+ (fiber, result) = receive
172
+ next unless fibers.include?(fiber)
173
+
174
+ fibers.each { |f| f.unmonitor }
175
+ if result.is_a?(Exception)
176
+ raise result
177
+ else
178
+ return [fiber, result]
179
+ end
187
180
  end
181
+ ensure
182
+ Fiber.current.message_on_child_termination = false
188
183
  end
189
184
 
190
185
  # Creates and schedules with priority an out-of-band fiber that runs the
@@ -209,6 +204,14 @@ module Polyphony
209
204
  (@children ||= {}).keys
210
205
  end
211
206
 
207
+ def add_child(child_fiber)
208
+ (@children ||= {})[child_fiber] = true
209
+ end
210
+
211
+ def remove_child(child_fiber)
212
+ @children.delete(child_fiber) if @children
213
+ end
214
+
212
215
  def spin(tag = nil, orig_caller = Kernel.caller, &block)
213
216
  f = Fiber.new { |v| f.run(v) }
214
217
  f.prepare(tag, block, orig_caller, self)
@@ -218,7 +221,10 @@ module Polyphony
218
221
 
219
222
  def child_done(child_fiber, result)
220
223
  @children.delete(child_fiber)
221
- @on_child_done&.(child_fiber, result)
224
+
225
+ if result.is_a?(Exception) && !@message_on_child_termination
226
+ schedule_with_priority(result)
227
+ end
222
228
  end
223
229
 
224
230
  def terminate_all_children(graceful = false)
@@ -234,14 +240,7 @@ module Polyphony
234
240
  def await_all_children
235
241
  return unless @children && !@children.empty?
236
242
 
237
- results = @children.dup
238
- @on_child_done = proc do |c, r|
239
- results[c] = r
240
- schedule if @children.empty?
241
- end
242
- suspend
243
- @on_child_done = nil
244
- results.values
243
+ Fiber.await(*@children.keys)
245
244
  end
246
245
 
247
246
  def shutdown_all_children(graceful = false)
@@ -252,6 +251,18 @@ module Polyphony
252
251
  c.await
253
252
  end
254
253
  end
254
+
255
+ def detach
256
+ @parent.remove_child(self)
257
+ @parent = @thread.main_fiber
258
+ @parent.add_child(self)
259
+ end
260
+
261
+ def attach(parent)
262
+ @parent.remove_child(self)
263
+ @parent = parent
264
+ @parent.add_child(self)
265
+ end
255
266
  end
256
267
 
257
268
  # Fiber life cycle methods
@@ -262,7 +273,7 @@ module Polyphony
262
273
  @parent = parent
263
274
  @caller = caller
264
275
  @block = block
265
- __fiber_trace__(:fiber_create, self)
276
+ Thread.backend.trace(:fiber_create, self)
266
277
  schedule
267
278
  end
268
279
 
@@ -304,17 +315,16 @@ module Polyphony
304
315
 
305
316
  def restart_self(first_value)
306
317
  @mailbox = nil
307
- @when_done_procs = nil
308
- @waiting_fibers = nil
309
318
  run(first_value)
310
319
  end
311
320
 
312
321
  def finalize(result, uncaught_exception = false)
313
322
  result, uncaught_exception = finalize_children(result, uncaught_exception)
314
- __fiber_trace__(:fiber_terminate, self, result)
323
+ Thread.backend.trace(:fiber_terminate, self, result)
315
324
  @result = result
316
- @running = false
325
+
317
326
  inform_dependants(result, uncaught_exception)
327
+ @running = false
318
328
  ensure
319
329
  # Prevent fiber from being resumed after terminating
320
330
  @thread.fiber_unschedule(self)
@@ -332,17 +342,27 @@ module Polyphony
332
342
  end
333
343
 
334
344
  def inform_dependants(result, uncaught_exception)
345
+ if @monitors
346
+ msg = [self, result]
347
+ @monitors.each { |f| f << msg }
348
+ end
349
+
335
350
  @parent&.child_done(self, result)
336
- @when_done_procs&.each { |p| p.(result) }
337
- @waiting_fibers&.each_key { |f| f.schedule(result) }
338
-
339
- # propagate unaught exception to parent
340
- @parent&.schedule_with_priority(result) if uncaught_exception && !@waiting_fibers
341
351
  end
342
352
 
343
- def when_done(&block)
344
- @when_done_procs ||= []
345
- @when_done_procs << block
353
+ attr_accessor :message_on_child_termination
354
+
355
+ def monitor
356
+ @monitors ||= []
357
+ @monitors << Fiber.current
358
+ end
359
+
360
+ def unmonitor
361
+ @monitors.delete(Fiber.current) if @monitors
362
+ end
363
+
364
+ def dead?
365
+ state == :dead
346
366
  end
347
367
  end
348
368
  end