polyphony 0.52.0 → 0.55.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.
@@ -6,6 +6,7 @@ void Init_Backend();
6
6
  void Init_Queue();
7
7
  void Init_Event();
8
8
  void Init_Runqueue();
9
+ void Init_SocketExtensions();
9
10
  void Init_Thread();
10
11
  void Init_Tracing();
11
12
 
@@ -24,6 +25,8 @@ void Init_polyphony_ext() {
24
25
  Init_Thread();
25
26
  Init_Tracing();
26
27
 
28
+ Init_SocketExtensions();
29
+
27
30
  #ifdef POLYPHONY_PLAYGROUND
28
31
  playground();
29
32
  #endif
@@ -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);
@@ -0,0 +1,33 @@
1
+ #include "polyphony.h"
2
+
3
+ VALUE Socket_send(VALUE self, VALUE msg, VALUE flags) {
4
+ return Backend_send(BACKEND(), self, msg, flags);
5
+ }
6
+
7
+ VALUE Socket_write(int argc, VALUE *argv, VALUE self) {
8
+ VALUE ary = rb_ary_new_from_values(argc, argv);
9
+ VALUE result = Backend_sendv(BACKEND(), self, ary, INT2NUM(0));
10
+ RB_GC_GUARD(ary);
11
+ return result;
12
+ }
13
+
14
+ VALUE Socket_double_chevron(VALUE self, VALUE msg) {
15
+ Backend_send(BACKEND(), self, msg, INT2NUM(0));
16
+ return self;
17
+ }
18
+
19
+ void Init_SocketExtensions() {
20
+ rb_require("socket");
21
+
22
+ VALUE cSocket = rb_const_get(rb_cObject, rb_intern("Socket"));
23
+ VALUE cTCPSocket = rb_const_get(rb_cObject, rb_intern("TCPSocket"));
24
+
25
+ rb_define_method(cSocket, "send", Socket_send, 2);
26
+ rb_define_method(cTCPSocket, "send", Socket_send, 2);
27
+
28
+ rb_define_method(cSocket, "write", Socket_write, -1);
29
+ rb_define_method(cTCPSocket, "write", Socket_write, -1);
30
+
31
+ rb_define_method(cSocket, "<<", Socket_double_chevron, 1);
32
+ rb_define_method(cTCPSocket, "<<", Socket_double_chevron, 1);
33
+ }
@@ -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,7 +86,7 @@ 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
91
 
92
92
  if (__tracing_enabled__ && (rb_ivar_get(current_fiber, ID_ivar_running) != Qfalse))
