polyphony 0.29 → 0.30

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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +14 -0
  4. data/Gemfile.lock +1 -1
  5. data/TODO.md +15 -10
  6. data/docs/getting-started/tutorial.md +3 -3
  7. data/docs/index.md +2 -3
  8. data/docs/{technical-overview → main-concepts}/concurrency.md +62 -15
  9. data/docs/{technical-overview → main-concepts}/design-principles.md +21 -8
  10. data/docs/{technical-overview → main-concepts}/exception-handling.md +80 -38
  11. data/docs/{technical-overview → main-concepts}/extending.md +4 -3
  12. data/docs/{technical-overview → main-concepts}/fiber-scheduling.md +3 -3
  13. data/docs/{technical-overview.md → main-concepts.md} +2 -2
  14. data/examples/core/xx-at_exit.rb +29 -0
  15. data/examples/core/xx-fork-terminate.rb +27 -0
  16. data/examples/core/xx-pingpong.rb +18 -0
  17. data/examples/core/xx-stop.rb +20 -0
  18. data/ext/gyro/async.c +1 -1
  19. data/ext/gyro/extconf.rb +0 -3
  20. data/ext/gyro/gyro.c +7 -8
  21. data/ext/gyro/gyro.h +2 -0
  22. data/ext/gyro/queue.c +6 -6
  23. data/ext/gyro/selector.c +32 -1
  24. data/ext/gyro/thread.c +55 -9
  25. data/ext/gyro/timer.c +1 -0
  26. data/lib/polyphony/core/exceptions.rb +4 -1
  27. data/lib/polyphony/core/global_api.rb +1 -6
  28. data/lib/polyphony/core/thread_pool.rb +3 -3
  29. data/lib/polyphony/extensions/core.rb +7 -1
  30. data/lib/polyphony/extensions/fiber.rb +159 -72
  31. data/lib/polyphony/extensions/io.rb +2 -4
  32. data/lib/polyphony/extensions/openssl.rb +0 -17
  33. data/lib/polyphony/extensions/thread.rb +46 -22
  34. data/lib/polyphony/version.rb +1 -1
  35. data/lib/polyphony.rb +20 -18
  36. data/test/coverage.rb +1 -1
  37. data/test/helper.rb +7 -3
  38. data/test/test_fiber.rb +285 -72
  39. data/test/test_global_api.rb +7 -52
  40. data/test/test_io.rb +8 -0
  41. data/test/test_signal.rb +1 -0
  42. data/test/test_thread.rb +76 -56
  43. data/test/test_thread_pool.rb +27 -5
  44. data/test/test_throttler.rb +1 -0
  45. metadata +12 -12
  46. data/lib/polyphony/core/supervisor.rb +0 -114
  47. data/lib/polyphony/line_reader.rb +0 -82
  48. data/test/test_gyro.rb +0 -25
  49. data/test/test_supervisor.rb +0 -180
data/ext/gyro/gyro.c CHANGED
@@ -47,11 +47,11 @@ VALUE SYM_fiber_switchpoint;
47
47
  VALUE SYM_fiber_terminate;
48
48
 
49
49
 
50
- // static VALUE Gyro_break_set(VALUE self) {
51
- // break_flag = 1;
52
- // ev_break(EV_DEFAULT, EVBREAK_ALL);
53
- // return Qnil;
54
- // }
50
+ static VALUE Gyro_break_set(VALUE self) {
51
+ // break_flag = 1;
52
+ ev_break(Gyro_Selector_current_thread_ev_loop(), EVBREAK_ALL);
53
+ return Qnil;
54
+ }
55
55
 
56
56
  // static VALUE Gyro_break_get(VALUE self) {
57
57
  // return (break_flag == 0) ? Qfalse : Qtrue;
@@ -82,7 +82,6 @@ static VALUE Gyro_unref(VALUE self) {
82
82
  }
83
83
 
