polyphony 0.64 → 0.68

Sign up to get free protection for your applications and to get access to all the features.
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?