tipi 0.30 → 0.31

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: 92c33ccc40f7b8f4f53fded65ab5902c81bcf0a92598fa5b2be3ada409a78721
4
- data.tar.gz: b4f80d28c10539b44d1554fbd8e640c253919070cc576efbf62249eee8efeeb7
3
+ metadata.gz: 8eb4db3e433119acc5b10688d6a558de425f8f4b390bbd280055bd9429acde60
4
+ data.tar.gz: 90f6dfc0ba6b99acb8b3d251845774f146641bbc922cd5b25658a7bf3fc97ef1
5
5
  SHA512:
6
- metadata.gz: 6a5712289bb6adc18e4c6fa24495157c19c41831036adc2e5ef082a640b09df426d015de5f00cb48d1d78d0855ce078c1cc7f2c15a1cec24a68cf022d45f9eea
7
- data.tar.gz: 55e627cb9a6e91a846460ee30f3480ad77e4cb9124a40cd2fc68bf9abba7d6ecdbc52530ac92271d78c4a75fc9654be706efb59dba491ebbc6bc70532bc42aaa
6
+ metadata.gz: 80aa5c2464a8812b79dc6fa15c0230dde2bb839f66737b1fac03f8c7d51fbf1bc6ba3f7f1ad1bc6882f6ad8184ecb614b44dd43cdc67a403ae30dac541137579
7
+ data.tar.gz: 8e77ba6c910898761ef63f435ad70e8bcd0f3f7ff14f5e86be2ea675c9387affa191a6ccc0c5f44565b6b8d1ea661606eb2e8a5c4cbb0ac4ed9119fa26828e27
@@ -1,3 +1,9 @@
1
+ ## 0.31 2020-07-28
2
+
3
+ * Fix websocket server code
4
+ * Implement configuration layer (WIP)
5
+ * Improve performance of rack adapter
6
+
1
7
  ## 0.30 2020-07-15
2
8
 
3
9
  * Rename project to Tipi
@@ -1,10 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- tipi (0.30)
4
+ tipi (0.31)
5
5
  http-2 (~> 0.10.0)
6
6
  http_parser.rb (~> 0.6.0)
7
- polyphony (~> 0.43.5)
7
+ polyphony (~> 0.44)
8
8
  rack (>= 2.0.8, < 2.3.0)
9
9
  websocket (~> 1.2.8)
10
10
 
@@ -16,7 +16,7 @@ GEM
16
16
  docile (1.3.2)
17
17
  http-2 (0.10.2)
18
18
  http_parser.rb (0.6.0)
19
- json (2.1.0)
19
+ json (2.3.1)
20
20
  localhost (1.1.4)
21
21
  minitest (5.11.3)
22
22
  minitest-reporters (1.4.2)
@@ -24,7 +24,7 @@ GEM
24
24
  builder
25
25
  minitest (>= 5.0)
26
26
  ruby-progressbar
27
- polyphony (0.43.5)
27
+ polyphony (0.44.0)
28
28
  rack (2.2.3)
29
29
  rake (12.3.3)
30
30
  ruby-progressbar (1.10.1)
data/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ <p align="center"><img src="docs/tipi-logo.png" /></p>
2
+
1
3
  # Tipi - the All-in-one Web Server for Ruby Apps
2
4
 
