PERF: run post timings in background

This means that if a very large amount of registered users hit
a single topic we will handle it gracefully, even if db gets slow.
This commit is contained in:
Sam 2018-01-19 08:26:18 +11:00
parent 90a8ea617b
commit 12872d03be
8 changed files with 67 additions and 33 deletions

5
.gitignore vendored
View File

@ -117,9 +117,12 @@ bundler_stubs/*
vendor/bundle/* vendor/bundle/*
*.db *.db
#ignore jetbrains ide file # ignore jetbrains ide file
*.iml *.iml
# vim swap
*.swn
# ignore nodejs files # ignore nodejs files
/node_modules /node_modules
/package-lock.json /package-lock.json

View File

@ -144,7 +144,7 @@ GEM
railties (>= 4.2.0) railties (>= 4.2.0)
thor (>= 0.14, < 2.0) thor (>= 0.14, < 2.0)
jwt (1.5.6) jwt (1.5.6)
kgio (2.11.0) kgio (2.11.1)
libv8 (6.3.292.48.1) libv8 (6.3.292.48.1)
listen (3.1.5) listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4) rb-fsevent (~> 0.9, >= 0.9.4)
@ -389,7 +389,7 @@ GEM
unf_ext unf_ext
unf_ext (0.0.7.4) unf_ext (0.0.7.4)
unicode-display_width (1.3.0) unicode-display_width (1.3.0)
unicorn (5.3.1) unicorn (5.4.0)
kgio (~> 2.6) kgio (~> 2.6)
raindrops (~> 0.7) raindrops (~> 0.7)
uniform_notifier (1.10.0) uniform_notifier (1.10.0)

View File

@ -585,14 +585,22 @@ class TopicsController < ApplicationController
end end
def timings def timings
PostTiming.process_timings( allowed_params = topic_params
current_user,
topic_params[:topic_id].to_i, topic_id = allowed_params[:topic_id].to_i
topic_params[:topic_time].to_i, topic_time = allowed_params[:topic_time].to_i
(topic_params[:timings].to_h || {}).map { |post_number, t| [post_number.to_i, t.to_i] }, timings = allowed_params[:timings].to_h || {}
mobile: view_context.mobile_view?
) hijack do
render body: nil PostTiming.process_timings(
current_user,
topic_id,
topic_time,
timings.map { |post_number, t| [post_number.to_i, t.to_i] },
mobile: view_context.mobile_view?
)
render body: nil
end
end end
def feed def feed

View File

@ -155,6 +155,9 @@ module Discourse
# for some reason still seeing it in Rails 4 # for some reason still seeing it in Rails 4
config.middleware.delete Rack::Lock config.middleware.delete Rack::Lock
# wrong place in middleware stack AND request tracker handles it
config.middleware.delete Rack::Runtime
# ETags are pointless, we are dynamically compressing # ETags are pointless, we are dynamically compressing
# so nginx strips etags, may revisit when mainline nginx # so nginx strips etags, may revisit when mainline nginx
# supports etags (post 1.7) # supports etags (post 1.7)

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
require_dependency 'method_profiler'
# This module allows us to hijack a request and send it to the client in the deferred job queue # This module allows us to hijack a request and send it to the client in the deferred job queue
# For cases where we are making remote calls like onebox or proxying files and so on this helps # For cases where we are making remote calls like onebox or proxying files and so on this helps
# free up a unicorn worker while the remote IO is happening # free up a unicorn worker while the remote IO is happening
@ -13,20 +15,19 @@ module Hijack
request.env['discourse.request_tracker.skip'] = true request.env['discourse.request_tracker.skip'] = true
request_tracker = request.env['discourse.request_tracker'] request_tracker = request.env['discourse.request_tracker']
# unicorn will re-cycle env, this ensures we keep the original copy # in the past unicorn would recycle env, this is not longer the case
env_copy = request.env.dup env = request.env
request_copy = ActionDispatch::Request.new(env_copy) request_copy = ActionDispatch::Request.new(env)
transfer_timings = MethodProfiler.transfer if defined? MethodProfiler transfer_timings = MethodProfiler.transfer
io = hijack.call io = hijack.call
Scheduler::Defer.later("hijack #{params["controller"]} #{params["action"]}") do Scheduler::Defer.later("hijack #{params["controller"]} #{params["action"]}") do
MethodProfiler.start(transfer_timings) if defined? MethodProfiler MethodProfiler.start(transfer_timings)
begin begin
Thread.current[Logster::Logger::LOGSTER_ENV] = env_copy Thread.current[Logster::Logger::LOGSTER_ENV] = env
# do this first to confirm we have a working connection # do this first to confirm we have a working connection
# before doing any work # before doing any work
io.write "HTTP/1.1 " io.write "HTTP/1.1 "
@ -43,7 +44,7 @@ module Hijack
instance.instance_eval(&blk) instance.instance_eval(&blk)
rescue => e rescue => e
# TODO we need to reuse our exception handling in ApplicationController # TODO we need to reuse our exception handling in ApplicationController
Discourse.warn_exception(e, message: "Failed to process hijacked response correctly", env: env_copy) Discourse.warn_exception(e, message: "Failed to process hijacked response correctly", env: env)
end end
unless instance.response_body || response.committed? unless instance.response_body || response.committed?
@ -56,36 +57,40 @@ module Hijack
headers = response.headers headers = response.headers
# add cors if needed # add cors if needed
if cors_origins = env_copy[Discourse::Cors::ORIGINS_ENV] if cors_origins = env[Discourse::Cors::ORIGINS_ENV]
Discourse::Cors.apply_headers(cors_origins, env_copy, headers) Discourse::Cors.apply_headers(cors_origins, env, headers)
end end
headers['Content-Length'] = body.bytesize headers['Content-Length'] = body.bytesize
headers['Content-Type'] = response.content_type || "text/plain" headers['Content-Type'] = response.content_type || "text/plain"
headers['Connection'] = "close" headers['Connection'] = "close"
status_string = Rack::Utils::HTTP_STATUS_CODES[instance.status.to_i] || "Unknown" status_string = Rack::Utils::HTTP_STATUS_CODES[response.status.to_i] || "Unknown"
io.write "#{instance.status} #{status_string}\r\n" io.write "#{response.status} #{status_string}\r\n"
headers.each do |name, val| headers.each do |name, val|
io.write "#{name}: #{val}\r\n" io.write "#{name}: #{val}\r\n"
end end
timings = MethodProfiler.stop
if timings && duration = timings[:total_duration]
io.write "X-Runtime: #{"%0.6f" % duration}\r\n"
end
io.write "\r\n" io.write "\r\n"
io.write body io.write body
rescue Errno::EPIPE, IOError rescue Errno::EPIPE, IOError
# happens if client terminated before we responded, ignore # happens if client terminated before we responded, ignore
io = nil io = nil
ensure ensure
MethodProfiler.clear
Thread.current[Logster::Logger::LOGSTER_ENV] = nil Thread.current[Logster::Logger::LOGSTER_ENV] = nil
io.close if io rescue nil io.close if io rescue nil
if request_tracker if request_tracker
status = instance.status rescue 500 status = instance.status rescue 500
timings = MethodProfiler.stop if defined? MethodProfiler request_tracker.log_request_info(env, [status, headers || {}, []], timings)
request_tracker.log_request_info(env_copy, [status, headers || {}, []], timings)
end end
end end
end end

View File

@ -37,6 +37,10 @@ class MethodProfiler
} }
end end
def self.clear
Thread.current[:_method_profiler] = nil
end
def self.stop def self.stop
finish = Process.clock_gettime(Process::CLOCK_MONOTONIC) finish = Process.clock_gettime(Process::CLOCK_MONOTONIC)
if data = Thread.current[:_method_profiler] if data = Thread.current[:_method_profiler]

View File

@ -1,6 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
require_dependency 'middleware/anonymous_cache' require_dependency 'middleware/anonymous_cache'
require_dependency 'method_profiler'
class Middleware::RequestTracker class Middleware::RequestTracker
@ -15,7 +16,6 @@ class Middleware::RequestTracker
def self.register_detailed_request_logger(callback) def self.register_detailed_request_logger(callback)
unless @patched_instrumentation unless @patched_instrumentation
require_dependency "method_profiler"
MethodProfiler.patch(PG::Connection, [ MethodProfiler.patch(PG::Connection, [
:exec, :async_exec, :exec_prepared, :send_query_prepared, :query :exec, :async_exec, :exec_prepared, :send_query_prepared, :query
], :sql) ], :sql)
@ -134,9 +134,13 @@ class Middleware::RequestTracker
end end
env["discourse.request_tracker"] = self env["discourse.request_tracker"] = self
MethodProfiler.start if @@detailed_request_loggers MethodProfiler.start
result = @app.call(env) result = @app.call(env)
info = MethodProfiler.stop if @@detailed_request_loggers info = MethodProfiler.stop
# possibly transferred?
if info
env["X-Runtime"] = "%0.6f" % info[:total_duration]
end
result result
ensure ensure
log_request_info(env, result, info) unless env["discourse.request_tracker.skip"] log_request_info(env, result, info) unless env["discourse.request_tracker.skip"]

View File

@ -141,36 +141,43 @@ describe Hijack do
end end
it "renders a redirect correctly" do it "renders a redirect correctly" do
Process.stubs(:clock_gettime).returns(1.0)
tester.hijack_test do tester.hijack_test do
Process.stubs(:clock_gettime).returns(2.0)
redirect_to 'http://awesome.com' redirect_to 'http://awesome.com'
end end
result = "HTTP/1.1 302 Found\r\nLocation: http://awesome.com\r\nContent-Type: text/html\r\nContent-Length: 84\r\nConnection: close\r\n\r\n<html><body>You are being <a href=\"http://awesome.com\">redirected</a>.</body></html>" result = "HTTP/1.1 302 Found\r\nLocation: http://awesome.com\r\nContent-Type: text/html\r\nContent-Length: 84\r\nConnection: close\r\nX-Runtime: 1.000000\r\n\r\n<html><body>You are being <a href=\"http://awesome.com\">redirected</a>.</body></html>"
expect(tester.io.string).to eq(result) expect(tester.io.string).to eq(result)
end end
it "renders stuff correctly if is empty" do it "renders stuff correctly if is empty" do
Process.stubs(:clock_gettime).returns(1.0)
tester.hijack_test do tester.hijack_test do
Process.stubs(:clock_gettime).returns(2.0)
render body: nil render body: nil
end end
result = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" result = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 0\r\nConnection: close\r\nX-Runtime: 1.000000\r\n\r\n"
expect(tester.io.string).to eq(result) expect(tester.io.string).to eq(result)
end end
it "renders stuff correctly if it works" do it "renders stuff correctly if it works" do
Process.stubs(:clock_gettime).returns(1.0)
tester.hijack_test do tester.hijack_test do
Process.stubs(:clock_gettime).returns(2.0)
render plain: "hello world" render plain: "hello world"
end end
result = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 11\r\nConnection: close\r\n\r\nhello world" result = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 11\r\nConnection: close\r\nX-Runtime: 1.000000\r\n\r\nhello world"
expect(tester.io.string).to eq(result) expect(tester.io.string).to eq(result)
end end
it "returns 500 by default" do it "returns 500 by default" do
Process.stubs(:clock_gettime).returns(1.0)
tester.hijack_test tester.hijack_test
expected = "HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/html\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" expected = "HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/html\r\nContent-Length: 0\r\nConnection: close\r\nX-Runtime: 0.000000\r\n\r\n"
expect(tester.io.string).to eq(expected) expect(tester.io.string).to eq(expected)
end end