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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 831c85a16a22fe3877044430a4d1cc4a270d18dc73e9ceca8d4826623ab0ddc8
4
- data.tar.gz: f0432473abb769be2805ad354dddf44465789ebe2d5220f194660e4c6521ec87
3
+ metadata.gz: f32962c9f1004bc9c63afcaddaad91dc8878da5e8290b8e9d42d591433311c0b
4
+ data.tar.gz: 3b43ad185ccb0778d88d9c8ac1043bbff291ac0a55d5bee80da8b6e08a1f6493
5
5
  SHA512:
6
- metadata.gz: 9bfecbed04a8052c3a885dfde73975694313801d2a8341260041be84e4910ac459af71766f7df6fa96e5afad66a737b97e36531811bd47cab407ad43b239e073
7
- data.tar.gz: 2caa40f7193cf1954b69adc5810844ad4e578a4472382602a6ee4204d548e8327344f913bc0aef076030e7deacb911cab1733b281f7a2e8849ffcbd009a00ebc
6
+ metadata.gz: b4ba80ea441862f22398e624996144e9ed7f8297b56e2f14035c537671282f4338b3843ff192959a7031134e761d273c5050043b08b5051c1653ecc3f986cddb
7
+ data.tar.gz: '04850a036f5278d221b6610259e70fbb7e44e8afd04237d9a6906fa826a836c786734123894a0b7197c5c98502a9b5304896b09a1e7b6d16433e363dbd1803e6'
@@ -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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- polyphony (0.46.1)
4
+ polyphony (0.47.4)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
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 { |c| handle_client(c) }
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(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
@@ -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
+ }