84
84
  static VALUE Gyro_suspend(VALUE self) {
85
- rb_ivar_set(self, ID_runnable_value, Qnil);
86
85
  VALUE ret = Thread_switch_fiber(rb_thread_current());
87
86
 
88
87
  if (RTEST(rb_obj_is_kind_of(ret, rb_eException))) {
@@ -117,7 +116,7 @@ static VALUE Fiber_state(VALUE self) {
117
116
  return SYM_waiting;
118
117
  }
119
118
 
120
- inline void Gyro_schedule_fiber(VALUE fiber, VALUE value) {
119
+ void Gyro_schedule_fiber(VALUE fiber, VALUE value) {
121
120
  VALUE thread = rb_ivar_get(fiber, ID_ivar_thread);
122
121
  if (thread != Qnil) {
123
122
  Thread_schedule_fiber(thread, fiber, value);
@@ -140,7 +139,7 @@ void Init_Gyro() {
140
139
  rb_define_singleton_method(mGyro, "unref", Gyro_unref, 0);
141
140
  rb_define_singleton_method(mGyro, "trace", Gyro_trace, 1);
142
141
 
143
- // rb_define_singleton_method(mGyro, "break!", Gyro_break_set, 0);
142
+ rb_define_singleton_method(mGyro, "break!", Gyro_break_set, 0);
144
143
  // rb_define_singleton_method(mGyro, "break?", Gyro_break_get, 0);
145
144
 
146
145
  rb_define_global_function("snooze", Gyro_snooze, 0);
data/ext/gyro/gyro.h CHANGED
@@ -48,7 +48,9 @@ VALUE Thread_switch_fiber(VALUE thread);
48
48
  VALUE Fiber_await();
49
49
  VALUE Thread_schedule_fiber(VALUE thread, VALUE fiber, VALUE value);
50
50
  VALUE Thread_post_fork(VALUE thread);
51
+ VALUE Gyro_Selector_break_out_of_ev_loop(VALUE self);
51
52
 
53
+ VALUE Gyro_Queue_push(VALUE self, VALUE value);
52
54
 
53
55
  #define OBJ_ID(obj) (NUM2LONG(rb_funcall(obj, rb_intern("object_id"), 0)))
54
56
  #define INSPECT(...) (rb_funcall(rb_cObject, rb_intern("p"), __VA_ARGS__))
data/ext/gyro/queue.c CHANGED
@@ -75,7 +75,9 @@ VALUE Gyro_Queue_shift(VALUE self) {
75
75
 
76
76
  VALUE async = rb_funcall(cGyro_Async, ID_new, 0);
77
77
  rb_ary_push(queue->wait_queue, async);
78
- return Gyro_Async_await(async);
78
+ VALUE ret = Gyro_Async_await(async);
79
+ RB_GC_GUARD(async);
80
+ return ret;
79
81
  }
80
82
 
81
83
  VALUE Gyro_Queue_shift_no_wait(VALUE self) {
@@ -85,7 +87,7 @@ VALUE Gyro_Queue_shift_no_wait(VALUE self) {
85
87
  return rb_ary_shift(queue->queue);
86
88
  }
87
89
 
88
- VALUE Gyro_Queue_shift_all(VALUE self) {
90
+ VALUE Gyro_Queue_shift_each(VALUE self) {
89
91
  struct Gyro_Queue *queue;
90
92
  GetGyro_Queue(self, queue);
91
93
 
@@ -97,9 +99,7 @@ VALUE Gyro_Queue_shift_all(VALUE self) {
97
99
  for (long i = 0; i < len; i++) {
98
100
  rb_yield(RARRAY_AREF(old_queue, i));
99
101
  }
100
- // while (RARRAY_LEN(old_queue) > 0) {
101
- // rb_yield(rb_ary_shift(old_queue));
102
- // }
102
+ RB_GC_GUARD(old_queue);
103
103
  return self;
104
104
  }
105
105
  else {
@@ -135,7 +135,7 @@ void Init_Gyro_Queue() {
135
135
 
136
136
  rb_define_method(cGyro_Queue, "shift_no_wait", Gyro_Queue_shift_no_wait, 0);
137
137
 
138
- rb_define_method(cGyro_Queue, "shift_each", Gyro_Queue_shift_all, 0);
138
+ rb_define_method(cGyro_Queue, "shift_each", Gyro_Queue_shift_each, 0);
139
139
  rb_define_method(cGyro_Queue, "clear", Gyro_Queue_clear, 0);
140
140
  rb_define_method(cGyro_Queue, "empty?", Gyro_Queue_empty_p, 0);
141
141
  }
data/ext/gyro/selector.c CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  struct Gyro_Selector {
4
4
  struct ev_loop *ev_loop;
5
- long run_no_wait_count;
5
+ long run_no_wait_count;
6
+ int ev_loop_running;
7
+ struct ev_async async;
6
8
  };
7
9
 
8
10
  VALUE cGyro_Selector = Qnil;
@@ -13,6 +15,7 @@ static void Gyro_Selector_mark(void *ptr) {
13
15
 
14
16
  static void Gyro_Selector_free(void *ptr) {
15
17
  struct Gyro_Selector *selector = ptr;
18
+ ev_async_stop(selector->ev_loop, &selector->async);
16
19
  if (selector->ev_loop && !ev_is_default_loop(selector->ev_loop)) {
17
20
  // printf("Selector garbage collected before being stopped!\n");
18
21
  ev_loop_destroy(selector->ev_loop);
@@ -67,6 +70,11 @@ long Gyro_Selector_pending_count(VALUE self) {
67
70
  return ev_pending_count(selector->ev_loop);
68
71
  }
69
72
 
73
+ void dummy_async_callback(struct ev_loop *ev_loop, struct ev_async *ev_async, int revents) {
74
+ // This callback does nothing, the selector's async is used solely for waking
75
+ // up the event loop.
76
+ }
77
+
70
78
  static VALUE Gyro_Selector_initialize(VALUE self, VALUE thread) {
71
79
  struct Gyro_Selector *selector;
72
80
  GetGyro_Selector(self, selector);
@@ -75,6 +83,9 @@ static VALUE Gyro_Selector_initialize(VALUE self, VALUE thread) {
75
83
  selector->ev_loop = use_default_loop ? EV_DEFAULT : ev_loop_new(EVFLAG_NOSIGMASK);
76
84
  selector->run_no_wait_count = 0;
77
85
 
86
+ ev_async_init(&selector->async, dummy_async_callback);
87
+ ev_async_start(selector->ev_loop, &selector->async);
88
+
78
89
  ev_run(selector->ev_loop, EVRUN_NOWAIT);
79
90
 
80
91
  return Qnil;
@@ -86,7 +97,9 @@ inline VALUE Gyro_Selector_run(VALUE self, VALUE current_fiber) {
86
97
  if (selector->ev_loop) {
87
98
  selector->run_no_wait_count = 0;
88
99
  FIBER_TRACE(2, SYM_fiber_ev_loop_enter, current_fiber);
100
+ selector->ev_loop_running = 1;
89
101
  ev_run(selector->ev_loop, EVRUN_ONCE);
102
+ selector->ev_loop_running = 0;
90
103
  FIBER_TRACE(2, SYM_fiber_ev_loop_leave, current_fiber);
91
104
  }
92
105
  return Qnil;
@@ -118,6 +131,23 @@ VALUE Gyro_Selector_stop(VALUE self) {
118
131
  return Qnil;
119
132
  }
120
133
 
134
+ VALUE Gyro_Selector_break_out_of_ev_loop(VALUE self) {
135
+ struct Gyro_Selector *selector;
136
+ GetGyro_Selector(self, selector);
137
+
138
+ if (selector->ev_loop_running) {
139
+ // Since the loop will run until at least one event has occurred, we signal
140
+ // the associated async watcher, which will cause the ev loop to return. In
141
+ // contrast to using `ev_break` to break out of the loop, which should be
142
+ // called from the same thread (from within the ev_loop), using an
143
+ // `ev_async` allows us to interrupt the event loop across threads.
144
+ ev_async_send(selector->ev_loop, &selector->async);
145
+ return Qtrue;
146
+ }
147
+
148
+ return Qnil;
149
+ }
150
+
121
151
  inline static VALUE Gyro_Selector_wait_readable(VALUE self, VALUE io) {
122
152
  VALUE watcher = IO_read_watcher(io);
123
153
  return Gyro_IO_await(watcher);
@@ -143,4 +173,5 @@ void Init_Gyro_Selector() {
143
173
  rb_define_method(cGyro_Selector, "wait_readable", Gyro_Selector_wait_readable, 1);
144
174
  rb_define_method(cGyro_Selector, "wait_writable", Gyro_Selector_wait_writable, 1);
145
175
  rb_define_method(cGyro_Selector, "wait_timeout", Gyro_Selector_wait_timeout, 1);
176
+ rb_define_method(cGyro_Selector, "break_out_of_ev_loop", Gyro_Selector_break_out_of_ev_loop, 0);
146
177
  }
data/ext/gyro/thread.c CHANGED
@@ -53,30 +53,33 @@ static VALUE Thread_stop_event_selector(VALUE self) {
53
53
  if (selector != Qnil) {
54
54
  rb_funcall(selector, ID_stop, 0);
55
55
  }
56
+ // Nullify the selector in order to prevent running the
57
+ // selector after the thread is done running.
58
+ rb_ivar_set(self, ID_ivar_event_selector, Qnil);
56
59
 
57
60
  return self;
58
61
  }
59
62
 
60
- inline VALUE Thread_ref(VALUE self) {
63
+ VALUE Thread_ref(VALUE self) {
61
64
  VALUE count = rb_ivar_get(self, ID_fiber_ref_count);
62
65
  int new_count = NUM2INT(count) + 1;
63
66
  rb_ivar_set(self, ID_fiber_ref_count, INT2NUM(new_count));
64
67
  return self;
65
68
  }
66
69
 
67
- inline VALUE Thread_unref(VALUE self) {
70
+ VALUE Thread_unref(VALUE self) {
68
71
  VALUE count = rb_ivar_get(self, ID_fiber_ref_count);
69
72
  int new_count = NUM2INT(count) - 1;
70
73
  rb_ivar_set(self, ID_fiber_ref_count, INT2NUM(new_count));
71
74
  return self;
72
75
  }
73
76
 
74
- inline int Thread_fiber_ref_count(VALUE self) {
77
+ int Thread_fiber_ref_count(VALUE self) {
75
78
  VALUE count = rb_ivar_get(self, ID_fiber_ref_count);
76
79
  return NUM2INT(count);
77
80
  }
78
81
 
79
- inline void Thread_fiber_reset_ref_count(VALUE self) {
82
+ void Thread_fiber_reset_ref_count(VALUE self) {
80
83
  rb_ivar_set(self, ID_fiber_ref_count, INT2NUM(0));
81
84
  }
82
85
 
@@ -97,7 +100,10 @@ static VALUE Thread_fiber_scheduling_stats(VALUE self) {
97
100
  return stats;
98
101
  }
99
102
 
100
- inline VALUE Thread_schedule_fiber(VALUE self, VALUE fiber, VALUE value) {
103
+ VALUE Thread_schedule_fiber(VALUE self, VALUE fiber, VALUE value) {
104
+ if (rb_fiber_alive_p(fiber) != Qtrue) {
105
+ return self;
106
+ }
101
107
  FIBER_TRACE(3, SYM_fiber_schedule, fiber, value);
102
108
  // if fiber is already scheduled, just set the scheduled value, then return
103
109
  rb_ivar_set(fiber, ID_runnable_value, value);
@@ -108,13 +114,24 @@ inline VALUE Thread_schedule_fiber(VALUE self, VALUE fiber, VALUE value) {
108
114
  VALUE queue = rb_ivar_get(self, ID_run_queue);
109
115
  rb_ary_push(queue, fiber);
110
116
  rb_ivar_set(fiber, ID_runnable, Qtrue);
117
+
118
+ if (rb_thread_current() != self) {
119
+ // if the fiber scheduling is done across threads, we need to make sure the
120
+ // target thread is woken up in case it is in the middle of running its
121
+ // event selector. Otherwise it's gonna be stuck waiting for an event to
122
+ // happen, not knowing that it there's already a fiber ready to run in its
123
+ // run queue.
124
+ VALUE selector = rb_ivar_get(self, ID_ivar_event_selector);
125
+ if (selector != Qnil) {
126
+ Gyro_Selector_break_out_of_ev_loop(selector);
127
+ }
128
+ }
111
129
  return self;
112
130
  }
113
131
 
114
132
  VALUE Thread_switch_fiber(VALUE self) {
115
- VALUE current_fiber;
133
+ VALUE current_fiber = rb_fiber_current();
116
134
  if (__tracing_enabled__) {
117
- current_fiber = rb_fiber_current();
118
135
  if (rb_ivar_get(current_fiber, ID_ivar_running) != Qfalse) {
119
136
  rb_funcall(rb_cObject, ID_fiber_trace, 2, SYM_fiber_switchpoint, current_fiber);
120
137
  }
@@ -170,7 +187,7 @@ VALUE Thread_post_fork(VALUE self) {
170
187
  return self;
171
188
  }
172
189
 
173
- inline VALUE Fiber_await() {
190
+ VALUE Fiber_await() {
174
191
  VALUE thread = rb_thread_current();
175
192
  Thread_ref(thread);
176
193
  VALUE ret = Thread_switch_fiber(thread);
@@ -179,10 +196,37 @@ inline VALUE Fiber_await() {
179
196
  return ret;
180
197
  }
181
198
 
182
- inline VALUE Thread_current_event_selector() {
199
+ VALUE Thread_current_event_selector() {
183
200
  return rb_ivar_get(rb_thread_current(), ID_ivar_event_selector);
184
201
  }
185
202
 
203
+ VALUE Thread_fiber_break_out_of_ev_loop(VALUE self, VALUE resume_obj) {
204
+ VALUE selector = rb_ivar_get(self, ID_ivar_event_selector);
205
+ Thread_schedule_fiber(self, rb_fiber_current(), resume_obj);
206
+
207
+ if (Gyro_Selector_break_out_of_ev_loop(selector) == Qnil) {
208
+ // we're not inside the ev_loop, so we just do a switchpoint
209
+ Thread_switch_fiber(self);
210
+ }
211
+
212
+ return self;
213
+ }
214
+
215
+ VALUE Thread_join_perform(VALUE self) {
216
+ if (!RTEST(rb_funcall(self, rb_intern("alive?"), 0))) {
217
+ return self;
218
+ }
219
+
220
+ VALUE async = rb_funcall(cGyro_Async, ID_new, 0);
221
+ VALUE wait_queue = rb_ivar_get(self, rb_intern("@join_wait_queue"));
222
+
223
+ Gyro_Queue_push(wait_queue, async);
224
+
225
+ VALUE ret = Gyro_Async_await(async);
226
+ RB_GC_GUARD(async);
227
+ return ret;
228
+ }
229
+
186
230
  void Init_Thread() {
187
231
  cQueue = rb_const_get(rb_cObject, rb_intern("Queue"));
188
232
 
@@ -196,10 +240,12 @@ void Init_Thread() {
196
240
  rb_define_method(rb_cThread, "stop_event_selector", Thread_stop_event_selector, 0);
197
241
  rb_define_method(rb_cThread, "reset_fiber_scheduling", Thread_reset_fiber_scheduling, 0);
198
242
  rb_define_method(rb_cThread, "fiber_scheduling_stats", Thread_fiber_scheduling_stats, 0);
243
+ rb_define_method(rb_cThread, "break_out_of_ev_loop", Thread_fiber_break_out_of_ev_loop, 1);
199
244
 
200
245
  rb_define_method(rb_cThread, "schedule_fiber", Thread_schedule_fiber, 2);
201
246
  rb_define_method(rb_cThread, "switch_fiber", Thread_switch_fiber, 0);
202
247
 
248
+ rb_define_method(rb_cThread, "join_perform", Thread_join_perform, 0);
203
249
 
204
250
  ID_create_event_selector = rb_intern("create_event_selector");
205
251
  ID_empty = rb_intern("empty?");
data/ext/gyro/timer.c CHANGED
@@ -129,6 +129,7 @@ VALUE Gyro_Timer_await(VALUE self) {
129
129
  timer->fiber = Qnil;
130
130
  timer->selector = Qnil;
131
131
  ev_timer_stop(timer->ev_loop, &timer->ev_timer);
132
+ timer->ev_loop = 0;
132
133
  }
133
134
  RB_GC_GUARD(self);
134
135
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- export :Interrupt, :MoveOn, :Cancel
3
+ export :Interrupt, :MoveOn, :Cancel, :Terminate
4
4
 
5
5
  # Common exception class for interrupting fibers. These exceptions allow
6
6
  # control of fibers. Interrupt exceptions can encapsulate a value and thus
@@ -24,3 +24,6 @@ class MoveOn < Interrupt; end
24
24
  # Cancel is used to interrupt a long-running blocking operation, bubbling the
25
25
  # exception up through cancel scopes and supervisors.
26
26
  class Cancel < Interrupt; end
27
+
28
+ # Terminate is used to interrupt a fiber once its parent fiber has terminated.
29
+ class Terminate < Interrupt; end
@@ -6,7 +6,6 @@ import '../extensions/core'
6
6
  import '../extensions/fiber'
7
7
 
8
8
  Exceptions = import '../core/exceptions'
9
- Supervisor = import '../core/supervisor'
10
9
  Throttler = import '../core/throttler'
11
10
 
12
11
  # Global API methods to be included in ::Object
@@ -30,7 +29,7 @@ module API
30
29
  end
31
30
 
32
31
  def spin(tag = nil, &block)
33
- Fiber.spin(tag, caller, &block)
32
+ Fiber.current.spin(tag, caller, &block)
34
33
  end
35
34
 
36
35
  def spin_loop(&block)
@@ -78,10 +77,6 @@ module API
78
77
  Thread.current.fiber_unref
79
78
  end
80
79
 
81
- def supervise(&block)
82
- Supervisor.new.await(&block)
83
- end
84
-
85
80
  def throttled_loop(rate, count: nil, &block)
86
81
  throttler = Throttler.new(rate)
87
82
  if count
@@ -22,9 +22,9 @@ class ThreadPool
22
22
  def process(&block)
23
23
  setup unless @task_queue
24
24
 
25
- watcher = Gyro::Async.new
26
- @task_queue << [block, watcher]
27
- watcher.await
25
+ async = Gyro::Async.new
26
+ @task_queue << [block, async]
27
+ async.await
28
28
  end
29
29
 
30
30
  def cast(&block)
@@ -14,6 +14,8 @@ class ::Exception
14
14
 
15
15
  alias_method :orig_initialize, :initialize
16
16
 
17
+ EXIT_EXCEPTION_CLASSES = [::Interrupt, ::SystemExit].freeze
18
+
17
19
  def initialize(*args)
18
20
  @__raising_fiber__ = Fiber.current
19
21
  orig_initialize(*args)
@@ -21,11 +23,15 @@ class ::Exception
21
23
 
22
24
  alias_method_once :orig_backtrace, :backtrace
23
25
  def backtrace
24
- unless @first_backtrace_call
26
+ unless @first_backtrace_call || EXIT_EXCEPTION_CLASSES.include?(self.class)
25
27
  @first_backtrace_call = true
26
28
  return orig_backtrace
27
29
  end
28
30
 
31
+ sanitized_backtrace
32
+ end
33
+
34
+ def sanitized_backtrace
29
35
  if @__raising_fiber__
30
36
  backtrace = orig_backtrace || []
31
37
  sanitize(backtrace + @__raising_fiber__.caller)