discourse/config/initializers/200-first_middlewares.rb
Alan Guo Xiang Tan 9c781af9d1
PERF: Reject anonymous requests with 503 upon aggressive queuing (#36519)
This commit adds a default protection to all Discourse app which will
turn away all anonymous requests that has queued for more than the
duration configured
by the `reject_anonymous_min_queue_seconds` global setting, 1 second
by default. A queue time of 1 second is a sign that the app is very
overloaded so we are opting to turn away anonymous requests when that
happens.

While there may be better solutions like introducing an adaptive
concurrency limit somewhere in the stack, we like this solution for its
simplicity. We may consider the more complex solutions in the future but
the time is not now.

Other significant changes in this commit:

1. Moved setting of the `REQUEST_QUEUE_SECONDS` key in `env` to
   `Middleware::ProcessingRequest`.
2. Introduction of the `reject_anonymous_min_queue_seconds` global
   Setting.
2025-12-18 10:23:42 +08:00

126 lines
4.4 KiB
Ruby

# frozen_string_literal: true
# we want MessageBus to be close to the front
# this is important cause the vast majority of web requests go to it
# this allows us to avoid full middleware crawls each time
#
# We aren't manipulating the middleware stack directly because of
# https://github.com/rails/rails/pull/27936
Rails.configuration.middleware.unshift(MessageBus::Rack::Middleware)
# no reason to track this in development, that is 300+ redis calls saved per
# page view (we serve all assets out of thin in development)
if !Rails.env.development? || ENV["TRACK_REQUESTS"]
require "middleware/request_tracker"
Rails.configuration.middleware.unshift(Middleware::RequestTracker)
Rails.configuration.middleware.move_before(Middleware::RequestTracker, ActionDispatch::RemoteIp)
MethodProfiler.ensure_discourse_instrumentation! if GlobalSetting.enable_performance_http_headers
end
require "middleware/overload_protections"
Rails.configuration.middleware.unshift(Middleware::OverloadProtections)
require "middleware/processing_request"
Rails.configuration.middleware.unshift(Middleware::ProcessingRequest)
if Rails.env.test?
# In test mode we can't insert/remove middlewares
# Therefore we insert a small helper which effectively switches the multisite
# middleware on/off based on the Rails.configuration.multisite value
class TestMultisiteMiddleware < RailsMultisite::Middleware
def call(env)
return @app.call(env) if !Rails.configuration.multisite
super(env)
end
end
Rails.configuration.middleware.unshift TestMultisiteMiddleware,
RailsMultisite::DiscoursePatches.config
class BlockRequestsMiddleware
RSPEC_CURRENT_EXAMPLE_COOKIE_STRING = "rspec_current_example_location"
cattr_accessor :current_example_location
@@block_requests = false
def self.block_requests!
@@block_requests = true
end
def self.allow_requests!
@@block_requests = false
end
def initialize(app)
@app = app
end
def call(env)
request = Rack::Request.new(env)
if (
@@block_requests ||
(
request.xhr? && self.class.current_example_location.present? &&
self.class.current_example_location !=
request.cookies[RSPEC_CURRENT_EXAMPLE_COOKIE_STRING]
)
)
return [
503,
{ "Content-Type" => "text/plain" },
[
"Blocked by BlockRequestsMiddleware for requests initiated by #{request.cookies[RSPEC_CURRENT_EXAMPLE_COOKIE_STRING]} when running #{self.class.current_example_location}",
]
]
end
status, headers, body = @app.call(env)
if headers["Content-Type"]&.match?(/html/) && BlockRequestsMiddleware.current_example_location
headers["Set-Cookie"] = [
headers["Set-Cookie"],
"#{RSPEC_CURRENT_EXAMPLE_COOKIE_STRING}=#{BlockRequestsMiddleware.current_example_location}; path=/;",
].compact.join("\n")
end
[status, headers, body]
end
end
Rails.configuration.middleware.unshift BlockRequestsMiddleware
elsif Rails.configuration.multisite
assets_hostnames = GlobalSetting.cdn_hostnames
if assets_hostnames.empty?
assets_hostnames = Discourse::Application.config.database_configuration[Rails.env]["host_names"]
end
RailsMultisite::ConnectionManagement.asset_hostnames = assets_hostnames
# Multisite needs to be first, because the request tracker and message bus rely on it
Rails.configuration.middleware.unshift RailsMultisite::Middleware,
RailsMultisite::DiscoursePatches.config
Rails.configuration.middleware.delete ActionDispatch::Executor
if defined?(RailsFailover::ActiveRecord) && Rails.configuration.active_record_rails_failover
Rails.configuration.middleware.insert_after(
RailsMultisite::Middleware,
RailsFailover::ActiveRecord::Middleware,
)
end
if Rails.env.development?
# Automatically allow development multisite hosts
RailsMultisite::ConnectionManagement.instance.db_spec_cache.each do |db, specification|
next if db == "default"
Rails.configuration.hosts.concat(specification.spec.configuration_hash[:host_names])
end
end
elsif defined?(RailsFailover::ActiveRecord) && Rails.configuration.active_record_rails_failover
Rails.configuration.middleware.insert_before(
MessageBus::Rack::Middleware,
RailsFailover::ActiveRecord::Middleware,
)
end