polyphony 0.45.5 → 0.47.2

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +2 -0
  3. data/.gitmodules +0 -0
  4. data/CHANGELOG.md +23 -0
  5. data/Gemfile.lock +1 -1
  6. data/README.md +3 -3
  7. data/Rakefile +1 -1
  8. data/TODO.md +21 -22
  9. data/bin/test +4 -0
  10. data/examples/core/enumerable.rb +64 -0
  11. data/examples/performance/fiber_resume.rb +43 -0
  12. data/examples/performance/fiber_transfer.rb +13 -4
  13. data/examples/performance/thread-vs-fiber/compare.rb +59 -0
  14. data/examples/performance/thread-vs-fiber/em_server.rb +33 -0
  15. data/examples/performance/thread-vs-fiber/polyphony_server.rb +10 -21
  16. data/examples/performance/thread-vs-fiber/threaded_server.rb +22 -15
  17. data/examples/performance/thread_switch.rb +44 -0
  18. data/ext/liburing/liburing.h +585 -0
  19. data/ext/liburing/liburing/README.md +4 -0
  20. data/ext/liburing/liburing/barrier.h +73 -0
  21. data/ext/liburing/liburing/compat.h +15 -0
  22. data/ext/liburing/liburing/io_uring.h +343 -0
  23. data/ext/liburing/queue.c +333 -0
  24. data/ext/liburing/register.c +187 -0
  25. data/ext/liburing/setup.c +210 -0
  26. data/ext/liburing/syscall.c +54 -0
  27. data/ext/liburing/syscall.h +18 -0
  28. data/ext/polyphony/backend.h +0 -14
  29. data/ext/polyphony/backend_common.h +129 -0
  30. data/ext/polyphony/backend_io_uring.c +995 -0
  31. data/ext/polyphony/backend_io_uring_context.c +74 -0
  32. data/ext/polyphony/backend_io_uring_context.h +53 -0
  33. data/ext/polyphony/{libev_backend.c → backend_libev.c} +304 -294
  34. data/ext/polyphony/event.c +1 -1
  35. data/ext/polyphony/extconf.rb +31 -13
  36. data/ext/polyphony/fiber.c +35 -24
  37. data/ext/polyphony/libev.c +4 -0
  38. data/ext/polyphony/libev.h +8 -2
  39. data/ext/polyphony/liburing.c +8 -0
  40. data/ext/polyphony/playground.c +51 -0
  41. data/ext/polyphony/polyphony.c +8 -5
  42. data/ext/polyphony/polyphony.h +23 -19
  43. data/ext/polyphony/polyphony_ext.c +10 -4
  44. data/ext/polyphony/queue.c +100 -35
  45. data/ext/polyphony/thread.c +10 -10
  46. data/lib/polyphony/adapters/trace.rb +2 -2
  47. data/lib/polyphony/core/exceptions.rb +0 -4
  48. data/lib/polyphony/core/global_api.rb +45 -21
  49. data/lib/polyphony/core/resource_pool.rb +12 -1
  50. data/lib/polyphony/extensions/core.rb +9 -15
  51. data/lib/polyphony/extensions/debug.rb +13 -0
  52. data/lib/polyphony/extensions/fiber.rb +8 -4
  53. data/lib/polyphony/extensions/openssl.rb +6 -0
  54. data/lib/polyphony/extensions/socket.rb +73 -10
  55. data/lib/polyphony/version.rb +1 -1
  56. data/test/helper.rb +36 -4
  57. data/test/io_uring_test.rb +55 -0
  58. data/test/stress.rb +4 -1
  59. data/test/test_backend.rb +63 -6
  60. data/test/test_ext.rb +1 -2
  61. data/test/test_fiber.rb +55 -20
  62. data/test/test_global_api.rb +107 -35
  63. data/test/test_queue.rb +117 -0
  64. data/test/test_resource_pool.rb +21 -0
  65. data/test/test_socket.rb +2 -2
  66. data/test/test_throttler.rb +3 -6
  67. data/test/test_trace.rb +7 -5
  68. metadata +28 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fe5d86b07fc6f29d01897a7790deac8f0544bb61dbd486fb8ea104dd315b0e80
