zapp 0.1.0 → 0.2.2

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 (122) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -0
  3. data/Gemfile.lock +5 -3
  4. data/bin/zapp +1 -2
  5. data/examples/config/app.rb +5 -0
  6. data/examples/config/puma.rb +10 -0
  7. data/examples/config/zapp.rb +13 -0
  8. data/lib/rack/handler/{zap.rb → zapp.rb} +4 -3
  9. data/lib/zapp/cli.rb +5 -0
  10. data/lib/zapp/configuration.rb +10 -2
  11. data/lib/zapp/http_context/context.rb +10 -11
  12. data/lib/zapp/http_context/request.rb +10 -6
  13. data/lib/zapp/http_context/response.rb +10 -5
  14. data/lib/zapp/logger/base.rb +103 -0
  15. data/lib/zapp/logger.rb +21 -47
  16. data/lib/zapp/pipe.rb +14 -0
  17. data/lib/zapp/server.rb +38 -16
  18. data/lib/zapp/socket_pipe/receiver.rb +24 -0
  19. data/lib/zapp/socket_pipe/sender.rb +17 -0
  20. data/lib/zapp/version.rb +1 -1
  21. data/lib/zapp/worker/request_processor.rb +109 -0
  22. data/lib/zapp/worker.rb +18 -69
  23. data/lib/zapp/worker_pool.rb +17 -18
  24. data/lib/zapp.rb +18 -11
  25. data/zapp.gemspec +2 -0
  26. metadata +26 -100
  27. data/bin/console +0 -15
  28. data/bin/setup +0 -8
  29. data/examples/rails-app/.browserslistrc +0 -1
  30. data/examples/rails-app/.gitattributes +0 -10
  31. data/examples/rails-app/.gitignore +0 -40
  32. data/examples/rails-app/.ruby-version +0 -1
  33. data/examples/rails-app/Gemfile +0 -58
  34. data/examples/rails-app/Gemfile.lock +0 -253
  35. data/examples/rails-app/Rakefile +0 -8
  36. data/examples/rails-app/app/assets/config/manifest.js +0 -2
  37. data/examples/rails-app/app/assets/images/.keep +0 -0
  38. data/examples/rails-app/app/assets/stylesheets/application.css +0 -15
  39. data/examples/rails-app/app/channels/application_cable/channel.rb +0 -6
  40. data/examples/rails-app/app/channels/application_cable/connection.rb +0 -6
  41. data/examples/rails-app/app/controllers/application_controller.rb +0 -4
  42. data/examples/rails-app/app/controllers/concerns/.keep +0 -0
  43. data/examples/rails-app/app/helpers/application_helper.rb +0 -4
  44. data/examples/rails-app/app/javascript/channels/consumer.js +0 -6
  45. data/examples/rails-app/app/javascript/channels/index.js +0 -5
  46. data/examples/rails-app/app/javascript/packs/application.js +0 -13
  47. data/examples/rails-app/app/jobs/application_job.rb +0 -9
  48. data/examples/rails-app/app/mailers/application_mailer.rb +0 -6
  49. data/examples/rails-app/app/models/application_record.rb +0 -5
  50. data/examples/rails-app/app/models/concerns/.keep +0 -0
  51. data/examples/rails-app/app/views/layouts/application.html.erb +0 -16
  52. data/examples/rails-app/app/views/layouts/mailer.html.erb +0 -13
  53. data/examples/rails-app/app/views/layouts/mailer.text.erb +0 -1
  54. data/examples/rails-app/babel.config.js +0 -82
  55. data/examples/rails-app/bin/bundle +0 -118
  56. data/examples/rails-app/bin/rails +0 -7
  57. data/examples/rails-app/bin/rake +0 -7
  58. data/examples/rails-app/bin/setup +0 -38
  59. data/examples/rails-app/bin/spring +0 -16
  60. data/examples/rails-app/bin/webpack +0 -21
  61. data/examples/rails-app/bin/webpack-dev-server +0 -21
  62. data/examples/rails-app/bin/yarn +0 -19
  63. data/examples/rails-app/bin/zapp +0 -1
  64. data/examples/rails-app/config/application.rb +0 -24
  65. data/examples/rails-app/config/boot.rb +0 -6
  66. data/examples/rails-app/config/cable.yml +0 -10
  67. data/examples/rails-app/config/credentials.yml.enc +0 -1
  68. data/examples/rails-app/config/database.yml +0 -25
  69. data/examples/rails-app/config/environment.rb +0 -7
  70. data/examples/rails-app/config/environments/development.rb +0 -78
  71. data/examples/rails-app/config/environments/production.rb +0 -122
  72. data/examples/rails-app/config/environments/test.rb +0 -62
  73. data/examples/rails-app/config/initializers/application_controller_renderer.rb +0 -9
  74. data/examples/rails-app/config/initializers/assets.rb +0 -16
  75. data/examples/rails-app/config/initializers/backtrace_silencers.rb +0 -10
  76. data/examples/rails-app/config/initializers/content_security_policy.rb +0 -31
  77. data/examples/rails-app/config/initializers/cookies_serializer.rb +0 -7
  78. data/examples/rails-app/config/initializers/filter_parameter_logging.rb +0 -8
  79. data/examples/rails-app/config/initializers/inflections.rb +0 -17
  80. data/examples/rails-app/config/initializers/mime_types.rb +0 -5
  81. data/examples/rails-app/config/initializers/permissions_policy.rb +0 -12
  82. data/examples/rails-app/config/initializers/wrap_parameters.rb +0 -16
  83. data/examples/rails-app/config/locales/en.yml +0 -33
  84. data/examples/rails-app/config/puma.rb +0 -45
  85. data/examples/rails-app/config/routes.rb +0 -5
  86. data/examples/rails-app/config/spring.rb +0 -8
  87. data/examples/rails-app/config/storage.yml +0 -34
  88. data/examples/rails-app/config/webpack/development.js +0 -5
  89. data/examples/rails-app/config/webpack/environment.js +0 -3
  90. data/examples/rails-app/config/webpack/production.js +0 -5
  91. data/examples/rails-app/config/webpack/test.js +0 -5
  92. data/examples/rails-app/config/webpacker.yml +0 -92
  93. data/examples/rails-app/config/zapp.rb +0 -10
  94. data/examples/rails-app/config.ru +0 -7
  95. data/examples/rails-app/db/seeds.rb +0 -8
  96. data/examples/rails-app/lib/assets/.keep +0 -0
  97. data/examples/rails-app/lib/tasks/.keep +0 -0
  98. data/examples/rails-app/log/.keep +0 -0
  99. data/examples/rails-app/package.json +0 -17
  100. data/examples/rails-app/postcss.config.js +0 -12
  101. data/examples/rails-app/public/404.html +0 -67
  102. data/examples/rails-app/public/422.html +0 -67
  103. data/examples/rails-app/public/500.html +0 -66
  104. data/examples/rails-app/public/apple-touch-icon-precomposed.png +0 -0
  105. data/examples/rails-app/public/apple-touch-icon.png +0 -0
  106. data/examples/rails-app/public/favicon.ico +0 -0
  107. data/examples/rails-app/public/robots.txt +0 -1
  108. data/examples/rails-app/storage/.keep +0 -0
  109. data/examples/rails-app/test/application_system_test_case.rb +0 -7
  110. data/examples/rails-app/test/channels/application_cable/connection_test.rb +0 -15
  111. data/examples/rails-app/test/controllers/.keep +0 -0
  112. data/examples/rails-app/test/fixtures/files/.keep +0 -0
  113. data/examples/rails-app/test/helpers/.keep +0 -0
  114. data/examples/rails-app/test/integration/.keep +0 -0
  115. data/examples/rails-app/test/mailers/.keep +0 -0
  116. data/examples/rails-app/test/models/.keep +0 -0
  117. data/examples/rails-app/test/system/.keep +0 -0
  118. data/examples/rails-app/test/test_helper.rb +0 -17
  119. data/examples/rails-app/tmp/.keep +0 -0
  120. data/examples/rails-app/tmp/pids/.keep +0 -0
  121. data/examples/rails-app/vendor/.keep +0 -0
  122. data/examples/rails-app/yarn.lock +0 -6973
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f666d4681200fdaf226cf78821e61fad6192f61f70bf8db9aa70a6c224a6a1e6
4
- data.tar.gz: c6c72378e757c93190b5c2e73ec83c87806ba3d13f1bc80670336605be538d7c
3
+ metadata.gz: 6b4a4b6bdf9ac52a419e899e41344faf5ff15fdde704f70e93fe71cee334e70e
4
+ data.tar.gz: 4bd738d7ca1919ceef29612c5bf9a07a26ad8bda944b9adfc52197435dbb1a0a
5
5
  SHA512:
6
- metadata.gz: ce692ed7205e2aa7157bf7180caf8d2c30ed973a394181f340dc456ef220ced24773bfe4cfa2e98a3cd5083e193cefb47b4daa043af9bb682878738e477e8576
7
- data.tar.gz: 978c754d57a872141d03f8d522c94dab93ee94807cd2a09266cb1b223411b9e6bec0656e92a0750276847375c865755c4fea67547adbd6b1c6c5d16fc16264f5
6
+ metadata.gz: 26554e64959a88e35bf1b7f9657909da61de5a92c980550df415ae0b5564f899b25922e1fbb69d79a39a72506fe903654a007555c8e85ce3b437eb1cbbf8ccb3
7
+ data.tar.gz: c790f81f5fa7d1867ee6f33c3fff92f3df44ece8051e9dc3f9ed260724dd0236a3d3fd1814ba3dcf54f3743713782445a5001f54fcb275872943665055593b64
data/.rubocop.yml CHANGED
@@ -5,12 +5,17 @@ AllCops:
5
5
  Exclude:
6
6
  - examples/**/*
7
7
 
8
+ Gemspec/RequireMFA:
9
+ Enabled: false
10
+
8
11
  Style/DocumentationMethod:
9
12
  Enabled: false
10
13
  Style/MissingElse:
