zapp 0.1.1 → 0.2.1
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 +4 -4
- data/.rubocop.yml +2 -0
- data/Gemfile.lock +4 -2
- data/bin/zapp +1 -2
- data/examples/rails-app/Gemfile.lock +15 -13
- data/examples/rails-app/config/zapp.rb +1 -1
- data/lib/rack/handler/{zap.rb → zapp.rb} +4 -3
- data/lib/zapp/configuration.rb +1 -1
- data/lib/zapp/http_context/context.rb +10 -11
- data/lib/zapp/http_context/request.rb +11 -7
- data/lib/zapp/http_context/response.rb +10 -5
- data/lib/zapp/logger.rb +29 -18
- data/lib/zapp/pipe.rb +14 -0
- data/lib/zapp/server.rb +36 -16
- data/lib/zapp/socket_pipe/receiver.rb +32 -0
- data/lib/zapp/socket_pipe/sender.rb +17 -0
- data/lib/zapp/version.rb +1 -1
- data/lib/zapp/worker/request_processor.rb +114 -0
- data/lib/zapp/worker.rb +19 -69
- data/lib/zapp/worker_pool.rb +15 -17
- data/lib/zapp.rb +13 -9
- data/zapp.gemspec +2 -0
- metadata +22 -6
- data/bin/console +0 -15
- data/bin/setup +0 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '083c3deaea0cc22f0bd269afbccdd556bf66743771aca04f63f125809c934473'
|
|
4
|
+
data.tar.gz: cd3a771ca1d0e898ddaa99c71b741412e4be8370286ec484b2059317cb632802
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 227f3dc9ce4c2cac931c89f2a3bca81f90f91031a78998258befb02515ee5eb1af386741646bdecc14e82649cc5cc876f360cbfb8a01bf84e119b062ab3a0eea
|
|
7
|
+
data.tar.gz: ffa29c579b1621eca0093e2c3d9a4f02975aa3be6a81aa17348aff7a7f5f54cecf4ea8aeb7829c6a821d4691c2ac9a2cd2d0be33204f2a02b1fd163b5c6990e3
|
data/.rubocop.yml
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -7,13 +7,14 @@ PATH
|
|
|
7
7
|
rack (~> 2.2.3)
|
|
8
8
|
rake (~> 13.0)
|
|
9
9
|
rspec (~> 3.0)
|
|
10
|
+
webrick
|
|
10
11
|
|
|
11
12
|
GEM
|
|
12
13
|
remote: https://rubygems.org/
|
|
13
14
|
specs:
|
|
14
15
|
ast (2.4.2)
|
|
15
16
|
coderay (1.1.3)
|
|
16
|
-
concurrent-ruby (1.1.
|
|
17
|
+
concurrent-ruby (1.1.10)
|
|
17
18
|
diff-lcs (1.4.4)
|
|
18
19
|
docile (1.4.0)
|
|
19
20
|
ffi (1.15.4)
|
|
@@ -53,7 +54,7 @@ GEM
|
|
|
53
54
|
method_source (~> 1.0)
|
|
54
55
|
puma (5.5.2)
|
|
55
56
|
nio4r (~> 2.0)
|
|
56
|
-
rack (2.2.
|
|
57
|
+
rack (2.2.4)
|
|
57
58
|
rainbow (3.0.0)
|
|
58
59
|
rake (13.0.6)
|
|
59
60
|
rb-fsevent (0.11.0)
|
|
@@ -95,6 +96,7 @@ GEM
|
|
|
95
96
|
simplecov_json_formatter (0.1.3)
|
|
96
97
|
thor (1.1.0)
|
|
97
98
|
unicode-display_width (2.1.0)
|
|
99
|
+
webrick (1.7.0)
|
|
98
100
|
|
|
99
101
|
PLATFORMS
|
|
100
102
|
x86_64-linux
|
data/bin/zapp
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ../..
|
|
3
3
|
specs:
|
|
4
|
-
zapp (0.1.
|
|
4
|
+
zapp (0.1.1)
|
|
5
5
|
concurrent-ruby (~> 1.1.9)
|
|
6
6
|
puma (~> 5.5.2)
|
|
7
7
|
rack (~> 2.2.3)
|
|
8
8
|
rake (~> 13.0)
|
|
9
9
|
rspec (~> 3.0)
|
|
10
|
+
webrick
|
|
10
11
|
|
|
11
12
|
GEM
|
|
12
13
|
remote: https://rubygems.org/
|
|
@@ -89,7 +90,7 @@ GEM
|
|
|
89
90
|
childprocess (4.1.0)
|
|
90
91
|
concurrent-ruby (1.1.9)
|
|
91
92
|
crass (1.0.6)
|
|
92
|
-
diff-lcs (1.
|
|
93
|
+
diff-lcs (1.5.0)
|
|
93
94
|
erubi (1.10.0)
|
|
94
95
|
ffi (1.15.4)
|
|
95
96
|
globalid (0.5.2)
|
|
@@ -158,19 +159,19 @@ GEM
|
|
|
158
159
|
ffi (~> 1.0)
|
|
159
160
|
regexp_parser (2.1.1)
|
|
160
161
|
rexml (3.2.5)
|
|
161
|
-
rspec (3.
|
|
162
|
-
rspec-core (~> 3.
|
|
163
|
-
rspec-expectations (~> 3.
|
|
164
|
-
rspec-mocks (~> 3.
|
|
165
|
-
rspec-core (3.
|
|
166
|
-
rspec-support (~> 3.
|
|
167
|
-
rspec-expectations (3.
|
|
162
|
+
rspec (3.11.0)
|
|
163
|
+
rspec-core (~> 3.11.0)
|
|
164
|
+
rspec-expectations (~> 3.11.0)
|
|
165
|
+
rspec-mocks (~> 3.11.0)
|
|
166
|
+
rspec-core (3.11.0)
|
|
167
|
+
rspec-support (~> 3.11.0)
|
|
168
|
+
rspec-expectations (3.11.0)
|
|
168
169
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
169
|
-
rspec-support (~> 3.
|
|
170
|
-
rspec-mocks (3.
|
|
170
|
+
rspec-support (~> 3.11.0)
|
|
171
|
+
rspec-mocks (3.11.1)
|
|
171
172
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
172
|
-
rspec-support (~> 3.
|
|
173
|
-
rspec-support (3.
|
|
173
|
+
rspec-support (~> 3.11.0)
|
|
174
|
+
rspec-support (3.11.0)
|
|
174
175
|
rubyzip (2.3.2)
|
|
175
176
|
sass-rails (6.0.0)
|
|
176
177
|
sassc-rails (~> 2.1, >= 2.1.1)
|
|
@@ -217,6 +218,7 @@ GEM
|
|
|
217
218
|
rack-proxy (>= 0.6.1)
|
|
218
219
|
railties (>= 5.2)
|
|
219
220
|
semantic_range (>= 2.3.0)
|
|
221
|
+
webrick (1.7.0)
|
|
220
222
|
websocket-driver (0.7.5)
|
|
221
223
|
websocket-extensions (>= 0.1.0)
|
|
222
224
|
websocket-extensions (0.1.5)
|
|
@@ -6,11 +6,12 @@ module Rack
|
|
|
6
6
|
module Handler
|
|
7
7
|
# Rack handler for the Zapp web server
|
|
8
8
|
class Zapp
|
|
9
|
+
register(:zapp, Rack::Handler::Zapp)
|
|
10
|
+
|
|
9
11
|
def self.run(app)
|
|
10
|
-
Zapp
|
|
12
|
+
Zapp.config.app = app
|
|
13
|
+
Zapp::Server.new.run
|
|
11
14
|
end
|
|
12
|
-
|
|
13
|
-
register(:zapp, Rack::Handler::Zapp)
|
|
14
15
|
end
|
|
15
16
|
end
|
|
16
17
|
end
|
data/lib/zapp/configuration.rb
CHANGED
|
@@ -44,7 +44,7 @@ module Zapp
|
|
|
44
44
|
{
|
|
45
45
|
Rack::RACK_VERSION => Rack::VERSION,
|
|
46
46
|
Rack::RACK_ERRORS => $stderr,
|
|
47
|
-
Rack::RACK_MULTITHREAD =>
|
|
47
|
+
Rack::RACK_MULTITHREAD => true,
|
|
48
48
|
Rack::RACK_MULTIPROCESS => true,
|
|
49
49
|
Rack::RACK_RUNONCE => false,
|
|
50
50
|
Rack::RACK_URL_SCHEME => %w[yes on 1].include?(ENV["HTTPS"]) ? "https" : "http"
|
|
@@ -1,31 +1,30 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
require_relative("request")
|
|
4
|
+
require_relative("response")
|
|
5
5
|
|
|
6
6
|
module Zapp
|
|
7
7
|
module HTTPContext
|
|
8
8
|
# Context containing request and response
|
|
9
9
|
class Context
|
|
10
|
-
attr_reader(:req, :res)
|
|
10
|
+
attr_reader(:req, :res, :socket)
|
|
11
11
|
|
|
12
|
-
def initialize(socket:)
|
|
12
|
+
def initialize(socket:, logger: Zapp::Logger)
|
|
13
13
|
@socket = socket
|
|
14
14
|
@req = Zapp::HTTPContext::Request.new(socket: socket)
|
|
15
15
|
@res = Zapp::HTTPContext::Response.new(socket: socket)
|
|
16
|
+
rescue Puma::HttpParserError => e
|
|
17
|
+
res.write(data: "Invalid HTTP request", status: 400, headers: {})
|
|
18
|
+
logger.warn("Puma parser error: #{e}")
|
|
19
|
+
logger.debug("HTTP request raw: #{context.req.raw}")
|
|
16
20
|
end
|
|
17
21
|
|
|
18
22
|
def close
|
|
19
23
|
@socket.close
|
|
20
24
|
end
|
|
21
25
|
|
|
22
|
-
def
|
|
23
|
-
|
|
24
|
-
clone_context.instance_variable_set(:@req, @req.dup)
|
|
25
|
-
clone_context.instance_variable_set(:@res, @res.dup)
|
|
26
|
-
clone_context.instance_variable_set(:@socket, @socket.dup)
|
|
27
|
-
|
|
28
|
-
clone_context
|
|
26
|
+
def client_closed?
|
|
27
|
+
req.data["HTTP_CONNECTION"] == "close"
|
|
29
28
|
end
|
|
30
29
|
end
|
|
31
30
|
end
|
|
@@ -1,27 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require("webrick")
|
|
4
|
+
|
|
3
5
|
module Zapp
|
|
4
6
|
module HTTPContext
|
|
5
7
|
# Represents an HTTP Request to be processed by a worker
|
|
6
8
|
class Request
|
|
7
9
|
attr_reader(:raw, :data, :body)
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
# Request parsing is done threaded, but not in separate Ractors.
|
|
12
|
+
# So we allocate an HTTP parser per thread and assigns it to this hash key on Thread.current
|
|
13
|
+
PARSER_THREAD_HASH_KEY = "PUMA_PARSER_INSTANCE"
|
|
11
14
|
|
|
15
|
+
def initialize(socket:)
|
|
12
16
|
# Max Request size of 8KB TODO: Make a config value for this setting
|
|
13
17
|
@raw = socket.readpartial(8192)
|
|
14
18
|
@data = {}
|
|
15
|
-
end
|
|
16
19
|
|
|
17
|
-
def parse!(parser: Puma::HttpParser.new)
|
|
18
20
|
parser.execute(data, raw, 0)
|
|
19
|
-
|
|
21
|
+
|
|
22
|
+
@body = Zapp::InputStream.new(string: "parser.body")
|
|
23
|
+
|
|
20
24
|
parser.reset
|
|
21
25
|
end
|
|
22
26
|
|
|
23
|
-
def
|
|
24
|
-
|
|
27
|
+
def parser
|
|
28
|
+
Thread.current[PARSER_THREAD_HASH_KEY] ||= Puma::HttpParser.new
|
|
25
29
|
end
|
|
26
30
|
end
|
|
27
31
|
end
|
|
@@ -4,21 +4,26 @@ module Zapp
|
|
|
4
4
|
module HTTPContext
|
|
5
5
|
# Represents an HTTP response being sent back to a client
|
|
6
6
|
class Response
|
|
7
|
+
attr_reader(:status, :data, :headers)
|
|
8
|
+
|
|
7
9
|
def initialize(socket:)
|
|
8
10
|
@socket = socket
|
|
9
11
|
end
|
|
10
12
|
|
|
11
|
-
# TODO: Add headers argument
|
|
12
13
|
def write(data:, status:, headers:)
|
|
13
|
-
|
|
14
|
+
@status = status
|
|
15
|
+
@data = data
|
|
16
|
+
@headers = headers
|
|
17
|
+
|
|
18
|
+
response = +"HTTP/1.1 #{status}\n"
|
|
14
19
|
|
|
15
|
-
response
|
|
20
|
+
response << "Content-Length: #{data.size}\n" unless headers["Content-Length"]
|
|
16
21
|
|
|
17
22
|
headers.each do |k, v|
|
|
18
|
-
response
|
|
23
|
+
response << "#{k}: #{v}\n"
|
|
19
24
|
end
|
|
20
25
|
|
|
21
|
-
response
|
|
26
|
+
response << "\n#{data}\n"
|
|
22
27
|
|
|
23
28
|
@socket.write(response)
|
|
24
29
|
end
|
data/lib/zapp/logger.rb
CHANGED
|
@@ -9,7 +9,14 @@ module Zapp
|
|
|
9
9
|
module Base
|
|
10
10
|
attr_writer(:level, :prefix)
|
|
11
11
|
|
|
12
|
-
LEVELS = {
|
|
12
|
+
LEVELS = { TRACE: 0, DEBUG: 1, INFO: 2, WARN: 3, ERROR: 4 }.freeze
|
|
13
|
+
|
|
14
|
+
FROZEN_ENV = ENV.map { |k, v| [k.freeze, v.freeze] }
|
|
15
|
+
.to_h.freeze
|
|
16
|
+
|
|
17
|
+
def trace(msg)
|
|
18
|
+
log("TRACE", msg)
|
|
19
|
+
end
|
|
13
20
|
|
|
14
21
|
def debug(msg)
|
|
15
22
|
log("DEBUG", msg)
|
|
@@ -28,36 +35,40 @@ module Zapp
|
|
|
28
35
|
end
|
|
29
36
|
|
|
30
37
|
def level
|
|
31
|
-
@level ||=
|
|
32
|
-
|
|
33
|
-
raise(
|
|
34
|
-
Zapp::ZappError,
|
|
35
|
-
"Invalid log level '#{ENV['ZAP_LOG_LEVEL']}', must be one of [#{LEVELS.keys.join(', ')}]"
|
|
36
|
-
)
|
|
37
|
-
else
|
|
38
|
-
LEVELS[ENV["ZAP_LOG_LEVEL"]]
|
|
39
|
-
end
|
|
40
|
-
else
|
|
41
|
-
LEVELS[:DEBUG]
|
|
42
|
-
end
|
|
43
|
-
end
|
|
38
|
+
@level ||= begin
|
|
39
|
+
log_level = FROZEN_ENV["LOG_LEVEL"]
|
|
44
40
|
|
|
45
|
-
|
|
41
|
+
if log_level == "" || log_level.nil?
|
|
42
|
+
LEVELS[:DEBUG]
|
|
43
|
+
else
|
|
44
|
+
resolved_level = LEVELS[log_level.upcase.to_sym]
|
|
46
45
|
|
|
47
|
-
|
|
46
|
+
if resolved_level.nil?
|
|
47
|
+
raise(
|
|
48
|
+
Zapp::ZappError,
|
|
49
|
+
"Invalid log level '#{log_level.upcase}', must be one of [#{LEVELS.keys.join(', ')}]"
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
resolved_level
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def log(current_level, msg, **_tags)
|
|
48
59
|
puts("--- #{@prefix} [#{current_level}] #{msg}") if level <= LEVELS[current_level.to_sym]
|
|
49
60
|
end
|
|
50
61
|
end
|
|
51
62
|
include(Zapp::Logger::Base)
|
|
52
63
|
|
|
53
64
|
def initialize
|
|
54
|
-
@prefix = "Zap"
|
|
55
65
|
yield(self) if block_given?
|
|
56
66
|
end
|
|
57
67
|
|
|
58
68
|
class << self
|
|
59
69
|
include(Zapp::Logger::Base)
|
|
60
|
-
@prefix = "Zap"
|
|
61
70
|
end
|
|
62
71
|
end
|
|
63
72
|
end
|
|
73
|
+
|
|
74
|
+
Zapp::Logger.prefix = "Zapp"
|
data/lib/zapp/pipe.rb
ADDED
data/lib/zapp/server.rb
CHANGED
|
@@ -3,40 +3,42 @@
|
|
|
3
3
|
module Zapp
|
|
4
4
|
# The Zap HTTP Server, listens on a TCP connection and processes incoming requests
|
|
5
5
|
class Server
|
|
6
|
-
attr_reader(:
|
|
6
|
+
attr_reader(:worker_pool, :socket_pipe_receiver)
|
|
7
7
|
|
|
8
8
|
def initialize
|
|
9
|
-
@
|
|
10
|
-
@
|
|
9
|
+
@socket_pipe = Zapp::Pipe.new
|
|
10
|
+
@context_pipe = Zapp::Pipe.new
|
|
11
|
+
|
|
12
|
+
@socket_pipe_receiver = Zapp::SocketPipe::Receiver.new(pipe: @socket_pipe)
|
|
13
|
+
|
|
14
|
+
@worker_pool = Zapp::WorkerPool.new(app: Zapp.config.app, socket_pipe: @socket_pipe, context_pipe: @context_pipe)
|
|
11
15
|
end
|
|
12
16
|
|
|
13
17
|
def run
|
|
14
|
-
parser = Puma::HttpParser.new
|
|
15
|
-
|
|
16
18
|
log_start
|
|
17
19
|
|
|
18
20
|
loop do
|
|
19
|
-
socket =
|
|
20
|
-
next if socket.eof?
|
|
21
|
+
socket = socket_pipe_receiver.take
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
context.req.parse!(parser: parser)
|
|
23
|
+
next if socket.eof?
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
parsing_thread_pool.post do
|
|
26
|
+
ctx = Zapp::HTTPContext::Context.new(socket: socket)
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
worker_pool.process(context: ctx) unless ctx.client_closed? # Parsing failed
|
|
29
|
+
end
|
|
30
|
+
rescue Errno::ECONNRESET
|
|
31
|
+
next
|
|
31
32
|
end
|
|
32
33
|
rescue SignalException, IRB::Abort => e
|
|
33
34
|
shutdown(e)
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
def shutdown(err = nil)
|
|
37
|
-
Zapp::Logger.info("Received signal #{err}") unless err.nil?
|
|
38
|
+
Zapp::Logger.info("Received signal #{err.class.name}") unless err.nil?
|
|
38
39
|
Zapp::Logger.info("Gracefully shutting down workers, allowing request processing to finish")
|
|
39
40
|
|
|
41
|
+
socket_pipe_receiver.drain
|
|
40
42
|
worker_pool.drain
|
|
41
43
|
|
|
42
44
|
Zapp::Logger.info("Done. See you next time!")
|
|
@@ -45,11 +47,29 @@ module Zapp
|
|
|
45
47
|
private
|
|
46
48
|
|
|
47
49
|
def log_start
|
|
48
|
-
Zapp::Logger.info("
|
|
50
|
+
Zapp::Logger.info("
|
|
51
|
+
⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡
|
|
52
|
+
⚡ ███████╗ █████╗ ██████╗ ██████╗ ⚡
|
|
53
|
+
⚡ ╚══███╔╝██╔══██╗██╔══██╗██╔══██╗ ⚡
|
|
54
|
+
⚡ ███╔╝ ███████║██████╔╝██████╔╝ ⚡
|
|
55
|
+
⚡ ███╔╝ ██╔══██║██╔═══╝ ██╔═══╝ ⚡
|
|
56
|
+
⚡ ███████╗██║ ██║██║ ██║ ⚡
|
|
57
|
+
⚡ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ⚡
|
|
58
|
+
⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡
|
|
59
|
+
")
|
|
60
|
+
Zapp::Logger.info("Zapp version: #{Zapp::VERSION}")
|
|
49
61
|
Zapp::Logger.info("Environment: #{Zapp.config.mode}")
|
|
50
62
|
Zapp::Logger.info("Serving: #{Zapp.config.env[Rack::RACK_URL_SCHEME]}://#{Zapp.config.host}:#{Zapp.config.port}")
|
|
51
63
|
Zapp::Logger.info("Parallel workers: #{Zapp.config.parallelism}")
|
|
52
64
|
Zapp::Logger.info("Ready to accept requests")
|
|
53
65
|
end
|
|
66
|
+
|
|
67
|
+
def parsing_thread_pool
|
|
68
|
+
@parsing_thread_pool ||= Concurrent::ThreadPoolExecutor.new(
|
|
69
|
+
min_threads: Zapp.config.parallelism,
|
|
70
|
+
max_threads: Zapp.config.parallelism,
|
|
71
|
+
max_queue: Zapp.config.parallelism * 1_000
|
|
72
|
+
)
|
|
73
|
+
end
|
|
54
74
|
end
|
|
55
75
|
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zapp
|
|
4
|
+
module SocketPipe
|
|
5
|
+
class Receiver
|
|
6
|
+
attr_reader(:pipe, :raw_tcp_pipe)
|
|
7
|
+
|
|
8
|
+
def initialize(pipe:)
|
|
9
|
+
@pipe = pipe
|
|
10
|
+
@raw_tcp_pipe = Ractor.new(Zapp.config, name: "raw-tcp-pipe") do |config|
|
|
11
|
+
server = TCPServer.new(config.host, config.port)
|
|
12
|
+
|
|
13
|
+
loop do
|
|
14
|
+
Ractor.yield(server.accept)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def take
|
|
20
|
+
Ractor.select(pipe, raw_tcp_pipe)[1]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def drain
|
|
24
|
+
Thread.new do
|
|
25
|
+
loop do
|
|
26
|
+
take
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/zapp/version.rb
CHANGED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zapp
|
|
4
|
+
class Worker < Ractor
|
|
5
|
+
# Processes HTTP requests
|
|
6
|
+
class RequestProcessor
|
|
7
|
+
attr_reader(:app, :config, :socket_pipe_sender, :context_pipe)
|
|
8
|
+
|
|
9
|
+
def initialize(context_pipe:, socket_pipe:, app:, config:)
|
|
10
|
+
@app = app
|
|
11
|
+
@config = config
|
|
12
|
+
@socket_pipe_sender = Zapp::SocketPipe::Sender.new(pipe: socket_pipe)
|
|
13
|
+
@context_pipe = context_pipe
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def loop
|
|
17
|
+
while (context = context_pipe.take)
|
|
18
|
+
if context == Zapp::WorkerPool::SIGNALS[:EXIT]
|
|
19
|
+
logger.trace("Received exit signal, shutting down")
|
|
20
|
+
thread_pool.shutdown
|
|
21
|
+
break
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
process = lambda {
|
|
26
|
+
process(context: context)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if config.log_requests
|
|
30
|
+
log_request_time(context: context, &process)
|
|
31
|
+
else
|
|
32
|
+
process.call
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# We send sockets that the client hasn't closed yet,
|
|
36
|
+
# back to the main ractor for HTTP request parsing again
|
|
37
|
+
socket_pipe_sender.push(context.socket) unless context.client_closed?
|
|
38
|
+
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# Processes an HTTP request
|
|
45
|
+
def process(context:)
|
|
46
|
+
env = prepare_env(data: context.req.data, body: context.req.body, env: config.env.dup)
|
|
47
|
+
|
|
48
|
+
status, headers, response_body_stream = @app.call(env)
|
|
49
|
+
|
|
50
|
+
response_body = body_stream_to_string(response_body_stream)
|
|
51
|
+
|
|
52
|
+
context.res.write(data: response_body, status: status, headers: headers)
|
|
53
|
+
rescue StandardError => e
|
|
54
|
+
context.res.write(data: "An unexpected error occurred", status: 500, headers: {})
|
|
55
|
+
logger.error("#{e}\n\n#{e.backtrace&.join(",\n")}") if config.log_uncaught_errors
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Merges HTTP data and body into the env to be passed to the rack app
|
|
59
|
+
def prepare_env(data:, body:, env:)
|
|
60
|
+
data["QUERY_STRING"] = ""
|
|
61
|
+
data["SERVER_NAME"] = data["HTTP_HOST"] || ""
|
|
62
|
+
data["PATH_INFO"] = data["REQUEST_PATH"]
|
|
63
|
+
data["SCRIPT_NAME"] = ""
|
|
64
|
+
|
|
65
|
+
env.update(data)
|
|
66
|
+
|
|
67
|
+
env.update(Rack::RACK_INPUT => body)
|
|
68
|
+
|
|
69
|
+
env
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def log_request_time(context:)
|
|
73
|
+
start = Time.now.to_f * 1000
|
|
74
|
+
|
|
75
|
+
yield
|
|
76
|
+
|
|
77
|
+
request_time = ((Time.now.to_f * 1000) - start).truncate(2)
|
|
78
|
+
method = context.req.data["REQUEST_METHOD"]
|
|
79
|
+
path = context.req.data["PATH_INFO"]
|
|
80
|
+
status = context.res.status
|
|
81
|
+
|
|
82
|
+
logger.info(
|
|
83
|
+
"#{method} #{path} - Completed in #{request_time}ms with status #{status}"
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Loops over a body stream and returns a single string
|
|
88
|
+
def body_stream_to_string(stream)
|
|
89
|
+
response_body = ""
|
|
90
|
+
stream.each do |s|
|
|
91
|
+
response_body += s
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
stream.close if stream.respond_to?(:close)
|
|
95
|
+
|
|
96
|
+
response_body
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def thread_pool
|
|
100
|
+
@thread_pool ||= Concurrent::ThreadPoolExecutor.new(
|
|
101
|
+
min_threads: config.threads_per_worker,
|
|
102
|
+
max_threads: config.threads_per_worker,
|
|
103
|
+
max_queue: 1000,
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def logger
|
|
108
|
+
@logger ||= config.logger_class.new do |l|
|
|
109
|
+
l.prefix = Ractor.current.name
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
data/lib/zapp/worker.rb
CHANGED
|
@@ -1,87 +1,37 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative("worker/request_processor")
|
|
4
|
+
|
|
3
5
|
module Zapp
|
|
4
6
|
# One worker processing requests in parallel
|
|
5
7
|
class Worker < Ractor
|
|
6
8
|
class << self
|
|
7
|
-
def new(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
Zapp::Worker.process(context: context, app: app, logger: logger, config: config)
|
|
24
|
-
end
|
|
25
|
-
end
|
|
9
|
+
def new(context_pipe:, socket_pipe:, app:, index:)
|
|
10
|
+
super(
|
|
11
|
+
context_pipe,
|
|
12
|
+
socket_pipe,
|
|
13
|
+
app,
|
|
14
|
+
Zapp.config.dup,
|
|
15
|
+
name: name(index)
|
|
16
|
+
) do |context_pipe, socket_pipe, app, config|
|
|
17
|
+
processor = Zapp::Worker::RequestProcessor.new(
|
|
18
|
+
socket_pipe: socket_pipe,
|
|
19
|
+
context_pipe: context_pipe,
|
|
20
|
+
app: app,
|
|
21
|
+
config: config
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
processor.loop
|
|
26
25
|
end
|
|
27
26
|
end
|
|
28
27
|
|
|
29
|
-
# Processes an HTTP request
|
|
30
|
-
def process(context:, app:, logger:, config:)
|
|
31
|
-
prepare_env(data: context.req.data, body: context.req.body, env: config.env)
|
|
32
|
-
|
|
33
|
-
status, headers, response_body_stream = app.call(config.env)
|
|
34
|
-
|
|
35
|
-
response_body = body_stream_to_string(response_body_stream)
|
|
36
|
-
|
|
37
|
-
context.res.write(data: response_body, status: status, headers: headers)
|
|
38
|
-
rescue StandardError => e
|
|
39
|
-
context.res.write(data: "An unexpected error occurred", status: 500, headers: {})
|
|
40
|
-
logger.error("#{e}\n\n#{e.backtrace&.join(",\n")}") if config.log_uncaught_errors
|
|
41
|
-
ensure
|
|
42
|
-
context.close
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def log_request_time(logger:)
|
|
46
|
-
start = Time.now.to_f * 1000
|
|
47
|
-
|
|
48
|
-
yield
|
|
49
|
-
ensure
|
|
50
|
-
logger.info("Processed request in #{((Time.now.to_f * 1000) - start).truncate(2)}ms")
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Loops over a body stream and returns a single string
|
|
54
|
-
def body_stream_to_string(stream)
|
|
55
|
-
response_body = ""
|
|
56
|
-
stream.each do |s|
|
|
57
|
-
response_body += s
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
stream.close if stream.respond_to?(:close)
|
|
61
|
-
|
|
62
|
-
response_body
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Merges HTTP data and body into the env to be passed to the rack app
|
|
66
|
-
def prepare_env(data:, body:, env:)
|
|
67
|
-
data["QUERY_STRING"] = ""
|
|
68
|
-
data["SERVER_NAME"] = data["HTTP_HOST"] || ""
|
|
69
|
-
data["PATH_INFO"] = data["REQUEST_PATH"]
|
|
70
|
-
data["SCRIPT_NAME"] = ""
|
|
71
|
-
|
|
72
|
-
env.update(data)
|
|
73
|
-
|
|
74
|
-
env.update(Rack::RACK_INPUT => body)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
28
|
# Index based name of the worker
|
|
78
29
|
def name(index)
|
|
79
|
-
"
|
|
30
|
+
"zapp-http-#{index + 1}"
|
|
80
31
|
end
|
|
81
32
|
end
|
|
82
33
|
|
|
83
34
|
def terminate
|
|
84
|
-
Zapp::Logger.debug("Terminating worker #{name}")
|
|
85
35
|
take
|
|
86
36
|
end
|
|
87
37
|
end
|
data/lib/zapp/worker_pool.rb
CHANGED
|
@@ -3,38 +3,36 @@
|
|
|
3
3
|
module Zapp
|
|
4
4
|
# Manages and dispatches work to a pool of Zap::Worker's
|
|
5
5
|
class WorkerPool
|
|
6
|
-
attr_reader(:
|
|
6
|
+
attr_reader(:context_pipe, :workers, :parallelism)
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Ractor.yield(Ractor.receive)
|
|
12
|
-
end
|
|
13
|
-
end
|
|
8
|
+
SIGNALS = {
|
|
9
|
+
EXIT: :exit
|
|
10
|
+
}.freeze
|
|
14
11
|
|
|
12
|
+
def initialize(app:, context_pipe:, socket_pipe:)
|
|
13
|
+
@context_pipe = context_pipe
|
|
15
14
|
@workers = []
|
|
16
15
|
Zapp.config.parallelism.times do |i|
|
|
17
16
|
@workers << Worker.new(
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
context_pipe: context_pipe,
|
|
18
|
+
socket_pipe: socket_pipe,
|
|
19
|
+
app: app,
|
|
20
20
|
index: i
|
|
21
21
|
)
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
# Sends
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def process(context:, shutdown: false)
|
|
29
|
-
pipe.send([context.dup, shutdown], move: true)
|
|
25
|
+
# Sends a socket to one of our workers
|
|
26
|
+
def process(context:)
|
|
27
|
+
context_pipe.send(context)
|
|
30
28
|
end
|
|
31
29
|
|
|
32
30
|
# Finishes processing of all requests and shuts down workers
|
|
33
31
|
def drain
|
|
34
|
-
Zapp.config.parallelism.times { process(context:
|
|
32
|
+
Zapp.config.parallelism.times { process(context: SIGNALS[:EXIT]) }
|
|
35
33
|
workers.map(&:terminate)
|
|
36
|
-
rescue Ractor::
|
|
37
|
-
#
|
|
34
|
+
rescue Ractor::ClosedError
|
|
35
|
+
# Ractor has already exited
|
|
38
36
|
end
|
|
39
37
|
end
|
|
40
38
|
end
|
data/lib/zapp.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# External dependencies are loaded here
|
|
4
|
+
require("irb")
|
|
4
5
|
require("socket")
|
|
5
6
|
require("concurrent")
|
|
6
7
|
require("puma")
|
|
@@ -23,12 +24,15 @@ module Zapp
|
|
|
23
24
|
end
|
|
24
25
|
end
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
require_relative("zapp/version")
|
|
28
|
+
require_relative("zapp/logger")
|
|
29
|
+
require_relative("zapp/configuration")
|
|
30
|
+
require_relative("zapp/input_stream")
|
|
31
|
+
require_relative("zapp/http_context/context")
|
|
32
|
+
require_relative("zapp/pipe")
|
|
33
|
+
require_relative("zapp/socket_pipe/sender")
|
|
34
|
+
require_relative("zapp/socket_pipe/receiver")
|
|
35
|
+
require_relative("zapp/worker")
|
|
36
|
+
require_relative("zapp/worker_pool")
|
|
37
|
+
require_relative("zapp/server")
|
|
38
|
+
require_relative("zapp/cli")
|
data/zapp.gemspec
CHANGED
|
@@ -31,6 +31,8 @@ Gem::Specification.new do |spec|
|
|
|
31
31
|
# Use Puma's C-based HttpParser, it's fast as hell
|
|
32
32
|
spec.add_dependency("puma", "~> 5.5.2")
|
|
33
33
|
|
|
34
|
+
spec.add_dependency("webrick")
|
|
35
|
+
|
|
34
36
|
# Concurrent ruby for managing Thread pools
|
|
35
37
|
spec.add_dependency("concurrent-ruby", "~> 1.1.9")
|
|
36
38
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: zapp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mathias H Steffensen
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2022-07-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack
|
|
@@ -38,6 +38,20 @@ dependencies:
|
|
|
38
38
|
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
40
|
version: 5.5.2
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: webrick
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
41
55
|
- !ruby/object:Gem::Dependency
|
|
42
56
|
name: concurrent-ruby
|
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -98,8 +112,6 @@ files:
|
|
|
98
112
|
- LICENSE.txt
|
|
99
113
|
- README.md
|
|
100
114
|
- Rakefile
|
|
101
|
-
- bin/console
|
|
102
|
-
- bin/setup
|
|
103
115
|
- bin/zapp
|
|
104
116
|
- examples/rails-app/.browserslistrc
|
|
105
117
|
- examples/rails-app/.gitattributes
|
|
@@ -195,7 +207,7 @@ files:
|
|
|
195
207
|
- examples/rails-app/tmp/pids/.keep
|
|
196
208
|
- examples/rails-app/vendor/.keep
|
|
197
209
|
- examples/rails-app/yarn.lock
|
|
198
|
-
- lib/rack/handler/
|
|
210
|
+
- lib/rack/handler/zapp.rb
|
|
199
211
|
- lib/zapp.rb
|
|
200
212
|
- lib/zapp/cli.rb
|
|
201
213
|
- lib/zapp/configuration.rb
|
|
@@ -205,9 +217,13 @@ files:
|
|
|
205
217
|
- lib/zapp/input_stream.rb
|
|
206
218
|
- lib/zapp/logger.rb
|
|
207
219
|
- lib/zapp/parser.rb
|
|
220
|
+
- lib/zapp/pipe.rb
|
|
208
221
|
- lib/zapp/server.rb
|
|
222
|
+
- lib/zapp/socket_pipe/receiver.rb
|
|
223
|
+
- lib/zapp/socket_pipe/sender.rb
|
|
209
224
|
- lib/zapp/version.rb
|
|
210
225
|
- lib/zapp/worker.rb
|
|
226
|
+
- lib/zapp/worker/request_processor.rb
|
|
211
227
|
- lib/zapp/worker_pool.rb
|
|
212
228
|
- zapp.gemspec
|
|
213
229
|
homepage: https://github.com/mathiashsteffensen/zapp
|
|
@@ -232,7 +248,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
232
248
|
- !ruby/object:Gem::Version
|
|
233
249
|
version: '0'
|
|
234
250
|
requirements: []
|
|
235
|
-
rubygems_version: 3.3
|
|
251
|
+
rubygems_version: 3.2.3
|
|
236
252
|
signing_key:
|
|
237
253
|
specification_version: 4
|
|
238
254
|
summary: A Web Server based on Ractors, for Rack-based Ruby applications
|
data/bin/console
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
require("bundler/setup")
|
|
5
|
-
require("zapp")
|
|
6
|
-
|
|
7
|
-
# You can add fixtures and/or initialization code here to make experimenting
|
|
8
|
-
# with your gem easier. You can also use a different console, if you like.
|
|
9
|
-
|
|
10
|
-
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
11
|
-
# require "pry"
|
|
12
|
-
# Pry.start
|
|
13
|
-
|
|
14
|
-
require("irb")
|
|
15
|
-
IRB.start(__FILE__)
|