polyphony 0.28 → 0.29

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +0 -4
  3. data/CHANGELOG.md +12 -0
  4. data/Gemfile.lock +1 -1
  5. data/LICENSE +1 -1
  6. data/README.md +23 -21
  7. data/Rakefile +2 -0
  8. data/TODO.md +0 -3
  9. data/docs/_includes/prevnext.html +17 -0
  10. data/docs/_layouts/default.html +106 -0
  11. data/docs/_sass/custom/custom.scss +21 -0
  12. data/docs/faq.md +13 -10
  13. data/docs/getting-started/installing.md +2 -0
  14. data/docs/getting-started/tutorial.md +5 -3
  15. data/docs/index.md +4 -5
  16. data/docs/technical-overview/concurrency.md +21 -19
  17. data/docs/technical-overview/design-principles.md +12 -20
  18. data/docs/technical-overview/exception-handling.md +70 -1
  19. data/docs/technical-overview/extending.md +1 -0
  20. data/docs/technical-overview/fiber-scheduling.md +109 -88
  21. data/docs/user-guide/all-about-timers.md +126 -0
  22. data/docs/user-guide/web-server.md +2 -2
  23. data/docs/user-guide.md +1 -1
  24. data/examples/core/xx-deferring-an-operation.rb +2 -2
  25. data/examples/core/xx-sleep-forever.rb +9 -0
  26. data/examples/core/xx-snooze-starve.rb +16 -0
  27. data/examples/core/xx-spin_error_backtrace.rb +1 -1
  28. data/examples/core/xx-trace.rb +1 -2
  29. data/examples/core/xx-worker-thread.rb +30 -0
  30. data/examples/io/xx-happy-eyeballs.rb +37 -0
  31. data/ext/gyro/gyro.c +8 -3
  32. data/ext/gyro/gyro.h +7 -1
  33. data/ext/gyro/queue.c +35 -3
  34. data/ext/gyro/selector.c +31 -2
  35. data/ext/gyro/thread.c +18 -16
  36. data/lib/polyphony/core/global_api.rb +0 -1
  37. data/lib/polyphony/core/thread_pool.rb +5 -0
  38. data/lib/polyphony/core/throttler.rb +0 -1
  39. data/lib/polyphony/extensions/fiber.rb +14 -3
  40. data/lib/polyphony/extensions/thread.rb +16 -4
  41. data/lib/polyphony/irb.rb +7 -1
  42. data/lib/polyphony/trace.rb +44 -11
  43. data/lib/polyphony/version.rb +1 -1
  44. data/lib/polyphony.rb +1 -0
  45. data/test/helper.rb +1 -3
  46. data/test/test_async.rb +1 -1
  47. data/test/test_cancel_scope.rb +3 -3
  48. data/test/test_fiber.rb +157 -54
  49. data/test/test_global_api.rb +51 -1
  50. data/test/test_gyro.rb +4 -156
  51. data/test/test_io.rb +1 -1
  52. data/test/test_supervisor.rb +2 -2
  53. data/test/test_thread.rb +72 -1
  54. data/test/test_thread_pool.rb +6 -2
  55. data/test/test_throttler.rb +7 -5
  56. data/test/test_trace.rb +6 -6
  57. metadata +10 -5
  58. data/examples/core/xx-extended_fibers.rb +0 -150
  59. data/examples/core/xx-mt-scheduler.rb +0 -349
data/ext/gyro/selector.c CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  struct Gyro_Selector {
4
4
  struct ev_loop *ev_loop;
5
+ long run_no_wait_count;
5
6
  };
6
7
 
7
8
  VALUE cGyro_Selector = Qnil;
@@ -52,6 +53,13 @@ inline struct ev_loop *Gyro_Selector_current_thread_ev_loop() {
52
53
  return selector->ev_loop;
53
54
  }
54
55
 