4
- data.tar.gz: 3e1086b9395d63835a09051c52b7f8496013f190149df35532333b6c27b74169
3
+ metadata.gz: 8f9e7ffec31a2009f8000e7b88473e21581de37640ee2d5c6357550883d7bc7c
4
+ data.tar.gz: d3f8262b8373e7a337dacc0c8e04b1b25ddf0254445b146787a5aa82520dee30
5
5
  SHA512:
6
- metadata.gz: d6c7a78a46b9084f60ee838e82b7cde6f287eea2c500f779e5014103a400da98dc066adef58631033591a8de5278cc4f7b95ba8dcdac3ad22ecd68668a7d68e4
7
- data.tar.gz: d32e7def9aa63ecb5c53b46020079e3384809ff786478e0b87fce1b141abb85d3c69d1f490b1d0a3bcbfaf972a3779939ac42bec512b12132d3b46b159754de5
6
+ metadata.gz: 0203bd8d66851f3eeed446f7537183e5b66a422f5f5c68b79a1b99f5c3762e243d1027cd77ccbe255012b9450a56b336c9da2e78e91b867adee8f3353351db7c
7
+ data.tar.gz: 74ef01b9828d4ba0c6a67cd8a402551484c935234fb7d259146d6cfd5033149f7376aa98068dc8f30168c0ec783015208326455795926d02b385e1bda27d1f37
@@ -23,6 +23,8 @@ jobs:
23
23
  run: |
24
24
  gem install bundler
25
25
  bundle install
26
+ - name: Show Linux kernel version
27
+ run: uname -r
26
28
  - name: Compile C-extension
27
29
  run: bundle exec rake compile
28
30
  - name: Run tests
