polyphony 0.46.0 → 0.47.3

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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -0
  3. data/Gemfile.lock +1 -1
  4. data/TODO.md +54 -23
  5. data/bin/test +4 -0
  6. data/examples/core/enumerable.rb +64 -0
  7. data/examples/performance/fiber_resume.rb +43 -0
  8. data/examples/performance/fiber_transfer.rb +13 -4
  9. data/examples/performance/thread-vs-fiber/compare.rb +59 -0
  10. data/examples/performance/thread-vs-fiber/em_server.rb +33 -0
  11. data/examples/performance/thread-vs-fiber/polyphony_server.rb +9 -19
  12. data/examples/performance/thread-vs-fiber/threaded_server.rb +22 -15
  13. data/examples/performance/thread_switch.rb +44 -0
  14. data/ext/polyphony/backend_common.h +20 -0
  15. data/ext/polyphony/backend_io_uring.c +127 -16
  16. data/ext/polyphony/backend_io_uring_context.c +1 -0
  17. data/ext/polyphony/backend_io_uring_context.h +1 -0
  18. data/ext/polyphony/backend_libev.c +102 -0
  19. data/ext/polyphony/fiber.c +11 -7
  20. data/ext/polyphony/polyphony.c +3 -0
  21. data/ext/polyphony/polyphony.h +7 -7
  22. data/ext/polyphony/queue.c +99 -34
  23. data/ext/polyphony/thread.c +1 -3
  24. data/lib/polyphony/core/exceptions.rb +0 -4
  25. data/lib/polyphony/core/global_api.rb +49 -31
  26. data/lib/polyphony/extensions/core.rb +9 -15
  27. data/lib/polyphony/extensions/fiber.rb +8 -2
  28. data/lib/polyphony/extensions/openssl.rb +6 -0
  29. data/lib/polyphony/extensions/socket.rb +18 -4
  30. data/lib/polyphony/version.rb +1 -1
  31. data/test/helper.rb +1 -1
  32. data/test/stress.rb +1 -1
  33. data/test/test_backend.rb +59 -0
  34. data/test/test_fiber.rb +33 -4
  35. data/test/test_global_api.rb +85 -1
  36. data/test/test_queue.rb +117 -0
  37. data/test/test_signal.rb +18 -0
  38. data/test/test_socket.rb +2 -2
  39. metadata +8 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 890abc2b84ed305f591c697764ea16255059aeb3cd4ddd23195b81f78f5f6daf
4
- data.tar.gz: a02442318f82682ba1fa3a87e2b4e7ad8ae06d365a7d4a4b5c45689b2e55472f
3
+ metadata.gz: 89890bc8a81fd4f0a80f6d84c7f5138cfda12bc99848e74ef8140abf681640e2
4
+ data.tar.gz: d18e210b24f3d0eb2f83c1dcb188e7c1c691f949231ef0b3d07bd926c988f4d6
5
5
  SHA512:
