zapp 0.1.0 → 0.2.2

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