polyphony 0.53.1 → 0.57.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);
@@ -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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polyphony
4
- VERSION = '0.53.1'
4
+ VERSION = '0.57.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
 
@@ -281,6 +281,68 @@ 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
+ ensure
344
+ GC.enable
345
+ end
284
346
  end
285
347
 
286
348
  class BackendChainTest < MiniTest::Test
@@ -309,6 +371,36 @@ class BackendChainTest < MiniTest::Test
309
371
  assert_equal 'hello world', i.read
310
372
  end
311
373
 
374
+ def test_simple_send_chain
375
+ port = rand(1234..5678)
376
+ server = TCPServer.new('127.0.0.1', port)
377
+
378
+ server_fiber = spin do
379
+ while (socket = server.accept)
380
+ spin do
381
+ while (data = socket.gets(8192))
382
+ socket << data
383
+ end
384
+ end
385
+ end
386
+ end
387
+
388
+ snooze
389
+ client = TCPSocket.new('127.0.0.1', port)
390
+
391
+ result = Thread.backend.chain(
392
+ [:send, client, 'hello', 0],
393
+ [:send, client, " world\n", 0]
394
+ )
395
+ sleep 0.1
396
+ assert_equal "hello world\n", client.recv(8192)
397
+ client.close
398
+ ensure
399
+ server_fiber&.stop
400
+ server_fiber&.await
401
+ server&.close
402
+ end
403
+
312
404
  def chunk_header(len)
313
405
  "Content-Length: #{len}\r\n\r\n"
314
406
  end
@@ -346,14 +438,28 @@ class BackendChainTest < MiniTest::Test
346
438
 
347
439
  assert_raises(RuntimeError) {
348
440
  Thread.backend.chain(
349
- [:read, o]
441
+ [:read, i]
442
+ )
443
+ }
444
+
445
+ assert_raises(RuntimeError) {
446
+ Thread.backend.chain(
447
+ [:write, o, 'abc'],
448
+ [:write, o, 'abc'],
449
+ [:write, o, 'abc'],
450
+ [:read, i]
350
451
  )
351
452
  }
352
453
 
353
- assert_raises(TypeError) {
454
+ assert_raises(RuntimeError) {
354
455
  Thread.backend.chain(
355
456
  [:write, o]
356
457
  )
357
458
  }
459
+
460
+ # Eventually we should add some APIs to the io_uring backend to query the
461
+ # contxt store, then add some tests here to verify that the chain op ctx is
462
+ # released properly before raising the error (for the time being this has
463
+ # been verified manually).
358
464
  end
359
465
  end