56
+ inline ev_tstamp Gyro_Selector_now(VALUE self) {
57
+ struct Gyro_Selector *selector;
58
+ GetGyro_Selector(self, selector);
59
+
60
+ return ev_now(selector->ev_loop);
61
+ }
62
+
55
63
  long Gyro_Selector_pending_count(VALUE self) {
56
64
  struct Gyro_Selector *selector;
57
65
  GetGyro_Selector(self, selector);
@@ -65,19 +73,40 @@ static VALUE Gyro_Selector_initialize(VALUE self, VALUE thread) {
65
73
 
66
74
  int use_default_loop = (rb_thread_current() == rb_thread_main());
67
75
  selector->ev_loop = use_default_loop ? EV_DEFAULT : ev_loop_new(EVFLAG_NOSIGMASK);
76
+ selector->run_no_wait_count = 0;
77
+
78
+ ev_run(selector->ev_loop, EVRUN_NOWAIT);
68
79
 
69
80
  return Qnil;
70
81
  }
71
82
 
72
- inline VALUE Gyro_Selector_run(VALUE self) {
83
+ inline VALUE Gyro_Selector_run(VALUE self, VALUE current_fiber) {
73
84
  struct Gyro_Selector *selector;
74
85
  GetGyro_Selector(self, selector);
75
86
  if (selector->ev_loop) {
87
+ selector->run_no_wait_count = 0;
88
+ FIBER_TRACE(2, SYM_fiber_ev_loop_enter, current_fiber);
76
89
  ev_run(selector->ev_loop, EVRUN_ONCE);
90
+ FIBER_TRACE(2, SYM_fiber_ev_loop_leave, current_fiber);
77
91
  }
78
92
  return Qnil;
79
93
  }
80
94
 
95
+ inline void Gyro_Selector_run_no_wait(VALUE self, VALUE current_fiber, long runnable_count) {
96
+ struct Gyro_Selector *selector;
97
+ GetGyro_Selector(self, selector);
98
+
99
+ selector->run_no_wait_count++;
100
+ if (selector->run_no_wait_count < runnable_count || selector->run_no_wait_count < 10) {
101
+ return;
102
+ }
103
+
104
+ selector->run_no_wait_count = 0;
105
+ FIBER_TRACE(2, SYM_fiber_ev_loop_enter, current_fiber);
106
+ ev_run(selector->ev_loop, EVRUN_NOWAIT);
107
+ FIBER_TRACE(2, SYM_fiber_ev_loop_leave, current_fiber);
108
+ }
109
+
81
110
  VALUE Gyro_Selector_stop(VALUE self) {
82
111
  struct Gyro_Selector *selector;
83
112
  GetGyro_Selector(self, selector);
@@ -109,7 +138,7 @@ void Init_Gyro_Selector() {
109
138
  rb_define_alloc_func(cGyro_Selector, Gyro_Selector_allocate);
110
139
 
111
140
  rb_define_method(cGyro_Selector, "initialize", Gyro_Selector_initialize, 1);
112
- rb_define_method(cGyro_Selector, "run", Gyro_Selector_run, 0);
141
+ rb_define_method(cGyro_Selector, "run", Gyro_Selector_run, 1);
113
142
  rb_define_method(cGyro_Selector, "stop", Gyro_Selector_stop, 0);
114
143
  rb_define_method(cGyro_Selector, "wait_readable", Gyro_Selector_wait_readable, 1);
115
144
  rb_define_method(cGyro_Selector, "wait_writable", Gyro_Selector_wait_writable, 1);
data/ext/gyro/thread.c CHANGED
@@ -98,13 +98,16 @@ static VALUE Thread_fiber_scheduling_stats(VALUE self) {
98
98
  }
99
99
 
100
100
  inline VALUE Thread_schedule_fiber(VALUE self, VALUE fiber, VALUE value) {
101
+ FIBER_TRACE(3, SYM_fiber_schedule, fiber, value);
101
102
  // if fiber is already scheduled, just set the scheduled value, then return
102
103
  rb_ivar_set(fiber, ID_runnable_value, value);
103
- if (rb_ivar_get(fiber, ID_runnable) == Qnil) {
104
- VALUE queue = rb_ivar_get(self, ID_run_queue);
105
- rb_ary_push(queue, fiber);
106
- rb_ivar_set(fiber, ID_runnable, Qtrue);
104
+ if (rb_ivar_get(fiber, ID_runnable) != Qnil) {
105
+ return self;
107
106
  }
107
+
108
+ VALUE queue = rb_ivar_get(self, ID_run_queue);
109
+ rb_ary_push(queue, fiber);
110
+ rb_ivar_set(fiber, ID_runnable, Qtrue);
108
111
  return self;
109
112
  }
110
113
 
@@ -118,6 +121,7 @@ VALUE Thread_switch_fiber(VALUE self) {
118
121
  }
119
122
  VALUE queue = rb_ivar_get(self, ID_run_queue);
120
123
  VALUE selector = rb_ivar_get(self, ID_ivar_event_selector);
124
+
121
125
  VALUE next_fiber;
122
126
 
123
127
  while (1) {
@@ -125,17 +129,18 @@ VALUE Thread_switch_fiber(VALUE self) {
125
129
  // if (break_flag != 0) {
126
130
  // return Qnil;
127
131
  // }
128
- if ((next_fiber != Qnil) || (Thread_fiber_ref_count(self) == 0)) {
132
+ int ref_count = Thread_fiber_ref_count(self);
133
+ if (next_fiber != Qnil) {
134
+ if (ref_count > 0) {
135
+ Gyro_Selector_run_no_wait(selector, current_fiber, RARRAY_LEN(queue));
136
+ }
129
137
  break;
130
138
  }
131
-
132
- if (__tracing_enabled__) {
133
- rb_funcall(rb_cObject, ID_fiber_trace, 2, SYM_fiber_ev_loop_enter, current_fiber);
134
- }
135
- Gyro_Selector_run(selector);
136
- if (__tracing_enabled__) {
137
- rb_funcall(rb_cObject, ID_fiber_trace, 2, SYM_fiber_ev_loop_leave, current_fiber);
139
+ if (ref_count == 0) {
140
+ break;
138
141
  }
142
+
143
+ Gyro_Selector_run(selector, current_fiber);
139
144
  }
140
145
 
141
146
  if (next_fiber == Qnil) {
@@ -144,10 +149,7 @@ VALUE Thread_switch_fiber(VALUE self) {
144
149
 
145
150
  // run next fiber
146
151
  VALUE value = rb_ivar_get(next_fiber, ID_runnable_value);
147
-
148
- if (__tracing_enabled__) {
149
- rb_funcall(rb_cObject, ID_fiber_trace, 3, SYM_fiber_run, next_fiber, value);
150
- }
152
+ FIBER_TRACE(3, SYM_fiber_run, next_fiber, value);
151
153
 
152
154
  rb_ivar_set(next_fiber, ID_runnable, Qnil);
153
155
  RB_GC_GUARD(next_fiber);
@@ -32,7 +32,6 @@ module API
32
32
  def spin(tag = nil, &block)
33
33
  Fiber.spin(tag, caller, &block)
34
34
  end
35
- alias_method :defer, :spin
36
35
 
37
36
  def spin_loop(&block)
38
37
  spin { loop(&block) }
@@ -8,6 +8,11 @@ require 'etc'
8
8
  class ThreadPool
9
9
  attr_reader :size
10
10
 
11
+ def self.process(&block)
12
+ @default_pool ||= new
13
+ @default_pool.process(&block)
14
+ end
15
+
11
16
  def initialize(size = Etc.nprocessors)
12
17
  @size = size
13
18
  @task_queue = ::Queue.new
@@ -14,7 +14,6 @@ class Throttler
14
14
  @timer.await
15
15
  block.call(self)
16
16
  end
17
-
18
17
  alias_method :process, :call
19
18
 
20
19
  def stop
@@ -55,11 +55,12 @@ module FiberMessaging
55
55
  if @receive_waiting && @running
56
56
  schedule value
57
57
  else
58
- @queued_messages ||= []
58
+ @queued_messages ||= Gyro::Queue.new
59
59
  @queued_messages << value
60
60
  end
61
61
  snooze
62
62
  end
63
+ alias_method :send, :<<
63
64
 
64
65
  def receive
65
66
  if !@queued_messages || @queued_messages&.empty?
@@ -110,11 +111,11 @@ class ::Fiber
110
111
  f
111
112
  end
112
113
 
113
- attr_accessor :tag
114
- Fiber.current.tag = :main
114
+ attr_accessor :tag, :thread
115
115
 
116
116
  def setup(tag, block, caller)
117
117
  __fiber_trace__(:fiber_create, self)
118
+ @thread = Thread.current
118
119
  @tag = tag
119
120
  @calling_fiber = Fiber.current
120
121
  @caller = caller
@@ -122,12 +123,20 @@ class ::Fiber
122
123
  schedule
123
124
  end
124
125
 
126
+ def setup_main_fiber
127
+ @tag = :main
128
+ @thread = Thread.current
129
+ @running = true
130
+ end
131
+
125
132
  def run(first_value)
126
133
  Kernel.raise first_value if first_value.is_a?(Exception)
127
134
 
128
135
  start_execution(first_value)
129
136
  rescue ::Interrupt, ::SystemExit => e
130
137
  Thread.current.main_fiber.transfer e.class.new
138
+ rescue ::SignalException => e
139
+ Thread.current.main_fiber.transfer e
131
140
  rescue Exceptions::MoveOn => e
132
141
  finish_execution(e.value)
133
142
  rescue Exception => e
@@ -187,3 +196,5 @@ class ::Fiber
187
196
  end
188
197
  end
189
198
  end
199
+
200
+ Fiber.current.setup_main_fiber
@@ -4,6 +4,10 @@ Exceptions = import '../core/exceptions'
4
4
 
5
5
  # Thread extensions
6
6
  class ::Thread
7
+ def self.join_queue_mutex
8
+ @join_queue_mutex ||= Mutex.new
9
+ end
10
+
7
11
  attr_reader :main_fiber
8
12
 
9
13
  alias_method :orig_initialize, :initialize
@@ -11,10 +15,11 @@ class ::Thread
11
15
  @join_wait_queue = Gyro::Queue.new
12
16
  @block = block
13
17
  orig_initialize do
18
+ Fiber.current.setup_main_fiber
14
19
  setup_fiber_scheduling
15
20
  block.(*args)
16
- signal_waiters
17
21
  ensure
22
+ signal_waiters
18
23
  stop_event_selector
19
24
  end
20
25
  end
@@ -25,10 +30,12 @@ class ::Thread
25
30
 
26
31
  alias_method :orig_join, :join
27
32
  def join(timeout = nil)
28
- return unless alive?
29
-
30
33
  async = Gyro::Async.new
31
- @join_wait_queue << async
34
+ Thread.join_queue_mutex.synchronize do
35
+ return unless alive?
36
+
37
+ @join_wait_queue << async
38
+ end
32
39
 
33
40
  if timeout
34
41
  move_on_after(timeout) { async.await }
@@ -49,4 +56,9 @@ class ::Thread
49
56
  def location
50
57
  @block.source_location.join(':')
51
58
  end
59
+
60
+ def <<(value)
61
+ main_fiber << value
62
+ end
63
+ alias_method :send, :<<
52
64
  end
data/lib/polyphony/irb.rb CHANGED
@@ -7,7 +7,13 @@ require 'polyphony'
7
7
  # readline to return
8
8
  module ::Readline
9
9
  alias_method :orig_readline, :readline
10
+
10
11
  def readline(*args)
11
- Polyphony::ThreadPool.process { orig_readline(*args) }
12
+ async = Gyro::Async.new
13
+ Thread.new do
14
+ result = orig_readline(*args)
15
+ async.signal!(result)
16
+ end
17
+ async.await
12
18
  end
13
19
  end
@@ -6,9 +6,10 @@ require 'polyphony'
6
6
 
7
7
  STOCK_EVENTS = %i[line call return c_call c_return b_call b_return].freeze
8
8
 
9
- def new
9
+ def new(*events)
10
10
  start_stamp = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
11
- ::TracePoint.new(*STOCK_EVENTS) { |tp| yield trace_record(tp, start_stamp) }
11
+ events = STOCK_EVENTS if events.empty?
12
+ ::TracePoint.new(*events) { |tp| yield trace_record(tp, start_stamp) }
12
13
  end
13
14
 
14
15
  def trace_record(trp, start_stamp)
@@ -16,7 +17,7 @@ def trace_record(trp, start_stamp)
16
17
  { stamp: stamp, self: trp.self, binding: trp.binding, event: trp.event,
17
18
  fiber: tp_fiber(trp), lineno: trp.lineno, method_id: trp.method_id,
18
19
  file: trp.path, parameters: tp_params(trp),
19
- return_value: tp_return_value(trp),
20
+ return_value: tp_return_value(trp), schedule_value: tp_schedule_value(trp),
20
21
  exception: tp_raised_exception(trp) }
21
22
  end
22
23
 
@@ -36,6 +37,12 @@ def tp_return_value(trp)
36
37
  RETURN_VALUE_EVENTS.include?(trp.event) ? trp.return_value : nil
37
38
  end
38
39
 
40
+ SCHEDULE_VALUE_EVENTS = %i[fiber_schedule fiber_run].freeze
41
+
42
+ def tp_schedule_value(trp)
43
+ SCHEDULE_VALUE_EVENTS.include?(trp.event) ? trp.value : nil
44
+ end
45
+
39
46
  def tp_raised_exception(trp)
40
47
  trp.event == :raise && trp.raised_exception
41
48
  end
@@ -79,19 +86,45 @@ class FiberTracePoint
79
86
  end
80
87
 
81
88
  class << ::TracePoint
89
+ POLYPHONY_FILE_REGEXP = /^#{::Exception::POLYPHONY_DIR}/.freeze
90
+
82
91
  alias_method :orig_new, :new
83
92
  def new(*args, &block)
84
- polyphony_file_regexp = /^#{::Exception::POLYPHONY_DIR}/
93
+ events_mask, fiber_events_mask = event_masks(args)
85
94
 
86
- orig_new(*args) do |tp|
87
- # next unless !$watched_fiber || Fiber.current == $watched_fiber
95
+ orig_new(*events_mask) do |tp|
96
+ handle_tp_event(tp, fiber_events_mask, &block)
97
+ end
98
+ end
88
99
 
89
- if tp.method_id == :__fiber_trace__
90
- block.(FiberTracePoint.new(tp)) if tp.event == :c_return
91
- else
92
- next if tp.path =~ polyphony_file_regexp
100
+ def handle_tp_event(tpoint, fiber_events_mask)
101
+ # next unless !$watched_fiber || Fiber.current == $watched_fiber
93
102
 
94
- block.(tp)
103
+ if tpoint.method_id == :__fiber_trace__
104
+ return if tpoint.event != :c_return
105
+ return unless fiber_events_mask.include?(tpoint.return_value[0])
106
+
107
+ tpoint = FiberTracePoint.new(tpoint)
108
+ elsif tpoint.path =~ POLYPHONY_FILE_REGEXP
109
+ return
110
+ end
111
+
112
+ yield tpoint
113
+ end
114
+
115
+ ALL_FIBER_EVENTS = %i[
116
+ fiber_create fiber_terminate fiber_schedule fiber_switchpoint fiber_run
117
+ fiber_ev_loop_enter fiber_ev_loop_leave
118
+ ].freeze
119
+
120
+ def event_masks(events)
121
+ events.each_with_object([[], []]) do |e, masks|
122
+ case e
123
+ when /fiber_/
124
+ masks[1] += e == :fiber_all ? ALL_FIBER_EVENTS : [e]
125
+ masks[0] << :c_return unless masks[0].include?(:c_return)
126
+ else
127
+ masks[0] << e
95
128
  end
96
129
  end
97
130
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polyphony
4
- VERSION = '0.28'
4
+ VERSION = '0.29'
5
5
  end
data/lib/polyphony.rb CHANGED
@@ -62,6 +62,7 @@ module Polyphony
62
62
  def fork(&block)
63
63
  pid = Kernel.fork do
64
64
  Gyro.post_fork
65
+ Fiber.current.setup_main_fiber
65
66
  block.()
66
67
  end
67
68
  pid
data/test/helper.rb CHANGED
@@ -20,9 +20,7 @@ Minitest::Reporters.use! [
20
20
 
21
21
  class MiniTest::Test
22
22
  def setup
23
- # for some reason, the first call to sleep in the context of tests returns
24
- # too early
25
- sleep 0
23
+ Fiber.current.setup_main_fiber
26
24
  end
27
25
 
28
26
  def teardown
data/test/test_async.rb CHANGED
@@ -26,7 +26,7 @@ class AsyncTest < MiniTest::Test
26
26
  loop {
27
27
  a.await
28
28
  count += 1
29
- defer { coproc.stop }
29
+ spin { coproc.stop }
30
30
  }
31
31
  }
32
32
  snooze
@@ -6,7 +6,7 @@ class CancelScopeTest < MiniTest::Test
6
6
  def test_that_cancel_scope_can_cancel_provided_block
7
7
  buffer = []
8
8
  Polyphony::CancelScope.new { |scope|
9
- defer { scope.cancel! }
9
+ spin { scope.cancel! }
10
10
  buffer << 1
11
11
  snooze
12
12
  buffer << 2
@@ -77,7 +77,7 @@ class CancelScopeTest < MiniTest::Test
77
77
  scope.call {
78
78
  sleep 0.005
79
79
  scope.reset_timeout
80
- sleep 0.008
80
+ sleep 0.005
81
81
  }
82
82
 
83
83
  assert !scope.cancelled?
@@ -86,7 +86,7 @@ class CancelScopeTest < MiniTest::Test
86
86
  def test_on_cancel
87
87
  buffer = []
88
88
  Polyphony::CancelScope.new { |scope|
89
- defer { scope.cancel! }
89
+ spin { scope.cancel! }
90
90
  scope.on_cancel { buffer << :cancelled }
91
91
  buffer << 1
92
92
  snooze