polyphony 0.43 → 0.43.5

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +1 -1
  3. data/CHANGELOG.md +29 -0
  4. data/Gemfile.lock +2 -2
  5. data/README.md +0 -1
  6. data/docs/_sass/custom/custom.scss +10 -0
  7. data/docs/favicon.ico +0 -0
  8. data/docs/getting-started/overview.md +2 -2
  9. data/docs/index.md +6 -3
  10. data/docs/main-concepts/design-principles.md +23 -34
  11. data/docs/main-concepts/fiber-scheduling.md +1 -1
  12. data/docs/polyphony-logo.png +0 -0
  13. data/examples/adapters/concurrent-ruby.rb +9 -0
  14. data/examples/adapters/redis_blpop.rb +12 -0
  15. data/examples/core/xx-daemon.rb +14 -0
  16. data/examples/performance/mem-usage.rb +34 -28
  17. data/examples/performance/messaging.rb +29 -0
  18. data/examples/performance/multi_snooze.rb +11 -9
  19. data/ext/polyphony/libev_agent.c +181 -151
  20. data/ext/polyphony/libev_queue.c +129 -57
  21. data/ext/polyphony/polyphony.c +0 -6
  22. data/ext/polyphony/polyphony.h +12 -5
  23. data/ext/polyphony/polyphony_ext.c +0 -2
  24. data/ext/polyphony/ring_buffer.c +120 -0
  25. data/ext/polyphony/ring_buffer.h +28 -0
  26. data/ext/polyphony/thread.c +13 -13
  27. data/lib/polyphony.rb +26 -10
  28. data/lib/polyphony/adapters/redis.rb +3 -2
  29. data/lib/polyphony/core/global_api.rb +5 -3
  30. data/lib/polyphony/core/resource_pool.rb +19 -9
  31. data/lib/polyphony/core/thread_pool.rb +1 -1
  32. data/lib/polyphony/extensions/core.rb +40 -0
  33. data/lib/polyphony/extensions/fiber.rb +8 -13
  34. data/lib/polyphony/extensions/io.rb +17 -16
  35. data/lib/polyphony/extensions/socket.rb +12 -2
  36. data/lib/polyphony/version.rb +1 -1
  37. data/test/q.rb +24 -0
  38. data/test/test_agent.rb +13 -7
  39. data/test/test_fiber.rb +3 -3
  40. data/test/test_global_api.rb +50 -17
  41. data/test/test_io.rb +10 -2
  42. data/test/test_queue.rb +26 -1
  43. data/test/test_resource_pool.rb +12 -0
  44. data/test/test_throttler.rb +6 -5
  45. metadata +11 -3
  46. data/ext/polyphony/socket.c +0 -213
@@ -0,0 +1,28 @@
1
+ #ifndef RING_BUFFER_H
2
+ #define RING_BUFFER_H
3
+
4
+ #include "ruby.h"
5
+
6
+ typedef struct ring_buffer {
7
+ VALUE *entries;
8
+ unsigned int size;
9
+ unsigned int count;
10
+ unsigned int head;
11
+ unsigned int tail;
12
+ } ring_buffer;
13
+
14
+ void ring_buffer_init(ring_buffer *buffer);
15
+ void ring_buffer_free(ring_buffer *buffer);
16
+ void ring_buffer_mark(ring_buffer *buffer);
17
+ int ring_buffer_empty_p(ring_buffer *buffer);
18
+ void ring_buffer_clear(ring_buffer *buffer);
19
+
20
+ VALUE ring_buffer_shift(ring_buffer *buffer);
21
+ void ring_buffer_unshift(ring_buffer *buffer, VALUE value);
22
+ void ring_buffer_push(ring_buffer *buffer, VALUE value);
23
+
24
+ void ring_buffer_shift_each(ring_buffer *buffer);
25
+ VALUE ring_buffer_shift_all(ring_buffer *buffer);
26
+ void ring_buffer_delete(ring_buffer *buffer, VALUE value);
27
+
28
+ #endif /* RING_BUFFER_H */
@@ -1,23 +1,19 @@
1
1
  #include "polyphony.h"
2
2
 
