zapp 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.circleci/config.yml +21 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.rubocop.yml +46 -0
- data/.ruby-version +1 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +110 -0
- data/Guardfile +23 -0
- data/LICENSE.txt +21 -0
- data/README.md +91 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/bin/zapp +7 -0
- data/examples/rails-app/.browserslistrc +1 -0
- data/examples/rails-app/.gitattributes +10 -0
- data/examples/rails-app/.gitignore +40 -0
- data/examples/rails-app/.ruby-version +1 -0
- data/examples/rails-app/Gemfile +58 -0
- data/examples/rails-app/Gemfile.lock +253 -0
- data/examples/rails-app/Rakefile +8 -0
- data/examples/rails-app/app/assets/config/manifest.js +2 -0
- data/examples/rails-app/app/assets/images/.keep +0 -0
- data/examples/rails-app/app/assets/stylesheets/application.css +15 -0
- data/examples/rails-app/app/channels/application_cable/channel.rb +6 -0
- data/examples/rails-app/app/channels/application_cable/connection.rb +6 -0
- data/examples/rails-app/app/controllers/application_controller.rb +4 -0
- data/examples/rails-app/app/controllers/concerns/.keep +0 -0
- data/examples/rails-app/app/helpers/application_helper.rb +4 -0
- data/examples/rails-app/app/javascript/channels/consumer.js +6 -0
- data/examples/rails-app/app/javascript/channels/index.js +5 -0
- data/examples/rails-app/app/javascript/packs/application.js +13 -0
- data/examples/rails-app/app/jobs/application_job.rb +9 -0
- data/examples/rails-app/app/mailers/application_mailer.rb +6 -0
- data/examples/rails-app/app/models/application_record.rb +5 -0
- data/examples/rails-app/app/models/concerns/.keep +0 -0
- data/examples/rails-app/app/views/layouts/application.html.erb +16 -0
- data/examples/rails-app/app/views/layouts/mailer.html.erb +13 -0
- data/examples/rails-app/app/views/layouts/mailer.text.erb +1 -0
- data/examples/rails-app/babel.config.js +82 -0
- data/examples/rails-app/bin/bundle +118 -0
- data/examples/rails-app/bin/rails +7 -0
- data/examples/rails-app/bin/rake +7 -0
- data/examples/rails-app/bin/setup +38 -0
- data/examples/rails-app/bin/spring +16 -0
- data/examples/rails-app/bin/webpack +21 -0
- data/examples/rails-app/bin/webpack-dev-server +21 -0
- data/examples/rails-app/bin/yarn +19 -0
- data/examples/rails-app/bin/zapp +1 -0
- data/examples/rails-app/config/application.rb +24 -0
- data/examples/rails-app/config/boot.rb +6 -0
- data/examples/rails-app/config/cable.yml +10 -0
- data/examples/rails-app/config/credentials.yml.enc +1 -0
- data/examples/rails-app/config/database.yml +25 -0
- data/examples/rails-app/config/environment.rb +7 -0
- data/examples/rails-app/config/environments/development.rb +78 -0
- data/examples/rails-app/config/environments/production.rb +122 -0
- data/examples/rails-app/config/environments/test.rb +62 -0
- data/examples/rails-app/config/initializers/application_controller_renderer.rb +9 -0
- data/examples/rails-app/config/initializers/assets.rb +16 -0
- data/examples/rails-app/config/initializers/backtrace_silencers.rb +10 -0
- data/examples/rails-app/config/initializers/content_security_policy.rb +31 -0
- data/examples/rails-app/config/initializers/cookies_serializer.rb +7 -0
- data/examples/rails-app/config/initializers/filter_parameter_logging.rb +8 -0
- data/examples/rails-app/config/initializers/inflections.rb +17 -0
- data/examples/rails-app/config/initializers/mime_types.rb +5 -0
- data/examples/rails-app/config/initializers/permissions_policy.rb +12 -0
- data/examples/rails-app/config/initializers/wrap_parameters.rb +16 -0
- data/examples/rails-app/config/locales/en.yml +33 -0
- data/examples/rails-app/config/puma.rb +45 -0
- data/examples/rails-app/config/routes.rb +5 -0
- data/examples/rails-app/config/spring.rb +8 -0
- data/examples/rails-app/config/storage.yml +34 -0
- data/examples/rails-app/config/webpack/development.js +5 -0
- data/examples/rails-app/config/webpack/environment.js +3 -0
- data/examples/rails-app/config/webpack/production.js +5 -0
- data/examples/rails-app/config/webpack/test.js +5 -0
- data/examples/rails-app/config/webpacker.yml +92 -0
- data/examples/rails-app/config/zapp.rb +10 -0
- data/examples/rails-app/config.ru +7 -0
- data/examples/rails-app/db/seeds.rb +8 -0
- data/examples/rails-app/lib/assets/.keep +0 -0
- data/examples/rails-app/lib/tasks/.keep +0 -0
- data/examples/rails-app/log/.keep +0 -0
- data/examples/rails-app/package.json +17 -0
- data/examples/rails-app/postcss.config.js +12 -0
- data/examples/rails-app/public/404.html +67 -0
- data/examples/rails-app/public/422.html +67 -0
- data/examples/rails-app/public/500.html +66 -0
- data/examples/rails-app/public/apple-touch-icon-precomposed.png +0 -0
- data/examples/rails-app/public/apple-touch-icon.png +0 -0
- data/examples/rails-app/public/favicon.ico +0 -0
- data/examples/rails-app/public/robots.txt +1 -0
- data/examples/rails-app/storage/.keep +0 -0
- data/examples/rails-app/test/application_system_test_case.rb +7 -0
- data/examples/rails-app/test/channels/application_cable/connection_test.rb +15 -0
- data/examples/rails-app/test/controllers/.keep +0 -0
- data/examples/rails-app/test/fixtures/files/.keep +0 -0
- data/examples/rails-app/test/helpers/.keep +0 -0
- data/examples/rails-app/test/integration/.keep +0 -0
- data/examples/rails-app/test/mailers/.keep +0 -0
- data/examples/rails-app/test/models/.keep +0 -0
- data/examples/rails-app/test/system/.keep +0 -0
- data/examples/rails-app/test/test_helper.rb +17 -0
- data/examples/rails-app/tmp/.keep +0 -0
- data/examples/rails-app/tmp/pids/.keep +0 -0
- data/examples/rails-app/vendor/.keep +0 -0
- data/examples/rails-app/yarn.lock +6973 -0
- data/lib/rack/handler/zap.rb +16 -0
- data/lib/zapp/cli.rb +45 -0
- data/lib/zapp/configuration.rb +131 -0
- data/lib/zapp/http_context/context.rb +32 -0
- data/lib/zapp/http_context/request.rb +28 -0
- data/lib/zapp/http_context/response.rb +27 -0
- data/lib/zapp/input_stream.rb +51 -0
- data/lib/zapp/logger.rb +63 -0
- data/lib/zapp/parser.rb +14 -0
- data/lib/zapp/server.rb +55 -0
- data/lib/zapp/version.rb +5 -0
- data/lib/zapp/worker.rb +88 -0
- data/lib/zapp/worker_pool.rb +40 -0
- data/lib/zapp.rb +34 -0
- data/zapp.gemspec +42 -0
- metadata +239 -0
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require("rack/handler")
|
4
|
+
|
5
|
+
module Rack
|
6
|
+
module Handler
|
7
|
+
# Rack handler for the Zapp web server
|
8
|
+
class Zapp
|
9
|
+
def self.run(app)
|
10
|
+
Zapp::Server.new(app: app).run
|
11
|
+
end
|
12
|
+
|
13
|
+
register(:zapp, Rack::Handler::Zapp)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/zapp/cli.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require("optionparser")
|
4
|
+
|
5
|
+
module Zapp
|
6
|
+
# Provides the CLI utility for easily running Ruby 3.0.0+ applications with Zap
|
7
|
+
class CLI
|
8
|
+
def run
|
9
|
+
parse_options
|
10
|
+
|
11
|
+
Zapp::Server.new.run
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def parse_options
|
17
|
+
begin
|
18
|
+
parse_config_file(location: "./config/zapp.rb")
|
19
|
+
rescue StandardError
|
20
|
+
# Ignored
|
21
|
+
end
|
22
|
+
|
23
|
+
OptionParser.new do |opts|
|
24
|
+
opts.banner = "Usage: bundle exec zapp [options]"
|
25
|
+
|
26
|
+
opts.on("-c", "--config-file=FILE", "Config file to use") do |file|
|
27
|
+
parse_config_file(location: file)
|
28
|
+
end
|
29
|
+
|
30
|
+
opts.on("-h", "--help", "Prints this help") do
|
31
|
+
puts(opts)
|
32
|
+
exit
|
33
|
+
end
|
34
|
+
end.parse!
|
35
|
+
end
|
36
|
+
|
37
|
+
def parse_config_file(location:)
|
38
|
+
config = File.read(
|
39
|
+
File.absolute_path(location)
|
40
|
+
)
|
41
|
+
|
42
|
+
Zapp.config.instance_eval(config)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require("etc")
|
4
|
+
require("singleton")
|
5
|
+
require("ostruct")
|
6
|
+
|
7
|
+
module Zapp
|
8
|
+
# Class holding the configuration values used by Zap
|
9
|
+
class Configuration
|
10
|
+
attr_writer(
|
11
|
+
:rackup_file,
|
12
|
+
:parallelism,
|
13
|
+
:threads_per_worker,
|
14
|
+
:logger_class,
|
15
|
+
:log_requests,
|
16
|
+
:log_uncaught_errors,
|
17
|
+
:host,
|
18
|
+
:port,
|
19
|
+
:app
|
20
|
+
)
|
21
|
+
|
22
|
+
attr_accessor(:env, :mode)
|
23
|
+
|
24
|
+
DEFAULT_OPTIONS = {
|
25
|
+
# Rack up file to use
|
26
|
+
rackup_file: "config.ru",
|
27
|
+
|
28
|
+
# Default to number of CPUs available
|
29
|
+
# This is the amount of workers to run processing requests
|
30
|
+
parallelism: Etc.nprocessors,
|
31
|
+
# Number of Thread's to run within each worker
|
32
|
+
threads_per_worker: 5,
|
33
|
+
|
34
|
+
# Default logging behavior
|
35
|
+
logger_class: Zapp::Logger,
|
36
|
+
log_requests: true,
|
37
|
+
log_uncaught_errors: true,
|
38
|
+
|
39
|
+
host: "localhost",
|
40
|
+
port: 3000,
|
41
|
+
|
42
|
+
mode: ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development",
|
43
|
+
env: ENV.to_hash.merge(
|
44
|
+
{
|
45
|
+
Rack::RACK_VERSION => Rack::VERSION,
|
46
|
+
Rack::RACK_ERRORS => $stderr,
|
47
|
+
Rack::RACK_MULTITHREAD => false,
|
48
|
+
Rack::RACK_MULTIPROCESS => true,
|
49
|
+
Rack::RACK_RUNONCE => false,
|
50
|
+
Rack::RACK_URL_SCHEME => %w[yes on 1].include?(ENV["HTTPS"]) ? "https" : "http"
|
51
|
+
}
|
52
|
+
)
|
53
|
+
}.freeze
|
54
|
+
|
55
|
+
def initialize
|
56
|
+
DEFAULT_OPTIONS.each_key do |key|
|
57
|
+
public_send("#{key}=", DEFAULT_OPTIONS[key])
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def rack_builder
|
62
|
+
@rack_builder ||= begin
|
63
|
+
require("rack")
|
64
|
+
require("rack/builder")
|
65
|
+
Rack::Builder
|
66
|
+
rescue LoadError => e
|
67
|
+
Zapp::Logger.error("Failed to load Rack #{e}")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def app(new = nil)
|
72
|
+
@app = new unless new.nil?
|
73
|
+
|
74
|
+
@app ||= begin
|
75
|
+
raise(Zapp::ZapError, "Missing rackup file '#{rackup_file}'") unless File.exist?(rackup_file)
|
76
|
+
|
77
|
+
rack_app, = rack_builder.parse_file(rackup_file)
|
78
|
+
|
79
|
+
rack_app
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def parallelism(new = nil)
|
84
|
+
return @parallelism if new.nil?
|
85
|
+
|
86
|
+
@parallelism = new
|
87
|
+
end
|
88
|
+
|
89
|
+
def threads_per_worker(new = nil)
|
90
|
+
return @threads_per_worker if new.nil?
|
91
|
+
|
92
|
+
@threads_per_worker = new
|
93
|
+
end
|
94
|
+
|
95
|
+
def logger_class(new = nil)
|
96
|
+
return @logger_class if new.nil?
|
97
|
+
|
98
|
+
@logger_class = new
|
99
|
+
end
|
100
|
+
|
101
|
+
def log_requests(new = nil)
|
102
|
+
return @log_requests if new.nil?
|
103
|
+
|
104
|
+
@log_requests = new
|
105
|
+
end
|
106
|
+
|
107
|
+
def log_uncaught_errors(new = nil)
|
108
|
+
return @log_uncaught_errors if new.nil?
|
109
|
+
|
110
|
+
@log_uncaught_errors = new
|
111
|
+
end
|
112
|
+
|
113
|
+
def host(new = nil)
|
114
|
+
return @host if new.nil?
|
115
|
+
|
116
|
+
@host = new
|
117
|
+
end
|
118
|
+
|
119
|
+
def port(new = nil)
|
120
|
+
return @port if new.nil?
|
121
|
+
|
122
|
+
@port = new
|
123
|
+
end
|
124
|
+
|
125
|
+
def rackup_file(new = nil)
|
126
|
+
return @rackup_file if new.nil?
|
127
|
+
|
128
|
+
@rackup_file = new
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require("zapp/http_context/request")
|
4
|
+
require("zapp/http_context/response")
|
5
|
+
|
6
|
+
module Zapp
|
7
|
+
module HTTPContext
|
8
|
+
# Context containing request and response
|
9
|
+
class Context
|
10
|
+
attr_reader(:req, :res)
|
11
|
+
|
12
|
+
def initialize(socket:)
|
13
|
+
@socket = socket
|
14
|
+
@req = Zapp::HTTPContext::Request.new(socket: socket)
|
15
|
+
@res = Zapp::HTTPContext::Response.new(socket: socket)
|
16
|
+
end
|
17
|
+
|
18
|
+
def close
|
19
|
+
@socket.close
|
20
|
+
end
|
21
|
+
|
22
|
+
def dup
|
23
|
+
clone_context = super
|
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
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Zapp
|
4
|
+
module HTTPContext
|
5
|
+
# Represents an HTTP Request to be processed by a worker
|
6
|
+
class Request
|
7
|
+
attr_reader(:raw, :data, :body)
|
8
|
+
|
9
|
+
def initialize(socket:)
|
10
|
+
raise(EOFError) if socket.eof?
|
11
|
+
|
12
|
+
# Max Request size of 8KB TODO: Make a config value for this setting
|
13
|
+
@raw = socket.readpartial(8192)
|
14
|
+
@data = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def parse!(parser: Puma::HttpParser.new)
|
18
|
+
parser.execute(data, raw, 0)
|
19
|
+
@body = Zapp::InputStream.new(string: parser.body)
|
20
|
+
parser.reset
|
21
|
+
end
|
22
|
+
|
23
|
+
def parsed?
|
24
|
+
body.is_a?(Zapp::InputStream) && !data.nil? && data != {}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Zapp
|
4
|
+
module HTTPContext
|
5
|
+
# Represents an HTTP response being sent back to a client
|
6
|
+
class Response
|
7
|
+
def initialize(socket:)
|
8
|
+
@socket = socket
|
9
|
+
end
|
10
|
+
|
11
|
+
# TODO: Add headers argument
|
12
|
+
def write(data:, status:, headers:)
|
13
|
+
response = "HTTP/1.1 #{status}\n"
|
14
|
+
|
15
|
+
response += "Content-Length: #{data.size}\n" unless headers["Content-Length"]
|
16
|
+
|
17
|
+
headers.each do |k, v|
|
18
|
+
response += "#{k}: #{v}\n"
|
19
|
+
end
|
20
|
+
|
21
|
+
response += "\n#{data}\n"
|
22
|
+
|
23
|
+
@socket.write(response)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Zapp
|
4
|
+
# Represents an input stream with the HTTP data passed to rack.input
|
5
|
+
# Read the Input Stream part of the Rack Specification here https://github.com/rack/rack/blob/master/SPEC.rdoc#label-The+Input+Stream
|
6
|
+
class InputStream
|
7
|
+
def initialize(string:)
|
8
|
+
@string = string
|
9
|
+
@next_index_to_read = 0
|
10
|
+
end
|
11
|
+
|
12
|
+
def read(length = nil, buffer = nil)
|
13
|
+
returning = if length.nil?
|
14
|
+
raw_read
|
15
|
+
else
|
16
|
+
string = raw_read(end_index: @next_index_to_read + length)
|
17
|
+
string == "" ? nil : string
|
18
|
+
end
|
19
|
+
|
20
|
+
if buffer.nil?
|
21
|
+
returning
|
22
|
+
else
|
23
|
+
buffer << returning
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def each(&block)
|
28
|
+
[read].each(&block)
|
29
|
+
end
|
30
|
+
|
31
|
+
def gets
|
32
|
+
return unless @next_index_to_read < @string.length
|
33
|
+
|
34
|
+
read
|
35
|
+
end
|
36
|
+
|
37
|
+
def rewind
|
38
|
+
@next_index_to_read = 0
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def raw_read(end_index: @string.length)
|
44
|
+
returning = @string.slice(@next_index_to_read...end_index)
|
45
|
+
|
46
|
+
@next_index_to_read = end_index
|
47
|
+
|
48
|
+
returning
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/zapp/logger.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Zapp
|
4
|
+
# The default logger for zap
|
5
|
+
class Logger
|
6
|
+
# Base contains all the logging functionality and is included both as class and instance methods of Zap::Logger
|
7
|
+
# This allows logging without creating new instances,
|
8
|
+
# while allowing Ractors to create their own instances for thread safety
|
9
|
+
module Base
|
10
|
+
attr_writer(:level, :prefix)
|
11
|
+
|
12
|
+
LEVELS = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 }.freeze
|
13
|
+
|
14
|
+
def debug(msg)
|
15
|
+
log("DEBUG", msg)
|
16
|
+
end
|
17
|
+
|
18
|
+
def info(msg)
|
19
|
+
log("INFO", msg)
|
20
|
+
end
|
21
|
+
|
22
|
+
def warn(msg)
|
23
|
+
log("WARN", msg)
|
24
|
+
end
|
25
|
+
|
26
|
+
def error(msg)
|
27
|
+
log("ERROR", msg)
|
28
|
+
end
|
29
|
+
|
30
|
+
def level
|
31
|
+
@level ||= if ENV["ZAPP_LOG_LEVEL"] != "" && !ENV["ZAPP_LOG_LEVEL"].nil?
|
32
|
+
if LEVELS[ENV["ZAPP_LOG_LEVEL"]].nil?
|
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
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def log(current_level, msg)
|
48
|
+
puts("--- #{@prefix} [#{current_level}] #{msg}") if level <= LEVELS[current_level.to_sym]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
include(Zapp::Logger::Base)
|
52
|
+
|
53
|
+
def initialize
|
54
|
+
@prefix = "Zap"
|
55
|
+
yield(self) if block_given?
|
56
|
+
end
|
57
|
+
|
58
|
+
class << self
|
59
|
+
include(Zapp::Logger::Base)
|
60
|
+
@prefix = "Zap"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/zapp/parser.rb
ADDED
data/lib/zapp/server.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Zapp
|
4
|
+
# The Zap HTTP Server, listens on a TCP connection and processes incoming requests
|
5
|
+
class Server
|
6
|
+
attr_reader(:tcp_connection, :worker_pool)
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@tcp_connection = TCPServer.new(Zapp.config.host, Zapp.config.port)
|
10
|
+
@worker_pool = Zapp::WorkerPool.new(app: Zapp.config.app)
|
11
|
+
end
|
12
|
+
|
13
|
+
def run
|
14
|
+
parser = Puma::HttpParser.new
|
15
|
+
|
16
|
+
log_start
|
17
|
+
|
18
|
+
loop do
|
19
|
+
socket = tcp_connection.accept
|
20
|
+
next if socket.eof?
|
21
|
+
|
22
|
+
context = Zapp::HTTPContext::Context.new(socket: socket)
|
23
|
+
|
24
|
+
context.req.parse!(parser: parser)
|
25
|
+
|
26
|
+
worker_pool.process(context: context)
|
27
|
+
|
28
|
+
rescue Puma::HttpParserError => e
|
29
|
+
context.res.write(data: "Invalid HTTP request", status: 500, headers: {})
|
30
|
+
Zapp::Logger.warn("Puma parser error: #{e}")
|
31
|
+
end
|
32
|
+
rescue SignalException, IRB::Abort => e
|
33
|
+
shutdown(e)
|
34
|
+
end
|
35
|
+
|
36
|
+
def shutdown(err = nil)
|
37
|
+
Zapp::Logger.info("Received signal #{err}") unless err.nil?
|
38
|
+
Zapp::Logger.info("Gracefully shutting down workers, allowing request processing to finish")
|
39
|
+
|
40
|
+
worker_pool.drain
|
41
|
+
|
42
|
+
Zapp::Logger.info("Done. See you next time!")
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def log_start
|
48
|
+
Zapp::Logger.info("Zap version: #{Zapp::VERSION}")
|
49
|
+
Zapp::Logger.info("Environment: #{Zapp.config.mode}")
|
50
|
+
Zapp::Logger.info("Serving: #{Zapp.config.env[Rack::RACK_URL_SCHEME]}://#{Zapp.config.host}:#{Zapp.config.port}")
|
51
|
+
Zapp::Logger.info("Parallel workers: #{Zapp.config.parallelism}")
|
52
|
+
Zapp::Logger.info("Ready to accept requests")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/zapp/version.rb
ADDED
data/lib/zapp/worker.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Zapp
|
4
|
+
# One worker processing requests in parallel
|
5
|
+
class Worker < Ractor
|
6
|
+
class << self
|
7
|
+
def new(pipe_:, app_:, index:)
|
8
|
+
# Logger with the name as prefix
|
9
|
+
logger_ = Zapp.config.logger_class.new do |logger|
|
10
|
+
logger.prefix = name(index)
|
11
|
+
end
|
12
|
+
|
13
|
+
super(pipe_, app_, logger_, Zapp.config.dup, name: name(index)) do |pipe, app, logger, config|
|
14
|
+
logger.level = 0
|
15
|
+
while (context, shutdown = pipe.take)
|
16
|
+
break if shutdown
|
17
|
+
|
18
|
+
if config.log_requests
|
19
|
+
Zapp::Worker.log_request_time(logger: logger) do
|
20
|
+
Zapp::Worker.process(context: context, app: app, logger: logger, config: config)
|
21
|
+
end
|
22
|
+
else
|
23
|
+
Zapp::Worker.process(context: context, app: app, logger: logger, config: config)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
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
|
+
# Index based name of the worker
|
78
|
+
def name(index)
|
79
|
+
"zap-http-#{index + 1}"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def terminate
|
84
|
+
Zapp::Logger.debug("Terminating worker #{name}")
|
85
|
+
take
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Zapp
|
4
|
+
# Manages and dispatches work to a pool of Zap::Worker's
|
5
|
+
class WorkerPool
|
6
|
+
attr_reader(:pipe, :workers, :parallelism)
|
7
|
+
|
8
|
+
def initialize(app:)
|
9
|
+
@pipe = Ractor.new do
|
10
|
+
loop do
|
11
|
+
Ractor.yield(Ractor.receive)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
@workers = []
|
16
|
+
Zapp.config.parallelism.times do |i|
|
17
|
+
@workers << Worker.new(
|
18
|
+
pipe_: pipe,
|
19
|
+
app_: app,
|
20
|
+
index: i
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Sends data through the pipe to one of our workers,
|
26
|
+
# sends a tuple of [context, shutdown], if shutdown is true it breaks from its processing loop
|
27
|
+
# otherwise the worker processes the HTTP context
|
28
|
+
def process(context:, shutdown: false)
|
29
|
+
pipe.send([context.dup, shutdown], move: true)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Finishes processing of all requests and shuts down workers
|
33
|
+
def drain
|
34
|
+
Zapp.config.parallelism.times { process(context: nil, shutdown: true) }
|
35
|
+
workers.map(&:terminate)
|
36
|
+
rescue Ractor::RemoteError
|
37
|
+
# Ignored
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/zapp.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# External dependencies are loaded here
|
4
|
+
require("socket")
|
5
|
+
require("concurrent")
|
6
|
+
require("puma")
|
7
|
+
require("rack")
|
8
|
+
|
9
|
+
# Zapp is a web server for Rack-based Ruby 3.0.0+ applications
|
10
|
+
module Zapp
|
11
|
+
class ZappError < StandardError; end
|
12
|
+
|
13
|
+
class << self
|
14
|
+
def config(reset: false)
|
15
|
+
@config = Zapp::Configuration.new if reset
|
16
|
+
|
17
|
+
@config ||= Zapp::Configuration.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def configure
|
21
|
+
yield(config)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require("zapp/version")
|
27
|
+
require("zapp/logger")
|
28
|
+
require("zapp/configuration")
|
29
|
+
require("zapp/input_stream")
|
30
|
+
require("zapp/http_context/context")
|
31
|
+
require("zapp/worker")
|
32
|
+
require("zapp/worker_pool")
|
33
|
+
require("zapp/server")
|
34
|
+
require("zapp/cli")
|
data/zapp.gemspec
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative("lib/zapp/version")
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "zapp"
|
7
|
+
spec.version = Zapp::VERSION
|
8
|
+
spec.authors = ["Mathias H Steffensen"]
|
9
|
+
spec.email = ["mathiashsteffensen@protonmail.com"]
|
10
|
+
|
11
|
+
spec.summary = "A Web Server based on Ractors, for Rack-based Ruby applications"
|
12
|
+
spec.homepage = "https://github.com/mathiashsteffensen/zapp"
|
13
|
+
spec.license = "MIT"
|
14
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
|
15
|
+
|
16
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
17
|
+
spec.metadata["source_code_uri"] = "https://github.com/mathiashsteffensen/zapp"
|
18
|
+
spec.metadata["changelog_uri"] = "https://github.com/mathiashsteffensen/zapp/blob/master/CHANGELOG.md"
|
19
|
+
|
20
|
+
# Which files should be added to the gem when it is released.
|
21
|
+
spec.files =
|
22
|
+
Dir.chdir(File.expand_path(__dir__)) do
|
23
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:spec|example)/}) }
|
24
|
+
end
|
25
|
+
spec.bindir = "bin"
|
26
|
+
spec.require_paths = ["lib"]
|
27
|
+
|
28
|
+
# This is of course a web server for Rack applications
|
29
|
+
spec.add_dependency("rack", "~> 2.2.3")
|
30
|
+
|
31
|
+
# Use Puma's C-based HttpParser, it's fast as hell
|
32
|
+
spec.add_dependency("puma", "~> 5.5.2")
|
33
|
+
|
34
|
+
# Concurrent ruby for managing Thread pools
|
35
|
+
spec.add_dependency("concurrent-ruby", "~> 1.1.9")
|
36
|
+
|
37
|
+
# Rake for task running
|
38
|
+
spec.add_dependency("rake", "~> 13.0")
|
39
|
+
|
40
|
+
# RSpec for testing
|
41
|
+
spec.add_dependency("rspec", "~> 3.0")
|
42
|
+
end
|