zapp 0.1.0
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 +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
|