polyphony 0.53.1 → 0.57.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +1 -1
- data/.gitignore +3 -1
- data/CHANGELOG.md +50 -24
- data/Gemfile.lock +3 -1
- data/TODO.md +0 -3
- data/examples/core/idle_gc.rb +21 -0
- data/examples/core/queue.rb +19 -0
- data/examples/io/https_server.rb +30 -0
- data/examples/io/pipe.rb +11 -0
- data/examples/io/splice_chunks.rb +29 -0
- data/examples/io/stdio.rb +8 -0
- data/ext/polyphony/backend_common.c +186 -0
- data/ext/polyphony/backend_common.h +25 -130
- data/ext/polyphony/backend_io_uring.c +369 -108
- data/ext/polyphony/backend_io_uring_context.c +14 -2
- data/ext/polyphony/backend_io_uring_context.h +11 -11
- data/ext/polyphony/backend_libev.c +406 -94
- data/ext/polyphony/polyphony.c +17 -15
- data/ext/polyphony/polyphony.h +3 -0
- data/ext/polyphony/runqueue.c +29 -1
- data/ext/polyphony/thread.c +19 -4
- data/lib/polyphony/core/sync.rb +8 -0
- data/lib/polyphony/extensions/openssl.rb +24 -17
- data/lib/polyphony/extensions/socket.rb +6 -20
- data/lib/polyphony/version.rb +1 -1
- data/polyphony.gemspec +1 -0
- data/test/helper.rb +3 -3
- data/test/test_backend.rb +109 -3
- data/test/test_fiber.rb +0 -1
- data/test/test_io.rb +6 -3
- data/test/test_signal.rb +1 -1
- data/test/test_sync.rb +43 -0
- data/test/test_thread_pool.rb +1 -1
- data/test/test_timer.rb +16 -10
- metadata +23 -2
data/ext/polyphony/polyphony.c
CHANGED
@@ -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
|
162
|
-
ID_caller
|
163
|
-
ID_clear
|
164
|
-
ID_each
|
165
|
-
ID_inspect
|
166
|
-
ID_invoke
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
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
|
}
|
data/ext/polyphony/polyphony.h
CHANGED
@@ -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);
|
data/ext/polyphony/runqueue.c
CHANGED
@@ -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
|
-
|
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);
|
data/ext/polyphony/thread.c
CHANGED
@@ -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
|
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
|
-
|
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
|
-
|
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
|
}
|
data/lib/polyphony/core/sync.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
require 'openssl'
|
4
4
|
require_relative './socket'
|
5
5
|
|
6
|
-
#
|
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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
39
|
+
buf = +''
|
40
40
|
while true
|
41
|
-
result = recvfrom_nonblock(maxlen, flags,
|
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 =
|
169
|
-
|
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 =
|
253
|
-
|
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
|
|
data/lib/polyphony/version.rb
CHANGED
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
|
-
|
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.
|
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,
|
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(
|
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
|