zapp 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6a636e3ae991f63f7f6f4ecc460d9c3c574e5019719c97e8398298de5e44376b
4
- data.tar.gz: 616c8a2f3d3fb01a724b3b430880fe8e91d8f09d85101f4bf3d9c8e4755377a2
3
+ metadata.gz: '083c3deaea0cc22f0bd269afbccdd556bf66743771aca04f63f125809c934473'
4
+ data.tar.gz: cd3a771ca1d0e898ddaa99c71b741412e4be8370286ec484b2059317cb632802
5
5
  SHA512:
6
- metadata.gz: 05a64fbadcff8a7466b052a5a87706ec2d036334a34a8729a18de1085de8f72ba5a5a2a4100cb9b6f7cc5474b439869452faff0a7606b7f23ee09ee893d20575
7
- data.tar.gz: 06c68050d5b794402032c051492ad5e17a25f79e4ffe853d536713fe6b789cad31a4c44cbb1a59891c89f6d805a3fc92012c55fdee6de818f57a6101a69780af
6
+ metadata.gz: 227f3dc9ce4c2cac931c89f2a3bca81f90f91031a78998258befb02515ee5eb1af386741646bdecc14e82649cc5cc876f360cbfb8a01bf84e119b062ab3a0eea
7
+ data.tar.gz: ffa29c579b1621eca0093e2c3d9a4f02975aa3be6a81aa17348aff7a7f5f54cecf4ea8aeb7829c6a821d4691c2ac9a2cd2d0be33204f2a02b1fd163b5c6990e3
data/.rubocop.yml CHANGED
@@ -11,6 +11,8 @@ Style/MissingElse:
11
11
  Enabled: false
12
12
  Style/StringLiterals:
13
13
  EnforcedStyle: double_quotes
14
+ Style/InlineComments:
15
+ Enabled: false
14
16
  Style/Copyright:
15
17
  Enabled: false
16
18
  Style/ConstantVisibility:
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.9)
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.3)
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,7 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require("bundler/setup")
5
- require("zapp")
4
+ require_relative("../lib/zapp")
6
5
 
7
6
  Zapp::CLI.new.run
@@ -1,12 +1,13 @@
1
1
  PATH
2
2
  remote: ../..
3
3
  specs:
4
- zapp (0.1.0)
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.4.4)
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.10.0)
162
- rspec-core (~> 3.10.0)
163
- rspec-expectations (~> 3.10.0)
164
- rspec-mocks (~> 3.10.0)
165
- rspec-core (3.10.1)
166
- rspec-support (~> 3.10.0)
167
- rspec-expectations (3.10.1)
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.10.0)
170
- rspec-mocks (3.10.2)
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.10.0)
173
- rspec-support (3.10.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,5 +6,5 @@ class App
6
6
  end
7
7
  end
8
8
 
9
- parallelism(2)
9
+ parallelism(4)
10
10
  app(App)
@@ -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::Server.new(app: app).run
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
@@ -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 => false,
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
- require("zapp/http_context/request")
4
- require("zapp/http_context/response")
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 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
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
- def initialize(socket:)
10
- raise(EOFError) if socket.eof?
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
- @body = Zapp::InputStream.new(string: parser.body)
21
+
22
+ @body = Zapp::InputStream.new(string: "parser.body")
23
+
20
24
  parser.reset
21
25
  end
22
26
 
23
- def parsed?
24
- body.is_a?(Zapp::InputStream) && !data.nil? && data != {}
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
- response = "HTTP/1.1 #{status}\n"
14
+ @status = status
15
+ @data = data
16
+ @headers = headers
17
+
18
+ response = +"HTTP/1.1 #{status}\n"
14
19
 
15
- response += "Content-Length: #{data.size}\n" unless headers["Content-Length"]
20
+ response << "Content-Length: #{data.size}\n" unless headers["Content-Length"]
16
21
 
17
22
  headers.each do |k, v|
18
- response += "#{k}: #{v}\n"
23
+ response << "#{k}: #{v}\n"
19
24
  end
20
25
 
21
- response += "\n#{data}\n"
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 = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 }.freeze
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 ||= 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
38
+ @level ||= begin
39
+ log_level = FROZEN_ENV["LOG_LEVEL"]
44
40
 
45
- private
41
+ if log_level == "" || log_level.nil?
42
+ LEVELS[:DEBUG]
43
+ else
44
+ resolved_level = LEVELS[log_level.upcase.to_sym]
46
45
 
47
- def log(current_level, msg)
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
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zapp
4
+ # Light wrapper around a Ractor for piping messages CSP style
5
+ module Pipe
6
+ def self.new
7
+ Ractor.new do
8
+ loop do
9
+ Ractor.yield(Ractor.receive)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
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(:tcp_connection, :worker_pool)
6
+ attr_reader(:worker_pool, :socket_pipe_receiver)
7
7
 
8
8
  def initialize
9
- @tcp_connection = TCPServer.new(Zapp.config.host, Zapp.config.port)
10
- @worker_pool = Zapp::WorkerPool.new(app: Zapp.config.app)
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 = tcp_connection.accept
20
- next if socket.eof?
21
+ socket = socket_pipe_receiver.take
21
22
 
22
- context = Zapp::HTTPContext::Context.new(socket: socket)
23
-
24
- context.req.parse!(parser: parser)
23
+ next if socket.eof?
25
24
 
26
- worker_pool.process(context: context)
25
+ parsing_thread_pool.post do
26
+ ctx = Zapp::HTTPContext::Context.new(socket: socket)
27
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}")
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("Zap version: #{Zapp::VERSION}")
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
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zapp
4
+ module SocketPipe
5
+ class Sender
6
+ attr_reader(:pipe)
7
+
8
+ def initialize(pipe:)
9
+ @pipe = pipe
10
+ end
11
+
12
+ def push(socket)
13
+ pipe.send(socket)
14
+ end
15
+ end
16
+ end
17
+ end
data/lib/zapp/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zapp
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.1"
5
5
  end
@@ -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(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
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
- "zap-http-#{index + 1}"
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
@@ -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(:pipe, :workers, :parallelism)
6
+ attr_reader(:context_pipe, :workers, :parallelism)
7
7
 
8
- def initialize(app:)
9
- @pipe = Ractor.new do
10
- loop do
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
- pipe_: pipe,
19
- app_: app,
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 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)
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: nil, shutdown: true) }
32
+ Zapp.config.parallelism.times { process(context: SIGNALS[:EXIT]) }
35
33
  workers.map(&:terminate)
36
- rescue Ractor::RemoteError
37
- # Ignored
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
- 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")
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.1.1
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: 2021-11-21 00:00:00.000000000 Z
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/zap.rb
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.0.dev
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__)
data/bin/setup DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
-
8
- # Do any other automated setup that you need to do here