6
- metadata.gz: 506a2fdbdee9e6c5bb94b976489537792941326080441f3d93924cd9657db5188b29e4ced6e1c8a6f8db06ae211da0522b3ac042e45365be1af2e68f8b157b0e
7
- data.tar.gz: cbc0e333731e035c2094abfa72d425e649c210a8c9d3aec467446f615315cfc6e1f6de664321099ccdf93ddd6e1bb0ba484b2b8b53922725b2df8483aedefeab
6
+ metadata.gz: c95bef00de396f601f91937c780202a26d0e9c5a3eb48f564d1dcb70d89b30c12ebbbc89c9ddb10b81b013f9a89398468b8b3b4c1d3158d00110e3e6ea5355a6
7
+ data.tar.gz: 3c31dac6a239e73b6dbf028edfeced82ac6b2d17ba2f5f2960a41aa9d74e57336082bbe038a8ed6701a2ceefaf68cacb74764f9f71c81ce1e36d5868702e5346
@@ -1,3 +1,27 @@
1
+ ## 0.47.3
2
+
3
+ * Enable I/O in signal handlers (#45)
4
+ * Accept `:interval` argument in `#spin_loop`
5
+
6
+ ## 0.47.2
7
+
8
+ * Fix API compatibility between TCPSocket and IO
9
+
10
+ ## 0.47.0
11
+
12
+ * Implement `#spin_scope` used for creating blocking fiber scopes
13
+ * Reimplement `move_on_after`, `cancel_after`, `Timeout.timeout` using
14
+ `Backend#timeout` (avoids creating canceller fiber for most common use case)
15
+ * Implement `Backend#timeout` API
16
+ * Implemented capped queues
17
+
18
+ ## 0.46.1
19
+
20
+ * Add `TCPServer#accept_loop`, `OpenSSL::SSL::SSLSocket#accept_loop` method
21
+ * Fix compilation error on MacOS (#43)
22
+ * Fix backtrace for `Timeout.timeout`
23
+ * Add `Backend#timer_loop`
24
+
1
25
  ## 0.46.0
2
26
 
3
27
  * Implement [io_uring backend](https://github.com/digital-fabric/polyphony/pull/44)
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- polyphony (0.46.0)
4
+ polyphony (0.47.3)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/TODO.md CHANGED
@@ -1,16 +1,56 @@
1
- - change fiber_trace method to return nil, change trace logic to use provided
2
- arguments instead of return values for fiber events
3
- - allow backend selection at runtime
4
- - add Backend#timer_loop that does what throttled_loop does, on lower level
5
- - Adapter for io/console (what does `IO#raw` do?)
6
- - Adapter for Pry and IRB (Which fixes #5 and #6)
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
+
33
+ ## Roadmap for Polyphony 1.0
34
+
35
+ - Check why worker-thread example doesn't work.
36
+ - Add test that mimics the original design for Monocrono:
37
+ - 256 fibers each waiting for a message
38
+ - When message received do some blocking work using a `ThreadPool`
39
+ - Send messages, collect responses, check for correctness
7
40
  - Improve `#supervise`. It does not work as advertised, and seems to exhibit an
8
41
  inconsistent behaviour (see supervisor example).
9
- - Fix backtrace for `Timeout.timeout` API (see timeout example).
10
- - Check why worker-thread example doesn't work.
11
42
 
12
- 0.47
43
+ - io_uring
44
+ - Use playground.c to find out why we when submitting and waiting for
45
+ completion in single syscall signals seem to be blocked until the syscall
46
+ returns. Is this a bug in io_uring/liburing?
13
47
 
48
+ -----------------------------------------------------
49
+
50
+ - Add `Backend#splice(in, out, nbytes)` API
51
+ - Adapter for io/console (what does `IO#raw` do?)
52
+ - Adapter for Pry and IRB (Which fixes #5 and #6)
53
+ - allow backend selection at runtime
14
54
  - Debugging
15
55
  - Eat your own dogfood: need a good tool to check what's going on when some
16
56
  test fails
@@ -148,6 +188,11 @@
148
188
  - `IO.foreach`
149
189
  - `Process.waitpid`
150
190
 
191
+ ### Quic / HTTP/3
192
+
193
+ - Python impl: https://github.com/aiortc/aioquic/
194
+ - Go impl: https://github.com/lucas-clemente/quic-go
195
+
151
196
  ### DNS client
152
197
 
153
198
  ```ruby
@@ -179,17 +224,3 @@ Prior art:
179
224
 
180
225
  - https://github.com/socketry/async-dns
181
226
 
182
- ## Work on API
183
-
184
- - Add option for setting the exception raised on cancelling using `#cancel_after`:
185
-
186
- ```ruby
187
- cancel_after(3, with_error: MyErrorClass) do
188
- do_my_thing
189
- end
190
- # or a RuntimeError with message
191
- cancel_after(3, with_error: 'Cancelled due to timeout') do
192
- do_my_thing
193
- end
194
- ```
195
-
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+ clear && POLYPHONY_USE_LIBEV=1 rake recompile && ruby test/run.rb
4
+ clear && rake recompile && ruby test/run.rb
@@ -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,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)
@@ -12,6 +12,7 @@ end
12
12
  def run(num_fibers)
13
13
  count = 0
14
14
 
15
+ GC.start
15
16
  GC.disable
16
17
 
17
18
  first = nil
@@ -36,13 +37,21 @@ def run(num_fibers)
36
37
  last.next = first
37
38
 
38
39
  t0 = Time.now
40
+ puts "start transfer..."
39
41
  first.transfer
40
42
  elapsed = Time.now - t0
41
43
 
42
- puts "fibers: #{num_fibers} count: #{count} rate: #{count / elapsed}"
43
- GC.start
44
+ rss = `ps -o rss= -p #{Process.pid}`.to_i
45
+
46
+ puts "fibers: #{num_fibers} rss: #{rss} count: #{count} rate: #{count / elapsed}"
47
+ rescue Exception => e
48
+ puts "Stopped at #{count} fibers"
49
+ p e
44
50
  end
45
51
 
52
+ puts "pid: #{Process.pid}"
46
53
  run(100)
47
- run(1000)
48
- run(10000)
54
+ # run(1000)
55
+ # run(10000)
56
+ # run(100000)
57
+ # run(400000)
@@ -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
@@ -4,41 +4,31 @@ require 'bundler/setup'
4
4
  require 'polyphony'
5
5
  require 'http/parser'
6
6
 
7
- $connection_count = 0
8
-
9
7
  def handle_client(socket)
10
- $connection_count += 1
8
+ pending_requests = []
11
9
  parser = Http::Parser.new
12
- reqs = []
13
- parser.on_message_complete = proc do |env|
14
- reqs << Object.new # parser
15
- end
10
+ parser.on_message_complete = proc { pending_requests << parser }
11
+
16
12
  socket.recv_loop do |data|
17
13
  parser << data
18
- while (req = reqs.shift)
19
- handle_request(socket, req)
20
- req = nil
21
- end
14
+ write_response(socket) while pending_requests.shift
22
15
  end
23
16
  rescue IOError, SystemCallError => e
24
17
  # do nothing
25
18
  ensure
26
- $connection_count -= 1
27
19
  socket&.close
28
20
  end
29
21
 
30
- def handle_request(client, parser)
22
+ def write_response(socket)
31
23
  status_code = "200 OK"
32
24
  data = "Hello world!\n"
33
25
  headers = "Content-Type: text/plain\r\nContent-Length: #{data.bytesize}\r\n"
34
- client.write "HTTP/1.1 #{status_code}\r\n#{headers}\r\n#{data}"
26
+ socket.write "HTTP/1.1 #{status_code}\r\n#{headers}\r\n#{data}"
35
27
  end
36
28
 
37
29
  server = TCPServer.open('0.0.0.0', 1234)
38
- puts "pid #{Process.pid}"
39
- puts "listening on port 1234"
30
+ puts "pid #{Process.pid} Polyphony (#{Thread.current.backend.kind}) listening on port 1234"
40
31
 
41
- loop do
42
- client = server.accept
43
- spin { handle_client(client) }
32
+ server.accept_loop do |c|
33
+ spin { handle_client(c) }
44
34
  end
@@ -1,23 +1,30 @@
1
- require 'thread'
2
1
  require 'http/parser'
3
2
  require 'socket'
4
3
 
5
- def handle_client(client)
6
- Thread.new do
7
- parser = Http::Parser.new
8
- parser.on_message_complete = proc do |env|
9
- status_code = 200
10
- data = "Hello world!\n"
11
- headers = "Content-Length: #{data.bytesize}\r\n"
12
- client.write "HTTP/1.1 #{status_code}\r\n#{headers}\r\n#{data}"
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(1234)
20
- puts "Listening on port 1234"
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