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