polyphony 0.49.1 → 0.52.0

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +1 -1
  3. data/CHANGELOG.md +35 -0
  4. data/Gemfile.lock +7 -68
  5. data/TODO.md +6 -0
  6. data/examples/core/forking.rb +2 -2
  7. data/examples/core/nested.rb +21 -0
  8. data/examples/core/suspend.rb +13 -0
  9. data/examples/core/terminate_main_fiber.rb +12 -0
  10. data/examples/performance/thread-vs-fiber/polyphony_server.rb +2 -4
  11. data/ext/polyphony/backend_common.h +58 -8
  12. data/ext/polyphony/backend_io_uring.c +158 -35
  13. data/ext/polyphony/backend_libev.c +192 -25
  14. data/ext/polyphony/event.c +1 -1
  15. data/ext/polyphony/extconf.rb +7 -2
  16. data/ext/polyphony/fiber.c +2 -1
  17. data/ext/polyphony/polyphony.c +94 -0
  18. data/ext/polyphony/polyphony.h +29 -2
  19. data/ext/polyphony/queue.c +1 -1
  20. data/ext/polyphony/runqueue.c +7 -1
  21. data/ext/polyphony/runqueue_ring_buffer.c +9 -0
  22. data/ext/polyphony/runqueue_ring_buffer.h +1 -0
  23. data/ext/polyphony/thread.c +14 -0
  24. data/lib/polyphony/adapters/irb.rb +1 -1
  25. data/lib/polyphony/adapters/mysql2.rb +1 -1
  26. data/lib/polyphony/adapters/postgres.rb +5 -5
  27. data/lib/polyphony/adapters/process.rb +4 -4
  28. data/lib/polyphony/core/exceptions.rb +1 -0
  29. data/lib/polyphony/core/global_api.rb +6 -6
  30. data/lib/polyphony/core/sync.rb +1 -1
  31. data/lib/polyphony/core/throttler.rb +1 -1
  32. data/lib/polyphony/core/timer.rb +63 -20
  33. data/lib/polyphony/extensions/core.rb +5 -5
  34. data/lib/polyphony/extensions/fiber.rb +11 -8
  35. data/lib/polyphony/extensions/io.rb +13 -22
  36. data/lib/polyphony/extensions/openssl.rb +6 -6
  37. data/lib/polyphony/extensions/socket.rb +41 -41
  38. data/lib/polyphony/extensions/thread.rb +1 -2
  39. data/lib/polyphony/version.rb +1 -1
  40. data/polyphony.gemspec +6 -5
  41. data/test/helper.rb +2 -3
  42. data/test/stress.rb +2 -0
  43. data/test/test_backend.rb +58 -5
  44. data/test/test_fiber.rb +31 -0
  45. data/test/test_global_api.rb +2 -2
  46. data/test/test_io.rb +84 -1
  47. data/test/test_kernel.rb +1 -1
  48. data/test/test_signal.rb +2 -3
  49. data/test/test_socket.rb +61 -0
  50. data/test/test_timer.rb +41 -8
  51. metadata +21 -60
@@ -23,9 +23,9 @@
23
23
  #define COND_TRACE(...) if (__tracing_enabled__) { TRACE(__VA_ARGS__); }
24
24
 
25
25
  // exceptions
26
- #define TEST_EXCEPTION(ret) (RTEST(rb_obj_is_kind_of(ret, rb_eException)))
26
+ #define TEST_EXCEPTION(ret) (rb_obj_is_kind_of(ret, rb_eException) == Qtrue)
27
27
  #define RAISE_EXCEPTION(e) rb_funcall(e, ID_invoke, 0);
28
- #define RAISE_IF_EXCEPTION(ret) if (RTEST(rb_obj_is_kind_of(ret, rb_eException))) { RAISE_EXCEPTION(ret); }
28
+ #define RAISE_IF_EXCEPTION(ret) if (rb_obj_is_kind_of(ret, rb_eException) == Qtrue) { RAISE_EXCEPTION(ret); }
29
29
  #define RAISE_IF_NOT_NIL(ret) if (ret != Qnil) { RAISE_EXCEPTION(ret); }
