tipi 0.30 → 0.31

Sign up to get free protection for your applications and to get access to all the features.
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)