zapp 0.1.1 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml 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