polyphony 0.53.0 → 0.56.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.
@@ -10,6 +10,7 @@ ID ID_each;
10
10
  ID ID_inspect;
11
11
  ID ID_invoke;
12
12
  ID ID_new;
13
+ ID ID_ivar_blocking_mode;
13
14
  ID ID_ivar_io;
14
15
  ID ID_ivar_runnable;
15
16
  ID ID_ivar_running;
@@ -158,19 +159,20 @@ void Init_Polyphony() {
158
159
 
159
160
  cTimeoutException = rb_define_class_under(mPolyphony, "TimeoutException", rb_eException);
160
161
 
161
- ID_call = rb_intern("call");
162
- ID_caller = rb_intern("caller");
163
- ID_clear = rb_intern("clear");
164
- ID_each = rb_intern("each");
165
- ID_inspect = rb_intern("inspect");
166
- ID_invoke = rb_intern("invoke");
167
- ID_ivar_io = rb_intern("@io");
168
- ID_ivar_runnable = rb_intern("@runnable");
169
- ID_ivar_running = rb_intern("@running");
170
- ID_ivar_thread = rb_intern("@thread");
171
- ID_new = rb_intern("new");
172
- ID_signal = rb_intern("signal");
173
- ID_size = rb_intern("size");
174
- ID_switch_fiber = rb_intern("switch_fiber");
175
- ID_transfer = rb_intern("transfer");
162
+ ID_call = rb_intern("call");
163
+ ID_caller = rb_intern("caller");
164
+ ID_clear = rb_intern("clear");
165
+ ID_each = rb_intern("each");
166
+ ID_inspect = rb_intern("inspect");
167
+ ID_invoke = rb_intern("invoke");
168
+ ID_ivar_blocking_mode = rb_intern("@blocking_mode");
169
+ ID_ivar_io = rb_intern("@io");
170
+ ID_ivar_runnable = rb_intern("@runnable");
171
+ ID_ivar_running = rb_intern("@running");
172
+ ID_ivar_thread = rb_intern("@thread");
173
+ ID_new = rb_intern("new");
174
+ ID_signal = rb_intern("signal");
175
+ ID_size = rb_intern("size");
176
+ ID_switch_fiber = rb_intern("switch_fiber");
177
+ ID_transfer = rb_intern("transfer");
176
178
  }
@@ -47,6 +47,7 @@ extern ID ID_fiber_trace;
47
47
  extern ID ID_inspect;
48
48
  extern ID ID_invoke;
49
49
  extern ID ID_ivar_backend;
50
+ extern ID ID_ivar_blocking_mode;
50
51
  extern ID ID_ivar_io;
51
52
  extern ID ID_ivar_runnable;
52
53
  extern ID ID_ivar_running;
@@ -90,6 +91,7 @@ int Runqueue_index_of(VALUE self, VALUE fiber);
90
91
  void Runqueue_clear(VALUE self);
91
92
  long Runqueue_len(VALUE self);
92
93
  int Runqueue_empty_p(VALUE self);
94
+ int Runqueue_should_poll_nonblocking(VALUE self);
93
95
 
94
96
  #ifdef POLYPHONY_BACKEND_LIBEV
95
97
  #define Backend_recv_loop Backend_read_loop
@@ -123,6 +125,7 @@ unsigned int Backend_pending_count(VALUE self);
123
125
  VALUE Backend_poll(VALUE self, VALUE nowait, VALUE current_fiber, VALUE runqueue);
124
126
  VALUE Backend_wait_event(VALUE self, VALUE raise_on_exception);
125
127
  VALUE Backend_wakeup(VALUE self);
128
+ VALUE Backend_run_idle_tasks(VALUE self);
126
129
 
127
130
  VALUE Thread_schedule_fiber(VALUE thread, VALUE fiber, VALUE value);
128
131
  VALUE Thread_schedule_fiber_with_priority(VALUE thread, VALUE fiber, VALUE value);
@@ -3,6 +3,8 @@
3
3
 
4
4
  typedef struct queue {
5
5
  runqueue_ring_buffer entries;
6
+ unsigned int high_watermark;
7
+ unsigned int switch_count;
6
8
  } Runqueue_t;
