polyphony 0.53.2 → 0.58

Sign up to get free protection for your applications and to get access to all the features.
@@ -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);
@@ -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
  }
@@ -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
 
@@ -105,4 +105,12 @@ class ::Thread
105
105
  main_fiber << value
106
106
  end
107
107
  alias_method :send, :<<
108
+
109
+ def idle_gc_period=(period)
110
+ backend.idle_gc_period = period
111
+ end
112
+
113
+ def on_idle(&block)
114
+ backend.idle_block = block
115
+ end
108
116
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polyphony
4
- VERSION = '0.53.2'
4
+ VERSION = '0.58'
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
 
@@ -281,6 +281,97 @@ class BackendTest < MiniTest::Test
281
281
  f.await
282
282
  end
283
283
  end
284
+
285
+
286
+ def test_splice_chunks
287
+ body = 'abcd' * 250
288
+ chunk_size = 750
289
+
290
+ buf = +''
291
+ r, w = IO.pipe
292
+ reader = spin do
293
+ r.read_loop { |data| buf << data }
294
+ end
295
+
296
+ i, o = IO.pipe
297
+ writer = spin do
298
+ o << body
299
+ o.close
300
+ end
301
+ Thread.current.backend.splice_chunks(
302
+ i,
303
+ w,
304
+ "Content-Type: foo\r\n\r\n",
305
+ "0\r\n\r\n",
306
+ ->(len) { "#{len.to_s(16)}\r\n" },
307
+ "\r\n",
308
+ chunk_size
309
+ )
310
+ w.close
311
+ reader.await
312
+
313
+ expected = "Content-Type: foo\r\n\r\n#{750.to_s(16)}\r\n#{body[0..749]}\r\n#{250.to_s(16)}\r\n#{body[750..999]}\r\n0\r\n\r\n"
314
+ assert_equal expected, buf
315
+ ensure
316
+ o.close
317
+ w.close
318
+ end
319
+
320
+ def test_idle_gc
321
+ GC.disable
322
+
323
+ count = GC.count
324
+ snooze
325
+ assert_equal count, GC.count
326
+ sleep 0.01
327
+ assert_equal count, GC.count
328
+
329
+ @backend.idle_gc_period = 0.1
330
+ snooze
331
+ assert_equal count, GC.count
332
+ sleep 0.05
333
+ assert_equal count, GC.count
334
+ # The idle tasks are ran at most once per fiber switch, before the backend
335
+ # is polled. Therefore, the second sleep will not have triggered a GC, since
336
+ # only 0.05s have passed since the gc period was set.
337
+ sleep 0.07
338
+ assert_equal count, GC.count
339
+ # Upon the third sleep the GC should be triggered, at 0.12s post setting the
340
+ # GC period.
341
+ sleep 0.05
342
+ assert_equal count + 1, GC.count
343
+
344
+ @backend.idle_gc_period = 0
345
+ count = GC.count
346
+ sleep 0.001
347
+ sleep 0.002
348
+ sleep 0.003
349
+ assert_equal count, GC.count
350
+ ensure
351
+ GC.enable
352
+ end
353
+
354
+ def test_idle_block
355
+ counter = 0
356
+
357
+ @backend.idle_block = proc { counter += 1 }
358
+
359
+ 3.times { snooze }
360
+ assert_equal 0, counter
361
+
362
+ sleep 0.01
363
+ assert_equal 1, counter
364
+ sleep 0.01
365
+ assert_equal 2, counter
366
+
367
+ assert_equal 2, counter
368
+ 3.times { snooze }
369
+ assert_equal 2, counter
370
+
371
+ @backend.idle_block = nil
372
+ sleep 0.01
373
+ assert_equal 2, counter
374
+ end
284
375
  end
285
376
 
286
377
  class BackendChainTest < MiniTest::Test
@@ -309,6 +400,36 @@ class BackendChainTest < MiniTest::Test
309
400
  assert_equal 'hello world', i.read
310
401
  end
311
402
 
403
+ def test_simple_send_chain
404
+ port = rand(1234..5678)
405
+ server = TCPServer.new('127.0.0.1', port)
406
+
407
+ server_fiber = spin do
408
+ while (socket = server.accept)
409
+ spin do
410
+ while (data = socket.gets(8192))
411
+ socket << data
412
+ end
413
+ end
414
+ end
415
+ end
416
+
417
+ snooze
418
+ client = TCPSocket.new('127.0.0.1', port)
419
+
420
+ result = Thread.backend.chain(
421
+ [:send, client, 'hello', 0],
422
+ [:send, client, " world\n", 0]
423
+ )
424
+ sleep 0.1
425
+ assert_equal "hello world\n", client.recv(8192)
426
+ client.close
427
+ ensure
428
+ server_fiber&.stop
429
+ server_fiber&.await
430
+ server&.close
431
+ end
432
+
312
433
  def chunk_header(len)
313
434
  "Content-Length: #{len}\r\n\r\n"
314
435
  end
@@ -346,7 +467,16 @@ class BackendChainTest < MiniTest::Test
346
467
 
347
468
  assert_raises(RuntimeError) {
348
469
  Thread.backend.chain(
349
- [:read, o]
470
+ [:read, i]
471
+ )
472
+ }
473
+
474
+ assert_raises(RuntimeError) {
475
+ Thread.backend.chain(
476
+ [:write, o, 'abc'],
477
+ [:write, o, 'abc'],
478
+ [:write, o, 'abc'],
479
+ [:read, i]
350
480
  )
351
481
  }
352
482
 
@@ -355,5 +485,10 @@ class BackendChainTest < MiniTest::Test
355
485
  [:write, o]
356
486
  )
357
487
  }
488
+
489
+ # Eventually we should add some APIs to the io_uring backend to query the
490
+ # contxt store, then add some tests here to verify that the chain op ctx is
491
+ # released properly before raising the error (for the time being this has
492
+ # been verified manually).
358
493
  end
359
494
  end