From 221234945476fe6a33fc5ee57d3b8a7b64c25790 Mon Sep 17 00:00:00 2001 From: Max Wofford Date: Wed, 25 Jun 2025 20:20:07 -0400 Subject: [PATCH] Add rps tracker to footer (#364) --- app/controllers/application_controller.rb | 5 ++ app/helpers/application_helper.rb | 5 ++ app/lib/request_counter.rb | 57 +++++++++++++++++++++++ app/views/layouts/application.html.erb | 1 + 4 files changed, 68 insertions(+) create mode 100644 app/lib/request_counter.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 2f42bd7..c10008e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -3,6 +3,7 @@ class ApplicationController < ActionController::Base before_action :honeybadger_context, if: :current_user before_action :initialize_cache_counters before_action :try_rack_mini_profiler_enable + before_action :track_request after_action :track_action around_action :switch_time_zone, if: :current_user @@ -30,6 +31,10 @@ class ApplicationController < ActionController::Base ahoy.track "Ran action", request.path_parameters end + def track_request + RequestCounter.increment + end + def try_rack_mini_profiler_enable if current_user && current_user.is_admin? Rack::MiniProfiler.authorize_request diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 6653306..b75902b 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -5,6 +5,11 @@ module ApplicationHelper { hits: hits, misses: misses } end + def requests_per_second + rps = RequestCounter.per_second + rps == :high_load ? "lots of req/sec" : "#{rps} req/sec" + end + def admin_tool(class_name = "", element = "div", **options, &block) return unless current_user&.is_admin? concat content_tag(element, class: "admin-tool #{class_name}", **options, &block) diff --git a/app/lib/request_counter.rb b/app/lib/request_counter.rb new file mode 100644 index 0000000..b42934c --- /dev/null +++ b/app/lib/request_counter.rb @@ -0,0 +1,57 @@ +class RequestCounter + WINDOW_SIZE = 10 # seconds - shorter window for more responsive rates + HIGH_LOAD_THRESHOLD = 500 # req/sec to disable tracking + CIRCUIT_BREAKER_DURATION = 30 # seconds to stay disabled + + @buckets = {} + @disabled_until = nil + + class << self + def increment + return if disabled? + + current_time = Time.current.to_i + @buckets[current_time] = (@buckets[current_time] || 0) + 1 + + # Check if we should disable due to high load + check_circuit_breaker(current_time) + + # Periodically clean old buckets (1% chance) + cleanup if rand(100) == 0 + end + + def per_second + return :high_load if disabled? + + current_time = Time.current.to_i + cutoff = current_time - WINDOW_SIZE + + total = @buckets.select { |timestamp, _| timestamp >= cutoff }.values.sum + (total.to_f / WINDOW_SIZE).round(2) + end + + private + + def disabled? + @disabled_until && Time.current.to_i < @disabled_until + end + + def check_circuit_breaker(current_time) + # Check last 5 seconds for high load + recent_total = @buckets.select { |ts, _| ts >= current_time - 5 }.values.sum + + if recent_total > HIGH_LOAD_THRESHOLD * 5 # 5 seconds worth + @disabled_until = current_time + CIRCUIT_BREAKER_DURATION + @buckets.clear # Clear to reduce memory + end + end + + def cleanup + return if disabled? # Skip cleanup when disabled + + current_time = Time.current.to_i + cutoff = current_time - WINDOW_SIZE - 10 # extra buffer + @buckets.reject! { |timestamp, _| timestamp < cutoff } + end + end +end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 142e72d..cfb9cfe 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -168,6 +168,7 @@ in the last 24 hours. (DB: <%= pluralize(QueryCount::Counter.counter, "query") %>, <%= QueryCount::Counter.counter_cache %> cached) (CACHE: <%= cache_stats[:hits] %> hits, <%= cache_stats[:misses] %> misses) + (<%= requests_per_second %>)

<% if session[:impersonater_user_id] %> <%= link_to "Stop impersonating", stop_impersonating_path, class: "impersonate-link", data: { turbo_prefetch: "false" } %>