3
5
  [![Gem Version](https://badge.fury.io/rb/tipi.svg)](http://rubygems.org/gems/tipi)
@@ -14,9 +16,13 @@ reverse-proxy such as Nginx.
14
16
 
15
17
  ## Features
16
18
 
17
- * **Full-blown, integrated, high-performance HTTP 1 / HTTP 2 / WebSocket server
18
- with TLS/SSL termination, automatic ALPN protocol selection, and body
19
- streaming**.
19
+ * High-performance, highly concurrent web server based on
20
+ [Polyphony](https://github.com/digital-fabric/polyphony)
21
+ * Full support for HTTP, HTTP/2, WebSocket protocols
22
+ * Built-in SSL termination for secure, encrypted connections
23
+ * Automatic ALPN protocol selection
24
+ * Request and response body streaming for efficient downloads and uploads
25
+ * Full support for Rack-based apps
20
26
 
21
27
  ## Documentation
22
28
 
data/bin/tipi CHANGED
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require File.expand_path('../lib/tipi', __dir__)
4
- require File.expand_path('../lib/tipi/rack_adapter', __dir__)
3
+ require 'bundler/setup'
4
+ require 'polyphony'
5
+ require File.expand_path('../lib/tipi/configuration', __dir__)
5
6
 
6
- app_path = ARGV.first || './config.ru'
7
- app = Tipi::RackAdapter.load(app_path)
8
- opts = { reuse_addr: true, dont_linger: true }
7
+ config = {}
8
+ #config[:forked] = 4
9
9
 
10
- puts "listening on port 1234"
11
- puts "pid: #{Process.pid}"
12
- Tipi.serve('0.0.0.0', 1234, opts, &app)
10
+ configuration_manager = spin { Tipi::Configuration.supervise_config }
11
+
12
+ configuration_manager << config
13
+ configuration_manager.await
Binary file
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ run lambda { |env|
4
+ [
5
+ 200,
6
+ {"Content-Type" => "text/plain"},
7
+ ["Hello, world!"]
8
+ ]
9
+ }
@@ -26,4 +26,4 @@ child_pids = []
26
26
  child_pids << pid
27
27
  end
28
28
 
29
- child_pids.each { |pid| Thread.current.agent.waitpid(pid) }
29
+ child_pids.each { |pid| Thread.current.backend.waitpid(pid) }
@@ -13,7 +13,7 @@ opts = {
13
13
  secure_context: authority.server_context
14
14
  }
15
15
 
16
- server = Polyphony::HTTP::Server.listen('0.0.0.0', 1234, opts)
16
+ server = Tipi.listen('0.0.0.0', 1234, opts)
17
17
 
18
18
  puts 'Listening on port 1234'
19
19
 
@@ -29,4 +29,4 @@ child_pids = []
29
29
  child_pids << pid
30
30
  end
31
31
 
32
- child_pids.each { |pid| Thread.current.agent.waitpid(pid) }
32
+ child_pids.each { |pid| Thread.current.backend.waitpid(pid) }
@@ -4,7 +4,7 @@ require 'bundler/setup'
4
4
  require 'tipi'
5
5
 
6
6
  app_path = ARGV.first || File.expand_path('./config.ru', __dir__)
7
- app = Polyphony::HTTP::Server::RackAdapter.load(app_path)
7
+ app = Tipi::RackAdapter.load(app_path)
8
8
  opts = { reuse_addr: true, dont_linger: true }
9
9
 
10
10
  puts 'listening on port 1234'
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'polyphony'
5
+ require 'websocket'
6
+
7
+ ::Exception.__disable_sanitized_backtrace__ = true
8
+
9
+ class WebsocketClient
10
+ def initialize(url, headers = {})
11
+ @socket = TCPSocket.new('127.0.0.1', 1234)
12
+ do_handshake(url, headers)
13
+ end
14
+
15
+ def do_handshake(url, headers)
16
+ handshake = WebSocket::Handshake::Client.new(url: url, headers: headers)
17
+ @socket << handshake.to_s
18
+ @socket.read_loop do |data|
19
+ handshake << data
20
+ break if handshake.finished?
21
+ end
22
+ raise 'Websocket handshake failed' unless handshake.valid?
23
+ @version = handshake.version
24
+
25
+ @reader = WebSocket::Frame::Incoming::Client.new(version: @version)
26
+ end
27
+
28
+ def receive
29
+ loop do
30
+ data = @socket.readpartial(8192)
31
+ @reader << data
32
+ parsed = @reader.next
33
+ return parsed if parsed
34
+ end
35
+ end
36
+
37
+ def send(data)
38
+ frame = WebSocket::Frame::Outgoing::Client.new(
39
+ version: @version,
40
+ data: data,
41
+ type: :text
42
+ )
43
+ @socket << frame.to_s
44
+ end
45
+ alias_method :<<, :send
46
+
47
+ def close
48
+ @socket.close
49
+ end
50
+ end
51
+
52
+
53
+ (1..3).each do |i|
54
+ spin do
55
+ client = WebsocketClient.new('ws://127.0.0.1:1234/', { Cookie: "SESSIONID=#{i * 10}" })
56
+ (1..3).each do |j|
57
+ sleep rand(0.2..0.5)
58
+ client.send "Hello from client #{i} (#{j})"
59
+ puts "server reply: #{client.receive}"
60
+ end
61
+ client.close
62
+ end
63
+ end
64
+
65
+ suspend
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/inline'
4
+
5
+ gemfile do
6
+ source 'https://rubygems.org'
7
+ gem 'polyphony', '~> 0.44'
8
+ gem 'tipi', '~> 0.31'
9
+ end
10
+
11
+ require 'tipi/websocket'
12
+
13
+ class WebsocketClient
14
+ def initialize(url, headers = {})
15
+ @socket = TCPSocket.new('127.0.0.1', 1234)
16
+ do_handshake(url, headers)
17
+ end
18
+
19
+ def do_handshake(url, headers)
20
+ handshake = WebSocket::Handshake::Client.new(url: url, headers: headers)
21
+ @socket << handshake.to_s
22
+ @socket.read_loop do |data|
23
+ handshake << data
24
+ break if handshake.finished?
25
+ end
26
+ raise 'Websocket handshake failed' unless handshake.valid?
27
+ @version = handshake.version
28
+
29
+ @reader = WebSocket::Frame::Incoming::Client.new(version: @version)
30
+ end
31
+
32
+ def receive
33
+ loop do
34
+ data = @socket.readpartial(8192)
35
+ @reader << data
36
+ parsed = @reader.next
37
+ return parsed if parsed
38
+ end
39
+ end
40
+
41
+ def send(data)
42
+ frame = WebSocket::Frame::Outgoing::Client.new(
43
+ version: @version,
44
+ data: data,
45
+ type: :text
46
+ )
47
+ @socket << frame.to_s
48
+ end
49
+ alias_method :<<, :send
50
+
51
+ def close
52
+ @socket.close
53
+ end
54
+ end
55
+
56
+ server = spin do
57
+ websocket_handler = Tipi::Websocket.handler do |conn|
58
+ while (msg = conn.recv)
59
+ conn << "you said: #{msg}"
60
+ end
61
+ end
62
+
63
+ opts = { upgrade: { websocket: websocket_handler } }
64
+ puts 'Listening on port http://127.0.0.1:1234/'
65
+ Tipi.serve('0.0.0.0', 1234, opts) do |req|
66
+ req.respond("Hello world!\n")
67
+ end
68
+ end
69
+
70
+ sleep 0.01 # wait for server to start
71
+
72
+ clients = (1..3).map do |i|
73
+ spin do
74
+ client = WebsocketClient.new('ws://127.0.0.1:1234/', { Cookie: "SESSIONID=#{i * 10}" })
75
+ (1..3).each do |j|
76
+ sleep rand(0.2..0.5)
77
+ client.send "Hello from client #{i} (#{j})"
78
+ puts "server reply: #{client.receive}"
79
+ end
80
+ client.close
81
+ end
82
+ end
83
+
84
+ Fiber.await(*clients)
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'bundler/setup'
4
4
  require 'tipi'
5
+ require 'tipi/websocket'
5
6
 
6
7
  def ws_handler(conn)
7
8
  while (msg = conn.recv)
@@ -13,7 +14,7 @@ opts = {
13
14
  reuse_addr: true,
14
15
  dont_linger: true,
15
16
  upgrade: {
16
- websocket: Polyphony::Websocket.handler(&method(:ws_handler))
17
+ websocket: Tipi::Websocket.handler(&method(:ws_handler))
17
18
  }
18
19
  }
19
20
 
@@ -3,6 +3,7 @@
3
3
  require 'polyphony'
4
4
  require_relative './tipi/http1_adapter'
5
5
  require_relative './tipi/http2_adapter'
6
+ require_relative './tipi/configuration'
6
7
 
7
8
  module Tipi
8
9
  ALPN_PROTOCOLS = %w[h2 http/1.1].freeze
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './handler'
4
+
5
+ module Tipi
6
+ module Configuration
7
+ class << self
8
+ def supervise_config
9
+ current_runner = nil
10
+ while (config = receive)
11
+ old_runner, current_runner = current_runner, spin { run(config) }
12
+ old_runner&.stop
13
+ end
14
+ end
15
+
16
+ def run(config)
17
+ start_listeners(config)
18
+ config[:forked] ? forked_supervise(config) : simple_supervise(config)
19
+ end
20
+
21
+ def start_listeners(config)
22
+ puts "listening on port 1234"
23
+ @server = Polyphony::Net.tcp_listen('0.0.0.0', 1234, { reuse_addr: true, dont_linger: true })
24
+ end
25
+
26
+ def simple_supervise(config)
27
+ virtual_hosts = setup_virtual_hosts(config)
28
+ start_acceptors(config, virtual_hosts)
29
+ suspend
30
+ # supervise(restart: true)
31
+ end
32
+
33
+ def forked_supervise(config)
34
+ config[:forked].times do
35
+ spin { Polyphony.watch_process { simple_supervise(config) } }
36
+ end
37
+ suspend
38
+ end
39
+
40
+ def setup_virtual_hosts(config)
41
+ {
42
+ '*': Tipi::DefaultHandler.new(config)
43
+ }
44
+ end
45
+
46
+ def start_acceptors(config, virtual_hosts)
47
+ spin do
48
+ puts "pid: #{Process.pid}"
49
+ while (connection = @server.accept)
50
+ spin { virtual_hosts[:'*'].call(connection) }
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './rack_adapter'
4
+ require_relative './http1_adapter'
5
+ require_relative './http2_adapter'
6
+
7
+ module Tipi
8
+ class DefaultHandler
9
+ def initialize(config)
10
+ @config = config
11
+
12
+ app_path = ARGV.first || './config.ru'
13
+ @app = Tipi::RackAdapter.load(app_path)
14
+ end
15
+
16
+ def call(socket)
17
+ socket.no_delay if socket.respond_to?(:no_delay)
18
+ adapter = protocol_adapter(socket, {})
19
+ adapter.each(&@app)
20
+ ensure
21
+ socket.close
22
+ end
23
+
24
+ ALPN_PROTOCOLS = %w[h2 http/1.1].freeze
25
+ H2_PROTOCOL = 'h2'
26
+
27
+ def protocol_adapter(socket, opts)
28
+ use_http2 = socket.respond_to?(:alpn_protocol) &&
29
+ socket.alpn_protocol == H2_PROTOCOL
30
+
31
+ klass = use_http2 ? HTTP2Adapter : HTTP1Adapter
32
+ klass.new(socket, opts)
33
+ end
34
+ end
35
+ end
@@ -31,30 +31,41 @@ module Tipi
31
31
  src = IO.read(path)
32
32
  instance_eval(src, path, 1)
33
33
  end
34
+
35
+ RACK_ENV = {
36
+ 'SCRIPT_NAME' => '',
37
+ 'rack.version' => Rack::VERSION,
38
+ 'SERVER_PORT' => '80', # ?
39
+ 'rack.url_scheme' => 'http', # ?
40
+ 'rack.errors' => STDERR, # ?
41
+ 'rack.multithread' => false,
42
+ 'rack.run_once' => false,
43
+ 'rack.hijack?' => false,
44
+ 'rack.hijack' => nil,
45
+ 'rack.hijack_io' => nil,
46
+ 'rack.session' => nil,
47
+ 'rack.logger' => nil,
48
+ 'rack.multipart.buffer_size' => nil,
49
+ 'rack.multipar.tempfile_factory' => nil
50
+ }
34
51
 
35
52
  def env(request)
36
- {
37
- 'REQUEST_METHOD' => request.method,
38
- 'SCRIPT_NAME' => '',
39
- 'PATH_INFO' => request.path,
40
- 'QUERY_STRING' => request.query_string || '',
41
- 'SERVER_NAME' => request.headers['Host'], # ?
42
- 'SERVER_PORT' => '80', # ?
43
- 'rack.version' => Rack::VERSION,
44
- 'rack.url_scheme' => 'https', # ?
45
- 'rack.input' => InputStream.new(request),
46
- 'rack.errors' => STDERR, # ?
47
- 'rack.multithread' => false,
48
- 'rack.run_once' => false,
49
- 'rack.hijack?' => false,
50
- 'rack.hijack' => nil,
51
- 'rack.hijack_io' => nil,
52
- 'rack.session' => nil,
53
- 'rack.logger' => nil,
54
- 'rack.multipart.buffer_size' => nil,
55
- 'rack.multipar.tempfile_factory' => nil
56
- }.tap do |env|
57
- request.headers.each { |k, v| env["HTTP_#{k.upcase}"] = v }
53
+ Hash.new do |h, k|
54
+ h[k] = env_value_from_request(request, k)
55
+ end
56
+ end
57
+
58
+ HTTP_HEADER_RE = /^HTTP_(.+)$/.freeze
59
+
60
+ def env_value_from_request(request, key)
61
+ case key
62
+ when 'REQUEST_METHOD' then request.method
63
+ when 'PATH_INFO' then request.path
64
+ when 'QUERY_STRING' then request.query_string || ''
65
+ when 'SERVER_NAME' then request.headers['Host']
66
+ when 'rack.input' then InputStream.new(request)
67
+ when HTTP_HEADER_RE then request.headers[$1.downcase]
68
+ else RACK_ENV[key]
58
69
  end
59
70
  end
60
71
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tipi
4
- VERSION = '0.30'
4
+ VERSION = '0.31'
5
5
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- export :handler
4
-
5
3
  require 'digest/sha1'
6
4
  require 'websocket'
7
5
 
@@ -27,7 +27,7 @@ class MiniTest::Test
27
27
  end
28
28
  Fiber.current.setup_main_fiber
29
29
  Fiber.current.instance_variable_set(:@auto_watcher, nil)
30
- Thread.current.agent = Polyphony::LibevAgent.new
30
+ Thread.current.backend = Polyphony::Backend.new
31
31
  sleep 0
32
32
  end
33
33
 
@@ -210,7 +210,6 @@ class HTTP1ServerTest < MiniTest::Test
210
210
  opts = {
211
211
  upgrade: {
212
212
  echo: lambda do |conn, _headers|
213
- p :echo1
214
213
  conn << <<~HTTP.http_lines
215
214
  HTTP/1.1 101 Switching Protocols
216
215
  Upgrade: echo
@@ -218,13 +217,7 @@ class HTTP1ServerTest < MiniTest::Test
218
217
 
219
218
  HTTP
220
219
 
221
- loop do
222
- data = conn.readpartial(8192)
223
- conn << data
224
- snooze
225
- rescue EOFError
226
- break
227
- end
220
+ conn.read_loop { |data| conn << data }
228
221
  done = true
229
222
  end
230
223
  }
@@ -277,7 +270,7 @@ class HTTP1ServerTest < MiniTest::Test
277
270
  connection.close
278
271
  assert !done
279
272
 
280
- 10.times { snooze }
273
+ 12.times { snooze }
281
274
  assert done
282
275
  end
283
276
 
@@ -19,7 +19,7 @@ Gem::Specification.new do |s|
19
19
 
20
20
  s.executables = ['tipi']
21
21
 
22
- s.add_runtime_dependency 'polyphony', '~>0.43.5'
22
+ s.add_runtime_dependency 'polyphony', '~>0.44'
23
23
 
24
24
  s.add_runtime_dependency 'http_parser.rb', '~>0.6.0'
25
25
  s.add_runtime_dependency 'http-2', '~>0.10.0'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tipi
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.30'
4
+ version: '0.31'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-07-15 00:00:00.000000000 Z
11
+ date: 2020-07-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: polyphony
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.43.5
19
+ version: '0.44'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.43.5
26
+ version: '0.44'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: http_parser.rb
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -175,9 +175,10 @@ files:
175
175
  - TODO.md
176
176
  - bin/tipi
177
177
  - docs/README.md
178
- - docs/summary.md
178
+ - docs/tipi-logo.png
179
179
  - examples/cuba.ru
180
180
  - examples/hanami-api.ru
181
+ - examples/hello.ru
181
182
  - examples/http_server.js
182
183
  - examples/http_server.rb
183
184
  - examples/http_server_forked.rb
@@ -192,11 +193,15 @@ files:
192
193
  - examples/rack_server.rb
193
194
  - examples/rack_server_https.rb
194
195
  - examples/rack_server_https_forked.rb
196
+ - examples/websocket_client.rb
197
+ - examples/websocket_demo.rb
195
198
  - examples/websocket_secure_server.rb
196
199
  - examples/websocket_server.rb
197
200
  - examples/ws_page.html
198
201
  - examples/wss_page.html
199
202
  - lib/tipi.rb
203
+ - lib/tipi/configuration.rb
204
+ - lib/tipi/handler.rb
200
205
  - lib/tipi/http1_adapter.rb
201
206
  - lib/tipi/http2_adapter.rb
202
207
  - lib/tipi/http2_stream.rb
@@ -234,7 +239,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
234
239
  - !ruby/object:Gem::Version
235
240
  version: '0'
236
241
  requirements: []
237
- rubygems_version: 3.0.6
242
+ rubygems_version: 3.1.2
238
243
  signing_key:
239
244
  specification_version: 4
240
245
  summary: Tipi - the All-in-one Web Server for Ruby Apps
@@ -1,60 +0,0 @@
1
- # Table of contents
2
-
3
- * [Polyphony - Easy Concurrency for Ruby](../README.md)
4
-
5
- ## Getting Started
6
-
7
- * [Installing](getting-started/installing.md)
8
- * [Tutorial](getting-started/tutorial.md)
9
-
10
- ## Technical overview
11
-
12
- * [Design Principles](technical-overview/design-principles.md)
13
- * [Concurrency the Easy Way](technical-overview/concurrency.md)
14
- * [How Fibers are Scheduled](technical-overview/fiber-scheduling.md)
15
- * [Exception Handling](technical-overview/exception-handling.md)
16
- * [Frequently Asked Questions](technical-overview/faq.md)
17
-
18
- ## How To
19
-
20
- * [Make an echo server](howto/echo-server.md)
21
- * [Make an HTTP server](howto/http-server.md)
22
- * [Make a Websocket server](howto/websocket-server.md)
23
- * [Use timers](howto/timers.md)
24
- * [Throttle recurrent operations](howto/throttle.md)
25
- * [Cancel ongoing operations](howto/cancel.md)
26
- * [Control coprocesses](howto/coprocesses.md)
27
- * [Synchronize concurrent operations](howto/synchronize.md)
28
- * [Perform CPU-bound operations](howto/cpu-bound.md)
29
- * [Control backpressure](howto/backpressure.md)
30
- * [Fork worker processes](howto/worker-processes.md)
31
-
32
- ## Polyphony extensions
33
-
34
- * [Postgresql](extensions/pg)
35
- * [Redis](extensions/redis)
36
- * [IRB](extensions/irb)
37
- * [Throttlers](#)
38
- * [Resource Pools](#)
39
- * [Synchronisation](#)
40
- * [Web Server](user-guide/web-server.md)
41
- * [Websocket Server](#)
42
- * [Reactor API](#)
43
-
44
- ## API Reference
45
-
46
- * [Polyphony::CancelScope](#)
47
- * [Polyphony::Coprocess](#)
48
- * [Gyro](#)
49
- * [Gyro::Async](#)
50
- * [Gyro::Child](#)
51
- * [Gyro::IO](#)
52
- * [Gyro::Timer](#)
53
- * [Kernel](#)
54
- * [Polyphony](#)
55
- * [Polyphony::Mutex](#)
56
- * [Polyphony::Pulser](#)
57
- * [Polyphony::ResourcePool](#)
58
- * [Polyphony::Throttler](#)
59
-
60
- ## [Contributing to Polyphony](contributing.md)