11
14
  Enabled: false
12
15
  Style/StringLiterals:
13
16
  EnforcedStyle: double_quotes
17
+ Style/InlineComment:
18
+ Enabled: false
14
19
  Style/Copyright:
15
20
  Enabled: false
16
21
  Style/ConstantVisibility:
data/Gemfile.lock CHANGED
@@ -1,19 +1,20 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- zapp (0.1.0)
4
+ zapp (0.2.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/
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
@@ -0,0 +1,5 @@
1
+ class App
2
+ def self.call(env)
3
+ [200, {}, ["Hello from Zapp", env.to_s]]
4
+ end
5
+ end
@@ -0,0 +1,10 @@
1
+ require_relative("app")
2
+
3
+ max_threads_count = ENV.fetch("RAILS_MAX_THREADS", 5)
4
+ min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
5
+ threads(min_threads_count, max_threads_count)
6
+ worker_timeout(3600) if ENV.fetch("RAILS_ENV", "development") == "development"
7
+ port(ENV.fetch("PORT", 3000))
8
+ environment(ENV.fetch("RAILS_ENV", "development"))
9
+ workers ENV.fetch("WEB_CONCURRENCY") { 2 }
10
+ app(App)
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require("json")
4
+
5
+ class App
6
+ def self.call(env)
7
+ [200, {}, ["Hello from Zapp", JSON.generate(env)]]
8
+ end
9
+ end
10
+
11
+ parallelism(4)
12
+ threads_per_worker(25)
13
+ 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
data/lib/zapp/cli.rb CHANGED
@@ -27,6 +27,11 @@ module Zapp
27
27
  parse_config_file(location: file)
28
28
  end
29
29
 
30
+ opts.on("-v", "--version", "Prints the version of Zapp currently running") do
31
+ puts("Zapp v#{Zapp::VERSION}")
32
+ exit
33
+ end
34
+
30
35
  opts.on("-h", "--help", "Prints this help") do
31
36
  puts(opts)
32
37
  exit
@@ -12,6 +12,7 @@ module Zapp
12
12
  :parallelism,
13
13
  :threads_per_worker,
14
14
  :logger_class,
15
+ :logger_out_io,
15
16
  :log_requests,
16
17
  :log_uncaught_errors,
17
18
  :host,
@@ -33,6 +34,7 @@ module Zapp
33
34
 
34
35
  # Default logging behavior
35
36
  logger_class: Zapp::Logger,
37
+ logger_out_io: $stdout,
36
38
  log_requests: true,
37
39
  log_uncaught_errors: true,
38
40
 
@@ -44,7 +46,7 @@ module Zapp
44
46
  {
45
47
  Rack::RACK_VERSION => Rack::VERSION,
46
48
  Rack::RACK_ERRORS => $stderr,
47
- Rack::RACK_MULTITHREAD => false,
49
+ Rack::RACK_MULTITHREAD => true,
48
50
  Rack::RACK_MULTIPROCESS => true,
49
51
  Rack::RACK_RUNONCE => false,
50
52
  Rack::RACK_URL_SCHEME => %w[yes on 1].include?(ENV["HTTPS"]) ? "https" : "http"
@@ -72,7 +74,7 @@ module Zapp
72
74
  @app = new unless new.nil?
73
75
 
74
76
  @app ||= begin
75
- raise(Zapp::ZapError, "Missing rackup file '#{rackup_file}'") unless File.exist?(rackup_file)
77
+ raise(Zapp::ZappError, "Missing rackup file '#{rackup_file}'") unless File.exist?(rackup_file)
76
78
 
77
79
  rack_app, = rack_builder.parse_file(rackup_file)
78
80
 
@@ -98,6 +100,12 @@ module Zapp
98
100
  @logger_class = new
99
101
  end
100
102
 
103
+ def logger_out_io(new = nil)
104
+ return @logger_out_io if new.nil?
105
+
106
+ @logger_out_io = new
107
+ end
108
+
101
109
  def log_requests(new = nil)
102
110
  return @log_requests if new.nil?
103
111
 
@@ -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 assign it to this hash key in 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)
21
+
19
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
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zapp
4
+ class Logger
5
+ # Base contains all the logging functionality and is included both as class and instance methods of Zap::Logger
6
+ # This allows logging without creating new instances,
7
+ # while allowing Ractors to create their own instances for thread safety
8
+ module Base
9
+ attr_writer(:level, :prefix)
10
+
11
+ LEVELS = { TRACE: 0, DEBUG: 1, INFO: 2, WARN: 3, ERROR: 4 }.freeze
12
+
13
+ FROZEN_ENV = ENV.map { |k, v| [k.freeze, v.freeze] }
14
+ .to_h.freeze
15
+
16
+ # The hash key in Ractor.current that stores the mutex for writing to output
17
+ OUT_IO_MUTEX_KEY = "ZAPP_LOGGER_OUT_IO_MUTEX"
18
+
19
+ def trace(msg) = log("TRACE", msg)
20
+
21
+ def debug(msg) = log("DEBUG", msg)
22
+
23
+ def info(msg) = log("INFO", msg)
24
+
25
+ def warn(msg) = log("WARN", msg)
26
+
27
+ def error(msg) = log("ERROR", msg)
28
+
29
+ def level
30
+ @level ||= begin
31
+ log_level = FROZEN_ENV["LOG_LEVEL"]
32
+
33
+ if log_level == "" || log_level.nil?
34
+ LEVELS[:DEBUG]
35
+ else
36
+ resolved_level = LEVELS[log_level.upcase.to_sym]
37
+
38
+ if resolved_level.nil?
39
+ raise(
40
+ Zapp::ZappError,
41
+ "Invalid log level '#{log_level.upcase}', must be one of [#{LEVELS.keys.join(', ')}]"
42
+ )
43
+ end
44
+
45
+ resolved_level
46
+ end
47
+ end
48
+ end
49
+
50
+ def log(current_level, msg, **_tags)
51
+ return unless level <= LEVELS[current_level.to_sym]
52
+
53
+ write("--- #{prefix} [#{current_level}] #{msg}\n")
54
+ end
55
+
56
+ def flush
57
+ writing_thread_pool.wait_for_termination(0.1)
58
+
59
+ out_io_mutex.synchronize do
60
+ out.flush
61
+ end
62
+ end
63
+
64
+ # @param new_out [IO]
65
+ def out=(new_out)
66
+ @out = new_out
67
+ end
68
+
69
+ protected
70
+
71
+ # @return [IO]
72
+ def out
73
+ @out ||= Zapp.config.logger_out_io
74
+ end
75
+
76
+ # @return [String]
77
+ def prefix = @prefix ||= Ractor.current.name
78
+
79
+ def write(msg)
80
+ writing_thread_pool.post do
81
+ out_io_mutex.synchronize do
82
+ out.print(msg)
83
+ end
84
+ end
85
+ end
86
+
87
+ # We really just use this as a queue
88
+ # TODO: There's probably a smarter way of doing this with less overhead,
89
+ # TODO: or maybe we should just actually write logs multi-threaded
90
+ def writing_thread_pool
91
+ @writing_thread_pool ||= Concurrent::ThreadPoolExecutor.new(
92
+ min_threads: 1,
93
+ max_threads: 1,
94
+ max_queue: 100
95
+ )
96
+ end
97
+
98
+ def out_io_mutex
99
+ Ractor.current[OUT_IO_MUTEX_KEY] ||= Thread::Mutex.new
100
+ end
101
+ end
102
+ end
103
+ end
data/lib/zapp/logger.rb CHANGED
@@ -1,63 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative("logger/base")
4
+
3
5
  module Zapp
4
- # The default logger for zap
6
+ # The default logger for Zapp
5
7
  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
8
+ include(Zapp::Logger::Base)
21
9
 
22
- def warn(msg)
23
- log("WARN", msg)
24
- end
10
+ def initialize
11
+ yield(self) if block_given?
12
+ end
25
13
 
26
- def error(msg)
27
- log("ERROR", msg)
28
- end
14
+ class << self
15
+ # The hash key in Ractor.current that stores the global Zapp::Logger instance
16
+ GLOBAL_INSTANCE_KEY = "ZAPP_LOGGER_INSTANCE"
29
17
 
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
18
+ def instance
19
+ Ractor.current[GLOBAL_INSTANCE_KEY] ||= new
43
20
  end
44
21
 
45
22
  private
46
23
 
47
- def log(current_level, msg)
48
- puts("--- #{@prefix} [#{current_level}] #{msg}") if level <= LEVELS[current_level.to_sym]
24
+ def method_missing(symbol, *args)
25
+ if respond_to_missing?(symbol)
26
+ instance.public_send(symbol, *args)
27
+ else
28
+ super
29
+ end
49
30
  end
50
- end
51
- include(Zapp::Logger::Base)
52
-
53
- def initialize
54
- @prefix = "Zap"
55
- yield(self) if block_given?
56
- end
57
31
 
58
- class << self
59
- include(Zapp::Logger::Base)
60
- @prefix = "Zap"
32
+ def respond_to_missing?(symbol, include_private = false)
33
+ instance.respond_to?(symbol) || super(symbol, include_private)
34
+ end
61
35
  end
62
36
  end
63
37
  end
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,53 +3,75 @@
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(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
 
40
41
  worker_pool.drain
41
42
 
42
43
  Zapp::Logger.info("Done. See you next time!")
44
+ Zapp::Logger.flush
43
45
  end
44
46
 
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
+ "
61
+ )
62
+ Zapp::Logger.info("Zapp version: #{Zapp::VERSION}")
49
63
  Zapp::Logger.info("Environment: #{Zapp.config.mode}")
50
64
  Zapp::Logger.info("Serving: #{Zapp.config.env[Rack::RACK_URL_SCHEME]}://#{Zapp.config.host}:#{Zapp.config.port}")
51
65
  Zapp::Logger.info("Parallel workers: #{Zapp.config.parallelism}")
52
66
  Zapp::Logger.info("Ready to accept requests")
53
67
  end
68
+
69
+ def parsing_thread_pool
70
+ @parsing_thread_pool ||= Concurrent::ThreadPoolExecutor.new(
71
+ min_threads: Zapp.config.parallelism,
72
+ max_threads: Zapp.config.parallelism,
73
+ max_queue: Zapp.config.parallelism * 1_000
74
+ )
75
+ end
54
76
  end
55
77
  end
@@ -0,0 +1,24 @@
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
+ end
23
+ end
24
+ 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.0"
4
+ VERSION = "0.2.2"
5
5
  end