3
3
  ID ID_deactivate_all_watchers_post_fork;
4
- ID ID_empty;
5
4
  ID ID_ivar_agent;
6
5
  ID ID_ivar_join_wait_queue;
7
6
  ID ID_ivar_main_fiber;
8
7
  ID ID_ivar_result;
9
8
  ID ID_ivar_terminated;
10
- ID ID_pop;
11
- ID ID_push;
12
9
  ID ID_run_queue;
13
10
  ID ID_runnable_next;
14
11
  ID ID_stop;
15
12
 
16
13
  static VALUE Thread_setup_fiber_scheduling(VALUE self) {
17
- VALUE queue;
14
+ VALUE queue = rb_funcall(cLibevQueue, ID_new, 0);
18
15
 
19
16
  rb_ivar_set(self, ID_ivar_main_fiber, rb_fiber_current());
20
- queue = rb_ary_new();
21
17
  rb_ivar_set(self, ID_run_queue, queue);
22
18
 
23
19
  return self;
@@ -64,7 +60,7 @@ VALUE Thread_schedule_fiber(VALUE self, VALUE fiber, VALUE value) {
64
60
  }
65
61
 
66
62
  queue = rb_ivar_get(self, ID_run_queue);
67
- rb_ary_push(queue, fiber);
63
+ LibevQueue_push(queue, fiber);
68
64
  rb_ivar_set(fiber, ID_runnable, Qtrue);
69
65
 
70
66
  if (rb_thread_current() != self) {
@@ -91,13 +87,13 @@ VALUE Thread_schedule_fiber_with_priority(VALUE self, VALUE fiber, VALUE value)
91
87
 
92
88
  // if fiber is already scheduled, remove it from the run queue
93
89
  if (rb_ivar_get(fiber, ID_runnable) != Qnil) {
94
- rb_ary_delete(queue, fiber);
90
+ LibevQueue_delete(queue, fiber);
95
91
  } else {
96
92
  rb_ivar_set(fiber, ID_runnable, Qtrue);
97
93
  }
98
94
 
99
95
  // the fiber is given priority by putting it at the front of the run queue
100
- rb_ary_unshift(queue, fiber);
96
+ LibevQueue_unshift(queue, fiber);
101
97
 
102
98
  if (rb_thread_current() != self) {
103
99
  // if the fiber scheduling is done across threads, we need to make sure the
@@ -127,7 +123,7 @@ VALUE Thread_switch_fiber(VALUE self) {
127
123
 
128
124
  ref_count = LibevAgent_ref_count(agent);
129
125
  while (1) {
130
- next_fiber = rb_ary_shift(queue);
126
+ next_fiber = LibevQueue_shift_no_wait(queue);
131
127
  if (next_fiber != Qnil) {
132
128
  if (ref_count > 0) {
133
129
  // this mechanism prevents event starvation in case the run queue never
@@ -154,9 +150,15 @@ VALUE Thread_switch_fiber(VALUE self) {
154
150
  value : rb_funcall(next_fiber, ID_transfer, 1, value);
155
151
  }
156
152
 
153
+ VALUE Thread_run_queue_trace(VALUE self) {
154
+ VALUE queue = rb_ivar_get(self, ID_run_queue);
155
+ LibevQueue_trace(queue);
156
+ return self;
157
+ }
158
+
157
159
  VALUE Thread_reset_fiber_scheduling(VALUE self) {
158
160
  VALUE queue = rb_ivar_get(self, ID_run_queue);
159
- rb_ary_clear(queue);
161
+ LibevQueue_clear(queue);
160
162
  Thread_fiber_reset_ref_count(self);
161
163
  return self;
162
164
  }
@@ -185,16 +187,14 @@ void Init_Thread() {
185
187
  rb_define_method(rb_cThread, "schedule_fiber_with_priority",
186
188
  Thread_schedule_fiber_with_priority, 2);
187
189
  rb_define_method(rb_cThread, "switch_fiber", Thread_switch_fiber, 0);
190
+ rb_define_method(rb_cThread, "run_queue_trace", Thread_run_queue_trace, 0);
188
191
 
189
192
  ID_deactivate_all_watchers_post_fork = rb_intern("deactivate_all_watchers_post_fork");
190
- ID_empty = rb_intern("empty?");
191
193
  ID_ivar_agent = rb_intern("@agent");
192
194
  ID_ivar_join_wait_queue = rb_intern("@join_wait_queue");
193
195
  ID_ivar_main_fiber = rb_intern("@main_fiber");
194
196
  ID_ivar_result = rb_intern("@result");
195
197
  ID_ivar_terminated = rb_intern("@terminated");
196
- ID_pop = rb_intern("pop");
197
- ID_push = rb_intern("push");
198
198
  ID_run_queue = rb_intern("run_queue");
199
199
  ID_runnable_next = rb_intern("runnable_next");
200
200
  ID_stop = rb_intern("stop");
@@ -6,6 +6,12 @@ require_relative './polyphony_ext'
6
6
  module Polyphony
7
7
  # Map Queue to Libev queue implementation
8
8
  Queue = LibevQueue
9
+
10
+ # replace core Queue class with our own
11
+ verbose = $VERBOSE
12
+ $VERBOSE = nil
13
+ Object.const_set(:Queue, Polyphony::Queue)
14
+ $VERBOSE = verbose
9
15
  end
10
16
 
11
17
  require_relative './polyphony/extensions/core'
@@ -94,17 +100,9 @@ module Polyphony
94
100
  Polyphony::Process.watch(cmd, &block)
95
101
  end
96
102
 
97
- def emit_signal_exception(exception, fiber = Thread.main.main_fiber)
98
- Thread.current.break_out_of_ev_loop(fiber, exception)
99
- end
100
-
101
- def install_terminating_signal_handler(signal, exception_class)
102
- trap(signal) { emit_signal_exception(exception_class.new) }
103
- end
104
-
105
103
  def install_terminating_signal_handlers
106
- install_terminating_signal_handler('SIGTERM', ::SystemExit)
107
- install_terminating_signal_handler('SIGINT', ::Interrupt)
104
+ trap('SIGTERM', SystemExit)
105
+ trap('SIGINT', Interrupt)
108
106
  end
109
107
 
110
108
  def terminate_threads
@@ -114,7 +112,25 @@ module Polyphony
114
112
  threads.each(&:kill)
115
113
  threads.each(&:join)
116
114
  end
115
+
116
+ attr_accessor :original_pid
117
+
118
+ def install_at_exit_handler
119
+ @original_pid = ::Process.pid
120
+
121
+ # This at_exit handler is needed only when the original process exits. Due to
122
+ # the behaviour of fibers on fork (and especially on exit from forked
123
+ # processes,) we use a separate mechanism to terminate fibers in forked
124
+ # processes (see Polyphony.fork).
125
+ at_exit do
126
+ next unless @original_pid == ::Process.pid
127
+
128
+ Polyphony.terminate_threads
129
+ Fiber.current.shutdown_all_children
130
+ end
131
+ end
117
132
  end
118
133
  end
119
134
 
120
135
  Polyphony.install_terminating_signal_handlers
136
+ Polyphony.install_at_exit_handler
@@ -6,7 +6,7 @@ require 'redis'
6
6
  require 'hiredis/reader'
7
7
 
8
8
  # Polyphony-based Redis driver
9
- class Driver
9
+ class Polyphony::RedisDriver
10
10
  def self.connect(config)
11
11
  raise 'unix sockets not supported' if config[:scheme] == 'unix'
12
12
 
@@ -43,6 +43,7 @@ class Driver
43
43
  end
44
44
 
45
45
  def format_command(args)
46
+ args = args.flatten
46
47
  (+"*#{args.size}\r\n").tap do |s|
47
48
  args.each do |a|
48
49
  a = a.to_s
@@ -63,4 +64,4 @@ class Driver
63
64
  end
64
65
  end
65
66
 
66
- Redis::Connection.drivers << Driver
67
+ Redis::Connection.drivers << Polyphony::RedisDriver
@@ -15,11 +15,13 @@ module Polyphony
15
15
  end
16
16
  end
17
17
 
18
- def cancel_after(interval, &block)
18
+ def cancel_after(interval, with_exception: Polyphony::Cancel, &block)
19
19
  fiber = ::Fiber.current
20
20
  canceller = spin do
21
21
  sleep interval
22
- fiber.schedule Polyphony::Cancel.new
22
+ exception = with_exception.is_a?(Class) ?
23
+ with_exception.new : RuntimeError.new(with_exception)
24
+ fiber.schedule exception
23
25
  end
24
26
  block ? cancel_after_wrap_block(canceller, &block) : canceller
25
27
  end
@@ -101,7 +103,7 @@ module Polyphony
101
103
 
102
104
  def sleep_forever
103
105
  Thread.current.agent.ref
104
- suspend
106
+ loop { sleep 60 }
105
107
  ensure
106
108
  Thread.current.agent.unref
107
109
  end
@@ -13,6 +13,7 @@ module Polyphony
13
13
 
14
14
  @stock = []
15
15
  @queue = []
16
+ @acquired_resources = {}
16
17
 
17
18
  @limit = opts[:limit] || 4
18
19
  @size = 0
@@ -23,16 +24,25 @@ module Polyphony
23
24
  end
24
25
 
25
26
  def acquire
26
- Thread.current.agent.ref
27
- resource = wait_for_resource
28
- return unless resource
29
-
30
- yield resource
31
- ensure
32
- Thread.current.agent.unref
33
- release(resource) if resource
27
+ fiber = Fiber.current
28
+ if @acquired_resources[fiber]
29
+ yield @acquired_resources[fiber]
30
+ else
31
+ begin
32
+ Thread.current.agent.ref
33
+ resource = wait_for_resource
34
+ return unless resource
35
+
36
+ @acquired_resources[fiber] = resource
37
+ yield resource
38
+ ensure
39
+ @acquired_resources[fiber] = nil
40
+ Thread.current.agent.unref
41
+ release(resource) if resource
42
+ end
43
+ end
34
44
  end
35
-
45
+
36
46
  def wait_for_resource
37
47
  fiber = Fiber.current
38
48
  @queue << fiber
@@ -49,7 +49,7 @@ module Polyphony
49
49
  end
50
50
 
51
51
  def run_queued_task
52
- (block, watcher) = @task_queue.pop
52
+ (block, watcher) = @task_queue.shift
53
53
  result = block.()
54
54
  watcher&.signal(result)
55
55
  rescue Exception => e
@@ -57,6 +57,12 @@ module ::Process
57
57
  fiber.define_singleton_method(:pid) { pid }
58
58
  fiber
59
59
  end
60
+
61
+ alias_method :orig_daemon, :daemon
62
+ def daemon(*args)
63
+ orig_daemon(*args)
64
+ Polyphony.original_pid = Process.pid
65
+ end
60
66
  end
61
67
  end
62
68
 
@@ -101,6 +107,15 @@ module ::Kernel
101
107
  $stdin.gets
102
108
  end
103
109
 
110
+ alias_method :orig_p, :p
111
+ def p(*args)
112
+ strs = args.inject([]) do |m, a|
113
+ m << a.inspect << "\n"
114
+ end
115
+ STDOUT.write *strs
116
+ args.size == 1 ? args.first : args
117
+ end
118
+
104
119
  alias_method :orig_system, :system
105
120
  def system(*args)
106
121
  Open3.popen2(*args) do |i, o, _t|
@@ -120,6 +135,31 @@ module ::Kernel
120
135
  break
121
136
  end
122
137
  end
138
+
139
+ alias_method :orig_trap, :trap
140
+ def trap(sig, command = nil, &block)
141
+ return orig_trap(sig, command) if command.is_a? String
142
+
143
+ block = command if command.respond_to?(:call) && !block
144
+ exception = command.is_a?(Class) && command.new
145
+
146
+ # The signal trap can be invoked at any time, including while the system
147
+ # agent is blocking while polling for events. In order to deal with this
148
+ # correctly, we spin a fiber that will run the signal handler code, then
149
+ # call break_out_of_ev_loop, which will put the fiber at the front of the
150
+ # run queue, then wake up the system agent.
151
+ #
152
+ # If the command argument is an exception class however, it will be raised
153
+ # directly in the context of the main fiber.
154
+ orig_trap(sig) do
155
+ if exception
156
+ Thread.current.break_out_of_ev_loop(Thread.main.main_fiber, exception)
157
+ else
158
+ fiber = spin { snooze; block.call }
159
+ Thread.current.break_out_of_ev_loop(fiber, nil)
160
+ end
161
+ end
162
+ end
123
163
  end
124
164
 
125
165
  # Override Timeout to use cancel scope
@@ -189,7 +189,7 @@ module Polyphony
189
189
  end
190
190
 
191
191
  def receive_pending
192
- @mailbox.shift_each
192
+ @mailbox.shift_all
193
193
  end
194
194
  end
195
195
 
@@ -221,6 +221,13 @@ module Polyphony
221
221
  def await_all_children
222
222
  return unless @children && !@children.empty?
223
223
 
224
+ # @results = @children.dup
225
+ # @on_child_done = proc do |c, r|
226
+ # @results[c] = r
227
+ # self.schedule if @children.empty?
228
+ # end
229
+ # suspend
230
+ # @results.values
224
231
  Fiber.await(*@children.keys)
225
232
  end
226
233
 
@@ -370,15 +377,3 @@ class ::Fiber
370
377
  end
371
378
 
372
379
  Fiber.current.setup_main_fiber
373
-
374
- # This at_exit handler is needed only when the original process exits. Due to
375
- # the behaviour of fibers on fork (and especially on exit from forked
376
- # processes,) we use a separate mechanism to terminate fibers in forked
377
- # processes (see Polyphony.fork).
378
- orig_pid = Process.pid
379
- at_exit do
380
- next unless orig_pid == Process.pid
381
-
382
- Polyphony.terminate_threads
383
- Fiber.current.shutdown_all_children
384
- end
@@ -97,7 +97,7 @@ class ::IO
97
97
  # end
98
98
 
99
99
  alias_method :orig_read, :read
100
- def read(len = 1 << 30)
100
+ def read(len = nil)
101
101
  @read_buffer ||= +''
102
102
  result = Thread.current.agent.read(self, @read_buffer, len, true)
103
103
  return nil unless result
@@ -108,19 +108,23 @@ class ::IO
108
108
  end
109
109
 
110
110
  alias_method :orig_readpartial, :read
111
- def readpartial(len)
111
+ def readpartial(len, str = nil)
112
112
  @read_buffer ||= +''
113
113
  result = Thread.current.agent.read(self, @read_buffer, len, false)
114
114
  raise EOFError unless result
115
115
 
116
- already_read = @read_buffer
116
+ if str
117
+ str << @read_buffer
118
+ else
119
+ str = @read_buffer
120
+ end
117
121
  @read_buffer = +''
118
- already_read
122
+ str
119
123
  end
120
124
 
121
125
  alias_method :orig_write, :write
122
- def write(str)
123
- Thread.current.agent.write(self, str)
126
+ def write(str, *args)
127
+ Thread.current.agent.write(self, str, *args)
124
128
  end
125
129
 
126
130
  alias_method :orig_write_chevron, :<<
@@ -166,16 +170,13 @@ class ::IO
166
170
  return
167
171
  end
168
172
 
169
- s = args.each_with_object(+'') do |a, str|
170
- if a.is_a?(Array)
171
- a.each { |a2| str << a2.to_s << "\n" }
172
- else
173
- a = a.to_s
174
- str << a
175
- str << "\n" unless a =~ /\n$/
176
- end
173
+ strs = args.inject([]) do |m, a|
174
+ a = a.to_s
175
+ m << a
176
+ m << "\n" unless a =~ /\n$/
177
+ m
177
178
  end
178
- write s
179
+ write *strs
179
180
  nil
180
181
  end
181
182
 
@@ -193,7 +194,7 @@ class ::IO
193
194
 
194
195
  alias_method :orig_write_nonblock, :write_nonblock
195
196
  def write_nonblock(string, _options = {})
196
- write(string, 0)
197
+ write(string)
197
198
  end
198
199
 
199
200
  alias_method :orig_read_nonblock, :read_nonblock