@@ -95,14 +95,24 @@ VALUE Thread_switch_fiber(VALUE self) {
95
95
  while (1) {
96
96
  next = Runqueue_shift(runqueue);
97
97
  if (next.fiber != Qnil) {
98
- if (!backend_was_polled && pending_count) {
98
+ // Polling for I/O op completion is normally done when the run queue is
99
+ // empty, but if the runqueue never empties, we'll never get to process
100
+ // any event completions. In order to prevent this, an anti-starve
101
+ // mechanism is employed, under the following conditions:
102
+ // - a blocking poll was not yet performed
103
+ // - there are pending blocking operations
104
+ // - the runqueue has signalled that a non-blocking poll should be
105
+ // performed
106
+ // - the run queue length high watermark has reached its threshold (currently 128)
107
+ // - the run queue switch counter has reached its threshold (currently 64)
108
+ if (!backend_was_polled && pending_ops_count && Runqueue_should_poll_nonblocking(runqueue)) {
99
109
  // this prevents event starvation in case the run queue never empties
100
110
  Backend_poll(backend, Qtrue, current_fiber, runqueue);
101
111
  }
102
112
  break;
103
113
  }
104
- if (pending_count == 0) break;
105
-
114
+
115
+ if (pending_ops_count == 0) break;
106
116
  Backend_poll(backend, Qnil, current_fiber, runqueue);
107
117
  backend_was_polled = 1;
108
118
  }
@@ -138,6 +148,10 @@ VALUE Thread_debug(VALUE self) {
138
148
  return self;
139
149
  }
140
150
 
151
+ VALUE Thread_class_backend(VALUE _self) {
152
+ return rb_ivar_get(rb_thread_current(), ID_ivar_backend);
153
+ }
154
+
141
155
  void Init_Thread() {
142
156
  rb_define_method(rb_cThread, "setup_fiber_scheduling", Thread_setup_fiber_scheduling, 0);
143
157
  rb_define_method(rb_cThread, "fiber_scheduling_stats", Thread_fiber_scheduling_stats, 0);
@@ -150,6 +164,8 @@ void Init_Thread() {
150
164
  rb_define_method(rb_cThread, "fiber_scheduling_index", Thread_fiber_scheduling_index, 1);
151
165
  rb_define_method(rb_cThread, "fiber_unschedule", Thread_fiber_unschedule, 1);
152
166
 
167
+ rb_define_singleton_method(rb_cThread, "backend", Thread_class_backend, 0);
168
+
153
169
  rb_define_method(rb_cThread, "debug!", Thread_debug, 0);
154
170
 
155
171
  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
@@ -247,4 +247,12 @@ class ::IO
247
247
  self
248
248
  end
249
249
  end
250
+
251
+ def splice(src, maxlen)
252
+ Polyphony.backend_splice(src, self, maxlen)
253
+ end
254
+
255
+ def splice_to_eof(src, chunksize = 8192)
256
+ Polyphony.backend_splice_to_eof(src, self, chunksize)
257
+ end
250
258
  end
@@ -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)
@@ -48,14 +48,17 @@ class ::Socket
48
48
  end
49
49
  end
50
50
 
51
- def send(mesg, flags)
52
- Polyphony.backend_send(self, mesg, flags)
53
- end
51
+ # def send(mesg, flags)
52
+ # Polyphony.backend_send(self, mesg, flags)
53
+ # end
54
54
 
55
- def write(*args)
56
- Polyphony.backend_sendv(self, args, 0)
57
- end
58
- alias_method :<<, :write
55
+ # def write(*args)
56
+ # Polyphony.backend_sendv(self, args, 0)
57
+ # end
58
+
59
+ # def <<(mesg)
60
+ # Polyphony.backend_send(self, mesg, 0)
61
+ # end
59
62
 
60
63
  def readpartial(maxlen, str = +'')
61
64
  Polyphony.backend_recv(self, str, maxlen)
@@ -150,26 +153,22 @@ class ::TCPSocket
150
153
  Polyphony.backend_recv_feed_loop(self, receiver, method, &block)
151
154
  end
152
155
 
153
- def send(mesg, flags)
154
- Polyphony.backend_send(self, mesg, flags)
155
- end
156
+ # def send(mesg, flags)
157
+ # Polyphony.backend_send(self, mesg, flags)
158
+ # end
156
159
 
157
- def write(*args)
158
- Polyphony.backend_sendv(self, args, 0)
159
- end
160
- alias_method :<<, :write
160
+ # def write(*args)
161
+ # Polyphony.backend_sendv(self, args, 0)
162
+ # end
161
163
 
162
- def readpartial(maxlen, str = nil)
163
- @read_buffer ||= +''
164
- result = Polyphony.backend_recv(self, @read_buffer, maxlen)
164
+ # def <<(mesg)
165
+ # Polyphony.backend_send(self, mesg, 0)
166
+ # end
167
+
168
+ def readpartial(maxlen, str = +'')
169
+ result = Polyphony.backend_recv(self, str, maxlen)
165
170
  raise EOFError unless result
166
171
 
167
- if str
168
- str << @read_buffer
169
- else
170
- str = @read_buffer
171
- end
172
- @read_buffer = +''
173
172
  str
174
173
  end
175
174
 
@@ -238,19 +237,15 @@ class ::UNIXSocket
238
237
  def write(*args)
239
238
  Polyphony.backend_sendv(self, args, 0)
240
239
  end
241
- alias_method :<<, :write
242
240
 
243
- def readpartial(maxlen, str = nil)
244
- @read_buffer ||= +''
245
- result = Polyphony.backend_recv(self, @read_buffer, maxlen)
241
+ def <<(mesg)
242
+ Polyphony.backend_send(self, mesg, 0)
243
+ end
244
+
245
+ def readpartial(maxlen, str = +'')
246
+ result = Polyphony.backend_recv(self, str, maxlen)
246
247
  raise EOFError unless result
247
248
 
248
- if str
249
- str << @read_buffer
250
- else
251
- str = @read_buffer
252
- end
253
- @read_buffer = +''
254
249
  str
255
250
  end
256
251
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polyphony
4
- VERSION = '0.52.0'
4
+ VERSION = '0.55.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
 
@@ -237,4 +237,167 @@ class BackendTest < MiniTest::Test
237
237
  end
238
238
  assert_equal [1], buffer
239
239
  end
240
+
241
+ def test_splice
242
+ i1, o1 = IO.pipe
243
+ i2, o2 = IO.pipe
244
+ len = nil
245
+
246
+ spin {
247
+ len = o2.splice(i1, 1000)
248
+ o2.close
249
+ }
250
+
251
+ o1.write('foobar')
252
+ result = i2.read
253
+
254
+ assert_equal 'foobar', result
255
+ assert_equal 6, len
256
+ end
257
+
258
+ def test_splice_to_eof
259
+ i1, o1 = IO.pipe
260
+ i2, o2 = IO.pipe
261
+ len = nil
262
+
263
+ f = spin {
264
+ len = o2.splice_to_eof(i1, 1000)
265
+ o2.close
266
+ }
267
+
268
+ o1.write('foo')
269
+ result = i2.readpartial(1000)
270
+ assert_equal 'foo', result
271
+
272
+ o1.write('bar')
273
+ result = i2.readpartial(1000)
274
+ assert_equal 'bar', result
275
+ o1.close
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
+ end
285
+
286
+ class BackendChainTest < MiniTest::Test
287
+ def setup
288
+ super
289
+ @prev_backend = Thread.current.backend
290
+ @backend = Polyphony::Backend.new
291
+ Thread.current.backend = @backend
292
+ end
293
+
294
+ def teardown
295
+ @backend.finalize
296
+ Thread.current.backend = @prev_backend
297
+ end
298
+
299
+ def test_simple_write_chain
300
+ i, o = IO.pipe
301
+
302
+ result = Thread.backend.chain(
303
+ [:write, o, 'hello'],
304
+ [:write, o, ' world']
305
+ )
306
+
307
+ assert_equal 6, result
308
+ o.close
309
+ assert_equal 'hello world', i.read
310
+ end
311
+
312
+ def test_simple_send_chain
313
+ port = rand(1234..5678)
314
+ server = TCPServer.new('127.0.0.1', port)
315
+
316
+ server_fiber = spin do
317
+ while (socket = server.accept)
318
+ spin do
319
+ while (data = socket.gets(8192))
320
+ socket << data
321
+ end
322
+ end
323
+ end
324
+ end
325
+
326
+ snooze
327
+ client = TCPSocket.new('127.0.0.1', port)
328
+
329
+ result = Thread.backend.chain(
330
+ [:send, client, 'hello', 0],
331
+ [:send, client, " world\n", 0]
332
+ )
333
+ sleep 0.1
334
+ assert_equal "hello world\n", client.recv(8192)
335
+ client.close
336
+ ensure
337
+ server_fiber&.stop
338
+ server_fiber&.await
339
+ server&.close
340
+ end
341
+
342
+ def chunk_header(len)
343
+ "Content-Length: #{len}\r\n\r\n"
344
+ end
345
+
346
+ def serve_io(from, to)
347
+ i, o = IO.pipe
348
+ backend = Thread.current.backend
349
+ while true
350
+ len = o.splice(from, 8192)
351
+ break if len == 0
352
+
353
+ backend.chain(
354
+ [:write, to, chunk_header(len)],
355
+ [:splice, i, to, len]
356
+ )
357
+ end
358
+ to.close
359
+ end
360
+
361
+ def test_chain_with_splice
362
+ from_r, from_w = IO.pipe
363
+ to_r, to_w = IO.pipe
364
+
365
+ result = nil
366
+ f = spin { serve_io(from_r, to_w) }
367
+
368
+ from_w << 'Hello world!'
369
+ from_w.close
370
+
371
+ assert_equal "Content-Length: 12\r\n\r\nHello world!", to_r.read
372
+ end
373
+
374
+ def test_invalid_op
375
+ i, o = IO.pipe
376
+
377
+ assert_raises(RuntimeError) {
378
+ Thread.backend.chain(
379
+ [:read, i]
380
+ )
381
+ }
382
+
383
+ assert_raises(RuntimeError) {
384
+ Thread.backend.chain(
385
+ [:write, o, 'abc'],
386
+ [:write, o, 'abc'],
387
+ [:write, o, 'abc'],
388
+ [:read, i]
389
+ )
390
+ }
391
+
392
+ assert_raises(RuntimeError) {
393
+ Thread.backend.chain(
394
+ [:write, o]
395
+ )
396
+ }
397
+
398
+ # Eventually we should add some APIs to the io_uring backend to query the
399
+ # contxt store, then add some tests here to verify that the chain op ctx is
400
+ # released properly before raising the error (for the time being this has
401
+ # been verified manually).
402
+ end
240
403
  end