polyphony 0.64 → 0.68

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +1 -1
  3. data/CHANGELOG.md +22 -0
  4. data/Gemfile.lock +1 -1
  5. data/TODO.md +10 -40
  6. data/bin/pdbg +112 -0
  7. data/examples/core/await.rb +9 -1
  8. data/ext/polyphony/backend_common.c +14 -1
  9. data/ext/polyphony/backend_common.h +3 -1
  10. data/ext/polyphony/backend_io_uring.c +85 -25
  11. data/ext/polyphony/backend_io_uring_context.c +42 -0
  12. data/ext/polyphony/backend_io_uring_context.h +6 -9
  13. data/ext/polyphony/backend_libev.c +85 -39
  14. data/ext/polyphony/fiber.c +20 -0
  15. data/ext/polyphony/polyphony.c +2 -0
  16. data/ext/polyphony/polyphony.h +5 -2
  17. data/ext/polyphony/queue.c +1 -1
  18. data/ext/polyphony/runqueue.c +7 -3
  19. data/ext/polyphony/runqueue.h +4 -3
  20. data/ext/polyphony/runqueue_ring_buffer.c +25 -14
  21. data/ext/polyphony/runqueue_ring_buffer.h +2 -0
  22. data/ext/polyphony/thread.c +2 -8
  23. data/lib/polyphony.rb +6 -0
  24. data/lib/polyphony/debugger.rb +225 -0
  25. data/lib/polyphony/extensions/debug.rb +1 -1
  26. data/lib/polyphony/extensions/fiber.rb +64 -71
  27. data/lib/polyphony/extensions/io.rb +4 -2
  28. data/lib/polyphony/extensions/openssl.rb +66 -0
  29. data/lib/polyphony/extensions/socket.rb +8 -2
  30. data/lib/polyphony/net.rb +1 -0
  31. data/lib/polyphony/version.rb +1 -1
  32. data/test/helper.rb +6 -5
  33. data/test/stress.rb +6 -2
  34. data/test/test_backend.rb +13 -4
  35. data/test/test_fiber.rb +35 -11
  36. data/test/test_global_api.rb +9 -4
  37. data/test/test_io.rb +2 -0
  38. data/test/test_socket.rb +14 -11
  39. data/test/test_supervise.rb +24 -24
  40. data/test/test_thread.rb +3 -0
  41. data/test/test_thread_pool.rb +1 -1
  42. data/test/test_throttler.rb +2 -2
  43. data/test/test_timer.rb +5 -3
  44. metadata +5 -3