30
30
 
31
31
  // Fiber#transfer
@@ -84,10 +84,37 @@ void Runqueue_push(VALUE self, VALUE fiber, VALUE value, int reschedule);
84
84
  void Runqueue_unshift(VALUE self, VALUE fiber, VALUE value, int reschedule);
85
85
  runqueue_entry Runqueue_shift(VALUE self);
86
86
  void Runqueue_delete(VALUE self, VALUE fiber);
87
+ int Runqueue_index_of(VALUE self, VALUE fiber);
87
88
  void Runqueue_clear(VALUE self);
88
89
  long Runqueue_len(VALUE self);
89
90
  int Runqueue_empty_p(VALUE self);
90
91
 
92
+ #ifdef POLYPHONY_BACKEND_LIBEV
93
+ #define Backend_recv_loop Backend_read_loop
94
+ #define Backend_recv_feed_loop Backend_feed_loop
95
+ #endif
96
+
97
+ // Backend public interface
98
+
99
+ VALUE Backend_accept(VALUE self, VALUE server_socket, VALUE socket_class);
100
+ VALUE Backend_accept_loop(VALUE self, VALUE server_socket, VALUE socket_class);
101
+ VALUE Backend_connect(VALUE self, VALUE io, VALUE addr, VALUE port);
102
+ VALUE Backend_feed_loop(VALUE self, VALUE io, VALUE receiver, VALUE method);
103
+ VALUE Backend_read(VALUE self, VALUE io, VALUE str, VALUE length, VALUE to_eof);
104
+ VALUE Backend_read_loop(VALUE self, VALUE io);
105
+ VALUE Backend_recv(VALUE self, VALUE io, VALUE str, VALUE length);
106
+ VALUE Backend_recv_loop(VALUE self, VALUE io);
107
+ VALUE Backend_recv_feed_loop(VALUE self, VALUE io, VALUE receiver, VALUE method);
108
+ VALUE Backend_send(VALUE self, VALUE io, VALUE msg, VALUE flags);
109
+ VALUE Backend_sendv(VALUE self, VALUE io, VALUE ary, VALUE flags);
110
+ VALUE Backend_sleep(VALUE self, VALUE duration);
111
+ VALUE Backend_timeout(int argc,VALUE *argv, VALUE self);
112
+ VALUE Backend_timer_loop(VALUE self, VALUE interval);
113
+ VALUE Backend_wait_event(VALUE self, VALUE raise);
114
+ VALUE Backend_wait_io(VALUE self, VALUE io, VALUE write);
115
+ VALUE Backend_waitpid(VALUE self, VALUE pid);
116
+ VALUE Backend_write_m(int argc, VALUE *argv, VALUE self);
117
+
91
118
  unsigned int Backend_pending_count(VALUE self);
92
119
  VALUE Backend_poll(VALUE self, VALUE nowait, VALUE current_fiber, VALUE runqueue);
93
120
  VALUE Backend_wait_event(VALUE self, VALUE raise_on_exception);
@@ -247,7 +247,7 @@ VALUE Queue_size_m(VALUE self) {
247
247
  }
248
248
 
