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.
Files changed (125) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +21 -0
  3. data/.gitignore +13 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +46 -0
  6. data/.ruby-version +1 -0
  7. data/Gemfile +13 -0
  8. data/Gemfile.lock +110 -0
  9. data/Guardfile +23 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +91 -0
  12. data/Rakefile +8 -0
  13. data/bin/console +15 -0
  14. data/bin/setup +8 -0
  15. data/bin/zapp +7 -0
  16. data/examples/rails-app/.browserslistrc +1 -0
  17. data/examples/rails-app/.gitattributes +10 -0
  18. data/examples/rails-app/.gitignore +40 -0
  19. data/examples/rails-app/.ruby-version +1 -0
  20. data/examples/rails-app/Gemfile +58 -0
  21. data/examples/rails-app/Gemfile.lock +253 -0
  22. data/examples/rails-app/Rakefile +8 -0
  23. data/examples/rails-app/app/assets/config/manifest.js +2 -0
  24. data/examples/rails-app/app/assets/images/.keep +0 -0
  25. data/examples/rails-app/app/assets/stylesheets/application.css +15 -0
  26. data/examples/rails-app/app/channels/application_cable/channel.rb +6 -0
  27. data/examples/rails-app/app/channels/application_cable/connection.rb +6 -0
  28. data/examples/rails-app/app/controllers/application_controller.rb +4 -0
  29. data/examples/rails-app/app/controllers/concerns/.keep +0 -0
  30. data/examples/rails-app/app/helpers/application_helper.rb +4 -0
  31. data/examples/rails-app/app/javascript/channels/consumer.js +6 -0
  32. data/examples/rails-app/app/javascript/channels/index.js +5 -0
  33. data/examples/rails-app/app/javascript/packs/application.js +13 -0
  34. data/examples/rails-app/app/jobs/application_job.rb +9 -0
  35. data/examples/rails-app/app/mailers/application_mailer.rb +6 -0
  36. data/examples/rails-app/app/models/application_record.rb +5 -0
  37. data/examples/rails-app/app/models/concerns/.keep +0 -0
  38. data/examples/rails-app/app/views/layouts/application.html.erb +16 -0
  39. data/examples/rails-app/app/views/layouts/mailer.html.erb +13 -0
  40. data/examples/rails-app/app/views/layouts/mailer.text.erb +1 -0
  41. data/examples/rails-app/babel.config.js +82 -0
  42. data/examples/rails-app/bin/bundle +118 -0
  43. data/examples/rails-app/bin/rails +7 -0
  44. data/examples/rails-app/bin/rake +7 -0
  45. data/examples/rails-app/bin/setup +38 -0
  46. data/examples/rails-app/bin/spring +16 -0
  47. data/examples/rails-app/bin/webpack +21 -0
  48. data/examples/rails-app/bin/webpack-dev-server +21 -0
  49. data/examples/rails-app/bin/yarn +19 -0
  50. data/examples/rails-app/bin/zapp +1 -0
  51. data/examples/rails-app/config/application.rb +24 -0
  52. data/examples/rails-app/config/boot.rb +6 -0
  53. data/examples/rails-app/config/cable.yml +10 -0
  54. data/examples/rails-app/config/credentials.yml.enc +1 -0
  55. data/examples/rails-app/config/database.yml +25 -0
  56. data/examples/rails-app/config/environment.rb +7 -0
  57. data/examples/rails-app/config/environments/development.rb +78 -0
  58. data/examples/rails-app/config/environments/production.rb +122 -0
  59. data/examples/rails-app/config/environments/test.rb +62 -0
  60. data/examples/rails-app/config/initializers/application_controller_renderer.rb +9 -0
  61. data/examples/rails-app/config/initializers/assets.rb +16 -0
  62. data/examples/rails-app/config/initializers/backtrace_silencers.rb +10 -0
  63. data/examples/rails-app/config/initializers/content_security_policy.rb +31 -0
  64. data/examples/rails-app/config/initializers/cookies_serializer.rb +7 -0
  65. data/examples/rails-app/config/initializers/filter_parameter_logging.rb +8 -0
  66. data/examples/rails-app/config/initializers/inflections.rb +17 -0
  67. data/examples/rails-app/config/initializers/mime_types.rb +5 -0
  68. data/examples/rails-app/config/initializers/permissions_policy.rb +12 -0
  69. data/examples/rails-app/config/initializers/wrap_parameters.rb +16 -0
  70. data/examples/rails-app/config/locales/en.yml +33 -0
  71. data/examples/rails-app/config/puma.rb +45 -0
  72. data/examples/rails-app/config/routes.rb +5 -0
  73. data/examples/rails-app/config/spring.rb +8 -0
  74. data/examples/rails-app/config/storage.yml +34 -0
  75. data/examples/rails-app/config/webpack/development.js +5 -0
  76. data/examples/rails-app/config/webpack/environment.js +3 -0
  77. data/examples/rails-app/config/webpack/production.js +5 -0
  78. data/examples/rails-app/config/webpack/test.js +5 -0
  79. data/examples/rails-app/config/webpacker.yml +92 -0
  80. data/examples/rails-app/config/zapp.rb +10 -0
  81. data/examples/rails-app/config.ru +7 -0
  82. data/examples/rails-app/db/seeds.rb +8 -0
  83. data/examples/rails-app/lib/assets/.keep +0 -0
  84. data/examples/rails-app/lib/tasks/.keep +0 -0
  85. data/examples/rails-app/log/.keep +0 -0
  86. data/examples/rails-app/package.json +17 -0
  87. data/examples/rails-app/postcss.config.js +12 -0
  88. data/examples/rails-app/public/404.html +67 -0
  89. data/examples/rails-app/public/422.html +67 -0
  90. data/examples/rails-app/public/500.html +66 -0
  91. data/examples/rails-app/public/apple-touch-icon-precomposed.png +0 -0
  92. data/examples/rails-app/public/apple-touch-icon.png +0 -0
  93. data/examples/rails-app/public/favicon.ico +0 -0
  94. data/examples/rails-app/public/robots.txt +1 -0
  95. data/examples/rails-app/storage/.keep +0 -0
  96. data/examples/rails-app/test/application_system_test_case.rb +7 -0
  97. data/examples/rails-app/test/channels/application_cable/connection_test.rb +15 -0
  98. data/examples/rails-app/test/controllers/.keep +0 -0
  99. data/examples/rails-app/test/fixtures/files/.keep +0 -0
  100. data/examples/rails-app/test/helpers/.keep +0 -0
  101. data/examples/rails-app/test/integration/.keep +0 -0
  102. data/examples/rails-app/test/mailers/.keep +0 -0
  103. data/examples/rails-app/test/models/.keep +0 -0
  104. data/examples/rails-app/test/system/.keep +0 -0
  105. data/examples/rails-app/test/test_helper.rb +17 -0
  106. data/examples/rails-app/tmp/.keep +0 -0
  107. data/examples/rails-app/tmp/pids/.keep +0 -0
  108. data/examples/rails-app/vendor/.keep +0 -0
  109. data/examples/rails-app/yarn.lock +6973 -0
  110. data/lib/rack/handler/zap.rb +16 -0
  111. data/lib/zapp/cli.rb +45 -0
  112. data/lib/zapp/configuration.rb +131 -0
  113. data/lib/zapp/http_context/context.rb +32 -0
  114. data/lib/zapp/http_context/request.rb +28 -0
  115. data/lib/zapp/http_context/response.rb +27 -0
  116. data/lib/zapp/input_stream.rb +51 -0
  117. data/lib/zapp/logger.rb +63 -0
  118. data/lib/zapp/parser.rb +14 -0
  119. data/lib/zapp/server.rb +55 -0
  120. data/lib/zapp/version.rb +5 -0
  121. data/lib/zapp/worker.rb +88 -0
  122. data/lib/zapp/worker_pool.rb +40 -0
  123. data/lib/zapp.rb +34 -0
  124. data/zapp.gemspec +42 -0
  125. 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
@@ -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
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zapp
4
+ # Parses HTTP Requests
5
+ class Parser
6
+ def initialize
7
+ @parser = Puma::HttpParser.new
8
+ end
9
+
10
+ def execute!(data, raw)
11
+ @parser.execute(data, raw, 0)
12
+ end
13
+ end
14
+ end
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zapp
4
+ VERSION = "0.1.0"
5
+ end
@@ -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