@@ -29,4 +29,6 @@ void runqueue_ring_buffer_push(runqueue_ring_buffer *buffer, VALUE fiber, VALUE
29
29
  void runqueue_ring_buffer_delete(runqueue_ring_buffer *buffer, VALUE fiber);
30
30
  int runqueue_ring_buffer_index_of(runqueue_ring_buffer *buffer, VALUE fiber);
31
31
 
32
+ void runqueue_ring_buffer_migrate(runqueue_ring_buffer *src, runqueue_ring_buffer *dest, VALUE fiber);
33
+
32
34
  #endif /* RUNQUEUE_RING_BUFFER_H */
@@ -22,14 +22,12 @@ VALUE Thread_fiber_unschedule(VALUE self, VALUE fiber) {
22
22
  return self;
23
23
  }
24
24
 
25
- VALUE Thread_schedule_fiber(VALUE self, VALUE fiber, VALUE value) {
25
+ inline void Thread_schedule_fiber(VALUE self, VALUE fiber, VALUE value) {
26
26
  schedule_fiber(self, fiber, value, 0);
27
- return self;
28
27
  }
29
28
 
30
- VALUE Thread_schedule_fiber_with_priority(VALUE self, VALUE fiber, VALUE value) {
29
+ inline void Thread_schedule_fiber_with_priority(VALUE self, VALUE fiber, VALUE value) {
31
30
  schedule_fiber(self, fiber, value, 1);
32
- return self;
33
31
  }
34
32
 
35
33
  VALUE Thread_switch_fiber(VALUE self) {
@@ -61,10 +59,6 @@ VALUE Thread_class_backend(VALUE _self) {
61
59
  void Init_Thread() {
62
60
  rb_define_method(rb_cThread, "setup_fiber_scheduling", Thread_setup_fiber_scheduling, 0);
63
61
  rb_define_method(rb_cThread, "schedule_and_wakeup", Thread_fiber_schedule_and_wakeup, 2);
64
-
65
- rb_define_method(rb_cThread, "schedule_fiber", Thread_schedule_fiber, 2);
66
- rb_define_method(rb_cThread, "schedule_fiber_with_priority",
67
- Thread_schedule_fiber_with_priority, 2);
68
62
  rb_define_method(rb_cThread, "switch_fiber", Thread_switch_fiber, 0);
69
63
  rb_define_method(rb_cThread, "fiber_unschedule", Thread_fiber_unschedule, 1);
70
64
 
data/lib/polyphony.rb CHANGED
@@ -121,3 +121,9 @@ end
121
121
 
122
122
  Polyphony.install_terminating_signal_handlers
123
123
  Polyphony.install_at_exit_handler
124
+
125
+ if (debug_socket_path = ENV['POLYPHONY_DEBUG_SOCKET_PATH'])
126
+ puts "Starting debug server on #{debug_socket_path}"
127
+ require 'polyphony/debugger'
128
+ Polyphony.start_debug_server(debug_socket_path)
129
+ end
@@ -0,0 +1,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'polyphony/extensions/debug'
4
+
5
+ module Polyphony
6
+ TP_EVENTS = [
7
+ :line,
8
+ :call,
9
+ :return,
10
+ :b_call,
11
+ :b_return
12
+ ]
13
+
14
+ def self.start_debug_server(socket_path)
15
+ server = DebugServer.new(socket_path)
16
+ controller = DebugController.new(server)
17
+ trace = TracePoint.new(*TP_EVENTS) { |tp| controller.handle_tp(trace, tp) }
18
+ trace.enable
19
+
20
+ at_exit do
21
+ Kernel.trace "program terminated"
22
+ trace.disable
23
+ server.stop
24
+ end
25
+ end
26
+
27
+ class DebugController
28
+ def initialize(server)
29
+ @server = server
30
+ @server.wait_for_client
31
+ @state = { fibers: {} }
32
+ @control_fiber = Fiber.new { |f| control_loop(f) }
33
+ @control_fiber.transfer Fiber.current
34
+ end
35
+
36
+ def control_loop(source_fiber)
37
+ @peer = source_fiber
38
+ cmd = { cmd: :initial }
39
+ loop do
40
+ cmd = send(:"cmd_#{cmd[:cmd]}", cmd)
41
+ end
42
+ end
43
+
44
+ POLYPHONY_LIB_DIR = File.expand_path('..', __dir__)
45
+
46
+ def get_next_trace_event
47
+ @peer.transfer.tap { |e| update_state(e) }
48
+ end
49
+
50
+ def update_state(event)
51
+ trace update_state: event
52
+ @state[:fiber] = event[:fiber]
53
+ @state[:path] = event[:path]
54
+ @state[:lineno] = event[:lineno]
55
+ update_fiber_state(event)
56
+ end
57
+
58
+ def update_fiber_state(event)
59
+ fiber_state = @state[:fibers][event[:fiber]] ||= { stack: [] }
60
+ case event[:kind]
61
+ when :call, :c_call, :b_call
62
+ fiber_state[:stack] << event
63
+ when :return, :c_return, :b_return
64
+ fiber_state[:stack].pop
65
+ end
66
+ fiber_state[:binding] = event[:binding]
67
+ fiber_state[:path] = event[:path]
68
+ fiber_state[:lineno] = event[:lineno]
69
+ end
70
+
71
+ def state_presentation(state)
72
+ {
73
+ fiber: fiber_id(state[:fiber]),
74
+ path: state[:path],
75
+ lineno: state[:lineno]
76
+ }
77
+ end
78
+
79
+ def fiber_id(fiber)
80
+ {
81
+ object_id: fiber.object_id,
82
+ tag: fiber.tag
83
+ }
84
+ end
85
+
86
+ def fiber_representation(fiber)
87
+ {
88
+ object_id: fiber.object_id,
89
+ tag: fiber.tag,
90
+ parent: fiber.parent && fiber_id(fiber.parent),
91
+ children: fiber.children.map { |c| fiber_id(c) }
92
+ }
93
+ end
94
+
95
+ def get_next_command(info)
96
+ @server.get_command(info)
97
+ end
98
+
99
+ def cmd_initial(cmd)
100
+ get_next_command(nil)
101
+ end
102
+
103
+ def info_listing(state)
104
+ {
105
+ kind: :listing,
106
+ fiber: fiber_id(state[:fiber]),
107
+ path: state[:path],
108
+ lineno: state[:lineno]
109
+ }
110
+ end
111
+
112
+ def info_state(state)
113
+ info_listing(state).merge(
114
+ kind: :state,
115
+ fibers: info_fiber_states(state[:fibers])
116
+ )
117
+ end
118
+
119
+ def info_fiber_states(fiber_states)
120
+ fiber_states.inject({}) do |h, (f, s)|
121
+ h[fiber_id(f)] = {
122
+ stack: s[:stack].map { |e| { path: e[:path], lineno: e[:lineno] } }
123
+ }
124
+ h
125
+ end
126
+ end
127
+
128
+ def cmd_step(cmd)
129
+ tp = nil
130
+ fiber = nil
131
+ while true
132
+ event = get_next_trace_event
133
+ @peer = event[:fiber]
134
+ if event[:kind] == :line && event[:path] !~ /#{POLYPHONY_LIB_DIR}/
135
+ return get_next_command(info_listing(@state))
136
+ end
137
+ end
138
+ rescue => e
139
+ trace "Uncaught error: #{e.inspect}"
140
+ @trace&.disable
141
+ end
142
+
143
+ def cmd_help(cmd)
144
+ get_next_command(kind: :help)
145
+ end
146
+
147
+ def cmd_list(cmd)
148
+ get_next_command(info_listing(@state))
149
+ end
150
+
151
+ def cmd_state(cmd)
152
+ get_next_command(info_state(@state))
153
+ end
154
+
155
+ def handle_tp(trace, tp)
156
+ return if Thread.current == @server.thread
157
+ return if Fiber.current == @control_fiber
158
+
159
+ kind = tp.event
160
+ event = {
161
+ fiber: Fiber.current,
162
+ kind: kind,
163
+ path: tp.path,
164
+ lineno: tp.lineno,
165
+ binding: tp.binding
166
+ }
167
+ case kind
168
+ when :call, :c_call, :b_call
169
+ event[:method_id] = tp.method_id
170
+ event[:parameters] = tp.parameters
171
+ when :return, :c_return, :b_return
172
+ event[:method_id] = tp.method_id
173
+ event[:return_value] = tp.return_value
174
+ end
175
+ @control_fiber.transfer(event)
176
+ end
177
+ end
178
+
179
+ class DebugServer
180
+ attr_reader :thread
181
+
182
+ def initialize(socket_path)
183
+ @socket_path = socket_path
184
+ @fiber = Fiber.current
185
+ start_server_thread
186
+ end
187
+
188
+ def start_server_thread
189
+ @thread = Thread.new do
190
+ puts("Listening on #{@socket_path}")
191
+ FileUtils.rm(@socket_path) if File.exists?(@socket_path)
192
+ socket = UNIXServer.new(@socket_path)
193
+ loop do
194
+ @client = socket.accept
195
+ end
196
+ end
197
+ end
198
+
199
+ def stop
200
+ @thread.kill
201
+ end
202
+
203
+ def handle_client(client)
204
+ @client = client
205
+ end
206
+
207
+ def wait_for_client
208
+ sleep 0.1 until @client
209
+ msg = @client.gets
210
+ @client.puts msg
211
+ end
212
+
213
+ def get_command(info)
214
+ @client&.orig_write "#{info.inspect}\n"
215
+ cmd = @client&.orig_gets&.chomp
216
+ eval(cmd)
217
+ rescue SystemCallError
218
+ nil
219
+ rescue => e
220
+ trace "Error in interact_with_client: #{e.inspect}"
221
+ e.backtrace[0..3].each { |l| trace l }
222
+ @client = nil
223
+ end
224
+ end
225
+ end
@@ -10,4 +10,4 @@ module ::Kernel
10
10
  format("%p\n", args.size == 1 ? args.first : args)
11
11
  end
12
12
  end
13
- end
13
+ end
@@ -7,6 +7,10 @@ require_relative '../core/exceptions'
7
7
  module Polyphony
8
8
  # Fiber control API
9
9
  module FiberControl
10
+ def monitor_mailbox
11
+ @monitor_mailbox ||= Polyphony::Queue.new
12
+ end
13
+
10
14
  def interrupt(value = nil)
11
15
  return if @running == false
12
16
 
@@ -75,47 +79,26 @@ module Polyphony
75
79
 
76
80
  # Fiber supervision
77
81
  module FiberSupervision
78
- def supervise(opts = {})
79
- @counter = 0
80
- @on_child_done = proc do |fiber, result|
81
- self << fiber unless result.is_a?(Exception)
82
- end
83
- while true
84
- supervise_perform(opts)
85
- end
86
- rescue Polyphony::MoveOn
87
- # generated in #supervise_perform to stop supervisor
88
- ensure
89
- @on_child_done = nil
90
- end
82
+ def supervise(*fibers, **opts, &block)
83
+ block ||= opts[:on_done] ||
84
+ (opts[:on_error] && supervise_on_error_proc(opts[:on_error]))
85
+ raise "No block given" unless block
91
86
 
92
- def supervise_perform(opts)
93
- fiber = receive
94
- if fiber && opts[:restart]
95
- restart_fiber(fiber, opts)
96
- elsif Fiber.current.children.empty?
97
- Fiber.current.stop
87
+ fibers.each do |f|
88
+ f.attach_to(self) unless f.parent == self
89
+ f.monitor(self)
98
90
  end
99
- rescue Polyphony::Restart
100
- restart_all_children
101
- rescue Exception => e
102
- Kernel.raise e if e.source_fiber.nil? || e.source_fiber == self
103
91
 
104
- if opts[:restart]
105
- restart_fiber(e.source_fiber, opts)
106
- elsif Fiber.current.children.empty?
107
- Fiber.current.stop
92
+ mailbox = monitor_mailbox
93
+
94
+ while true
95
+ (fiber, result) = mailbox.shift
96
+ block&.call(fiber, result)
108
97
  end
109
98
  end
110
99
 
111
- def restart_fiber(fiber, opts)
112
- opts[:watcher]&.send [:restart, fiber]
113
- case opts[:restart]
114
- when true
115
- fiber.restart
116
- when :one_for_all
117
- @children.keys.each(&:restart)
118
- end
100
+ def supervise_on_error_proc(on_error)
101
+ ->(f, r) { opts[:on_error].(f, r) if r.is_a?(Exception) }
119
102
  end
120
103
  end
121
104
 
@@ -124,23 +107,24 @@ module Polyphony
124
107
  def await(*fibers)
125
108
  return [] if fibers.empty?
126
109
 
127
- Fiber.current.message_on_child_termination = true
110
+ current_fiber = self.current
111
+ mailbox = current_fiber.monitor_mailbox
128
112
  results = {}
129
113
  fibers.each do |f|
130
114
  results[f] = nil
131
115
  if f.dead?
132
116
  # fiber already terminated, so queue message
133
- Fiber.current.send [f, f.result]
117
+ mailbox << [f, f.result]
134
118
  else
135
- f.monitor
119
+ f.monitor(current_fiber)
136
120
  end
137
121
  end
138
122
  exception = nil
139
123
  while !fibers.empty?
140
- (fiber, result) = receive
124
+ (fiber, result) = mailbox.shift
141
125
  next unless fibers.include?(fiber)
142
-
143
126
  fibers.delete(fiber)
127
+ current_fiber.remove_child(fiber) if fiber.parent == current_fiber
144
128
  if result.is_a?(Exception)
145
129
  exception ||= result
146
130
  fibers.each { |f| f.terminate }
@@ -148,16 +132,16 @@ module Polyphony
148
132
  results[fiber] = result
149
133
  end
150
134
  end
151
- results.values
152
- ensure
153
- Fiber.current.message_on_child_termination = false
154
135
  raise exception if exception
136
+ results.values
155
137
  end
156
138
  alias_method :join, :await
157
139
 
158
140
  def select(*fibers)
159
141
  return nil if fibers.empty?
160
142
 
143
+ current_fiber = self.current
144
+ mailbox = current_fiber.monitor_mailbox
161
145
  fibers.each do |f|
162
146
  if f.dead?
163
147
  result = f.result
@@ -165,21 +149,18 @@ module Polyphony
165
149
  end
166
150
  end
167
151
 
168
- Fiber.current.message_on_child_termination = true
169
- fibers.each { |f| f.monitor }
152
+ fibers.each { |f| f.monitor(current_fiber) }
170
153
  while true
171
- (fiber, result) = receive
154
+ (fiber, result) = mailbox.shift
172
155
  next unless fibers.include?(fiber)
173
156
 
174
- fibers.each { |f| f.unmonitor }
157
+ fibers.each { |f| f.unmonitor(current_fiber) }
175
158
  if result.is_a?(Exception)
176
159
  raise result
177
160
  else
178
161
  return [fiber, result]
179
162
  end
180
163
  end
181
- ensure
182
- Fiber.current.message_on_child_termination = false
183
164
  end
184
165
 
185
166
  # Creates and schedules with priority an out-of-band fiber that runs the
@@ -219,14 +200,6 @@ module Polyphony
219
200
  f
220
201
  end
221
202
 
222
- def child_done(child_fiber, result)
223
- @children.delete(child_fiber)
224
-
225
- if result.is_a?(Exception) && !@message_on_child_termination
226
- schedule_with_priority(result)
227
- end
228
- end
229
-
230
203
  def terminate_all_children(graceful = false)
231
204
  return unless @children
232
205
 
@@ -240,16 +213,25 @@ module Polyphony
240
213
  def await_all_children
241
214
  return unless @children && !@children.empty?
242
215
 
243
- Fiber.await(*@children.keys)
216
+ Fiber.await(*@children.keys.reject { |c| c.dead? })
244
217
  end
245
218
 
246
219
  def shutdown_all_children(graceful = false)
247
220
  return unless @children
248
221
 
249
222
  @children.keys.each do |c|
223
+ next if c.dead?
224
+
250
225
  c.terminate(graceful)
251
226
  c.await
252
227
  end
228
+ reap_dead_children
229
+ end
230
+
231
+ def reap_dead_children
232
+ return unless @children
233
+
234
+ @children.reject! { |f| f.dead? }
253
235
  end
254
236
 
255
237
  def detach
@@ -258,10 +240,17 @@ module Polyphony
258
240
  @parent.add_child(self)
259
241
  end
260
242
 
261
- def attach(parent)
243
+ def attach_to(fiber)
262
244
  @parent.remove_child(self)
263
- @parent = parent
264
- @parent.add_child(self)
245
+ @parent = fiber
246
+ fiber.add_child(self)
247
+ end
248
+
249
+ def attach_and_monitor(fiber)
250
+ @parent.remove_child(self)
251
+ @parent = fiber
252
+ fiber.add_child(self)
253
+ monitor(fiber)
265
254
  end
266
255
  end
267
256
 
@@ -323,7 +312,7 @@ module Polyphony
323
312
  Thread.backend.trace(:fiber_terminate, self, result)
324
313
  @result = result
325
314
 
326
- inform_dependants(result, uncaught_exception)
315
+ inform_monitors(result, uncaught_exception)
327
316
  @running = false
328
317
  ensure
329
318
  # Prevent fiber from being resumed after terminating
@@ -341,24 +330,28 @@ module Polyphony
341
330
  [e, true]
342
331
  end
343
332
 
344
- def inform_dependants(result, uncaught_exception)
333
+ def inform_monitors(result, uncaught_exception)
345
334
  if @monitors
346
335
  msg = [self, result]
347
- @monitors.each { |f| f << msg }
336
+ @monitors.each_key { |f| f.monitor_mailbox << msg }
348
337
  end
349
338
 
350
- @parent&.child_done(self, result)
339
+ if uncaught_exception && @parent
340
+ parent_is_monitor = @monitors&.has_key?(@parent)
341
+ @parent.schedule_with_priority(result) unless parent_is_monitor
342
+ end
351
343
  end
352
344
 
353
- attr_accessor :message_on_child_termination
345
+ def monitor(fiber)
346
+ (@monitors ||= {})[fiber] = true
347
+ end
354
348
 
355
- def monitor
356
- @monitors ||= []
357
- @monitors << Fiber.current
349
+ def unmonitor(fiber)
350
+ (@monitors ||= []).delete(fiber)
358
351
  end
359
352
 
360
- def unmonitor
361
- @monitors.delete(Fiber.current) if @monitors
353
+ def monitors
354
+ @monitors&.keys || []
362
355
  end
363
356
 
364
357
  def dead?