249
249
  void Init_Queue() {
250
- cQueue = rb_define_class_under(mPolyphony, "Queue", rb_cData);
250
+ cQueue = rb_define_class_under(mPolyphony, "Queue", rb_cObject);
251
251
  rb_define_alloc_func(cQueue, Queue_allocate);
252
252
 
253
253
  rb_define_method(cQueue, "initialize", Queue_initialize, -1);
@@ -74,6 +74,12 @@ void Runqueue_delete(VALUE self, VALUE fiber) {
74
74
  runqueue_ring_buffer_delete(&runqueue->entries, fiber);
75
75
  }
76
76
 
77
+ int Runqueue_index_of(VALUE self, VALUE fiber) {
78
+ Runqueue_t *runqueue;
79
+ GetRunqueue(self, runqueue);
80
+ return runqueue_ring_buffer_index_of(&runqueue->entries, fiber);
81
+ }
82
+
77
83
  void Runqueue_clear(VALUE self) {
78
84
  Runqueue_t *runqueue;
79
85
  GetRunqueue(self, runqueue);
@@ -95,7 +101,7 @@ int Runqueue_empty_p(VALUE self) {
95
101
  }
96
102
 
97
103
  void Init_Runqueue() {
98
- cRunqueue = rb_define_class_under(mPolyphony, "Runqueue", rb_cData);
104
+ cRunqueue = rb_define_class_under(mPolyphony, "Runqueue", rb_cObject);
99
105
  rb_define_alloc_func(cRunqueue, Runqueue_allocate);
100
106
 
101
107
  rb_define_method(cRunqueue, "initialize", Runqueue_initialize, 0);
@@ -80,6 +80,15 @@ void runqueue_ring_buffer_delete(runqueue_ring_buffer *buffer, VALUE fiber) {
80
80
  }
81
81
  }
82
82
 
83
+ int runqueue_ring_buffer_index_of(runqueue_ring_buffer *buffer, VALUE fiber) {
84
+ for (unsigned int i = 0; i < buffer->count; i++) {
85
+ unsigned int idx = (buffer->head + i) % buffer->size;
86
+ if (buffer->entries[idx].fiber == fiber)
87
+ return i;
88
+ }
89
+ return -1;
90
+ }
91
+
83
92
  void runqueue_ring_buffer_clear(runqueue_ring_buffer *buffer) {
84
93
  buffer->count = buffer->head = buffer->tail = 0;
85
94
  }
@@ -27,5 +27,6 @@ void runqueue_ring_buffer_unshift(runqueue_ring_buffer *buffer, VALUE fiber, VAL
27
27
  void runqueue_ring_buffer_push(runqueue_ring_buffer *buffer, VALUE fiber, VALUE value);
28
28
 
29
29
  void runqueue_ring_buffer_delete(runqueue_ring_buffer *buffer, VALUE fiber);
30
+ int runqueue_ring_buffer_index_of(runqueue_ring_buffer *buffer, VALUE fiber);
30
31
 
31
32
  #endif /* RUNQUEUE_RING_BUFFER_H */
@@ -59,6 +59,18 @@ void schedule_fiber(VALUE self, VALUE fiber, VALUE value, int prioritize) {
59
59
  }
60
60
  }
61
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));
66
+ }
67
+
68
+ VALUE Thread_fiber_unschedule(VALUE self, VALUE fiber) {
69
+ VALUE runqueue = rb_ivar_get(self, ID_ivar_runqueue);
70
+ Runqueue_delete(runqueue, fiber);
71
+ return self;
72
+ }
73
+
62
74
  VALUE Thread_schedule_fiber(VALUE self, VALUE fiber, VALUE value) {
63
75
  schedule_fiber(self, fiber, value, 0);
64
76
  return self;
@@ -135,6 +147,8 @@ void Init_Thread() {
135
147
  rb_define_method(rb_cThread, "schedule_fiber_with_priority",
136
148
  Thread_schedule_fiber_with_priority, 2);
137
149
  rb_define_method(rb_cThread, "switch_fiber", Thread_switch_fiber, 0);
150
+ rb_define_method(rb_cThread, "fiber_scheduling_index", Thread_fiber_scheduling_index, 1);
151
+ rb_define_method(rb_cThread, "fiber_unschedule", Thread_fiber_unschedule, 1);
138
152
 
139
153
  rb_define_method(rb_cThread, "debug!", Thread_debug, 0);
140
154
 
@@ -16,7 +16,7 @@ if Object.constants.include?(:Reline)
16
16
  fiber.cancel
17
17
  end
18
18
  read_ios.each do |io|
19
- Thread.current.backend.wait_io(io, false)
19
+ Polyphony.backend_wait_io(io, false)
20
20
  return [io]
21
21
  end
22
22
  rescue Polyphony::Cancel
@@ -13,7 +13,7 @@ Mysql2::Client.prepend(Module.new do
13
13
 
14
14
  def query(sql, **options)
15
15
  super
16
- Thread.current.backend.wait_io(@io, false)
16
+ Polyphony.backend_wait_io(@io, false)
17
17
  async_result
18
18
  end
19
19
  end)
@@ -15,8 +15,8 @@ module ::PG
15
15
  res = conn.connect_poll
16
16
  case res
17
17
  when PGRES_POLLING_FAILED then raise Error, conn.error_message
18
- when PGRES_POLLING_READING then Thread.current.backend.wait_io(socket_io, false)
19
- when PGRES_POLLING_WRITING then Thread.current.backend.wait_io(socket_io, true)
18
+ when PGRES_POLLING_READING then Polyphony.backend_wait_io(socket_io, false)
19
+ when PGRES_POLLING_WRITING then Polyphony.backend_wait_io(socket_io, true)
20
20
  when PGRES_POLLING_OK then return conn.setnonblocking(true)
21
21
  end
22
22
  end
@@ -42,7 +42,7 @@ class ::PG::Connection
42
42
 
43
43
  def get_result(&block)
44
44
  while is_busy
45
- Thread.current.backend.wait_io(socket_io, false)
45
+ Polyphony.backend_wait_io(socket_io, false)
46
46
  consume_input
47
47
  end
48
48
  orig_get_result(&block)
@@ -59,7 +59,7 @@ class ::PG::Connection
59
59
 
60
60
  def block(_timeout = 0)
61
61
  while is_busy
62
- Thread.current.backend.wait_io(socket_io, false)
62
+ Polyphony.backend_wait_io(socket_io, false)
63
63
  consume_input
64
64
  end
65
65
  end
@@ -97,7 +97,7 @@ class ::PG::Connection
97
97
  return move_on_after(timeout) { wait_for_notify(&block) } if timeout
98
98
 
99
99
  while true
100
- Thread.current.backend.wait_io(socket_io, false)
100
+ Polyphony.backend_wait_io(socket_io, false)
101
101
  consume_input
102
102
  notice = notifies
103
103
  next unless notice
@@ -7,7 +7,7 @@ module Polyphony
7
7
  def watch(cmd = nil, &block)
8
8
  terminated = nil
9
9
  pid = cmd ? Kernel.spawn(cmd) : Polyphony.fork(&block)
10
- Thread.current.backend.waitpid(pid)
10
+ Polyphony.backend_waitpid(pid)
11
11
  terminated = true
12
12
  ensure
13
13
  kill_process(pid) unless terminated || pid.nil?
@@ -23,9 +23,9 @@ module Polyphony
23
23
 
24
24
  def kill_and_await(sig, pid)
25
25
  ::Process.kill(sig, pid)
26
- Thread.current.backend.waitpid(pid)
27
- rescue Errno::ERSCH
28
- # ignore
26
+ Polyphony.backend_waitpid(pid)
27
+ rescue Errno::ESRCH
28
+ # process doesn't exist
29
29
  end
30
30
  end
31
31
  end
@@ -13,6 +13,7 @@ module Polyphony
13
13
  def initialize(value = nil)
14
14
  @caller_backtrace = caller
15
15
  @value = value
16
+ super
16
17
  end
17
18
  end
18
19
 
@@ -21,7 +21,7 @@ module Polyphony
21
21
  elsif block.arity > 0
22
22
  cancel_after_with_block(Fiber.current, interval, with_exception, &block)
23
23
  else
24
- Thread.current.backend.timeout(interval, with_exception, &block)
24
+ Polyphony.backend_timeout(interval, with_exception, &block)
25
25
  end
26
26
  end
27
27
 
@@ -29,7 +29,7 @@ module Polyphony
29
29
  spin do
30
30
  sleep interval
31
31
  exception = cancel_exception(with_exception)
32
- exception.__raising_fiber__ = nil
32
+ exception.raising_fiber = nil
33
33
  fiber.schedule exception
34
34
  end
35
35
  end
@@ -82,7 +82,7 @@ module Polyphony
82
82
  end
83
83
 
84
84
  def every(interval, &block)
85
- Thread.current.backend.timer_loop(interval, &block)
85
+ Polyphony.backend_timer_loop(interval, &block)
86
86
  end
87
87
 
88
88
  def move_on_after(interval, with_value: nil, &block)
@@ -91,7 +91,7 @@ module Polyphony
91
91
  elsif block.arity > 0
92
92
  move_on_after_with_block(Fiber.current, interval, with_value, &block)
93
93
  else
94
- Thread.current.backend.timeout(interval, nil, with_value, &block)
94
+ Polyphony.backend_timeout(interval, nil, with_value, &block)
95
95
  end
96
96
  end
97
97
 
@@ -129,11 +129,11 @@ module Polyphony
129
129
  def sleep(duration = nil)
130
130
  return sleep_forever unless duration
131
131
 
132
- Thread.current.backend.sleep duration
132
+ Polyphony.backend_sleep duration
133
133
  end
134
134
 
135
135
  def sleep_forever
136
- Thread.current.backend.wait_event(true)
136
+ Polyphony.backend_wait_event(true)
137
137
  end
138
138
 
139
139
  def throttled_loop(rate = nil, **opts, &block)
@@ -46,7 +46,7 @@ module Polyphony
46
46
  def wait(mutex, _timeout = nil)
47
47
  mutex.conditional_release
48
48
  @queue << Fiber.current
49
- Thread.current.backend.wait_event(true)
49
+ Polyphony.backend_wait_event(true)
50
50
  mutex.conditional_reacquire
51
51
  end
52
52
 
@@ -12,7 +12,7 @@ module Polyphony
12
12
  def call
13
13
  now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
14
14
  delta = @next_time - now
15
- Thread.current.backend.sleep(delta) if delta > 0
15
+ Polyphony.backend_sleep(delta) if delta > 0
16
16
  yield self
17
17
 
18
18
  while true
@@ -11,12 +11,45 @@ module Polyphony
11
11
  def stop
12
12
  @fiber.stop
13
13
  end
14
+
15
+ def sleep(duration)
16
+ fiber = Fiber.current
17
+ @timeouts[fiber] = {
18
+ interval: duration,
19
+ target_stamp: now + duration
20
+ }
21
+ Polyphony.backend_wait_event(true)
22
+ ensure
23
+ @timeouts.delete(fiber)
24
+ end
25
+
26
+ def after(interval, &block)
27
+ spin do
28
+ self.sleep interval
29
+ block.()
30
+ end
31
+ end
32
+
33
+ def every(interval)
34
+ fiber = Fiber.current
35
+ @timeouts[fiber] = {
36
+ interval: interval,
37
+ target_stamp: now + interval,
38
+ recurring: true
39
+ }
40
+ while true
41
+ Polyphony.backend_wait_event(true)
42
+ yield
43
+ end
44
+ ensure
45
+ @timeouts.delete(fiber)
46
+ end
14
47
 
15
- def cancel_after(duration, with_exception: Polyphony::Cancel)
48
+ def cancel_after(interval, with_exception: Polyphony::Cancel)
16
49
  fiber = Fiber.current
17
50
  @timeouts[fiber] = {
18
- duration: duration,
19
- target_stamp: Time.now + duration,
51
+ interval: interval,
52
+ target_stamp: now + interval,
20
53
  exception: with_exception
21
54
  }
22
55
  yield
@@ -24,12 +57,12 @@ module Polyphony
24
57
  @timeouts.delete(fiber)
25
58
  end
26
59
 
27
- def move_on_after(duration, with_value: nil)
60
+ def move_on_after(interval, with_value: nil)
28
61
  fiber = Fiber.current
29
62
  @timeouts[fiber] = {
30
- duration: duration,
31
- target_stamp: Time.now + duration,
32
- value: with_value
63
+ interval: interval,
64
+ target_stamp: now + interval,
65
+ exception: [Polyphony::MoveOn, with_value]
33
66
  }
34
67
  yield
35
68
  rescue Polyphony::MoveOn => e
@@ -37,36 +70,46 @@ module Polyphony
37
70
  ensure
38
71
  @timeouts.delete(fiber)
39
72
  end
40
-
73
+
41
74
  def reset
42
75
  record = @timeouts[Fiber.current]
43
76
  return unless record
44
77
 
45
- record[:target_stamp] = Time.now + record[:duration]
78
+ record[:target_stamp] = now + record[:interval]
46
79
  end
47
-
80
+
48
81
  private
49
82
 
83
+ def now
84
+ ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
85
+ end
86
+
50
87
  def timeout_exception(record)
51
88
  case (exception = record[:exception])
52
- when Class then exception.new
53
- when Array then exception[0].new(exception[1])
54
- when nil then Polyphony::MoveOn.new(record[:value])
55
- else RuntimeError.new(exception)
89
+ when Array
90
+ exception[0].new(exception[1])
91
+ when Class
92
+ exception.new
93
+ else
94
+ RuntimeError.new(exception)
56
95
  end
57
96
  end
58
97
 
59
98
  def update
60
- now = Time.now
61
- # elapsed = nil
99
+ return if @timeouts.empty?
100
+
62
101
  @timeouts.each do |fiber, record|
63
102
  next if record[:target_stamp] > now
64
103
 
65
- exception = timeout_exception(record)
66
- # (elapsed ||= []) << fiber
67
- fiber.schedule exception
104
+ value = record[:exception] ? timeout_exception(record) : record[:value]
105
+ fiber.schedule value
106
+
107
+ next unless record[:recurring]
108
+
109
+ while record[:target_stamp] <= now
110
+ record[:target_stamp] += record[:interval]
111
+ end
68
112
  end
69
- # elapsed&.each { |f| @timeouts.delete(f) }
70
113
  end
71
114
  end
72
115
  end
@@ -12,11 +12,11 @@ class ::Exception
12
12
  attr_accessor :__disable_sanitized_backtrace__
13
13
  end
14
14
 
15
- attr_accessor :source_fiber, :__raising_fiber__
15
+ attr_accessor :source_fiber, :raising_fiber
16
16
 
17
17
  alias_method :orig_initialize, :initialize
18
18
  def initialize(*args)
19
- @__raising_fiber__ = Fiber.current
19
+ @raising_fiber = Fiber.current
20
20
  orig_initialize(*args)
21
21
  end
22
22
 
@@ -31,10 +31,10 @@ class ::Exception
31
31
  end
32
32
 
33
33
  def sanitized_backtrace
34
- return sanitize(orig_backtrace) unless @__raising_fiber__
34
+ return sanitize(orig_backtrace) unless @raising_fiber
35
35
 
36
36
  backtrace = orig_backtrace || []
37
- sanitize(backtrace + @__raising_fiber__.caller)
37
+ sanitize(backtrace + @raising_fiber.caller)
38
38
  end
39
39
 
40
40
  POLYPHONY_DIR = File.expand_path(File.join(__dir__, '..'))
@@ -55,7 +55,7 @@ module ::Process
55
55
  class << self
56
56
  alias_method :orig_detach, :detach
57
57
  def detach(pid)
58
- fiber = spin { Thread.current.backend.waitpid(pid) }
58
+ fiber = spin { Polyphony.backend_waitpid(pid) }
59
59
  fiber.define_singleton_method(:pid) { pid }
60
60
  fiber
61
61
  end