polyphony 0.13 → 0.14

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 (135) hide show
  1. checksums.yaml +4 -4
  2. data/.gitbook.yaml +5 -0
  3. data/.gitignore +55 -0
  4. data/.rubocop.yml +49 -0
  5. data/CHANGELOG.md +13 -2
  6. data/Gemfile +3 -0
  7. data/Gemfile.lock +31 -0
  8. data/LICENSE +21 -0
  9. data/README.md +35 -18
  10. data/Rakefile +20 -0
  11. data/TODO.md +49 -0
  12. data/docs/getting-started/getting-started.md +10 -0
  13. data/docs/getting-started/tutorial.md +2 -0
  14. data/docs/summary.md +9 -0
  15. data/examples/core/cancel.rb +10 -0
  16. data/examples/core/channel_echo.rb +43 -0
  17. data/examples/core/enumerator.rb +14 -0
  18. data/examples/core/fork.rb +22 -0
  19. data/examples/core/genserver.rb +74 -0
  20. data/examples/core/lock.rb +20 -0
  21. data/examples/core/move_on.rb +11 -0
  22. data/examples/core/move_on_twice.rb +17 -0
  23. data/examples/core/move_on_with_ensure.rb +17 -0
  24. data/examples/core/multiple_async.rb +17 -0
  25. data/examples/core/nested_async.rb +18 -0
  26. data/examples/core/nested_cancel.rb +41 -0
  27. data/examples/core/nested_multiple_async.rb +19 -0
  28. data/examples/core/next_tick.rb +13 -0
  29. data/examples/core/pulse.rb +13 -0
  30. data/examples/core/resource.rb +29 -0
  31. data/examples/core/resource_cancel.rb +34 -0
  32. data/examples/core/resource_delegate.rb +32 -0
  33. data/examples/core/sleep.rb +9 -0
  34. data/examples/core/sleep2.rb +13 -0
  35. data/examples/core/spawn.rb +15 -0
  36. data/examples/core/spawn_cancel.rb +19 -0
  37. data/examples/core/spawn_error.rb +28 -0
  38. data/examples/core/supervisor.rb +22 -0
  39. data/examples/core/supervisor_with_cancel_scope.rb +24 -0
  40. data/examples/core/supervisor_with_error.rb +23 -0
  41. data/examples/core/supervisor_with_manual_move_on.rb +25 -0
  42. data/examples/core/thread.rb +30 -0
  43. data/examples/core/thread_cancel.rb +30 -0
  44. data/examples/core/thread_pool.rb +60 -0
  45. data/examples/core/throttle.rb +17 -0
  46. data/examples/fs/read.rb +37 -0
  47. data/examples/interfaces/pg_client.rb +38 -0
  48. data/examples/interfaces/pg_pool.rb +37 -0
  49. data/examples/interfaces/pg_query.rb +32 -0
  50. data/examples/interfaces/redis_channels.rb +119 -0
  51. data/examples/interfaces/redis_client.rb +21 -0
  52. data/examples/interfaces/redis_pubsub.rb +26 -0
  53. data/examples/interfaces/redis_pubsub_perf.rb +65 -0
  54. data/examples/io/config.ru +3 -0
  55. data/examples/io/echo_client.rb +22 -0
  56. data/examples/io/echo_server.rb +14 -0
  57. data/examples/io/echo_server_with_timeout.rb +33 -0
  58. data/examples/io/echo_stdin.rb +15 -0
  59. data/examples/io/happy_eyeballs.rb +32 -0
  60. data/examples/io/http_client.rb +19 -0
  61. data/examples/io/http_server.js +24 -0
  62. data/examples/io/http_server.rb +16 -0
  63. data/examples/io/http_server_forked.rb +27 -0
  64. data/examples/io/http_server_throttled.rb +16 -0
  65. data/examples/io/http_ws_server.rb +42 -0
  66. data/examples/io/https_client.rb +17 -0
  67. data/examples/io/https_server.rb +23 -0
  68. data/examples/io/https_wss_server.rb +46 -0
  69. data/examples/io/rack_server.rb +19 -0
  70. data/examples/io/rack_server_https.rb +24 -0
  71. data/examples/io/rack_server_https_forked.rb +32 -0
  72. data/examples/io/websocket_server.rb +33 -0
  73. data/examples/io/ws_page.html +34 -0
  74. data/examples/io/wss_page.html +34 -0
  75. data/examples/performance/perf_multi_snooze.rb +21 -0
  76. data/examples/performance/perf_snooze.rb +30 -0
  77. data/examples/performance/thread-vs-fiber/polyphony_server.rb +63 -0
  78. data/examples/performance/thread-vs-fiber/threaded_server.rb +27 -0
  79. data/examples/streams/lines.rb +27 -0
  80. data/examples/streams/stdio.rb +18 -0
  81. data/ext/ev/async.c +168 -0
  82. data/ext/ev/child.c +169 -0
  83. data/ext/ev/ev.h +32 -0
  84. data/ext/ev/ev_ext.c +20 -0
  85. data/ext/ev/ev_module.c +222 -0
  86. data/ext/ev/io.c +405 -0
  87. data/ext/ev/libev.h +9 -0
  88. data/ext/ev/signal.c +119 -0
  89. data/ext/ev/timer.c +197 -0
  90. data/ext/libev/Changes +513 -0
  91. data/ext/libev/LICENSE +37 -0
  92. data/ext/libev/README +58 -0
  93. data/ext/libev/README.embed +3 -0
  94. data/ext/libev/ev.c +5214 -0
  95. data/ext/libev/ev.h +849 -0
  96. data/ext/libev/ev_epoll.c +285 -0
  97. data/ext/libev/ev_kqueue.c +218 -0
  98. data/ext/libev/ev_poll.c +151 -0
  99. data/ext/libev/ev_port.c +189 -0
  100. data/ext/libev/ev_select.c +316 -0
  101. data/ext/libev/ev_vars.h +204 -0
  102. data/ext/libev/ev_win32.c +162 -0
  103. data/ext/libev/ev_wrap.h +200 -0
  104. data/ext/libev/test_libev_win32.c +123 -0
  105. data/lib/polyphony.rb +7 -2
  106. data/lib/polyphony/core.rb +1 -1
  107. data/lib/polyphony/core/{coroutine.rb → coprocess.rb} +10 -10
  108. data/lib/polyphony/core/exceptions.rb +5 -5
  109. data/lib/polyphony/core/supervisor.rb +16 -16
  110. data/lib/polyphony/core/thread.rb +1 -1
  111. data/lib/polyphony/extensions/io.rb +43 -42
  112. data/lib/polyphony/extensions/kernel.rb +10 -34
  113. data/lib/polyphony/extensions/postgres.rb +3 -2
  114. data/lib/polyphony/extensions/redis.rb +1 -1
  115. data/lib/polyphony/extensions/socket.rb +8 -4
  116. data/lib/polyphony/extensions/ssl.rb +0 -54
  117. data/lib/polyphony/http/agent.rb +4 -10
  118. data/lib/polyphony/http/http1.rb +25 -25
  119. data/lib/polyphony/http/http1_request.rb +38 -26
  120. data/lib/polyphony/http/http2.rb +4 -5
  121. data/lib/polyphony/http/http2_request.rb +12 -18
  122. data/lib/polyphony/http/rack.rb +1 -3
  123. data/lib/polyphony/http/server.rb +9 -9
  124. data/lib/polyphony/net.rb +2 -2
  125. data/lib/polyphony/resource_pool.rb +5 -1
  126. data/lib/polyphony/version.rb +1 -1
  127. data/lib/polyphony/websocket.rb +52 -0
  128. data/polyphony.gemspec +31 -0
  129. data/test/test_coprocess.rb +131 -0
  130. data/test/test_core.rb +274 -0
  131. data/test/test_ev.rb +117 -0
  132. data/test/test_io.rb +38 -0
  133. metadata +113 -7
  134. data/lib/polyphony/core/async.rb +0 -36
  135. data/lib/polyphony/net_old.rb +0 -299
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'modulation'
4
+
5
+ Polyphony = import('../../lib/polyphony')
6
+ import('../../lib/polyphony/extensions/redis')
7
+
8
+ redis = Redis.new
9
+
10
+ X = 10000
11
+
12
+ t0 = Time.now
13
+ X.times { redis.get('abc') }
14
+ puts "get rate: #{X / (Time.now - t0)} reqs/s"
15
+
16
+ puts "abc = #{redis.get('abc')}"
17
+
18
+ puts "updating value..."
19
+ redis.set('abc', Time.now.to_s)
20
+
21
+ puts "abc = #{redis.get('abc')}"
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'modulation'
4
+
5
+ Polyphony = import('../../lib/polyphony')
6
+ import('../../lib/polyphony/extensions/redis')
7
+
8
+ spawn do
9
+ redis = Redis.new
10
+ redis.subscribe('redis-channel') do |on|
11
+ on.message do |channel, message|
12
+ puts "##{channel}: #{message}"
13
+ redis.unsubscribe if message == "exit"
14
+ end
15
+ end
16
+ end
17
+
18
+ spawn do
19
+ redis = Redis.new
20
+ move_on_after(3) do
21
+ throttled_loop(1) do
22
+ redis.publish('redis-channel', Time.now)
23
+ end
24
+ end
25
+ redis.publish('redis-channel', 'exit')
26
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'modulation'
4
+ require 'json'
5
+
6
+ Polyphony = import('../../lib/polyphony')
7
+ import('../../lib/polyphony/extensions/redis')
8
+
9
+ X_SESSIONS = 1000
10
+ X_NODES = 10000
11
+ X_SUBSCRIPTIONS_PER_SESSION = 100
12
+
13
+ $sessions = []
14
+ X_SESSIONS.times do
15
+ $sessions << {
16
+ subscriptions: X_SUBSCRIPTIONS_PER_SESSION.times.map {
17
+ "node#{rand(X_NODES)}"
18
+ }.uniq
19
+ }
20
+ end
21
+
22
+ spawn do
23
+ redis = Redis.new
24
+ redis.subscribe('events') do |on|
25
+ on.message do |_, message|
26
+ distribute_event(JSON.parse(message, symbolize_names: true))
27
+ end
28
+ end
29
+ end
30
+
31
+ $update_count = 0
32
+
33
+ def distribute_event(event)
34
+ $update_count += 1
35
+ t0 = Time.now
36
+ count = 0
37
+ $sessions.each do |s|
38
+ count += 1 if s[:subscriptions].include?(event[:path])
39
+ end
40
+ elapsed = Time.now - t0
41
+ rate = X_SESSIONS / elapsed
42
+ # puts "elapsed: #{elapsed} (#{rate}/s)" if $update_count % 100 == 0
43
+ end
44
+
45
+ spawn do
46
+ redis = Redis.new
47
+ throttled_loop(1000) do
48
+ redis.publish('events', {path: "node#{rand(X_NODES)}"}.to_json)
49
+ end
50
+ end
51
+
52
+ spawn do
53
+ last_count = 0
54
+ last_stamp = Time.now
55
+ throttled_loop(1) do
56
+ now = Time.now
57
+ elapsed = now - last_stamp
58
+ delta = $update_count - last_count
59
+ puts "update rate: #{delta.to_f/elapsed}"
60
+ last_stamp = now
61
+ last_count = $update_count
62
+ end
63
+ end
64
+
65
+ Polyphony.trap(:int) { puts "bye..."; exit! }
@@ -0,0 +1,3 @@
1
+ run Proc.new { |env|
2
+ ['200', {'Content-Type' => 'text/html'}, ['A barebones rack app.']]
3
+ }
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'modulation'
4
+
5
+ Polyphony = import('../../lib/polyphony')
6
+
7
+ socket = Polyphony::Net.tcp_connect('127.0.0.1', 1234)
8
+
9
+ writer = spawn do
10
+ throttled_loop(1) { socket << "#{Time.now}\n" }
11
+ end
12
+
13
+ reader = spawn do
14
+ puts "received from echo server:"
15
+ while data = socket.read
16
+ STDOUT << data
17
+ end
18
+ end
19
+
20
+ sleep(5)
21
+ [reader, writer].each(&:stop)
22
+ socket.close
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'modulation'
4
+ Polyphony = import('../../lib/polyphony')
5
+
6
+ server = TCPServer.open(1234)
7
+ puts "Echoing on port 1234..."
8
+ while client = server.accept
9
+ spawn do
10
+ while data = client.readpartial(8192) rescue nil
11
+ client.write("you said: ", data.chomp, "!\n")
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'modulation'
4
+
5
+ Polyphony = import('../../lib/polyphony')
6
+
7
+ begin
8
+ server = Polyphony::Net.tcp_listen(nil, 1234, reuse_addr: true, dont_linger: true)
9
+ puts "listening on port 1234..."
10
+
11
+ loop do
12
+ client = server.accept
13
+ spawn do
14
+ cancel_scope = nil
15
+ move_on_after(5) do |s|
16
+ cancel_scope = s
17
+ loop do
18
+ data = client.read
19
+ s.reset_timeout
20
+ client.write(data)
21
+ end
22
+ end
23
+ client.write "Disconnecting due to inactivity\n" if cancel_scope.cancelled?
24
+ rescue => e
25
+ puts "client error: #{e.inspect}"
26
+ ensure
27
+ client.close
28
+ end
29
+ end
30
+ rescue Exception => e
31
+ puts "uncaught exception: #{e.inspect}"
32
+ server&.close
33
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'modulation'
4
+
5
+ Polyphony = import('../../lib/polyphony')
6
+
7
+ puts "Write something..."
8
+ move_on_after(5) do |scope|
9
+ loop do
10
+ data = STDIN.read
11
+ scope.reset_timeout
12
+ puts "you wrote: #{data}"
13
+ end
14
+ end
15
+ puts "quitting due to inactivity"
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'modulation'
4
+ Polyphony = import('../../lib/polyphony')
5
+
6
+ async def try_connect(supervisor, target)
7
+ puts "trying #{target[2]}"
8
+ socket = Polyphony::Net.tcp_connect(target[2], 80)
9
+ supervisor.stop!([target[2], socket])
10
+ rescue IOError, SystemCallError
11
+ end
12
+
13
+ def happy_eyeballs(hostname, port, max_wait_time: 0.025)
14
+ targets = Socket.getaddrinfo(hostname, port, :INET, :STREAM)
15
+ t0 = Time.now
16
+ cancel_after(5) do
17
+ success = supervise do |supervisor|
18
+ targets.each_with_index do |t, idx|
19
+ sleep(max_wait_time) if idx > 0
20
+ supervisor.spawn try_connect(supervisor, t)
21
+ end
22
+ end
23
+ if success
24
+ puts "success: #{success[0]} (#{Time.now - t0}s)"
25
+ else
26
+ puts "timed out (#{Time.now - t0}s)"
27
+ end
28
+ end
29
+ end
30
+
31
+ # Let's try it out:
32
+ happy_eyeballs("debian.org", "https")
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'modulation'
4
+
5
+ Polyphony = import('../../lib/polyphony')
6
+ Agent = import('../../lib/polyphony/http/agent')
7
+
8
+ def get_server_time
9
+ Agent.get('https://ui.realiteq.net/', q: :time).json
10
+ end
11
+
12
+ X = 50
13
+ puts "Making #{X} requests..."
14
+ t0 = Time.now
15
+ supervise do |s|
16
+ X.times { get_server_time }
17
+ end
18
+ elapsed = Time.now - t0
19
+ puts "count: #{X} elapsed: #{elapsed} rate: #{X / elapsed} reqs/s"
@@ -0,0 +1,24 @@
1
+ // For the sake of comparing performance, here's a node.js-based HTTP server
2
+ // doing roughly the same thing as http_server. Preliminary benchmarking shows
3
+ // the ruby version has a throughput (req/s) of about 2/3 of the JS version.
4
+
5
+ const http = require('http');
6
+
7
+ const MSG = 'Hello World';
8
+
9
+ const server = http.createServer((req, res) => {
10
+ // let requestCopy = {
11
+ // method: req.method,
12
+ // request_url: req.url,
13
+ // headers: req.headers
14
+ // };
15
+
16
+ // res.writeHead(200, { 'Content-Type': 'application/json' });
17
+ // res.end(JSON.stringify(requestCopy));
18
+
19
+ res.writeHead(200);
20
+ res.end(MSG)
21
+ });
22
+
23
+ server.listen(1235);
24
+ console.log('Listening on port 1235');
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'modulation'
4
+
5
+ Polyphony = import('../../lib/polyphony')
6
+ HTTPServer = import('../../lib/polyphony/http/server')
7
+
8
+ opts = { reuse_addr: true, dont_linger: true }
9
+ server = HTTPServer.serve('0.0.0.0', 1234, opts) do |req|
10
+ req.respond("Hello world!\n")
11
+ end
12
+ puts "pid: #{Process.pid}"
13
+ puts "Listening on port 1234..."
14
+ server.await
15
+ puts "bye bye"
16
+
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'modulation'
4
+ require 'localhost/authority'
5
+
6
+ Polyphony = import('../../lib/polyphony')
7
+ HTTPServer = import('../../lib/polyphony/http/server')
8
+
9
+ opts = {
10
+ reuse_addr: true,
11
+ dont_linger: true,
12
+ }
13
+ runner = HTTPServer.listener('0.0.0.0', 1234, opts) do |req|
14
+ req.respond("Hello world!\n")
15
+ end
16
+
17
+ puts "Listening on port 1234"
18
+
19
+ child_pids = []
20
+ 4.times do
21
+ child_pids << Polyphony.fork do
22
+ puts "forked pid: #{Process.pid}"
23
+ spawn(&runner)
24
+ end
25
+ end
26
+
27
+ child_pids.each { |pid| EV::Child.new(pid).await }
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'modulation'
4
+
5
+ Polyphony = import('../../lib/polyphony')
6
+ HTTPServer = import('../../lib/polyphony/http/server')
7
+
8
+ $throttler = throttle(1000)
9
+ opts = { reuse_addr: true, dont_linger: true }
10
+ server = HTTPServer.serve('0.0.0.0', 1234, opts) do |req|
11
+ $throttler.call { req.respond("Hello world!\n") }
12
+ end
13
+ puts "pid: #{Process.pid}"
14
+ puts "Listening on port 1234..."
15
+ server.await
16
+ puts "bye bye"
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'modulation'
4
+
5
+ STDOUT.sync = true
6
+
7
+ Polyphony = import('../../lib/polyphony')
8
+ HTTPServer = import('../../lib/polyphony/http/server')
9
+ Websocket = import('../../lib/polyphony/websocket')
10
+
11
+ def ws_handler(conn)
12
+ timer = spawn {
13
+ throttled_loop(1) {
14
+ conn << Time.now.to_s
15
+ }
16
+ }
17
+ while msg = conn.recv
18
+ # conn << "you said: #{msg}"
19
+ end
20
+ ensure
21
+ timer.stop
22
+ end
23
+
24
+ opts = {
25
+ reuse_addr: true,
26
+ dont_linger: true,
27
+ upgrade: {
28
+ websocket: Websocket.handler(&method(:ws_handler))
29
+ }
30
+ }
31
+
32
+ HTML = IO.read(File.join(__dir__, 'ws_page.html'))
33
+
34
+ server = HTTPServer.serve('0.0.0.0', 1234, opts) do |req|
35
+ req.respond(HTML, 'Content-Type' => 'text/html')
36
+ end
37
+
38
+ puts "pid: #{Process.pid}"
39
+ puts "Listening on port 1234..."
40
+ server.await
41
+ puts "bye bye"
42
+
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'modulation'
4
+
5
+ Polyphony = import('../../lib/polyphony')
6
+
7
+ spawn do
8
+ t0 = Time.now
9
+ io = Polyphony::Net.tcp_connect('google.com', 443, secure: true)
10
+ io.write("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n")
11
+ reply = io.read(2**16)
12
+ puts "time: #{Time.now - t0}"
13
+ puts
14
+ puts reply
15
+ rescue => e
16
+ p e
17
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'modulation'
4
+ require 'localhost/authority'
5
+
6
+ Polyphony = import('../../lib/polyphony')
7
+ HTTPServer = import('../../lib/polyphony/http/server')
8
+
9
+ spawn do
10
+ authority = Localhost::Authority.fetch
11
+ opts = {
12
+ reuse_addr: true,
13
+ dont_linger: true,
14
+ secure_context: authority.server_context
15
+ }
16
+ server = HTTPServer.serve('0.0.0.0', 1234, opts) do |req|
17
+ req.respond("Hello world!\n")
18
+ end
19
+ server.await
20
+ end
21
+
22
+ puts "pid: #{Process.pid}"
23
+ puts "Listening on port 1234..."
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'modulation'
4
+ require 'localhost/authority'
5
+
6
+ STDOUT.sync = true
7
+
8
+ Polyphony = import('../../lib/polyphony')
9
+ HTTPServer = import('../../lib/polyphony/http/server')
10
+ Websocket = import('../../lib/polyphony/websocket')
11
+
12
+ def ws_handler(conn)
13
+ timer = spawn {
14
+ throttled_loop(1) {
15
+ conn << Time.now.to_s rescue nil
16
+ }
17
+ }
18
+ while msg = conn.recv
19
+ puts "msg: #{msg}"
20
+ # conn << "you said: #{msg}"
21
+ end
22
+ ensure
23
+ timer.stop
24
+ end
25
+
26
+ authority = Localhost::Authority.fetch
27
+ opts = {
28
+ reuse_addr: true,
29
+ dont_linger: true,
30
+ secure_context: authority.server_context,
31
+ upgrade: {
32
+ websocket: Websocket.handler(&method(:ws_handler))
33
+ }
34
+ }
35
+
36
+ HTML = IO.read(File.join(__dir__, 'wss_page.html'))
37
+
38
+ server = HTTPServer.serve('0.0.0.0', 1234, opts) do |req|
39
+ req.respond(HTML, 'Content-Type' => 'text/html')
40
+ end
41
+
42
+ puts "pid: #{Process.pid}"
43
+ puts "Listening on port 1234..."
44
+ server.await
45
+ puts "bye bye"
46
+