7
9
 
8
10
  VALUE cRunqueue = Qnil;
@@ -43,6 +45,8 @@ static VALUE Runqueue_initialize(VALUE self) {
43
45
  GetRunqueue(self, runqueue);
44
46
 
45
47
  runqueue_ring_buffer_init(&runqueue->entries);
48
+ runqueue->high_watermark = 0;
49
+ runqueue->switch_count = 0;
46
50
 
47
51
  return self;
48
52
  }
@@ -53,6 +57,8 @@ void Runqueue_push(VALUE self, VALUE fiber, VALUE value, int reschedule) {
53
57
 
54
58
  if (reschedule) runqueue_ring_buffer_delete(&runqueue->entries, fiber);
55
59
  runqueue_ring_buffer_push(&runqueue->entries, fiber, value);
60
+ if (runqueue->entries.count > runqueue->high_watermark)
61
+ runqueue->high_watermark = runqueue->entries.count;
56
62
  }
57
63
 
58
64
  void Runqueue_unshift(VALUE self, VALUE fiber, VALUE value, int reschedule) {
@@ -60,12 +66,19 @@ void Runqueue_unshift(VALUE self, VALUE fiber, VALUE value, int reschedule) {
60
66
  GetRunqueue(self, runqueue);
61
67
  if (reschedule) runqueue_ring_buffer_delete(&runqueue->entries, fiber);
62
68
  runqueue_ring_buffer_unshift(&runqueue->entries, fiber, value);
69
+ if (runqueue->entries.count > runqueue->high_watermark)
70
+ runqueue->high_watermark = runqueue->entries.count;
63
71
  }
64
72
 
65
73
  runqueue_entry Runqueue_shift(VALUE self) {
66
74
  Runqueue_t *runqueue;
67
75
  GetRunqueue(self, runqueue);
68
- return runqueue_ring_buffer_shift(&runqueue->entries);
76
+ runqueue_entry entry = runqueue_ring_buffer_shift(&runqueue->entries);
77
+ if (entry.fiber == Qnil)
78
+ runqueue->high_watermark = 0;
79
+ else
80
+ runqueue->switch_count += 1;
81
+ return entry;
69
82
  }
70
83
 
71
84
  void Runqueue_delete(VALUE self, VALUE fiber) {
@@ -100,6 +113,21 @@ int Runqueue_empty_p(VALUE self) {
100
113
  return (runqueue->entries.count == 0);
101
114
  }
102
115
 
116
+ static const unsigned int ANTI_STARVE_HIGH_WATERMARK_THRESHOLD = 128;
117
+ static const unsigned int ANTI_STARVE_SWITCH_COUNT_THRESHOLD = 64;
118
+
119
+ int Runqueue_should_poll_nonblocking(VALUE self) {
120
+ Runqueue_t *runqueue;
121
+ GetRunqueue(self, runqueue);
122
+
123
+ if (runqueue->high_watermark < ANTI_STARVE_HIGH_WATERMARK_THRESHOLD) return 0;
124
+ if (runqueue->switch_count < ANTI_STARVE_SWITCH_COUNT_THRESHOLD) return 0;
125
+
126
+ // the
127
+ runqueue->switch_count = 0;
128
+ return 1;
129
+ }
130
+
103
131
  void Init_Runqueue() {
104
132
  cRunqueue = rb_define_class_under(mPolyphony, "Runqueue", rb_cObject);
105
133
  rb_define_alloc_func(cRunqueue, Runqueue_allocate);
@@ -21,7 +21,7 @@ static VALUE SYM_scheduled_fibers;
21
21
  static VALUE SYM_pending_watchers;
22
22
 
23
23
  static VALUE Thread_fiber_scheduling_stats(VALUE self) {
24
- VALUE backend = rb_ivar_get(self,ID_ivar_backend);
24
+ VALUE backend = rb_ivar_get(self, ID_ivar_backend);
25
25
  VALUE stats = rb_hash_new();
26
26
  VALUE runqueue = rb_ivar_get(self, ID_ivar_runqueue);
27
27
  long pending_count;
@@ -53,7 +53,7 @@ void schedule_fiber(VALUE self, VALUE fiber, VALUE value, int prioritize) {
53
53
  // event selector. Otherwise it's gonna be stuck waiting for an event to
54
54
  // happen, not knowing that it there's already a fiber ready to run in its
55
55
  // run queue.
56
- VALUE backend = rb_ivar_get(self,ID_ivar_backend);
56
+ VALUE backend = rb_ivar_get(self, ID_ivar_backend);
57
57
  Backend_wakeup(backend);
58
58
  }
59
59
  }
@@ -86,8 +86,9 @@ VALUE Thread_switch_fiber(VALUE self) {
86
86
  VALUE runqueue = rb_ivar_get(self, ID_ivar_runqueue);
87
87
  runqueue_entry next;
88
88
  VALUE backend = rb_ivar_get(self, ID_ivar_backend);
89
- unsigned int pending_count = Backend_pending_count(backend);
89
+ unsigned int pending_ops_count = Backend_pending_count(backend);
90
90
  unsigned int backend_was_polled = 0;
91
+ unsigned int idle_tasks_run_count = 0;
91
92
 
92
93
  if (__tracing_enabled__ && (rb_ivar_get(current_fiber, ID_ivar_running) != Qfalse))
93
94
  TRACE(2, SYM_fiber_switchpoint, current_fiber);
@@ -95,14 +96,28 @@ VALUE Thread_switch_fiber(VALUE self) {
95
96
  while (1) {
96
97
  next = Runqueue_shift(runqueue);
97
98
  if (next.fiber != Qnil) {
98
- if (!backend_was_polled && pending_count) {
99
+ // Polling for I/O op completion is normally done when the run queue is
100
+ // empty, but if the runqueue never empties, we'll never get to process
101
+ // any event completions. In order to prevent this, an anti-starve
102
+ // mechanism is employed, under the following conditions:
103
+ // - a blocking poll was not yet performed
104
+ // - there are pending blocking operations
105
+ // - the runqueue has signalled that a non-blocking poll should be
106
+ // performed
107
+ // - the run queue length high watermark has reached its threshold (currently 128)
108
+ // - the run queue switch counter has reached its threshold (currently 64)
109
+ if (!backend_was_polled && pending_ops_count && Runqueue_should_poll_nonblocking(runqueue)) {
99
110
  // this prevents event starvation in case the run queue never empties
100
111
  Backend_poll(backend, Qtrue, current_fiber, runqueue);
101
112
  }
102
113
  break;
103
114
  }
104
- if (pending_count == 0) break;
105
-
115
+
116
+ if (!idle_tasks_run_count) {
117
+ idle_tasks_run_count++;
118
+ Backend_run_idle_tasks(backend);
119
+ }
120
+ if (pending_ops_count == 0) break;
106
121
  Backend_poll(backend, Qnil, current_fiber, runqueue);
107
122
  backend_was_polled = 1;
108
123
  }
@@ -138,6 +153,10 @@ VALUE Thread_debug(VALUE self) {
138
153
  return self;
139
154
  }
140
155
 
156
+ VALUE Thread_class_backend(VALUE _self) {
157
+ return rb_ivar_get(rb_thread_current(), ID_ivar_backend);
158
+ }
159
+
141
160
  void Init_Thread() {
142
161
  rb_define_method(rb_cThread, "setup_fiber_scheduling", Thread_setup_fiber_scheduling, 0);
143
162
  rb_define_method(rb_cThread, "fiber_scheduling_stats", Thread_fiber_scheduling_stats, 0);
@@ -150,6 +169,8 @@ void Init_Thread() {
150
169
  rb_define_method(rb_cThread, "fiber_scheduling_index", Thread_fiber_scheduling_index, 1);
151
170
  rb_define_method(rb_cThread, "fiber_unschedule", Thread_fiber_unschedule, 1);
152
171
 
172
+ rb_define_singleton_method(rb_cThread, "backend", Thread_class_backend, 0);
173
+
153
174
  rb_define_method(rb_cThread, "debug!", Thread_debug, 0);
154
175
 
155
176
  ID_deactivate_all_watchers_post_fork = rb_intern("deactivate_all_watchers_post_fork");
@@ -35,6 +35,14 @@ module Polyphony
35
35
  @token = @store.shift
36
36
  @holding_fiber = Fiber.current
37
37
  end
38
+
39
+ def owned?
40
+ @holding_fiber == Fiber.current
41
+ end
42
+
43
+ def locked?
44
+ @holding_fiber
45
+ end
38
46
  end
39
47
 
40
48
  # Implements a fiber-aware ConditionVariable
@@ -3,7 +3,7 @@
3
3
  require 'openssl'
4
4
  require_relative './socket'
5
5
 
6
- # Open ssl socket helper methods (to make it compatible with Socket API)
6
+ # OpenSSL socket helper methods (to make it compatible with Socket API) and overrides
7
7
  class ::OpenSSL::SSL::SSLSocket
8
8
  alias_method :orig_initialize, :initialize
9
9
  def initialize(socket, context = nil)
@@ -23,22 +23,12 @@ class ::OpenSSL::SSL::SSLSocket
23
23
  io.reuse_addr
24
24
  end
25
25
 
26
- alias_method :orig_accept, :accept
27
- def accept
28
- while true
29
- result = accept_nonblock(exception: false)
30
- case result
31
- when :wait_readable then Polyphony.backend_wait_io(io, false)
32
- when :wait_writable then Polyphony.backend_wait_io(io, true)
33
- else
34
- return result
35
- end
36
- end
37
- end
38
-
39
- def accept_loop
40
- while true
41
- yield accept
26
+ def fill_rbuff
27
+ data = self.sysread(BLOCK_SIZE)
28
+ if data
29
+ @rbuffer << data
30
+ else
31
+ @eof = true
42
32
  end
43
33
  end
44
34
 
@@ -84,4 +74,21 @@ class ::OpenSSL::SSL::SSLSocket
84
74
  yield data
85
75
  end
86
76
  end
77
+ alias_method :recv_loop, :read_loop
78
+
79
+ alias_method :orig_peeraddr, :peeraddr
80
+ def peeraddr(_ = nil)
81
+ orig_peeraddr
82
+ end
83
+ end
84
+
85
+ # OpenSSL socket helper methods (to make it compatible with Socket API) and overrides
86
+ class ::OpenSSL::SSL::SSLServer
87
+ def accept_loop(ignore_errors = true)
88
+ loop do
89
+ yield accept
90
+ rescue SystemCallError, StandardError => e
91
+ raise e unless ignore_errors
92
+ end
93
+ end
87
94
  end
@@ -36,9 +36,9 @@ class ::Socket
36
36
  end
37
37
 
38
38
  def recvfrom(maxlen, flags = 0)
39
- @read_buffer ||= +''
39
+ buf = +''
40
40
  while true
41
- result = recvfrom_nonblock(maxlen, flags, @read_buffer, **NO_EXCEPTION)
41
+ result = recvfrom_nonblock(maxlen, flags, buf, **NO_EXCEPTION)
42
42
  case result
43
43
  when nil then raise IOError
44
44
  when :wait_readable then Polyphony.backend_wait_io(self, false)
@@ -165,17 +165,10 @@ class ::TCPSocket
165
165
  # Polyphony.backend_send(self, mesg, 0)
166
166
  # end
167
167
 
168
- def readpartial(maxlen, str = nil)
169
- @read_buffer ||= +''
170
- result = Polyphony.backend_recv(self, @read_buffer, maxlen)
168
+ def readpartial(maxlen, str = +'')
169
+ result = Polyphony.backend_recv(self, str, maxlen)
171
170
  raise EOFError unless result
172
171
 
173
- if str
174
- str << @read_buffer
175
- else
176
- str = @read_buffer
177
- end
178
- @read_buffer = +''
179
172
  str
180
173
  end
181
174
 
@@ -249,17 +242,10 @@ class ::UNIXSocket
249
242
  Polyphony.backend_send(self, mesg, 0)
250
243
  end
251
244
 
252
- def readpartial(maxlen, str = nil)
253
- @read_buffer ||= +''
254
- result = Polyphony.backend_recv(self, @read_buffer, maxlen)
245
+ def readpartial(maxlen, str = +'')
246
+ result = Polyphony.backend_recv(self, str, maxlen)
255
247
  raise EOFError unless result
256
248
 
257
- if str
258
- str << @read_buffer
259
- else
260
- str = @read_buffer
261
- end
262
- @read_buffer = +''
263
249
  str
264
250
  end
265
251
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polyphony
4
- VERSION = '0.53.0'
4
+ VERSION = '0.56.0'
5
5
  end
data/polyphony.gemspec CHANGED
@@ -37,6 +37,7 @@ Gem::Specification.new do |s|
37
37
  s.add_development_dependency 'mysql2', '0.5.3'
38
38
  s.add_development_dependency 'sequel', '5.34.0'
39
39
  s.add_development_dependency 'httparty', '0.17.1'
40
+ s.add_development_dependency 'localhost', '~>1.1.4'
40
41
 
41
42
  # s.add_development_dependency 'jekyll', '~>3.8.6'
42
43
  # s.add_development_dependency 'jekyll-remote-theme', '~>0.4.1'
data/test/helper.rb CHANGED
@@ -15,9 +15,9 @@ require 'minitest/reporters'
15
15
 
16
16
  ::Exception.__disable_sanitized_backtrace__ = true
17
17
 
18
- Minitest::Reporters.use! [
19
- Minitest::Reporters::SpecReporter.new
20
- ]
18
+ # Minitest::Reporters.use! [
19
+ # Minitest::Reporters::SpecReporter.new
20
+ # ]
21
21
 
22
22
  class ::Fiber
23
23
  attr_writer :auto_watcher
data/test/test_backend.rb CHANGED
@@ -26,7 +26,7 @@ class BackendTest < MiniTest::Test
26
26
  @backend.sleep 0.01
27
27
  count += 1
28
28
  }.await
29
- assert_in_delta 0.03, Time.now - t0, 0.005
29
+ assert_in_delta 0.03, Time.now - t0, 0.01
30
30
  assert_equal 3, count
31
31
  end
32
32
 
@@ -241,9 +241,10 @@ class BackendTest < MiniTest::Test
241
241
  def test_splice
242
242
  i1, o1 = IO.pipe
243
243
  i2, o2 = IO.pipe
244
+ len = nil
244
245
 
245
246
  spin {
246
- o2.splice(i1, 1000)
247
+ len = o2.splice(i1, 1000)
247
248
  o2.close
248
249
  }
249
250
 
@@ -251,14 +252,16 @@ class BackendTest < MiniTest::Test
251
252
  result = i2.read
252
253
 
253
254
  assert_equal 'foobar', result
255
+ assert_equal 6, len
254
256
  end
255
257
 
256
258
  def test_splice_to_eof
257
259
  i1, o1 = IO.pipe
258
260
  i2, o2 = IO.pipe
261
+ len = nil
259
262
 
260
263
  f = spin {
261
- o2.splice_to_eof(i1, 1000)
264
+ len = o2.splice_to_eof(i1, 1000)
262
265
  o2.close
263
266
  }
264
267
 
@@ -269,8 +272,159 @@ class BackendTest < MiniTest::Test
269
272
  o1.write('bar')
270
273
  result = i2.readpartial(1000)
271
274
  assert_equal 'bar', result
272
- ensure
273
- f.interrupt
275
+ o1.close
274
276
  f.await
277
+ assert_equal 6, len
278
+ ensure
279
+ if f.alive?
280
+ f.interrupt
281
+ f.await
282
+ end
283
+ end
284
+
285
+ def test_idle_gc
286
+ GC.disable
287
+
288
+ count = GC.count
289
+ snooze
290
+ assert_equal count, GC.count
291
+ sleep 0.01
292
+ assert_equal count, GC.count
293
+
294
+ @backend.idle_gc_period = 0.1
295
+ snooze
296
+ assert_equal count, GC.count
297
+ sleep 0.05
298
+ assert_equal count, GC.count
299
+ # The idle tasks are ran at most once per fiber switch, before the backend
300
+ # is polled. Therefore, the second sleep will not have triggered a GC, since
301
+ # only 0.05s have passed since the gc period was set.
302
+ sleep 0.07
303
+ assert_equal count, GC.count
304
+ # Upon the third sleep the GC should be triggered, at 0.12s post setting the
305
+ # GC period.
306
+ sleep 0.05
307
+ assert_equal count + 1, GC.count
308
+ ensure
309
+ GC.enable
310
+ end
311
+ end
312
+
313
+ class BackendChainTest < MiniTest::Test
314
+ def setup
315
+ super
316
+ @prev_backend = Thread.current.backend
317
+ @backend = Polyphony::Backend.new
318
+ Thread.current.backend = @backend
319
+ end
320
+
321
+ def teardown
322
+ @backend.finalize
323
+ Thread.current.backend = @prev_backend
324
+ end
325
+
326
+ def test_simple_write_chain
327
+ i, o = IO.pipe
328
+
329
+ result = Thread.backend.chain(
330
+ [:write, o, 'hello'],
331
+ [:write, o, ' world']
332
+ )
333
+
334
+ assert_equal 6, result
335
+ o.close
336
+ assert_equal 'hello world', i.read
337
+ end
338
+
339
+ def test_simple_send_chain
340
+ port = rand(1234..5678)
341
+ server = TCPServer.new('127.0.0.1', port)
342
+
343
+ server_fiber = spin do
344
+ while (socket = server.accept)
345
+ spin do
346
+ while (data = socket.gets(8192))
347
+ socket << data
348
+ end
349
+ end
350
+ end
351
+ end
352
+
353
+ snooze
354
+ client = TCPSocket.new('127.0.0.1', port)
355
+
356
+ result = Thread.backend.chain(
357
+ [:send, client, 'hello', 0],
358
+ [:send, client, " world\n", 0]
359
+ )
360
+ sleep 0.1
361
+ assert_equal "hello world\n", client.recv(8192)
362
+ client.close
363
+ ensure
364
+ server_fiber&.stop
365
+ server_fiber&.await
366
+ server&.close
367
+ end
368
+
369
+ def chunk_header(len)
370
+ "Content-Length: #{len}\r\n\r\n"
371
+ end
372
+
373
+ def serve_io(from, to)
374
+ i, o = IO.pipe
375
+ backend = Thread.current.backend
376
+ while true
377
+ len = o.splice(from, 8192)
378
+ break if len == 0
379
+
380
+ backend.chain(
381
+ [:write, to, chunk_header(len)],
382
+ [:splice, i, to, len]
383
+ )
384
+ end
385
+ to.close
386
+ end
387
+
388
+ def test_chain_with_splice
389
+ from_r, from_w = IO.pipe
390
+ to_r, to_w = IO.pipe
391
+
392
+ result = nil
393
+ f = spin { serve_io(from_r, to_w) }
394
+
395
+ from_w << 'Hello world!'
396
+ from_w.close
397
+
398
+ assert_equal "Content-Length: 12\r\n\r\nHello world!", to_r.read
399
+ end
400
+
401
+ def test_invalid_op
402
+ i, o = IO.pipe
403
+
404
+ assert_raises(RuntimeError) {
405
+ Thread.backend.chain(
406
+ [:read, i]
407
+ )
408
+ }
409
+
410
+ assert_raises(RuntimeError) {
411
+ Thread.backend.chain(
412
+ [:write, o, 'abc'],
413
+ [:write, o, 'abc'],
414
+ [:write, o, 'abc'],
415
+ [:read, i]
416
+ )
417
+ }
418
+
419
+ assert_raises(RuntimeError) {
420
+ Thread.backend.chain(
421
+ [:write, o]
422
+ )
423
+ }
424
+
425
+ # Eventually we should add some APIs to the io_uring backend to query the
426
+ # contxt store, then add some tests here to verify that the chain op ctx is
427
+ # released properly before raising the error (for the time being this has
428
+ # been verified manually).
275
429
  end
276
430
  end