polyphony 0.46.1 → 0.47.4
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/Gemfile.lock +1 -1
- data/TODO.md +37 -14
- data/examples/core/enumerable.rb +64 -0
- data/examples/io/unix_socket.rb +26 -0
- data/examples/performance/fiber_resume.rb +43 -0
- data/examples/performance/thread-vs-fiber/compare.rb +59 -0
- data/examples/performance/thread-vs-fiber/em_server.rb +33 -0
- data/examples/performance/thread-vs-fiber/polyphony_server.rb +4 -3
- data/examples/performance/thread-vs-fiber/threaded_server.rb +22 -15
- data/examples/performance/thread_switch.rb +44 -0
- data/ext/polyphony/backend_common.h +29 -0
- data/ext/polyphony/backend_io_uring.c +88 -13
- data/ext/polyphony/backend_io_uring_context.c +1 -0
- data/ext/polyphony/backend_io_uring_context.h +1 -0
- data/ext/polyphony/backend_libev.c +82 -15
- data/ext/polyphony/fiber.c +10 -1
- data/ext/polyphony/polyphony.c +3 -0
- data/ext/polyphony/polyphony.h +3 -6
- data/ext/polyphony/queue.c +99 -34
- data/lib/polyphony/core/global_api.rb +45 -32
- data/lib/polyphony/extensions/fiber.rb +8 -2
- data/lib/polyphony/extensions/socket.rb +74 -15
- data/lib/polyphony/version.rb +1 -1
- data/test/test_backend.rb +48 -0
- data/test/test_fiber.rb +33 -4
- data/test/test_global_api.rb +72 -1
- data/test/test_queue.rb +117 -0
- data/test/test_signal.rb +18 -0
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f32962c9f1004bc9c63afcaddaad91dc8878da5e8290b8e9d42d591433311c0b
|
4
|
+
data.tar.gz: 3b43ad185ccb0778d88d9c8ac1043bbff291ac0a55d5bee80da8b6e08a1f6493
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b4ba80ea441862f22398e624996144e9ed7f8297b56e2f14035c537671282f4338b3843ff192959a7031134e761d273c5050043b08b5051c1653ecc3f986cddb
|
7
|
+
data.tar.gz: '04850a036f5278d221b6610259e70fbb7e44e8afd04237d9a6906fa826a836c786734123894a0b7197c5c98502a9b5304896b09a1e7b6d16433e363dbd1803e6'
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,24 @@
|
|
1
|
+
## 0.47.4
|
2
|
+
|
3
|
+
* Add support for Unix sockets
|
4
|
+
|
5
|
+
## 0.47.3
|
6
|
+
|
7
|
+
* Enable I/O in signal handlers (#45)
|
8
|
+
* Accept `:interval` argument in `#spin_loop`
|
9
|
+
|
10
|
+
## 0.47.2
|
11
|
+
|
12
|
+
* Fix API compatibility between TCPSocket and IO
|
13
|
+
|
14
|
+
## 0.47.0
|
15
|
+
|
16
|
+
* Implement `#spin_scope` used for creating blocking fiber scopes
|
17
|
+
* Reimplement `move_on_after`, `cancel_after`, `Timeout.timeout` using
|
18
|
+
`Backend#timeout` (avoids creating canceller fiber for most common use case)
|
19
|
+
* Implement `Backend#timeout` API
|
20
|
+
* Implemented capped queues
|
21
|
+
|
1
22
|
## 0.46.1
|
2
23
|
|
3
24
|
* Add `TCPServer#accept_loop`, `OpenSSL::SSL::SSLSocket#accept_loop` method
|
data/Gemfile.lock
CHANGED
data/TODO.md
CHANGED
@@ -1,5 +1,42 @@
|
|
1
|
+
- Graceful shutdown again:
|
2
|
+
|
3
|
+
- Add `Polyphony::GracefulShutdown` exception
|
4
|
+
- Two exceptions for stopping fibers:
|
5
|
+
- `Polyphony::GracefulShutdown` - graceful shutdown
|
6
|
+
- `Polyphony::Terminate` - ungraceful shutdown
|
7
|
+
- Fiber API:
|
8
|
+
- `Fiber#shutdown_children` - graceful shutdown of all children
|
9
|
+
- `Fiber#terminate_children` - ungraceful shutdown of all children
|
10
|
+
|
11
|
+
- Add `Fiber#graceful_shutdown?` method
|
12
|
+
- Returns false unless a `Polyphony::GracefulShutdown` was raised
|
13
|
+
- Override `Polyphony::Terminate#invoke` to reset the `@graceful_shutdown` fiber
|
14
|
+
flag
|
15
|
+
|
16
|
+
And then we have:
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
spin do
|
20
|
+
loop { do_some_stuff }
|
21
|
+
ensure
|
22
|
+
return unless Fiber.current.graceful_shutdown?
|
23
|
+
|
24
|
+
shutdown_gracefully
|
25
|
+
end
|
26
|
+
```
|
27
|
+
|
28
|
+
- When a fiber is stopped it should use `Polyphony::Terminate` to stop child
|
29
|
+
fibers, *unless* it was stopped with a `Polyphony::GracefulShutdown` (which it
|
30
|
+
can check with `@graceful_shutdown`).
|
31
|
+
|
32
|
+
- More tight loops
|
33
|
+
- IO#gets_loop, Socket#gets_loop, OpenSSL::Socket#gets_loop (medium effort)
|
34
|
+
- Fiber#receive_loop (very little effort, should be implemented in C)
|
35
|
+
|
1
36
|
## Roadmap for Polyphony 1.0
|
2
37
|
|
38
|
+
- check integration with rb-inotify
|
39
|
+
|
3
40
|
- Check why worker-thread example doesn't work.
|
4
41
|
- Add test that mimics the original design for Monocrono:
|
5
42
|
- 256 fibers each waiting for a message
|
@@ -192,17 +229,3 @@ Prior art:
|
|
192
229
|
|
193
230
|
- https://github.com/socketry/async-dns
|
194
231
|
|
195
|
-
## Work on API
|
196
|
-
|
197
|
-
- Add option for setting the exception raised on cancelling using `#cancel_after`:
|
198
|
-
|
199
|
-
```ruby
|
200
|
-
cancel_after(3, with_error: MyErrorClass) do
|
201
|
-
do_my_thing
|
202
|
-
end
|
203
|
-
# or a RuntimeError with message
|
204
|
-
cancel_after(3, with_error: 'Cancelled due to timeout') do
|
205
|
-
do_my_thing
|
206
|
-
end
|
207
|
-
```
|
208
|
-
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'polyphony'
|
5
|
+
|
6
|
+
Exception.__disable_sanitized_backtrace__ = true
|
7
|
+
|
8
|
+
module Enumerable
|
9
|
+
def map_concurrently(&block)
|
10
|
+
spin do
|
11
|
+
results = []
|
12
|
+
each_with_index do |i, idx|
|
13
|
+
spin { results[idx] = block.(i) }
|
14
|
+
end
|
15
|
+
Fiber.current.await_all_children
|
16
|
+
results
|
17
|
+
end.await
|
18
|
+
end
|
19
|
+
|
20
|
+
def each_concurrently(max_fibers: nil, &block)
|
21
|
+
return each_concurrently_with_fiber_pool(max_fibers, &block) if max_fibers
|
22
|
+
|
23
|
+
spin do
|
24
|
+
results = []
|
25
|
+
each do |i|
|
26
|
+
spin(&block).schedule(i)
|
27
|
+
end
|
28
|
+
Fiber.current.await_all_children
|
29
|
+
end.await
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
def each_concurrently_with_fiber_pool(max_fibers, &block)
|
34
|
+
spin do
|
35
|
+
fiber_count = 0
|
36
|
+
workers = []
|
37
|
+
each do |i|
|
38
|
+
if fiber_count < max_fibers
|
39
|
+
workers << spin do
|
40
|
+
loop do
|
41
|
+
item = receive
|
42
|
+
break if item == :__stop__
|
43
|
+
block.(item)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
fiber = workers.shift
|
49
|
+
fiber << i
|
50
|
+
workers << fiber
|
51
|
+
end
|
52
|
+
workers.each { |f| f << :__stop__ }
|
53
|
+
Fiber.current.await_all_children
|
54
|
+
end.await
|
55
|
+
self
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
o = 1..3
|
60
|
+
o.each_concurrently(max_fibers: 2) do |i|
|
61
|
+
puts "#{Fiber.current} sleep #{i}"
|
62
|
+
sleep(i)
|
63
|
+
puts "wakeup #{i}"
|
64
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'polyphony'
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
unix_path = '/tmp/polyphony-unix-socket'
|
8
|
+
|
9
|
+
FileUtils.rm unix_path rescue nil
|
10
|
+
server = UNIXServer.new(unix_path)
|
11
|
+
spin do
|
12
|
+
server.accept_loop do |socket|
|
13
|
+
p [:accept, socket]
|
14
|
+
spin do
|
15
|
+
while (line = socket.gets)
|
16
|
+
socket.puts line
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
snooze
|
23
|
+
client = UNIXSocket.new('/tmp/polyphony-unix-socket')
|
24
|
+
p [:connected, client]
|
25
|
+
client.puts 'hello!'
|
26
|
+
p client.gets
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fiber'
|
4
|
+
|
5
|
+
class Fiber
|
6
|
+
attr_accessor :next
|
7
|
+
end
|
8
|
+
|
9
|
+
# This program shows how the performance of Fiber.transfer degrades as the fiber
|
10
|
+
# count increases
|
11
|
+
|
12
|
+
def run(num_fibers)
|
13
|
+
count = 0
|
14
|
+
|
15
|
+
GC.start
|
16
|
+
GC.disable
|
17
|
+
|
18
|
+
fibers = []
|
19
|
+
num_fibers.times do
|
20
|
+
fibers << Fiber.new { loop { Fiber.yield } }
|
21
|
+
end
|
22
|
+
|
23
|
+
t0 = Time.now
|
24
|
+
|
25
|
+
while count < 1000000
|
26
|
+
fibers.each do |f|
|
27
|
+
count += 1
|
28
|
+
f.resume
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
elapsed = Time.now - t0
|
33
|
+
|
34
|
+
puts "fibers: #{num_fibers} count: #{count} rate: #{count / elapsed}"
|
35
|
+
rescue Exception => e
|
36
|
+
puts "Stopped at #{count} fibers"
|
37
|
+
p e
|
38
|
+
end
|
39
|
+
|
40
|
+
run(100)
|
41
|
+
run(1000)
|
42
|
+
run(10000)
|
43
|
+
run(100000)
|
@@ -0,0 +1,59 @@
|
|
1
|
+
SERVERS = {
|
2
|
+
polyphony: {
|
3
|
+
port: 1234,
|
4
|
+
cmd: 'ruby examples/performance/thread-vs-fiber/polyphony_server.rb'
|
5
|
+
},
|
6
|
+
threaded: {
|
7
|
+
port: 1235,
|
8
|
+
cmd: 'ruby examples/performance/thread-vs-fiber/threaded_server.rb'
|
9
|
+
},
|
10
|
+
em: {
|
11
|
+
port: 1236,
|
12
|
+
cmd: 'ruby examples/performance/thread-vs-fiber/em_server.rb'
|
13
|
+
}
|
14
|
+
}
|
15
|
+
SETTINGS = [
|
16
|
+
'-t1 -c1',
|
17
|
+
'-t4 -c8',
|
18
|
+
'-t8 -c64',
|
19
|
+
'-t16 -c512',
|
20
|
+
'-t32 -c4096',
|
21
|
+
'-t64 -c8192',
|
22
|
+
'-t128 -c16384',
|
23
|
+
'-t256 -c32768'
|
24
|
+
]
|
25
|
+
|
26
|
+
def run_test(name, port, cmd, setting)
|
27
|
+
puts "*" * 80
|
28
|
+
puts "Run #{name} (#{port}): #{setting}"
|
29
|
+
puts "*" * 80
|
30
|
+
|
31
|
+
pid = spawn("#{cmd} > /dev/null 2>&1")
|
32
|
+
sleep 1
|
33
|
+
|
34
|
+
output = `wrk -d60 #{setting} \"http://127.0.0.1:#{port}/\"`
|
35
|
+
puts output
|
36
|
+
(output =~ /Requests\/sec:\s+(\d+)/) && $1.to_i
|
37
|
+
ensure
|
38
|
+
Process.kill('KILL', pid)
|
39
|
+
Process.wait(pid)
|
40
|
+
3.times { puts }
|
41
|
+
end
|
42
|
+
|
43
|
+
def perform_benchmark
|
44
|
+
results = []
|
45
|
+
SETTINGS.each do |s|
|
46
|
+
results << SERVERS.inject({}) do |h, (n, o)|
|
47
|
+
h[n] = run_test(n, o[:port], o[:cmd], s)
|
48
|
+
h
|
49
|
+
end
|
50
|
+
end
|
51
|
+
results
|
52
|
+
end
|
53
|
+
|
54
|
+
results = []
|
55
|
+
3.times { results << perform_benchmark }
|
56
|
+
|
57
|
+
require 'pp'
|
58
|
+
puts "results:"
|
59
|
+
pp results
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'http/parser'
|
3
|
+
require 'socket'
|
4
|
+
|
5
|
+
module HTTPServer
|
6
|
+
def post_init
|
7
|
+
@parser = Http::Parser.new
|
8
|
+
@pending_requests = []
|
9
|
+
@parser.on_message_complete = proc { @pending_requests << @parser }
|
10
|
+
end
|
11
|
+
|
12
|
+
def receive_data(data)
|
13
|
+
@parser << data
|
14
|
+
write_response while @pending_requests.shift
|
15
|
+
end
|
16
|
+
|
17
|
+
def write_response
|
18
|
+
status_code = "200 OK"
|
19
|
+
data = "Hello world!\n"
|
20
|
+
headers = "Content-Type: text/plain\r\nContent-Length: #{data.bytesize}\r\n"
|
21
|
+
send_data "HTTP/1.1 #{status_code}\r\n#{headers}\r\n#{data}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
EventMachine::run do
|
26
|
+
EventMachine::start_server(
|
27
|
+
'0.0.0.0',
|
28
|
+
1236,
|
29
|
+
HTTPServer
|
30
|
+
)
|
31
|
+
puts "pid #{Process.pid} EventMachine listening on port 1236"
|
32
|
+
|
33
|
+
end
|
@@ -27,7 +27,8 @@ def write_response(socket)
|
|
27
27
|
end
|
28
28
|
|
29
29
|
server = TCPServer.open('0.0.0.0', 1234)
|
30
|
-
puts "pid #{Process.pid}"
|
31
|
-
puts "listening on port 1234"
|
30
|
+
puts "pid #{Process.pid} Polyphony (#{Thread.current.backend.kind}) listening on port 1234"
|
32
31
|
|
33
|
-
server.accept_loop
|
32
|
+
server.accept_loop do |c|
|
33
|
+
spin { handle_client(c) }
|
34
|
+
end
|
@@ -1,23 +1,30 @@
|
|
1
|
-
require 'thread'
|
2
1
|
require 'http/parser'
|
3
2
|
require 'socket'
|
4
3
|
|
5
|
-
def handle_client(
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
end
|
14
|
-
client.read_loop { |data| parser << data }
|
15
|
-
client.close
|
4
|
+
def handle_client(socket)
|
5
|
+
pending_requests = []
|
6
|
+
parser = Http::Parser.new
|
7
|
+
parser.on_message_complete = proc { pending_requests << parser }
|
8
|
+
|
9
|
+
while (data = socket.recv(8192))
|
10
|
+
parser << data
|
11
|
+
write_response(socket) while pending_requests.shift
|
16
12
|
end
|
13
|
+
rescue IOError, SystemCallError => e
|
14
|
+
# ignore
|
15
|
+
ensure
|
16
|
+
socket.close
|
17
|
+
end
|
18
|
+
|
19
|
+
def write_response(socket)
|
20
|
+
status_code = "200 OK"
|
21
|
+
data = "Hello world!\n"
|
22
|
+
headers = "Content-Type: text/plain\r\nContent-Length: #{data.bytesize}\r\n"
|
23
|
+
socket.write "HTTP/1.1 #{status_code}\r\n#{headers}\r\n#{data}"
|
17
24
|
end
|
18
25
|
|
19
|
-
server = TCPServer.open(
|
20
|
-
puts "
|
26
|
+
server = TCPServer.open(1235)
|
27
|
+
puts "pid #{Process.pid} threaded listening on port 1235"
|
21
28
|
while socket = server.accept
|
22
|
-
handle_client(socket)
|
29
|
+
Thread.new { handle_client(socket) }
|
23
30
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fiber'
|
4
|
+
|
5
|
+
class Fiber
|
6
|
+
attr_accessor :next
|
7
|
+
end
|
8
|
+
|
9
|
+
# This program shows how the performance of Fiber.transfer degrades as the fiber
|
10
|
+
# count increases
|
11
|
+
|
12
|
+
def run(num_threads)
|
13
|
+
count = 0
|
14
|
+
|
15
|
+
GC.start
|
16
|
+
GC.disable
|
17
|
+
|
18
|
+
threads = []
|
19
|
+
t0 = Time.now
|
20
|
+
limit = 10_000_000 / num_threads
|
21
|
+
num_threads.times do
|
22
|
+
threads << Thread.new do
|
23
|
+
individual_count = 0
|
24
|
+
loop do
|
25
|
+
individual_count += 1
|
26
|
+
count += 1
|
27
|
+
break if individual_count == limit
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
threads.each(&:join)
|
33
|
+
elapsed = Time.now - t0
|
34
|
+
|
35
|
+
puts "threads: #{num_threads} count: #{count} rate: #{count / elapsed}"
|
36
|
+
rescue Exception => e
|
37
|
+
puts "Stopped at #{count} threads"
|
38
|
+
p e
|
39
|
+
end
|
40
|
+
|
41
|
+
run(100)
|
42
|
+
run(1000)
|
43
|
+
run(10000)
|
44
|
+
run(100000)
|
@@ -3,6 +3,26 @@
|
|
3
3
|
#include "ruby.h"
|
4
4
|
#include "ruby/io.h"
|
5
5
|
|
6
|
+
VALUE cTCPSocket;
|
7
|
+
VALUE cTCPServer;
|
8
|
+
VALUE cUNIXSocket;
|
9
|
+
VALUE cUNIXServer;
|
10
|
+
|
11
|
+
void Init_SocketClasses() {
|
12
|
+
rb_require("socket");
|
13
|
+
cTCPSocket = rb_const_get(rb_cObject, rb_intern("TCPSocket"));
|
14
|
+
cTCPServer = rb_const_get(rb_cObject, rb_intern("TCPServer"));
|
15
|
+
cUNIXSocket = rb_const_get(rb_cObject, rb_intern("UNIXSocket"));
|
16
|
+
cUNIXServer = rb_const_get(rb_cObject, rb_intern("UNIXServer"));
|
17
|
+
}
|
18
|
+
|
19
|
+
VALUE ConnectionSocketClass(VALUE server) {
|
20
|
+
if (RTEST(rb_obj_is_kind_of(server, cTCPServer))) return cTCPSocket;
|
21
|
+
if (RTEST(rb_obj_is_kind_of(server, cUNIXServer))) return cUNIXSocket;
|
22
|
+
|
23
|
+
rb_raise(rb_eRuntimeError, "Invalid server class");
|
24
|
+
}
|
25
|
+
|
6
26
|
//////////////////////////////////////////////////////////////////////
|
7
27
|
//////////////////////////////////////////////////////////////////////
|
8
28
|
// the following is copied verbatim from the Ruby source code (io.c)
|
@@ -118,3 +138,12 @@ inline double current_time() {
|
|
118
138
|
double t = ns;
|
119
139
|
return t / 1e9;
|
120
140
|
}
|
141
|
+
|
142
|
+
inline VALUE backend_timeout_exception(VALUE exception) {
|
143
|
+
if (RTEST(rb_obj_is_kind_of(exception, rb_cArray)))
|
144
|
+
return rb_funcall(rb_ary_entry(exception, 0), ID_new, 1, rb_ary_entry(exception, 1));
|
145
|
+
else if (RTEST(rb_obj_is_kind_of(exception, rb_cClass)))
|
146
|
+
return rb_funcall(exception, ID_new, 0);
|
147
|
+
else
|
148
|
+
return rb_funcall(rb_eRuntimeError, ID_new, 1, exception);
|
149
|
+
}
|