rails-threaded-proxy 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.md +46 -0
- data/VERSION +1 -1
- data/lib/threaded_proxy/client.rb +50 -14
- data/lib/threaded_proxy/controller.rb +2 -2
- data/rails-threaded-proxy.gemspec +6 -4
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2aae2ccc6678cd9afd9e3068a996bfe1d163f0569512a9e37b958c749aeb8096
|
4
|
+
data.tar.gz: 14079d5dfc8a7319c54469d859d2cb5e055ba4fae94d80cebda8cb9968bcd076
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: eb6a536989c9b554b79ce943ed143e65a1985b7d535dac729cc7b90f01961eda02325134201e6a027d34775cafde12aa084264cce5a9c59c4c88b9b9a31b147f
|
7
|
+
data.tar.gz: a4c6056afd3971cf18cd9d2ab5e2e3739b0c76a922c655024d1e34f00069b8cbe42f376c1611c040948798b767663e1a0eb7ad7ec6753af90b4e0fc3727808f9
|
data/LICENSE
CHANGED
data/README.md
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# rails-threaded-proxy
|
2
|
+
|
3
|
+
Asynchronous high throughput reverse proxy for rails
|
4
|
+
|
5
|
+
*Warning: experimental. Use at your own risk.*
|
6
|
+
|
7
|
+
## About
|
8
|
+
|
9
|
+
Rails concurrency is often limited to running many processes, which can be memory-intensive. Even for servers that support threads, it can be difficult running dozens or hundreds of threads. But you may have backend services that are slow to respond, and/or return very large responses. It is useful to put these services behind rails for authentication, but slow responses can tie up your rails workers preventing them from serving other clients.
|
10
|
+
|
11
|
+
`rails-threaded-proxy` disconnects the proxying from the rack request/response cycle, freeing up workers to serve other clients. It does this by running the origin request in a thread. But running in a thread is not enough: we need to be able to respond to the rails request, but rack owns the socket. So it hijacks the request: rack completes immediately but dissociates from the socket. Then we're free to manage the socket ourselves. Copying between sockets, we can achieve high throughput (100MB/s+) with minimal CPU and memory overhead.
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
class MyController
|
17
|
+
include ThreadedProxy::Controller
|
18
|
+
|
19
|
+
def my_backend
|
20
|
+
proxy_fetch "http://backend.service/path/to/endpoint", method: :post do |config|
|
21
|
+
config.on_headers do |client_response|
|
22
|
+
# override some response headers coming from the backend
|
23
|
+
client_response['content-security-policy'] = "sandbox;"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
```
|
29
|
+
|
30
|
+
## Requirements
|
31
|
+
|
32
|
+
Tested with Rails 7, but probably works in Rails 6+. Needs an application server that supports `rack.hijack`. (only tested on [https://puma.io/](Puma) so far)
|
33
|
+
|
34
|
+
## Caveats
|
35
|
+
|
36
|
+
* There isn't currently a way to limit concurrency. It is possible to run your server out of file descriptors, memory, etc.
|
37
|
+
* Since the proxying happens in a thread, callbacks are also run inside of the thread. Don't do anything non-threadsafe in callbacks.
|
38
|
+
* There is currently probably not sufficient error handling for edge cases. This is experimental.
|
39
|
+
|
40
|
+
## Attribution
|
41
|
+
|
42
|
+
Inspired by [https://github.com/axsuul/rails-reverse-proxy](rails-reverse-proxy), and tries to use similar API structure where possible. If you don't care about the specific benefits of `rails-threaded-proxy`, you should consider using `rails-reverse-proxy` instead.
|
43
|
+
|
44
|
+
## License
|
45
|
+
|
46
|
+
See LICENSE
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.4.0
|
@@ -9,7 +9,7 @@ module ThreadedProxy
|
|
9
9
|
class Client
|
10
10
|
DISALLOWED_RESPONSE_HEADERS = %w[keep-alive].freeze
|
11
11
|
|
12
|
-
|
12
|
+
HTTP_METHODS = {
|
13
13
|
'get' => Net::HTTP::Get,
|
14
14
|
'post' => Net::HTTP::Post,
|
15
15
|
'put' => Net::HTTP::Put,
|
@@ -19,6 +19,20 @@ module ThreadedProxy
|
|
19
19
|
'trace' => Net::HTTP::Trace
|
20
20
|
}.freeze
|
21
21
|
|
22
|
+
CALLBACK_METHODS = %i[
|
23
|
+
on_response
|
24
|
+
on_headers
|
25
|
+
on_body
|
26
|
+
on_complete
|
27
|
+
on_error
|
28
|
+
].freeze
|
29
|
+
|
30
|
+
CALLBACK_METHODS.each do |method_name|
|
31
|
+
define_method(method_name) do |&block|
|
32
|
+
@callbacks[method_name] = block
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
22
36
|
DEFAULT_OPTIONS = {
|
23
37
|
headers: {},
|
24
38
|
debug: false,
|
@@ -28,6 +42,13 @@ module ThreadedProxy
|
|
28
42
|
def initialize(origin_url, options = {})
|
29
43
|
@origin_url = Addressable::URI.parse(origin_url)
|
30
44
|
@options = DEFAULT_OPTIONS.merge(options)
|
45
|
+
|
46
|
+
@callbacks = {}
|
47
|
+
CALLBACK_METHODS.each do |method_name|
|
48
|
+
@callbacks[method_name] = proc {}
|
49
|
+
end
|
50
|
+
|
51
|
+
yield(self) if block_given?
|
31
52
|
end
|
32
53
|
|
33
54
|
def log(message)
|
@@ -38,7 +59,7 @@ module ThreadedProxy
|
|
38
59
|
request_method = @options[:method].to_s.downcase
|
39
60
|
request_headers = @options[:headers].merge('Connection' => 'close')
|
40
61
|
|
41
|
-
request_class =
|
62
|
+
request_class = HTTP_METHODS[request_method]
|
42
63
|
http_request = request_class.new(@origin_url, request_headers)
|
43
64
|
if @options[:body].respond_to?(:read)
|
44
65
|
http_request.body_stream = @options[:body]
|
@@ -55,21 +76,15 @@ module ThreadedProxy
|
|
55
76
|
|
56
77
|
http.start do
|
57
78
|
http.request(http_request) do |client_response|
|
58
|
-
|
59
|
-
|
79
|
+
@callbacks[:on_response].call(client_response, socket)
|
80
|
+
break if socket.closed?
|
60
81
|
|
61
|
-
yield client_response if block_given?
|
62
|
-
|
63
|
-
# start writing response
|
64
82
|
log('Writing response status and headers')
|
65
|
-
|
66
|
-
|
67
|
-
client_response.each_header do |key, value|
|
68
|
-
socket.write "#{key}: #{value}\r\n" unless DISALLOWED_RESPONSE_HEADERS.include?(key.downcase)
|
69
|
-
end
|
83
|
+
write_headers(client_response, socket)
|
84
|
+
break if socket.closed?
|
70
85
|
|
71
|
-
|
72
|
-
socket.
|
86
|
+
@callbacks[:on_body].call(client_response, socket)
|
87
|
+
break if socket.closed?
|
73
88
|
|
74
89
|
# There may have been some existing data in client_response's read buffer, flush it out
|
75
90
|
# before we manually connect the raw sockets
|
@@ -79,11 +94,32 @@ module ThreadedProxy
|
|
79
94
|
# Copy the rest of the client response to the socket
|
80
95
|
log('Copying response body to client')
|
81
96
|
http.copy_to(socket)
|
97
|
+
|
98
|
+
@callbacks[:on_complete].call(client_response)
|
82
99
|
end
|
100
|
+
rescue StandardError => e
|
101
|
+
@callbacks[:on_error].call(e) or raise
|
83
102
|
end
|
84
103
|
end
|
85
104
|
end
|
86
105
|
|
106
|
+
def write_headers(client_response, socket)
|
107
|
+
socket.write "HTTP/1.1 #{client_response.code} #{client_response.message}\r\n"
|
108
|
+
|
109
|
+
# We don't support reusing connections once we have disconnected them from rack
|
110
|
+
client_response['connection'] = 'close'
|
111
|
+
|
112
|
+
@callbacks[:on_headers].call(client_response, socket)
|
113
|
+
return if socket.closed?
|
114
|
+
|
115
|
+
client_response.each_header do |key, value|
|
116
|
+
socket.write "#{key}: #{value}\r\n" unless DISALLOWED_RESPONSE_HEADERS.include?(key.downcase)
|
117
|
+
end
|
118
|
+
|
119
|
+
# Done with headers
|
120
|
+
socket.write "\r\n"
|
121
|
+
end
|
122
|
+
|
87
123
|
def default_port(uri)
|
88
124
|
case uri.scheme
|
89
125
|
when 'http'
|
@@ -4,7 +4,7 @@ require_relative 'client'
|
|
4
4
|
|
5
5
|
module ThreadedProxy
|
6
6
|
module Controller
|
7
|
-
def proxy_fetch(origin_url, options = {})
|
7
|
+
def proxy_fetch(origin_url, options = {}, &block)
|
8
8
|
# hijack the response so we can take it outside of the rack request/response cycle
|
9
9
|
request.env['rack.hijack'].call
|
10
10
|
socket = request.env['rack.hijack_io']
|
@@ -25,7 +25,7 @@ module ThreadedProxy
|
|
25
25
|
options[:headers]['Content-Type'] = request.env['CONTENT_TYPE'] if request.env['CONTENT_TYPE']
|
26
26
|
end
|
27
27
|
|
28
|
-
client = Client.new(origin_url, options)
|
28
|
+
client = Client.new(origin_url, options, &block)
|
29
29
|
client.start(socket)
|
30
30
|
rescue Errno::EPIPE
|
31
31
|
# client disconnected before request finished; not an error
|
@@ -2,21 +2,22 @@
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
3
3
|
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
|
-
# stub: rails-threaded-proxy 0.
|
5
|
+
# stub: rails-threaded-proxy 0.4.0 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "rails-threaded-proxy".freeze
|
9
|
-
s.version = "0.
|
9
|
+
s.version = "0.4.0".freeze
|
10
10
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
12
12
|
s.require_paths = ["lib".freeze]
|
13
13
|
s.authors = ["Michael Nutt".freeze]
|
14
|
-
s.date = "2024-10-
|
14
|
+
s.date = "2024-10-15"
|
15
15
|
s.description = "Threaded reverse proxy for Ruby on Rails".freeze
|
16
16
|
s.email = "michael@nuttnet.net".freeze
|
17
17
|
s.executables = ["bundle".freeze, "htmldiff".freeze, "jeweler".freeze, "ldiff".freeze, "nokogiri".freeze, "racc".freeze, "rackup".freeze, "rake".freeze, "rdoc".freeze, "ri".freeze, "rspec".freeze, "rubocop".freeze, "semver".freeze]
|
18
18
|
s.extra_rdoc_files = [
|
19
|
-
"LICENSE"
|
19
|
+
"LICENSE",
|
20
|
+
"README.md"
|
20
21
|
]
|
21
22
|
s.files = [
|
22
23
|
".bundle/config",
|
@@ -26,6 +27,7 @@ Gem::Specification.new do |s|
|
|
26
27
|
"Gemfile",
|
27
28
|
"Gemfile.lock",
|
28
29
|
"LICENSE",
|
30
|
+
"README.md",
|
29
31
|
"Rakefile",
|
30
32
|
"VERSION",
|
31
33
|
"bin/bundle",
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rails-threaded-proxy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Nutt
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-10-
|
11
|
+
date: 2024-10-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: actionpack
|
@@ -155,6 +155,7 @@ executables:
|
|
155
155
|
extensions: []
|
156
156
|
extra_rdoc_files:
|
157
157
|
- LICENSE
|
158
|
+
- README.md
|
158
159
|
files:
|
159
160
|
- ".bundle/config"
|
160
161
|
- ".rspec"
|
@@ -163,6 +164,7 @@ files:
|
|
163
164
|
- Gemfile
|
164
165
|
- Gemfile.lock
|
165
166
|
- LICENSE
|
167
|
+
- README.md
|
166
168
|
- Rakefile
|
167
169
|
- VERSION
|
168
170
|
- bin/bundle
|