File without changes
@@ -1,3 +1,26 @@
1
+ ## 0.47.2
2
+
3
+ * Fix API compatibility between TCPSocket and IO
4
+
5
+ ## 0.47.0
6
+
7
+ * Implement `#spin_scope` used for creating blocking fiber scopes
8
+ * Reimplement `move_on_after`, `cancel_after`, `Timeout.timeout` using
9
+ `Backend#timeout` (avoids creating canceller fiber for most common use case)
10
+ * Implement `Backend#timeout` API
11
+ * Implemented capped queues
12
+
13
+ ## 0.46.1
14
+
15
+ * Add `TCPServer#accept_loop`, `OpenSSL::SSL::SSLSocket#accept_loop` method
16
+ * Fix compilation error on MacOS (#43)
17
+ * Fix backtrace for `Timeout.timeout`
18
+ * Add `Backend#timer_loop`
19
+
20
+ ## 0.46.0
21
+
22
+ * Implement [io_uring backend](https://github.com/digital-fabric/polyphony/pull/44)
23
+
1
24
  ## 0.45.5
2
25
 
3
26
  * Fix compilation error (#43)
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- polyphony (0.45.5)
4
+ polyphony (0.47.2)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -35,9 +35,9 @@
35
35
  Polyphony is a library for building concurrent applications in Ruby. Polyphony
36
36
  harnesses the power of [Ruby fibers](https://ruby-doc.org/core-2.5.1/Fiber.html)
37
37
  to provide a cooperative, sequential coroutine-based concurrency model. Under
38
- the hood, Polyphony uses [libev](https://github.com/enki/libev) as a
39
- high-performance event reactor that provides timers, I/O watchers and other
40
- asynchronous event primitives.
38
+ the hood, Polyphony uses
39
+ [io_uring](https://unixism.net/loti/what_is_io_uring.html) or
40
+ [libev](https://github.com/enki/libev) to maximize I/O performance.
41
41
 
42
42
  ## Features
43
43
 
data/Rakefile CHANGED
@@ -23,4 +23,4 @@ task :docs do
23
23
  exec 'RUBYOPT=-W0 jekyll serve -s docs -H ec2-18-156-117-172.eu-central-1.compute.amazonaws.com'
24
24
  end
25
25
 
26
- CLEAN.include "**/*.o", "**/*.so", "**/*.bundle", "**/*.jar", "pkg", "tmp"
26
+ CLEAN.include "**/*.o", "**/*.so", "**/*.so.*", "**/*.a", "**/*.bundle", "**/*.jar", "pkg", "tmp"
data/TODO.md CHANGED
@@ -1,16 +1,24 @@
1
- - io_uring
2
-
3
- 0.46
1
+ ## Roadmap for Polyphony 1.0
4
2
 
5
- - Adapter for io/console (what does `IO#raw` do?)
6
- - Adapter for Pry and IRB (Which fixes #5 and #6)
3
+ - Check why worker-thread example doesn't work.
4
+ - Add test that mimics the original design for Monocrono:
5
+ - 256 fibers each waiting for a message
6
+ - When message received do some blocking work using a `ThreadPool`
7
+ - Send messages, collect responses, check for correctness
7
8
  - Improve `#supervise`. It does not work as advertised, and seems to exhibit an
8
9
  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
10
 
12
- 0.47
11
+ - io_uring
12
+ - Use playground.c to find out why we when submitting and waiting for
13
+ completion in single syscall signals seem to be blocked until the syscall
14
+ returns. Is this a bug in io_uring/liburing?
15
+
16
+ -----------------------------------------------------
13
17
 
18
+ - Add `Backend#splice(in, out, nbytes)` API
19
+ - Adapter for io/console (what does `IO#raw` do?)
20
+ - Adapter for Pry and IRB (Which fixes #5 and #6)
21
+ - allow backend selection at runtime
14
22
  - Debugging
15
23
  - Eat your own dogfood: need a good tool to check what's going on when some
16
24
  test fails
@@ -148,6 +156,11 @@
148
156
  - `IO.foreach`
149
157
  - `Process.waitpid`
150
158
 
159
+ ### Quic / HTTP/3
160
+
161
+ - Python impl: https://github.com/aiortc/aioquic/
162
+ - Go impl: https://github.com/lucas-clemente/quic-go
163
+
151
164
  ### DNS client
152
165
 
153
166
  ```ruby
@@ -179,17 +192,3 @@ Prior art:
179
192
 
180
193
  - https://github.com/socketry/async-dns
181
194
 
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,42 +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
16
- socket.read_loop do |data|
10
+ parser.on_message_complete = proc { pending_requests << parser }
11
+
12
+ socket.recv_loop do |data|
17
13
  parser << data
18
- while (req = reqs.shift)
19
- handle_request(socket, req)
20
- req = nil
21
- snooze
22
- end
14
+ write_response(socket) while pending_requests.shift
23
15
  end
24
16
  rescue IOError, SystemCallError => e
25
17
  # do nothing
26
18
  ensure
27
- $connection_count -= 1
28
19
  socket&.close
29
20
  end
30
21
 
31
- def handle_request(client, parser)
22
+ def write_response(socket)
32
23
  status_code = "200 OK"
33
24
  data = "Hello world!\n"
34
25
  headers = "Content-Type: text/plain\r\nContent-Length: #{data.bytesize}\r\n"
35
- 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}"
36
27
  end
37
28
 
38
29
  server = TCPServer.open('0.0.0.0', 1234)
39
- puts "pid #{Process.pid}"
40
- puts "listening on port 1234"
30
+ puts "pid #{Process.pid} Polyphony (#{Thread.current.backend.kind}) listening on port 1234"
41
31
 
42
- loop do
43
- client = server.accept
44
- spin { handle_client(client) }
32
+ server.accept_loop do |c|
33
+ spin { handle_client(c) }
45
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