From 5103cce6d9aee5e91428f851f23a025dad91f153 Mon Sep 17 00:00:00 2001 From: Tin Tran Date: Wed, 17 Sep 2025 20:35:48 +0700 Subject: [PATCH 01/30] Add error logs to track Twelvedata invalid responses --- app/models/provider/twelve_data.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/models/provider/twelve_data.rb b/app/models/provider/twelve_data.rb index ccb97ff64d7..691ee4dd655 100644 --- a/app/models/provider/twelve_data.rb +++ b/app/models/provider/twelve_data.rb @@ -48,7 +48,10 @@ def fetch_exchange_rate(from:, to:, date:) end rate = JSON.parse(response.body).dig("rate") - + if rate.nil? + Rails.logger.warn("#{self.class.name} returned invalid rate data for pair from: #{from} to: #{to} on: #{date}") + raise InvalidExchangeRateError.new("Could not fetch exchange rate for #{from}/#{to} on #{date}") + end Rate.new(date: date.to_date, from:, to:, rate: rate) end end @@ -63,6 +66,10 @@ def fetch_exchange_rates(from:, to:, start_date:, end_date:) end data = JSON.parse(response.body).dig("values") + if data.nil? + Rails.logger.warn("#{self.class.name} returned invalid rate data for pair from: #{from} to: #{to} between: #{start_date} and #{end_date}") + raise InvalidExchangeRateError.new("Could not fetch exchange rates for #{from}/#{to} between #{start_date} and #{end_date}") + end data.map do |resp| rate = resp.dig("close") date = resp.dig("datetime") From 02fe6b038f01220bab36e295a3ee2fcdb017d31a Mon Sep 17 00:00:00 2001 From: Tin Tran Date: Thu, 18 Sep 2025 12:33:10 +0700 Subject: [PATCH 02/30] Support exotic currency pairs --- app/models/provider/twelve_data.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/models/provider/twelve_data.rb b/app/models/provider/twelve_data.rb index 691ee4dd655..fa613b9831b 100644 --- a/app/models/provider/twelve_data.rb +++ b/app/models/provider/twelve_data.rb @@ -58,8 +58,9 @@ def fetch_exchange_rate(from:, to:, date:) def fetch_exchange_rates(from:, to:, start_date:, end_date:) with_provider_response do - response = client.get("#{base_url}/time_series") do |req| - req.params["symbol"] = "#{from}/#{to}" + response = client.get("#{base_url}/time_series/cross") do |req| + req.params["base"] = "#{to}" + req.params["quote"] = "#{from}" req.params["start_date"] = start_date.to_s req.params["end_date"] = end_date.to_s req.params["interval"] = "1day" From b62f682de70c58243d7c9624d6ead869fff80b3b Mon Sep 17 00:00:00 2001 From: Tin Tran Date: Thu, 18 Sep 2025 12:49:43 +0700 Subject: [PATCH 03/30] Send app version to Sentry --- config/initializers/sentry.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb index d964a09b85d..1d5c4abe1fa 100644 --- a/config/initializers/sentry.rb +++ b/config/initializers/sentry.rb @@ -16,5 +16,6 @@ config.profiles_sample_rate = 0.25 config.profiler_class = Sentry::Vernier::Profiler + config.release = Maybe.version.to_s + "+" + Maybe.commit_sha end end From a56cbbf39854adf6cc48f3e466c18434c5967dbe Mon Sep 17 00:00:00 2001 From: Tin Tran Date: Thu, 18 Sep 2025 12:54:45 +0700 Subject: [PATCH 04/30] Enhance error logging for invalid exchange rate responses --- app/models/provider/twelve_data.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/provider/twelve_data.rb b/app/models/provider/twelve_data.rb index fa613b9831b..c7feef1c493 100644 --- a/app/models/provider/twelve_data.rb +++ b/app/models/provider/twelve_data.rb @@ -49,8 +49,8 @@ def fetch_exchange_rate(from:, to:, date:) rate = JSON.parse(response.body).dig("rate") if rate.nil? - Rails.logger.warn("#{self.class.name} returned invalid rate data for pair from: #{from} to: #{to} on: #{date}") - raise InvalidExchangeRateError.new("Could not fetch exchange rate for #{from}/#{to} on #{date}") + Rails.logger.warn("#{self.class.name} returned invalid rate data for pair from: #{from} to: #{to} on: #{date}, response: #{response.body}") + raise InvalidExchangeRateError.new("Could not fetch exchange rate for #{from}/#{to} on #{date}, response: #{response.body}") end Rate.new(date: date.to_date, from:, to:, rate: rate) end @@ -68,8 +68,8 @@ def fetch_exchange_rates(from:, to:, start_date:, end_date:) data = JSON.parse(response.body).dig("values") if data.nil? - Rails.logger.warn("#{self.class.name} returned invalid rate data for pair from: #{from} to: #{to} between: #{start_date} and #{end_date}") - raise InvalidExchangeRateError.new("Could not fetch exchange rates for #{from}/#{to} between #{start_date} and #{end_date}") + Rails.logger.warn("#{self.class.name} returned invalid rate data for pair from: #{from} to: #{to} between: #{start_date} and #{end_date}, response: #{response.body}") + raise InvalidExchangeRateError.new("Could not fetch exchange rates for #{from}/#{to} between #{start_date} and #{end_date}, response: #{response.body}") end data.map do |resp| rate = resp.dig("close") From 43447a3ae5bd77650eca6ae3ad5c47a605c3300a Mon Sep 17 00:00:00 2001 From: Tin Tran Date: Thu, 18 Sep 2025 13:11:20 +0700 Subject: [PATCH 05/30] Revert "Send app version to Sentry" This reverts commit b62f682de70c58243d7c9624d6ead869fff80b3b. --- config/initializers/sentry.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb index 1d5c4abe1fa..d964a09b85d 100644 --- a/config/initializers/sentry.rb +++ b/config/initializers/sentry.rb @@ -16,6 +16,5 @@ config.profiles_sample_rate = 0.25 config.profiler_class = Sentry::Vernier::Profiler - config.release = Maybe.version.to_s + "+" + Maybe.commit_sha end end From 1e177337f18432ccbc8c3ff05cdd4d4da0ec9961 Mon Sep 17 00:00:00 2001 From: Tin Tran Date: Thu, 18 Sep 2025 13:36:47 +0700 Subject: [PATCH 06/30] Add methods to fetch exchange rates and fallback to cross rates --- app/models/provider/twelve_data.rb | 44 +++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/app/models/provider/twelve_data.rb b/app/models/provider/twelve_data.rb index c7feef1c493..29f3876a9bc 100644 --- a/app/models/provider/twelve_data.rb +++ b/app/models/provider/twelve_data.rb @@ -55,22 +55,40 @@ def fetch_exchange_rate(from:, to:, date:) Rate.new(date: date.to_date, from:, to:, rate: rate) end end + def fetch_exchange_cross_rates(from:, to:, start_date:, end_date:) + response = client.get("#{base_url}/time_series/cross") do |req| + req.params["base"] = "#{from}" + req.params["quote"] = "#{from}" + req.params["start_date"] = start_date.to_s + req.params["end_date"] = end_date.to_s + req.params["interval"] = "1day" + end + data = JSON.parse(response.body).dig("values") + if data.nil? + Rails.logger.warn("#{self.class.name} returned invalid rate data for pair from: #{from} to: #{to} between: #{start_date} and #{end_date}, response: #{response.body}") + raise InvalidExchangeRateError.new("Could not fetch exchange rates for #{from}/#{to} between #{start_date} and #{end_date}, response: #{response.body}") + end + data + end + + def fetch_exchange_rates_internal(from:, to:, start_date:, end_date:) + response = client.get("#{base_url}/time_series") do |req| + req.params["symbol"] = "#{from}/#{to}" + req.params["start_date"] = start_date.to_s + req.params["end_date"] = end_date.to_s + req.params["interval"] = "1day" + end + data = JSON.parse(response.body).dig("values") + if data.nil? + Rails.logger.warn("#{self.class.name} returned invalid rate data for pair from: #{from} to: #{to} between: #{start_date} and #{end_date}, response: #{response.body}") + fetch_exchange_cross_rates(from:, to:, start_date:, end_date:) + end + data + end def fetch_exchange_rates(from:, to:, start_date:, end_date:) with_provider_response do - response = client.get("#{base_url}/time_series/cross") do |req| - req.params["base"] = "#{to}" - req.params["quote"] = "#{from}" - req.params["start_date"] = start_date.to_s - req.params["end_date"] = end_date.to_s - req.params["interval"] = "1day" - end - - data = JSON.parse(response.body).dig("values") - if data.nil? - Rails.logger.warn("#{self.class.name} returned invalid rate data for pair from: #{from} to: #{to} between: #{start_date} and #{end_date}, response: #{response.body}") - raise InvalidExchangeRateError.new("Could not fetch exchange rates for #{from}/#{to} between #{start_date} and #{end_date}, response: #{response.body}") - end + data = fetch_exchange_rates_internal(from:, to:, start_date:, end_date:) data.map do |resp| rate = resp.dig("close") date = resp.dig("datetime") From c95607b82f07114c1ace980ddf8264ffa7133630 Mon Sep 17 00:00:00 2001 From: Tin Tran Date: Thu, 18 Sep 2025 15:01:07 +0700 Subject: [PATCH 07/30] Fix quote parameter in fetch_exchange_cross_rates method --- app/models/provider/twelve_data.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/provider/twelve_data.rb b/app/models/provider/twelve_data.rb index 29f3876a9bc..0c451ec1724 100644 --- a/app/models/provider/twelve_data.rb +++ b/app/models/provider/twelve_data.rb @@ -58,7 +58,7 @@ def fetch_exchange_rate(from:, to:, date:) def fetch_exchange_cross_rates(from:, to:, start_date:, end_date:) response = client.get("#{base_url}/time_series/cross") do |req| req.params["base"] = "#{from}" - req.params["quote"] = "#{from}" + req.params["quote"] = "#{to}" req.params["start_date"] = start_date.to_s req.params["end_date"] = end_date.to_s req.params["interval"] = "1day" From ddcfc308b9a04646602ce1d35f281c852962ee4c Mon Sep 17 00:00:00 2001 From: Tin Tran Date: Fri, 19 Sep 2025 00:35:07 +0700 Subject: [PATCH 08/30] Enable Sentry logging and add enabled patches for enhanced monitoring --- config/initializers/sentry.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb index d964a09b85d..9c1e8a72e8e 100644 --- a/config/initializers/sentry.rb +++ b/config/initializers/sentry.rb @@ -16,5 +16,7 @@ config.profiles_sample_rate = 0.25 config.profiler_class = Sentry::Vernier::Profiler + config.enable_logs = true + config.enabled_patches = [ :logger, :http, :redis, :puma ] end end From d581248c1043c9958ef05fc5ada17e92f1c8c219 Mon Sep 17 00:00:00 2001 From: Tin Tran Date: Fri, 19 Sep 2025 00:40:27 +0700 Subject: [PATCH 09/30] Update sync warning threshold from 10 to 100 syncs --- app/models/sync.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/sync.rb b/app/models/sync.rb index ea66ebb4de0..775d8e79fa5 100644 --- a/app/models/sync.rb +++ b/app/models/sync.rb @@ -157,7 +157,7 @@ def report_error(error) def report_warnings todays_sync_count = syncable.syncs.where(created_at: Date.current.all_day).count - if todays_sync_count > 10 + if todays_sync_count > 100 Sentry.capture_exception( Error.new("#{syncable_type} (#{syncable.id}) has exceeded 10 syncs today (count: #{todays_sync_count})"), level: :warning From d7fe8569bf8a5d37838b78c49e947dfe1bd1a3a6 Mon Sep 17 00:00:00 2001 From: Tin Tran Date: Fri, 19 Sep 2025 00:44:27 +0700 Subject: [PATCH 10/30] Add random delay in fetch_exchange_cross_rates to avoid rate limiting --- app/models/provider/twelve_data.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/provider/twelve_data.rb b/app/models/provider/twelve_data.rb index 0c451ec1724..d9419f9c8d5 100644 --- a/app/models/provider/twelve_data.rb +++ b/app/models/provider/twelve_data.rb @@ -56,6 +56,8 @@ def fetch_exchange_rate(from:, to:, date:) end end def fetch_exchange_cross_rates(from:, to:, start_date:, end_date:) + # Add a random delay to avoid rate limiting + sleep(rand(60..300)) response = client.get("#{base_url}/time_series/cross") do |req| req.params["base"] = "#{from}" req.params["quote"] = "#{to}" From c69ab76090ccdb79a390bfc75a4ec945a41029e8 Mon Sep 17 00:00:00 2001 From: Tin Tran Date: Fri, 19 Sep 2025 14:16:03 +0700 Subject: [PATCH 11/30] Fetch cross rates only in symbol is invalid --- app/models/provider/twelve_data.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/provider/twelve_data.rb b/app/models/provider/twelve_data.rb index d9419f9c8d5..6fd50407666 100644 --- a/app/models/provider/twelve_data.rb +++ b/app/models/provider/twelve_data.rb @@ -80,12 +80,12 @@ def fetch_exchange_rates_internal(from:, to:, start_date:, end_date:) req.params["end_date"] = end_date.to_s req.params["interval"] = "1day" end - data = JSON.parse(response.body).dig("values") - if data.nil? + parsed = JSON.parse(response.body) + if parsed.dig("code") == 404 Rails.logger.warn("#{self.class.name} returned invalid rate data for pair from: #{from} to: #{to} between: #{start_date} and #{end_date}, response: #{response.body}") fetch_exchange_cross_rates(from:, to:, start_date:, end_date:) end - data + parsed.dig("values") end def fetch_exchange_rates(from:, to:, start_date:, end_date:) From 27b39e34e8ff75eff18b47557cb2b4f34f4a3d00 Mon Sep 17 00:00:00 2001 From: Tin Tran Date: Fri, 19 Sep 2025 15:16:05 +0700 Subject: [PATCH 12/30] Enhance error handling for invalid price data responses --- app/models/provider/twelve_data.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/models/provider/twelve_data.rb b/app/models/provider/twelve_data.rb index 6fd50407666..cf25a5abce9 100644 --- a/app/models/provider/twelve_data.rb +++ b/app/models/provider/twelve_data.rb @@ -180,6 +180,10 @@ def fetch_security_prices(symbol:, exchange_operating_mic: nil, start_date:, end end parsed = JSON.parse(response.body) + if !parsed.dig("code").nil? + Rails.logger.warn("#{self.class.name} returned invalid price data for security #{symbol} between #{start_date} and #{end_date}.") + raise InvalidSecurityPriceError.new("Could not fetch security prices for #{symbol} between #{start_date} and #{end_date}, response: #{response.body}") + end parsed.dig("values").map do |resp| price = resp.dig("close") date = resp.dig("datetime") From b460a1a642c34f02902ab0e408f9940a0cbecb80 Mon Sep 17 00:00:00 2001 From: Tin Tran Date: Fri, 19 Sep 2025 15:25:55 +0700 Subject: [PATCH 13/30] Add retry logic for handling Faraday errors in TwelveData provider --- app/models/provider/twelve_data.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/models/provider/twelve_data.rb b/app/models/provider/twelve_data.rb index cf25a5abce9..5ef22ebb100 100644 --- a/app/models/provider/twelve_data.rb +++ b/app/models/provider/twelve_data.rb @@ -216,7 +216,13 @@ def client max: 2, interval: 0.05, interval_randomness: 0.5, - backoff_factor: 2 + backoff_factor: 2, + exceptions: [ Faraday::TooManyRequestsError, Faraday::RetriableResponse ], + retry_statuses: [ 429 ], + retry_block: ->(env, _, retries, _) { + # Sleep between 1-10 minutes when retrying + sleep(60 + rand(540)) + } }) faraday.request :json From 3278d76aba9ec6d598929b1627c4da8201d8e308 Mon Sep 17 00:00:00 2001 From: Tin Tran Date: Sun, 21 Sep 2025 21:03:05 +0700 Subject: [PATCH 14/30] Add rails_db gem and configure initializer --- Gemfile | 2 ++ Gemfile.lock | 28 ++++++++++++++++++++++++++++ config/initializers/rails_db.rb | 30 ++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 config/initializers/rails_db.rb diff --git a/Gemfile b/Gemfile index 6b56a8a0122..e88d2e5bedf 100644 --- a/Gemfile +++ b/Gemfile @@ -117,3 +117,5 @@ group :test do gem "climate_control" gem "simplecov", require: false end + +gem "rails_db", "~> 2.5" diff --git a/Gemfile.lock b/Gemfile.lock index ff5267e01f1..4805e811b60 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -278,6 +278,18 @@ GEM json (2.12.2) jwt (2.10.2) base64 + kaminari (1.2.2) + activesupport (>= 4.1.0) + kaminari-actionview (= 1.2.2) + kaminari-activerecord (= 1.2.2) + kaminari-core (= 1.2.2) + kaminari-actionview (1.2.2) + actionview + kaminari-core (= 1.2.2) + kaminari-activerecord (1.2.2) + activerecord + kaminari-core (= 1.2.2) + kaminari-core (1.2.2) langfuse-ruby (0.1.4) concurrent-ruby (~> 1.0) faraday (>= 1.8, < 3.0) @@ -443,6 +455,14 @@ GEM rails-settings-cached (2.9.6) activerecord (>= 5.0.0) railties (>= 5.0.0) + rails_db (2.5.0) + activerecord + csv + kaminari + rails (>= 5.0.0) + ransack (>= 2.3.2) + simple_form (>= 5.0.1) + terminal-table railties (7.2.2.2) actionpack (= 7.2.2.2) activesupport (= 7.2.2.2) @@ -453,6 +473,10 @@ GEM zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.3.0) + ransack (4.3.0) + activerecord (>= 6.1.5) + activesupport (>= 6.1.5) + i18n rb-fsevent (0.11.2) rb-inotify (0.11.1) ffi (~> 1.0) @@ -552,6 +576,9 @@ GEM fugit (~> 1.8, >= 1.11.1) globalid (>= 1.0.1) sidekiq (>= 6.5.0) + simple_form (5.3.1) + actionpack (>= 5.2) + activemodel (>= 5.2) simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) @@ -678,6 +705,7 @@ DEPENDENCIES rack-mini-profiler rails (~> 7.2.2) rails-settings-cached + rails_db (~> 2.5) redcarpet redis (~> 5.4) rotp (~> 6.3) diff --git a/config/initializers/rails_db.rb b/config/initializers/rails_db.rb new file mode 100644 index 00000000000..359d9d7b79f --- /dev/null +++ b/config/initializers/rails_db.rb @@ -0,0 +1,30 @@ +if Object.const_defined?("RailsDb") + RailsDb.setup do |config| + # # enabled or not + config.enabled = true + + # # automatic engine routes mounting + config.automatic_routes_mount = true + + # set tables which you want to hide ONLY + # config.black_list_tables = [ "users", "accounts" ] + + # set tables which you want to show ONLY + # config.white_list_tables = [ "posts", "comments" ] + + # # Enable http basic authentication + config.http_basic_authentication_enabled = true + + # # Enable http basic authentication + config.http_basic_authentication_user_name = "rails_db" + + # # Enable http basic authentication + config.http_basic_authentication_password = ENV.fetch("RAILS_DB_PASSWORD", "rails_db") + + # # Enable verify access proc + # config.verify_access_proc = proc { |controller| true } + + # # Sandbox mode (only read-only operations) + # config.sandbox = false + end +end From 2af2060ebc53658f110f3c1d30543d4564d797e3 Mon Sep 17 00:00:00 2001 From: Tin Tran Date: Sun, 21 Sep 2025 22:14:03 +0700 Subject: [PATCH 15/30] Maybe fix segfault --- .ruby-version | 2 +- Dockerfile | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.ruby-version b/.ruby-version index f9892605c75..4b4ea1b6883 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.4.4 +3.4.6 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 2248d827880..4e0a6db6fb1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # syntax = docker/dockerfile:1 # Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile -ARG RUBY_VERSION=3.4.4 +ARG RUBY_VERSION=3.4.6 FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim AS base # Rails app lives here @@ -19,7 +19,7 @@ ENV RAILS_ENV="production" \ BUNDLE_PATH="/usr/local/bundle" \ BUNDLE_WITHOUT="development" \ BUILD_COMMIT_SHA=${BUILD_COMMIT_SHA} - + G # Throw-away build stage to reduce size of final image FROM base AS build From 5a85b4ea7f22151d7f7b2793a831b97cbee20e86 Mon Sep 17 00:00:00 2001 From: Tin Tran Date: Sun, 21 Sep 2025 22:23:40 +0700 Subject: [PATCH 16/30] Update Ruby version to 3.4.6 and Bundler to 2.7.2 in Gemfile.lock --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 4805e811b60..1d7969f840c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -735,7 +735,7 @@ DEPENDENCIES webmock RUBY VERSION - ruby 3.4.4p34 + ruby 3.4.6p54 BUNDLED WITH - 2.6.7 + 2.7.2 From 258116eecd7ca6b7e4d92bd17b2bbb322072407b Mon Sep 17 00:00:00 2001 From: Tin Tran Date: Sun, 21 Sep 2025 22:28:08 +0700 Subject: [PATCH 17/30] Add seed_dump gem for database seeding management --- Gemfile | 2 ++ Gemfile.lock | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/Gemfile b/Gemfile index e88d2e5bedf..d1e3a55ed72 100644 --- a/Gemfile +++ b/Gemfile @@ -119,3 +119,5 @@ group :test do end gem "rails_db", "~> 2.5" + +gem "seed_dump", "~> 3.3" diff --git a/Gemfile.lock b/Gemfile.lock index 1d7969f840c..8fc14260b3d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -550,6 +550,9 @@ GEM addressable (>= 2.3.5) faraday (>= 0.17.3, < 3) securerandom (0.4.1) + seed_dump (3.3.1) + activerecord (>= 4) + activesupport (>= 4) selenium-webdriver (4.34.0) base64 (~> 0.2) logger (~> 1.4) @@ -714,6 +717,7 @@ DEPENDENCIES ruby-lsp-rails ruby-openai rubyzip (~> 2.3) + seed_dump (~> 3.3) selenium-webdriver sentry-rails sentry-ruby From 839fee0c9fe5319687e5a310c5f4c0c0929ca854 Mon Sep 17 00:00:00 2001 From: Tin Tran Date: Sun, 21 Sep 2025 22:31:06 +0700 Subject: [PATCH 18/30] fix my mistake --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4e0a6db6fb1..6864a5c8682 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,7 +19,7 @@ ENV RAILS_ENV="production" \ BUNDLE_PATH="/usr/local/bundle" \ BUNDLE_WITHOUT="development" \ BUILD_COMMIT_SHA=${BUILD_COMMIT_SHA} - G + # Throw-away build stage to reduce size of final image FROM base AS build From 9e6e4cf9182d7a3d01013a5bd8164db66ed33ca0 Mon Sep 17 00:00:00 2001 From: Tin Tran Date: Mon, 22 Sep 2025 01:48:46 +0700 Subject: [PATCH 19/30] Dev environment in Docker --- docker-compose.yaml | 97 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 docker-compose.yaml diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 00000000000..c8305b079fd --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,97 @@ +x-db-env: &db_env + POSTGRES_USER: ${POSTGRES_USER:-maybe_user} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-maybe_password} + POSTGRES_DB: ${POSTGRES_DB:-maybe-development} + +x-rails-env: &rails_env + <<: *db_env + SECRET_KEY_BASE: ${SECRET_KEY_BASE:-a7523c3d0ae56415046ad8abae168d71074a79534a7062258f8d1d51ac2f76d3c3bc86d86b6b0b307df30d9a6a90a2066a3fa9e67c5e6f374dbd7dd4e0778e13} + SELF_HOSTED: "true" + RAILS_FORCE_SSL: "false" + RAILS_ASSUME_SSL: "false" + DB_HOST: db + DB_PORT: 5432 + REDIS_URL: redis://redis:6379/1 +# NOTE: enabling OpenAI will incur costs when you use AI-related features in the app (chat, rules). Make sure you have set appropriate spend limits on your account before adding this. + OPENAI_ACCESS_TOKEN: ${OPENAI_ACCESS_TOKEN} + RAILS_ENV: development + +services: + base: &base + build: + context: . + dockerfile: Dockerfile + args: + RAILS_ENV: 'development' + image: maybe:development + volumes: + - app-storage:/rails/storage + environment: + <<: *rails_env + env_file: .env + restart: unless-stopped + profiles: + - never + + web: + <<: *base + image: maybe-web:development + ports: + - 3000:3000 + command: ./bin/rails server -b 0.0.0.0 + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + networks: + - sure_net + + worker: + <<: *base + image: maybe-worker:development + command: bundle exec sidekiq + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + networks: + - sure_net + + db: + image: postgres:16 + restart: unless-stopped + volumes: + - maybe-dev-postgres-data:/var/lib/postgresql/data + environment: + <<: *db_env + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB" ] + interval: 5s + timeout: 5s + retries: 5 + networks: + - sure_net + + redis: + image: redis:latest + restart: unless-stopped + volumes: + - redis-data:/data + healthcheck: + test: [ "CMD", "redis-cli", "ping" ] + interval: 5s + timeout: 5s + retries: 5 + networks: + - sure_net + +volumes: + app-storage: + maybe-dev-postgres-data: + redis-data: + +networks: + sure_net: + driver: bridge From b4e60e748a3469402c25e403c7afa6b9db45a0c5 Mon Sep 17 00:00:00 2001 From: Tin Tran Date: Mon, 22 Sep 2025 02:17:45 +0700 Subject: [PATCH 20/30] Add ports for PostgreSQL and Redis services to worker and web processes running outside of Docker --- docker-compose.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker-compose.yaml b/docker-compose.yaml index c8305b079fd..0f24837d7ff 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -64,6 +64,8 @@ services: restart: unless-stopped volumes: - maybe-dev-postgres-data:/var/lib/postgresql/data + ports: + - 5432:5432 environment: <<: *db_env healthcheck: @@ -77,6 +79,8 @@ services: redis: image: redis:latest restart: unless-stopped + ports: + - 6379:6379 volumes: - redis-data:/data healthcheck: From aabbf96ae092c2edfbdaffbec066e31c91a24410 Mon Sep 17 00:00:00 2001 From: Tin Tran Date: Mon, 22 Sep 2025 02:18:27 +0700 Subject: [PATCH 21/30] These are not needed as I don't want to learn Docker watch --- docker-compose.yaml | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 0f24837d7ff..cb8febaf8dd 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -17,35 +17,6 @@ x-rails-env: &rails_env RAILS_ENV: development services: - base: &base - build: - context: . - dockerfile: Dockerfile - args: - RAILS_ENV: 'development' - image: maybe:development - volumes: - - app-storage:/rails/storage - environment: - <<: *rails_env - env_file: .env - restart: unless-stopped - profiles: - - never - - web: - <<: *base - image: maybe-web:development - ports: - - 3000:3000 - command: ./bin/rails server -b 0.0.0.0 - depends_on: - db: - condition: service_healthy - redis: - condition: service_healthy - networks: - - sure_net worker: <<: *base From 5c6f0defad74065f32585834ffe544e3562dd102 Mon Sep 17 00:00:00 2001 From: Tin Tran Date: Mon, 22 Sep 2025 02:19:30 +0700 Subject: [PATCH 22/30] This too --- docker-compose.yaml | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index cb8febaf8dd..647c8679c33 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -3,33 +3,7 @@ x-db-env: &db_env POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-maybe_password} POSTGRES_DB: ${POSTGRES_DB:-maybe-development} -x-rails-env: &rails_env - <<: *db_env - SECRET_KEY_BASE: ${SECRET_KEY_BASE:-a7523c3d0ae56415046ad8abae168d71074a79534a7062258f8d1d51ac2f76d3c3bc86d86b6b0b307df30d9a6a90a2066a3fa9e67c5e6f374dbd7dd4e0778e13} - SELF_HOSTED: "true" - RAILS_FORCE_SSL: "false" - RAILS_ASSUME_SSL: "false" - DB_HOST: db - DB_PORT: 5432 - REDIS_URL: redis://redis:6379/1 -# NOTE: enabling OpenAI will incur costs when you use AI-related features in the app (chat, rules). Make sure you have set appropriate spend limits on your account before adding this. - OPENAI_ACCESS_TOKEN: ${OPENAI_ACCESS_TOKEN} - RAILS_ENV: development - services: - - worker: - <<: *base - image: maybe-worker:development - command: bundle exec sidekiq - depends_on: - db: - condition: service_healthy - redis: - condition: service_healthy - networks: - - sure_net - db: image: postgres:16 restart: unless-stopped @@ -63,7 +37,6 @@ services: - sure_net volumes: - app-storage: maybe-dev-postgres-data: redis-data: From afed6742e9cfdccf00c9e6da4c7478f6f7620556 Mon Sep 17 00:00:00 2001 From: Tin Tran Date: Mon, 22 Sep 2025 21:36:15 +0700 Subject: [PATCH 23/30] Add rails trio --- Gemfile | 10 ++++++++++ Gemfile.lock | 39 +++++++++++++++++++++++++++++++++++++++ Procfile.dev | 1 - 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index d1e3a55ed72..b368930ad4c 100644 --- a/Gemfile +++ b/Gemfile @@ -121,3 +121,13 @@ end gem "rails_db", "~> 2.5" gem "seed_dump", "~> 3.3" + +gem "solid_cache", "~> 1.0" + +gem "sqlite3", "~> 2.7" + +gem "solid_queue", "~> 1.2" + +gem "solid_cable", "~> 3.0" + +gem "mission_control-jobs", "~> 1.1" diff --git a/Gemfile.lock b/Gemfile.lock index 8fc14260b3d..6a484943508 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -348,6 +348,16 @@ GEM logger mini_mime (1.1.5) minitest (5.25.5) + mission_control-jobs (1.1.0) + actioncable (>= 7.1) + actionpack (>= 7.1) + activejob (>= 7.1) + activerecord (>= 7.1) + importmap-rails (>= 1.2.1) + irb (~> 1.13) + railties (>= 7.1) + stimulus-rails + turbo-rails mocha (2.7.1) ruby2_keywords (>= 0.0.5) msgpack (1.8.0) @@ -591,7 +601,31 @@ GEM skylight (6.0.4) activesupport (>= 5.2.0) smart_properties (1.17.0) + solid_cable (3.0.12) + actioncable (>= 7.2) + activejob (>= 7.2) + activerecord (>= 7.2) + railties (>= 7.2) + solid_cache (1.0.7) + activejob (>= 7.2) + activerecord (>= 7.2) + railties (>= 7.2) + solid_queue (1.2.1) + activejob (>= 7.1) + activerecord (>= 7.1) + concurrent-ruby (>= 1.3.1) + fugit (~> 1.11.0) + railties (>= 7.1) + thor (>= 1.3.1) sorbet-runtime (0.5.12163) + sqlite3 (2.7.4-aarch64-linux-gnu) + sqlite3 (2.7.4-aarch64-linux-musl) + sqlite3 (2.7.4-arm-linux-gnu) + sqlite3 (2.7.4-arm-linux-musl) + sqlite3 (2.7.4-arm64-darwin) + sqlite3 (2.7.4-x86_64-darwin) + sqlite3 (2.7.4-x86_64-linux-gnu) + sqlite3 (2.7.4-x86_64-linux-musl) stackprof (0.2.27) stimulus-rails (1.3.4) railties (>= 6.0.0) @@ -696,6 +730,7 @@ DEPENDENCIES logtail-rails lookbook (= 2.3.11) lucide-rails! + mission_control-jobs (~> 1.1) mocha octokit ostruct @@ -726,6 +761,10 @@ DEPENDENCIES sidekiq-cron simplecov skylight + solid_cable (~> 3.0) + solid_cache (~> 1.0) + solid_queue (~> 1.2) + sqlite3 (~> 2.7) stackprof stimulus-rails stripe diff --git a/Procfile.dev b/Procfile.dev index eb6eadebd30..98923918ae4 100644 --- a/Procfile.dev +++ b/Procfile.dev @@ -1,3 +1,2 @@ web: bundle exec ${DEBUG:+rdbg -O -n -c --} bin/rails server -b 0.0.0.0 css: bundle exec bin/rails tailwindcss:watch 2>/dev/null -worker: bundle exec sidekiq From 665bcfac2575b79d2fc094410e7b12c3f7350976 Mon Sep 17 00:00:00 2001 From: Tin Tran Date: Mon, 22 Sep 2025 21:39:22 +0700 Subject: [PATCH 24/30] Configure action cable and active jobs to use the Trio --- app/controllers/concerns/onboardable.rb | 1 + app/controllers/concerns/self_hostable.rb | 1 - bin/jobs | 6 + config/application.rb | 2 + config/cable.yml | 19 ++- config/cache.yml | 17 +++ config/database.yml | 34 +++++- config/environments/development.rb | 3 + config/environments/production.rb | 7 +- config/puma.rb | 2 + config/queue.yml | 18 +++ config/recurring.yml | 34 ++++++ config/routes.rb | 1 + db/cable_schema.rb | 23 ++++ db/cache_schema.rb | 24 ++++ db/queue_schema.rb | 141 ++++++++++++++++++++++ db/schema.rb | 5 + 17 files changed, 325 insertions(+), 13 deletions(-) create mode 100755 bin/jobs create mode 100644 config/cache.yml create mode 100644 config/queue.yml create mode 100644 config/recurring.yml create mode 100644 db/cable_schema.rb create mode 100644 db/cache_schema.rb create mode 100644 db/queue_schema.rb diff --git a/app/controllers/concerns/onboardable.rb b/app/controllers/concerns/onboardable.rb index 60655094015..f2e69caf4c7 100644 --- a/app/controllers/concerns/onboardable.rb +++ b/app/controllers/concerns/onboardable.rb @@ -25,6 +25,7 @@ def redirectable_path?(path) return false if path.starts_with?("/subscription") return false if path.starts_with?("/onboarding") return false if path.starts_with?("/users") + return false if path.starts_with?("/jobs") return false if path.starts_with?("/api") # Exclude API endpoints from onboarding redirects [ diff --git a/app/controllers/concerns/self_hostable.rb b/app/controllers/concerns/self_hostable.rb index 4208e03ac54..1fa1b22b025 100644 --- a/app/controllers/concerns/self_hostable.rb +++ b/app/controllers/concerns/self_hostable.rb @@ -35,7 +35,6 @@ def verify_self_host_config end def redis_connected? - Redis.new.ping true rescue Redis::CannotConnectError false diff --git a/bin/jobs b/bin/jobs new file mode 100755 index 00000000000..dcf59f309ae --- /dev/null +++ b/bin/jobs @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby + +require_relative "../config/environment" +require "solid_queue/cli" + +SolidQueue::Cli.start(ARGV) diff --git a/config/application.rb b/config/application.rb index 134feb5a5e8..b56fe74fac9 100644 --- a/config/application.rb +++ b/config/application.rb @@ -41,5 +41,7 @@ class Application < Rails::Application # Enable Rack::Attack middleware for API rate limiting config.middleware.use Rack::Attack + MissionControl::Jobs.http_basic_auth_user = "mission_control_jobs" + MissionControl::Jobs.http_basic_auth_password = ENV["MISSION_CONTROL_JOBS_PASSWORD"] || "mission_control_jobs" end end diff --git a/config/cable.yml b/config/cable.yml index 3474a21af09..7ca155ef873 100644 --- a/config/cable.yml +++ b/config/cable.yml @@ -1,10 +1,21 @@ +# Async adapter only works within the same process, so for manually triggering cable updates from a console, +# and seeing results in the browser, you must do so from the web console (running inside the dev process), +# not a terminal started via bin/rails console! Add "console" to any action or any ERB template view +# to make the web console appear. + +default: &default + adapter: solid_cable + connects_to: + database: + writing: cable + polling_interval: 0.1.seconds + message_retention: 1.day + development: - adapter: async + <<: *default test: adapter: test production: - adapter: redis - url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> - channel_prefix: maybe_production + <<: *default diff --git a/config/cache.yml b/config/cache.yml new file mode 100644 index 00000000000..7bfedb386fb --- /dev/null +++ b/config/cache.yml @@ -0,0 +1,17 @@ +default: &default + store_options: + # Cap age of oldest cache entry to fulfill retention policies + max_age: <%= 60.days.to_i %> + max_size: <%= 1.gigabytes %> + namespace: <%= Rails.env %> + +development: &development + <<: *default + database: cache + +test: + <<: *default + +production: &production + <<: *default + databases: [production_cache1, production_cache2] \ No newline at end of file diff --git a/config/database.yml b/config/database.yml index d3a412bebe0..9d8081311e2 100644 --- a/config/database.yml +++ b/config/database.yml @@ -8,13 +8,39 @@ default: &default password: <%= ENV.fetch("POSTGRES_PASSWORD") { nil } %> development: - <<: *default - database: <%= ENV.fetch("POSTGRES_DB") { "maybe_development" } %> + primary: + <<: *default + database: <%= ENV.fetch("POSTGRES_DB") { "maybe_development" } %> + cache: + adapter: sqlite3 + database: storage/development_cache.sqlite3 + migrations_paths: db/cache_migrate + queue: + adapter: sqlite3 + database: storage/development_queue.sqlite3 + migrations_paths: db/queue_migrate + cable: + adapter: sqlite3 + database: storage/development_cable.sqlite3 + migrations_paths: db/cable_migrate test: <<: *default database: <%= ENV.fetch("POSTGRES_DB") { "maybe_test" } %> production: - <<: *default - database: <%= ENV.fetch("POSTGRES_DB") { "maybe_production" } %> + primary: + <<: *default + database: <%= ENV.fetch("POSTGRES_DB") { "maybe_production" } %> + cache: + adapter: sqlite3 + database: storage/production_cache.sqlite3 + migrations_paths: db/cache_migrate + queue: + adapter: sqlite3 + database: storage/production_queue.sqlite3 + migrations_paths: db/queue_migrate + cable: + adapter: sqlite3 + database: storage/production_cable.sqlite3 + migrations_paths: db/cable_migrate diff --git a/config/environments/development.rb b/config/environments/development.rb index 553da47e3e8..f8c5242ca61 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -67,6 +67,9 @@ config.assets.quiet = true config.active_record.verbose_query_logs = true config.active_job.verbose_enqueue_logs = true + config.active_job.queue_adapter = :solid_queue + config.solid_queue.connects_to = { database: { writing: :queue } } + config.solid_queue.logger = ActiveSupport::Logger.new(STDOUT) # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true diff --git a/config/environments/production.rb b/config/environments/production.rb index 671b5239fe0..9b1bfe0ce1a 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -69,9 +69,7 @@ # want to log everything, set the level to "debug". config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") - if ENV["CACHE_REDIS_URL"].present? - config.cache_store = :redis_cache_store, { url: ENV["CACHE_REDIS_URL"] } - end + config.cache_store = :solid_cache_store config.action_mailer.perform_caching = false config.action_mailer.deliver_later_queue_name = :high_priority @@ -108,5 +106,6 @@ # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } # set REDIS_URL for Sidekiq to use Redis - config.active_job.queue_adapter = :sidekiq + config.active_job.queue_adapter = :solid_queue + config.solid_queue.connects_to = { database: { writing: :queue } } end diff --git a/config/puma.rb b/config/puma.rb index 47a2362e275..160bbef9187 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -47,6 +47,8 @@ # Allow puma to be restarted by `bin/rails restart` command. plugin :tmp_restart +plugin :solid_queue + pidfile ENV["PIDFILE"] if ENV["PIDFILE"] if rails_env == "development" diff --git a/config/queue.yml b/config/queue.yml new file mode 100644 index 00000000000..9eace59c41e --- /dev/null +++ b/config/queue.yml @@ -0,0 +1,18 @@ +default: &default + dispatchers: + - polling_interval: 1 + batch_size: 500 + workers: + - queues: "*" + threads: 3 + processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %> + polling_interval: 0.1 + +development: + <<: *default + +test: + <<: *default + +production: + <<: *default diff --git a/config/recurring.yml b/config/recurring.yml new file mode 100644 index 00000000000..1b5043f2ea5 --- /dev/null +++ b/config/recurring.yml @@ -0,0 +1,34 @@ +# examples: +# periodic_cleanup: +# class: CleanSoftDeletedRecordsJob +# queue: background +# args: [ 1000, { batch_size: 500 } ] +# schedule: every hour +# periodic_cleanup_with_command: +# command: "SoftDeletedRecord.due.delete_all" +# priority: 2 +# schedule: at 5am every day + +development: + clear_solid_queue_finished_jobs: + command: "SolidQueue::Job.clear_finished_in_batches(sleep_between_batches: 0.3)" + schedule: every hour at minute 12 + + import_market_data: + class: "ImportMarketDataJob" + queue: "scheduled" + args: { mode: "full", clear_cache: false } + schedule: "0 22 * * 1-5" # 5:00 PM EST / 6:00 PM EDT (NY time) Monday through Friday + description: "Imports market data daily at 5:00 PM EST (1 hour after market close)" + + clean_syncs: + schedule: "0 * * * *" # every hour + class: "SyncCleanerJob" + queue: "scheduled" + description: "Cleans up stale syncs" + + run_security_health_checks: + schedule: "0 2 * * 1-5" # 2:00 AM EST / 3:00 AM EDT (NY time) Monday through Friday + class: "SecurityHealthCheckJob" + queue: "scheduled" + description: "Runs security health checks to detect issues with security data" diff --git a/config/routes.rb b/config/routes.rb index 9e4e80548ff..bcdcf25ca82 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -14,6 +14,7 @@ # Uses basic auth - see config/initializers/sidekiq.rb mount Sidekiq::Web => "/sidekiq" + mount MissionControl::Jobs::Engine, at: "/jobs" # AI chats resources :chats do diff --git a/db/cable_schema.rb b/db/cable_schema.rb new file mode 100644 index 00000000000..de80d1068a8 --- /dev/null +++ b/db/cable_schema.rb @@ -0,0 +1,23 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[7.2].define(version: 1) do + create_table "solid_cable_messages", force: :cascade do |t| + t.binary "channel", limit: 1024, null: false + t.binary "payload", limit: 536870912, null: false + t.datetime "created_at", null: false + t.integer "channel_hash", limit: 8, null: false + t.index ["channel"], name: "index_solid_cable_messages_on_channel" + t.index ["channel_hash"], name: "index_solid_cable_messages_on_channel_hash" + t.index ["created_at"], name: "index_solid_cable_messages_on_created_at" + end +end diff --git a/db/cache_schema.rb b/db/cache_schema.rb new file mode 100644 index 00000000000..fec220f6d64 --- /dev/null +++ b/db/cache_schema.rb @@ -0,0 +1,24 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[7.2].define(version: 1) do + create_table "solid_cache_entries", force: :cascade do |t| + t.binary "key", limit: 1024, null: false + t.binary "value", limit: 536870912, null: false + t.datetime "created_at", null: false + t.integer "key_hash", limit: 8, null: false + t.integer "byte_size", limit: 4, null: false + t.index ["byte_size"], name: "index_solid_cache_entries_on_byte_size" + t.index ["key_hash", "byte_size"], name: "index_solid_cache_entries_on_key_hash_and_byte_size" + t.index ["key_hash"], name: "index_solid_cache_entries_on_key_hash", unique: true + end +end diff --git a/db/queue_schema.rb b/db/queue_schema.rb new file mode 100644 index 00000000000..df3358c2874 --- /dev/null +++ b/db/queue_schema.rb @@ -0,0 +1,141 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[7.2].define(version: 1) do + create_table "solid_queue_blocked_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "queue_name", null: false + t.integer "priority", default: 0, null: false + t.string "concurrency_key", null: false + t.datetime "expires_at", null: false + t.datetime "created_at", null: false + t.index ["concurrency_key", "priority", "job_id"], name: "index_solid_queue_blocked_executions_for_release" + t.index ["expires_at", "concurrency_key"], name: "index_solid_queue_blocked_executions_for_maintenance" + t.index ["job_id"], name: "index_solid_queue_blocked_executions_on_job_id", unique: true + end + + create_table "solid_queue_claimed_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.bigint "process_id" + t.datetime "created_at", null: false + t.index ["job_id"], name: "index_solid_queue_claimed_executions_on_job_id", unique: true + t.index ["process_id", "job_id"], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id" + end + + create_table "solid_queue_failed_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.text "error" + t.datetime "created_at", null: false + t.index ["job_id"], name: "index_solid_queue_failed_executions_on_job_id", unique: true + end + + create_table "solid_queue_jobs", force: :cascade do |t| + t.string "queue_name", null: false + t.string "class_name", null: false + t.text "arguments" + t.integer "priority", default: 0, null: false + t.string "active_job_id" + t.datetime "scheduled_at" + t.datetime "finished_at" + t.string "concurrency_key" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["active_job_id"], name: "index_solid_queue_jobs_on_active_job_id" + t.index ["class_name"], name: "index_solid_queue_jobs_on_class_name" + t.index ["finished_at"], name: "index_solid_queue_jobs_on_finished_at" + t.index ["queue_name", "finished_at"], name: "index_solid_queue_jobs_for_filtering" + t.index ["scheduled_at", "finished_at"], name: "index_solid_queue_jobs_for_alerting" + end + + create_table "solid_queue_pauses", force: :cascade do |t| + t.string "queue_name", null: false + t.datetime "created_at", null: false + t.index ["queue_name"], name: "index_solid_queue_pauses_on_queue_name", unique: true + end + + create_table "solid_queue_processes", force: :cascade do |t| + t.string "kind", null: false + t.datetime "last_heartbeat_at", null: false + t.bigint "supervisor_id" + t.integer "pid", null: false + t.string "hostname" + t.text "metadata" + t.datetime "created_at", null: false + t.string "name", null: false + t.index ["last_heartbeat_at"], name: "index_solid_queue_processes_on_last_heartbeat_at" + t.index ["name", "supervisor_id"], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true + t.index ["supervisor_id"], name: "index_solid_queue_processes_on_supervisor_id" + end + + create_table "solid_queue_ready_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "queue_name", null: false + t.integer "priority", default: 0, null: false + t.datetime "created_at", null: false + t.index ["job_id"], name: "index_solid_queue_ready_executions_on_job_id", unique: true + t.index ["priority", "job_id"], name: "index_solid_queue_poll_all" + t.index ["queue_name", "priority", "job_id"], name: "index_solid_queue_poll_by_queue" + end + + create_table "solid_queue_recurring_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "task_key", null: false + t.datetime "run_at", null: false + t.datetime "created_at", null: false + t.index ["job_id"], name: "index_solid_queue_recurring_executions_on_job_id", unique: true + t.index ["task_key", "run_at"], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true + end + + create_table "solid_queue_recurring_tasks", force: :cascade do |t| + t.string "key", null: false + t.string "schedule", null: false + t.string "command", limit: 2048 + t.string "class_name" + t.text "arguments" + t.string "queue_name" + t.integer "priority", default: 0 + t.boolean "static", default: true, null: false + t.text "description" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["key"], name: "index_solid_queue_recurring_tasks_on_key", unique: true + t.index ["static"], name: "index_solid_queue_recurring_tasks_on_static" + end + + create_table "solid_queue_scheduled_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "queue_name", null: false + t.integer "priority", default: 0, null: false + t.datetime "scheduled_at", null: false + t.datetime "created_at", null: false + t.index ["job_id"], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true + t.index ["scheduled_at", "priority", "job_id"], name: "index_solid_queue_dispatch_all" + end + + create_table "solid_queue_semaphores", force: :cascade do |t| + t.string "key", null: false + t.integer "value", default: 1, null: false + t.datetime "expires_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["expires_at"], name: "index_solid_queue_semaphores_on_expires_at" + t.index ["key", "value"], name: "index_solid_queue_semaphores_on_key_and_value" + t.index ["key"], name: "index_solid_queue_semaphores_on_key", unique: true + end + + add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_claimed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_failed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_ready_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_recurring_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_scheduled_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade +end diff --git a/db/schema.rb b/db/schema.rb index dac00a6e5bd..8858c171f0f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -241,8 +241,11 @@ t.index "lower((name)::text)", name: "index_entries_on_lower_name" t.index ["account_id", "date"], name: "index_entries_on_account_id_and_date" t.index ["account_id"], name: "index_entries_on_account_id" + t.index ["amount"], name: "index_entries_on_amount" t.index ["date"], name: "index_entries_on_date" + t.index ["entryable_id", "entryable_type"], name: "index_entries_on_entryable" t.index ["entryable_type"], name: "index_entries_on_entryable_type" + t.index ["excluded"], name: "index_entries_on_excluded" t.index ["import_id"], name: "index_entries_on_import_id" end @@ -253,6 +256,7 @@ t.date "date", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.index ["date", "from_currency", "to_currency"], name: "index_exchange_rates_on_date_and_currencies" t.index ["from_currency", "to_currency", "date"], name: "index_exchange_rates_on_base_converted_date_unique", unique: true t.index ["from_currency"], name: "index_exchange_rates_on_from_currency" t.index ["to_currency"], name: "index_exchange_rates_on_to_currency" @@ -761,6 +765,7 @@ t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["tag_id"], name: "index_taggings_on_tag_id" + t.index ["taggable_id", "taggable_type"], name: "index_taggings_on_taggable_id_and_type" t.index ["taggable_type", "taggable_id"], name: "index_taggings_on_taggable" end From 8f150b699732c671340927363bfddfd847051f4f Mon Sep 17 00:00:00 2001 From: Tin Tran Date: Mon, 22 Sep 2025 21:58:11 +0700 Subject: [PATCH 25/30] Remove sidekiq --- .cursor/rules/project-conventions.mdc | 2 +- .devcontainer/docker-compose.yml | 14 ------------- CLAUDE.md | 6 +++--- Gemfile | 3 --- Gemfile.lock | 21 ------------------- app/models/sync.rb | 2 +- .../_super_admin_bar.html.erb | 2 +- compose.example.yml | 16 -------------- config/environments/production.rb | 1 - config/initializers/sidekiq.rb | 16 -------------- config/routes.rb | 5 ----- 11 files changed, 6 insertions(+), 82 deletions(-) delete mode 100644 config/initializers/sidekiq.rb diff --git a/.cursor/rules/project-conventions.mdc b/.cursor/rules/project-conventions.mdc index 8e1f15ddea3..fd28b3d2688 100644 --- a/.cursor/rules/project-conventions.mdc +++ b/.cursor/rules/project-conventions.mdc @@ -15,7 +15,7 @@ This rule serves as high-level documentation for how you should write code in th - Lucide Icons for icons - OpenAI for AI chat - Database: PostgreSQL -- Jobs: Sidekiq + Redis +- Jobs: SolidQueue - External - Payments: Stripe - User bank data syncing: Plaid diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 2c13c513f3b..155eeda06a9 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -29,20 +29,6 @@ services: - db - redis - worker: - build: - context: .. - dockerfile: .devcontainer/Dockerfile - volumes: - - ..:/workspace:cached - - bundle_cache:/bundle - command: bundle exec sidekiq - restart: unless-stopped - environment: - <<: *rails_env - depends_on: - - redis - redis: image: redis:latest volumes: diff --git a/CLAUDE.md b/CLAUDE.md index 96ed7ade030..1b491fbd47d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,7 +5,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Common Development Commands ### Development Server -- `bin/dev` - Start development server (Rails, Sidekiq, Tailwind CSS watcher) +- `bin/dev` - Start development server (Rails, Solid queue, Tailwind CSS watcher) - `bin/rails server` - Start Rails server only - `bin/rails console` - Open Rails console @@ -96,11 +96,11 @@ Two primary data ingestion methods: - Custom field mapping with transformation rules ### Background Processing -Sidekiq handles asynchronous tasks: +SolidQueue handles asynchronous tasks: - Account syncing (`SyncAccountsJob`) - Import processing (`ImportDataJob`) - AI chat responses (`CreateChatResponseJob`) -- Scheduled maintenance via sidekiq-cron +- Scheduled maintenance via SolidQueue ### Frontend Architecture - **Hotwire Stack**: Turbo + Stimulus for reactive UI without heavy JavaScript diff --git a/Gemfile b/Gemfile index b368930ad4c..8287babc769 100644 --- a/Gemfile +++ b/Gemfile @@ -31,15 +31,12 @@ gem "lookbook", "2.3.11" gem "hotwire_combobox" # Background Jobs -gem "sidekiq" -gem "sidekiq-cron" # Monitoring gem "vernier" gem "rack-mini-profiler" gem "sentry-ruby" gem "sentry-rails" -gem "sentry-sidekiq" gem "logtail-rails" gem "skylight", groups: [ :production ] diff --git a/Gemfile.lock b/Gemfile.lock index 6a484943508..3e6e4620acf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -146,9 +146,6 @@ GEM bigdecimal rexml crass (1.0.6) - cronex (0.15.0) - tzinfo - unicode (>= 0.4.4.5) css_parser (1.21.1) addressable csv (3.3.5) @@ -575,20 +572,6 @@ GEM sentry-ruby (5.26.0) bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) - sentry-sidekiq (5.26.0) - sentry-ruby (~> 5.26.0) - sidekiq (>= 3.0) - sidekiq (8.0.5) - connection_pool (>= 2.5.0) - json (>= 2.9.0) - logger (>= 1.6.2) - rack (>= 3.1.0) - redis-client (>= 0.23.2) - sidekiq-cron (2.3.0) - cronex (>= 0.13.0) - fugit (~> 1.8, >= 1.11.1) - globalid (>= 1.0.1) - sidekiq (>= 6.5.0) simple_form (5.3.1) actionpack (>= 5.2) activemodel (>= 5.2) @@ -651,7 +634,6 @@ GEM tzinfo (2.0.6) concurrent-ruby (~> 1.0) unaccent (0.4.0) - unicode (0.4.4.5) unicode-display_width (3.1.4) unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (4.0.4) @@ -756,9 +738,6 @@ DEPENDENCIES selenium-webdriver sentry-rails sentry-ruby - sentry-sidekiq - sidekiq - sidekiq-cron simplecov skylight solid_cable (~> 3.0) diff --git a/app/models/sync.rb b/app/models/sync.rb index 775d8e79fa5..8439c97901f 100644 --- a/app/models/sync.rb +++ b/app/models/sync.rb @@ -59,7 +59,7 @@ def clean def perform Rails.logger.tagged("Sync", id, syncable_type, syncable_id) do - # This can happen on server restarts or if Sidekiq enqueues a duplicate job + # This can happen on server restarts or if ~~Sidekiq~~ SolidQueue enqueues a duplicate job unless may_start? Rails.logger.warn("Sync #{id} is not in a valid state (#{aasm.from_state}) to start. Skipping sync.") return diff --git a/app/views/impersonation_sessions/_super_admin_bar.html.erb b/app/views/impersonation_sessions/_super_admin_bar.html.erb index a09a8250fa3..a6ad6b43a5e 100644 --- a/app/views/impersonation_sessions/_super_admin_bar.html.erb +++ b/app/views/impersonation_sessions/_super_admin_bar.html.erb @@ -4,7 +4,7 @@ Super Admin
- <%= link_to "Jobs", sidekiq_web_url, class: "text-white underline hover:text-gray-100" %> + <%= link_to "Jobs", sidekiq_web_url, class: "text-white underline hover:text-gray-100" %>
diff --git a/compose.example.yml b/compose.example.yml index 749fcf2cadb..46562f1e897 100644 --- a/compose.example.yml +++ b/compose.example.yml @@ -62,22 +62,6 @@ services: networks: - sure_net - worker: - image: ghcr.io/we-promise/sure:latest - command: bundle exec sidekiq - volumes: - - app-storage:/rails/storage - restart: unless-stopped - depends_on: - db: - condition: service_healthy - redis: - condition: service_healthy - environment: - <<: *rails_env - networks: - - sure_net - db: image: postgres:16 restart: unless-stopped diff --git a/config/environments/production.rb b/config/environments/production.rb index 9b1bfe0ce1a..31f9a0a59b1 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -105,7 +105,6 @@ # Skip DNS rebinding protection for the default health check endpoint. # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } - # set REDIS_URL for Sidekiq to use Redis config.active_job.queue_adapter = :solid_queue config.solid_queue.connects_to = { database: { writing: :queue } } end diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb deleted file mode 100644 index 70a6e476004..00000000000 --- a/config/initializers/sidekiq.rb +++ /dev/null @@ -1,16 +0,0 @@ -require "sidekiq/web" - -if Rails.env.production? - Sidekiq::Web.use(Rack::Auth::Basic) do |username, password| - configured_username = ::Digest::SHA256.hexdigest(ENV.fetch("SIDEKIQ_WEB_USERNAME", "maybe")) - configured_password = ::Digest::SHA256.hexdigest(ENV.fetch("SIDEKIQ_WEB_PASSWORD", "maybe")) - - ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(username), configured_username) && - ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(password), configured_password) - end -end - -Sidekiq::Cron.configure do |config| - # 10 min "catch-up" window in case worker process is re-deploying when cron tick occurs - config.reschedule_grace_period = 600 -end diff --git a/config/routes.rb b/config/routes.rb index bcdcf25ca82..e023cc5eb23 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,3 @@ -require "sidekiq/web" -require "sidekiq/cron/web" - Rails.application.routes.draw do use_doorkeeper # MFA routes @@ -12,8 +9,6 @@ mount Lookbook::Engine, at: "/design-system" - # Uses basic auth - see config/initializers/sidekiq.rb - mount Sidekiq::Web => "/sidekiq" mount MissionControl::Jobs::Engine, at: "/jobs" # AI chats From ab01d79f4fd8755bf24f2d6b664cc41db74707ad Mon Sep 17 00:00:00 2001 From: Tin Tran Date: Tue, 23 Sep 2025 00:04:40 +0700 Subject: [PATCH 26/30] Remove Redis --- .devcontainer/docker-compose.yml | 12 -- .github/workflows/ci.yml | 7 - Gemfile | 1 - Gemfile.lock | 5 - app/controllers/api/v1/base_controller.rb | 48 ------ app/controllers/concerns/self_hostable.rb | 26 ---- app/controllers/pages_controller.rb | 6 - app/services/api_rate_limiter.rb | 95 ------------ app/services/noop_api_rate_limiter.rb | 39 ----- .../pages/redis_configuration_error.html.erb | 59 -------- compose.example.yml | 17 --- config/initializers/sentry.rb | 2 +- config/routes.rb | 2 - docker-compose.yaml | 16 -- .../api/v1/base_controller_test.rb | 108 -------------- .../api/v1/transactions_controller_test.rb | 4 - .../api/v1/usage_controller_test.rb | 9 -- test/services/api_rate_limiter_test.rb | 138 ------------------ test/services/noop_api_rate_limiter_test.rb | 58 -------- 19 files changed, 1 insertion(+), 651 deletions(-) delete mode 100644 app/services/api_rate_limiter.rb delete mode 100644 app/services/noop_api_rate_limiter.rb delete mode 100644 app/views/pages/redis_configuration_error.html.erb delete mode 100644 test/services/api_rate_limiter_test.rb delete mode 100644 test/services/noop_api_rate_limiter_test.rb diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 155eeda06a9..5abfa84a097 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -9,7 +9,6 @@ x-rails-env: &rails_env POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres BUNDLE_PATH: /bundle - REDIS_URL: redis://redis:6379/1 services: app: @@ -27,16 +26,6 @@ services: <<: *rails_env depends_on: - db - - redis - - redis: - image: redis:latest - volumes: - - redis-data:/data - ports: - - "6379:6379" - restart: unless-stopped - db: image: postgres:latest volumes: @@ -49,5 +38,4 @@ services: volumes: postgres-data: - redis-data: bundle_cache: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eae6bb5e342..2306ec69af4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,7 +80,6 @@ jobs: PLAID_CLIENT_ID: foo PLAID_SECRET: bar DATABASE_URL: postgres://postgres:postgres@localhost:5432 - REDIS_URL: redis://localhost:6379 RAILS_ENV: test services: @@ -93,12 +92,6 @@ jobs: - 5432:5432 options: --health-cmd="pg_isready" --health-interval=10s --health-timeout=5s --health-retries=3 - redis: - image: redis - ports: - - 6379:6379 - options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3 - steps: - name: Install packages run: sudo apt-get update && sudo apt-get install --no-install-recommends -y google-chrome-stable curl libvips postgresql-client libpq-dev diff --git a/Gemfile b/Gemfile index 8287babc769..03e7a34e790 100644 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,6 @@ gem "rails", "~> 7.2.2" # Drivers gem "pg", "~> 1.5" -gem "redis", "~> 5.4" # Deployment gem "puma", ">= 5.0" diff --git a/Gemfile.lock b/Gemfile.lock index 3e6e4620acf..a2aee7455e3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -493,10 +493,6 @@ GEM erb psych (>= 4.0.0) redcarpet (3.6.1) - redis (5.4.0) - redis-client (>= 0.22.0) - redis-client (0.25.0) - connection_pool regexp_parser (2.10.0) reline (0.6.1) io-console (~> 0.5) @@ -727,7 +723,6 @@ DEPENDENCIES rails-settings-cached rails_db (~> 2.5) redcarpet - redis (~> 5.4) rotp (~> 6.3) rqrcode (~> 3.0) rubocop-rails-omakase diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb index f176fff4b68..7c2bdf719c9 100644 --- a/app/controllers/api/v1/base_controller.rb +++ b/app/controllers/api/v1/base_controller.rb @@ -16,11 +16,8 @@ class Api::V1::BaseController < ApplicationController before_action :force_json_format # Use our custom authentication that supports both OAuth and API keys before_action :authenticate_request! - before_action :check_api_key_rate_limit before_action :log_api_access - - # Override Doorkeeper's default behavior to return JSON instead of redirecting def doorkeeper_unauthorized_render_options(error: nil) { json: { error: "unauthorized", message: "Access token is invalid, expired, or missing" } } @@ -98,55 +95,10 @@ def authenticate_api_key @current_user = @api_key.user @api_key.update_last_used! @authentication_method = :api_key - @rate_limiter = ApiRateLimiter.limit(@api_key) setup_current_context_for_api true end - # Check rate limits for API key authentication - def check_api_key_rate_limit - return unless @authentication_method == :api_key && @rate_limiter - - if @rate_limiter.rate_limit_exceeded? - usage_info = @rate_limiter.usage_info - render_rate_limit_exceeded(usage_info) - return false - end - - # Increment request count for successful API key requests - @rate_limiter.increment_request_count! - - # Add rate limit headers to response - add_rate_limit_headers(@rate_limiter.usage_info) - end - - # Render rate limit exceeded response - def render_rate_limit_exceeded(usage_info) - response.headers["X-RateLimit-Limit"] = usage_info[:rate_limit].to_s - response.headers["X-RateLimit-Remaining"] = "0" - response.headers["X-RateLimit-Reset"] = usage_info[:reset_time].to_s - response.headers["Retry-After"] = usage_info[:reset_time].to_s - - Rails.logger.warn "API Rate Limit Exceeded: API Key #{@api_key.name} (User: #{@current_user.email}) - #{usage_info[:current_count]}/#{usage_info[:rate_limit]} requests" - - render_json({ - error: "rate_limit_exceeded", - message: "Rate limit exceeded. Try again in #{usage_info[:reset_time]} seconds.", - details: { - limit: usage_info[:rate_limit], - current: usage_info[:current_count], - reset_in_seconds: usage_info[:reset_time] - } - }, status: :too_many_requests) - end - - # Add rate limit headers to successful responses - def add_rate_limit_headers(usage_info) - response.headers["X-RateLimit-Limit"] = usage_info[:rate_limit].to_s - response.headers["X-RateLimit-Remaining"] = usage_info[:remaining].to_s - response.headers["X-RateLimit-Reset"] = usage_info[:reset_time].to_s - end - # Render unauthorized response def render_unauthorized render_json({ error: "unauthorized", message: "Access token or API key is invalid, expired, or missing" }, status: :unauthorized) diff --git a/app/controllers/concerns/self_hostable.rb b/app/controllers/concerns/self_hostable.rb index 1fa1b22b025..a863f17026b 100644 --- a/app/controllers/concerns/self_hostable.rb +++ b/app/controllers/concerns/self_hostable.rb @@ -3,8 +3,6 @@ module SelfHostable included do helper_method :self_hosted?, :self_hosted_first_login? - - prepend_before_action :verify_self_host_config end private @@ -15,28 +13,4 @@ def self_hosted? def self_hosted_first_login? self_hosted? && User.count.zero? end - - def verify_self_host_config - return unless self_hosted? - - # Special handling for Redis configuration error page - if controller_name == "pages" && action_name == "redis_configuration_error" - # If Redis is now working, redirect to home - if redis_connected? - redirect_to root_path, notice: "Redis is now configured properly! You can now setup your Maybe application." - end - - return - end - - unless redis_connected? - redirect_to redis_configuration_error_path - end - end - - def redis_connected? - true - rescue Redis::CannotConnectError - false - end end diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index 52e180425ba..c76c8f2a714 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -1,8 +1,6 @@ class PagesController < ApplicationController include Periodable - skip_authentication only: :redis_configuration_error - def dashboard @balance_sheet = Current.family.balance_sheet @accounts = Current.family.accounts.visible.with_attached_logo @@ -48,10 +46,6 @@ def feedback render layout: "settings" end - def redis_configuration_error - render layout: "blank" - end - private def github_provider Provider::Registry.get_provider(:github) diff --git a/app/services/api_rate_limiter.rb b/app/services/api_rate_limiter.rb deleted file mode 100644 index d3a771cf5c9..00000000000 --- a/app/services/api_rate_limiter.rb +++ /dev/null @@ -1,95 +0,0 @@ -class ApiRateLimiter - # Rate limit tiers (requests per hour) - RATE_LIMITS = { - standard: 100, - premium: 1000, - enterprise: 10000 - }.freeze - - DEFAULT_TIER = :standard - - def initialize(api_key) - @api_key = api_key - @redis = Redis.new - end - - # Check if the API key has exceeded its rate limit - def rate_limit_exceeded? - current_count >= rate_limit - end - - # Increment the request count for this API key - def increment_request_count! - key = redis_key - current_time = Time.current.to_i - window_start = (current_time / 3600) * 3600 # Hourly window - - @redis.multi do |transaction| - # Use a sliding window with hourly buckets - transaction.hincrby(key, window_start.to_s, 1) - transaction.expire(key, 7200) # Keep data for 2 hours to handle sliding window - end - end - - # Get current request count within the current hour - def current_count - key = redis_key - current_time = Time.current.to_i - window_start = (current_time / 3600) * 3600 - - count = @redis.hget(key, window_start.to_s) - count.to_i - end - - # Get the rate limit for this API key's tier - def rate_limit - tier = determine_tier - RATE_LIMITS[tier] - end - - # Calculate seconds until the rate limit resets - def reset_time - current_time = Time.current.to_i - next_window = ((current_time / 3600) + 1) * 3600 - next_window - current_time - end - - # Get detailed usage information - def usage_info - { - current_count: current_count, - rate_limit: rate_limit, - remaining: [ rate_limit - current_count, 0 ].max, - reset_time: reset_time, - tier: determine_tier - } - end - - # Class method to get usage for an API key without incrementing - def self.usage_for(api_key) - limit(api_key).usage_info - end - - def self.limit(api_key) - if Rails.application.config.app_mode.self_hosted? - # Use NoopApiRateLimiter for self-hosted mode - # This means no rate limiting is applied - NoopApiRateLimiter.new(api_key) - else - new(api_key) - end - end - - private - - def redis_key - "api_rate_limit:#{@api_key.id}" - end - - def determine_tier - # For now, all API keys are standard tier - # This can be extended later to support different tiers based on user subscription - # or API key configuration - DEFAULT_TIER - end -end diff --git a/app/services/noop_api_rate_limiter.rb b/app/services/noop_api_rate_limiter.rb deleted file mode 100644 index 116b6537ed8..00000000000 --- a/app/services/noop_api_rate_limiter.rb +++ /dev/null @@ -1,39 +0,0 @@ -class NoopApiRateLimiter - def initialize(api_key) - @api_key = api_key - end - - def rate_limit_exceeded? - false - end - - def increment_request_count! - # No operation - end - - def current_count - 0 - end - - def rate_limit - Float::INFINITY - end - - def reset_time - 0 - end - - def usage_info - { - current_count: 0, - rate_limit: Float::INFINITY, - remaining: Float::INFINITY, - reset_time: 0, - tier: :noop - } - end - - def self.usage_for(api_key) - new(api_key).usage_info - end -end diff --git a/app/views/pages/redis_configuration_error.html.erb b/app/views/pages/redis_configuration_error.html.erb deleted file mode 100644 index bf165ccb13d..00000000000 --- a/app/views/pages/redis_configuration_error.html.erb +++ /dev/null @@ -1,59 +0,0 @@ -<% content_for :title, "Redis Configuration Required - Maybe" %> - -
-
-
- -
-
- <%= icon "alert-triangle", class: "w-8 h-8 text-red-600" %> -
-

Redis Configuration Required

-

Your self-hosted Maybe installation needs Redis to be properly configured.

-
- - -
-
-
- <%= icon "info", class: "w-5 h-5 text-amber-600 mt-0.5 mr-3 flex-shrink-0" %> -
-

Why is Redis required?

-

Maybe uses Redis to power Sidekiq background jobs for tasks like syncing account data, processing imports, and other background operations that keep your financial data up to date.

-
-
-
- - -
- <%= render DS::Link.new( - text: "View Setup Guide", - href: "https://github.com/maybe-finance/maybe/blob/main/docs/hosting/docker.md", - variant: "primary", - size: "lg", - icon: "external-link", - full_width: true, - target: "_blank", - rel: "noopener noreferrer" - ) %> -

Follow our complete Docker setup guide to configure Redis

-
-
- - -
-
-

Once you've configured Redis, refresh this page to continue.

- <%= render DS::Button.new( - text: "Refresh Page", - variant: "secondary", - icon: "refresh-cw", - type: "button", - full_width: true, - onclick: "window.location.reload()" - ) %> -
-
-
-
-
diff --git a/compose.example.yml b/compose.example.yml index 46562f1e897..957a6c3636c 100644 --- a/compose.example.yml +++ b/compose.example.yml @@ -40,7 +40,6 @@ x-rails-env: &rails_env RAILS_ASSUME_SSL: "false" DB_HOST: db DB_PORT: 5432 - REDIS_URL: redis://redis:6379/1 # NOTE: enabling OpenAI will incur costs when you use AI-related features in the app (chat, rules). Make sure you have set appropriate spend limits on your account before adding this. OPENAI_ACCESS_TOKEN: ${OPENAI_ACCESS_TOKEN} @@ -57,8 +56,6 @@ services: depends_on: db: condition: service_healthy - redis: - condition: service_healthy networks: - sure_net @@ -77,23 +74,9 @@ services: networks: - sure_net - redis: - image: redis:latest - restart: unless-stopped - volumes: - - redis-data:/data - healthcheck: - test: [ "CMD", "redis-cli", "ping" ] - interval: 5s - timeout: 5s - retries: 5 - networks: - - sure_net - volumes: app-storage: postgres-data: - redis-data: networks: sure_net: diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb index 9c1e8a72e8e..1a6e9143f78 100644 --- a/config/initializers/sentry.rb +++ b/config/initializers/sentry.rb @@ -17,6 +17,6 @@ config.profiler_class = Sentry::Vernier::Profiler config.enable_logs = true - config.enabled_patches = [ :logger, :http, :redis, :puma ] + config.enabled_patches = [ :logger, :http, :puma ] end end diff --git a/config/routes.rb b/config/routes.rb index e023cc5eb23..8773dfe995d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -264,8 +264,6 @@ post "stripe" end - get "redis-configuration-error", to: "pages#redis_configuration_error" - # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. # Can be used by load balancers and uptime monitors to verify that the app is live. get "up" => "rails/health#show", as: :rails_health_check diff --git a/docker-compose.yaml b/docker-compose.yaml index 647c8679c33..af2d736dd2e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -21,24 +21,8 @@ services: networks: - sure_net - redis: - image: redis:latest - restart: unless-stopped - ports: - - 6379:6379 - volumes: - - redis-data:/data - healthcheck: - test: [ "CMD", "redis-cli", "ping" ] - interval: 5s - timeout: 5s - retries: 5 - networks: - - sure_net - volumes: maybe-dev-postgres-data: - redis-data: networks: sure_net: diff --git a/test/controllers/api/v1/base_controller_test.rb b/test/controllers/api/v1/base_controller_test.rb index af9e7066dba..036b356c7cf 100644 --- a/test/controllers/api/v1/base_controller_test.rb +++ b/test/controllers/api/v1/base_controller_test.rb @@ -22,14 +22,6 @@ class Api::V1::BaseControllerTest < ActionDispatch::IntegrationTest display_key: @plain_api_key, scopes: [ "read_write" ] ) - - # Clear any existing rate limit data - Redis.new.del("api_rate_limit:#{@api_key.id}") - end - - teardown do - # Clean up Redis data after each test - Redis.new.del("api_rate_limit:#{@api_key.id}") end test "should require authentication" do @@ -325,106 +317,6 @@ class Api::V1::BaseControllerTest < ActionDispatch::IntegrationTest assert_equal "forbidden", response_body["error"] end - test "should include rate limit headers on successful API key requests" do - get "/api/v1/test", headers: { "X-Api-Key" => @plain_api_key } - - assert_response :success - assert_not_nil response.headers["X-RateLimit-Limit"] - assert_not_nil response.headers["X-RateLimit-Remaining"] - assert_not_nil response.headers["X-RateLimit-Reset"] - - assert_equal "100", response.headers["X-RateLimit-Limit"] - assert_equal "99", response.headers["X-RateLimit-Remaining"] - end - - test "should increment rate limit count with each request" do - # First request - get "/api/v1/test", headers: { "X-Api-Key" => @plain_api_key } - assert_response :success - assert_equal "99", response.headers["X-RateLimit-Remaining"] - - # Second request - get "/api/v1/test", headers: { "X-Api-Key" => @plain_api_key } - assert_response :success - assert_equal "98", response.headers["X-RateLimit-Remaining"] - end - - test "should return 429 when rate limit exceeded" do - # Make 100 requests to exhaust the rate limit - 100.times do - get "/api/v1/test", headers: { "X-Api-Key" => @plain_api_key } - assert_response :success - end - - # 101st request should be rate limited - get "/api/v1/test", headers: { "X-Api-Key" => @plain_api_key } - assert_response :too_many_requests - - response_body = JSON.parse(response.body) - assert_equal "rate_limit_exceeded", response_body["error"] - assert_includes response_body["message"], "Rate limit exceeded" - - # Check response headers - assert_equal "100", response.headers["X-RateLimit-Limit"] - assert_equal "0", response.headers["X-RateLimit-Remaining"] - assert_not_nil response.headers["X-RateLimit-Reset"] - assert_not_nil response.headers["Retry-After"] - end - - test "should not apply rate limiting to OAuth requests" do - # This would need to be implemented based on your OAuth setup - # For now, just verify that requests without API keys don't trigger rate limiting - get "/api/v1/test" - assert_response :unauthorized - - # Should not have rate limit headers for unauthorized requests - assert_nil response.headers["X-RateLimit-Limit"] - end - - test "should provide detailed rate limit information in 429 response" do - # Exhaust the rate limit - 100.times do - get "/api/v1/test", headers: { "X-Api-Key" => @plain_api_key } - end - - # Make the rate-limited request - get "/api/v1/test", headers: { "X-Api-Key" => @plain_api_key } - assert_response :too_many_requests - - response_body = JSON.parse(response.body) - assert_equal "rate_limit_exceeded", response_body["error"] - assert response_body["details"]["limit"] == 100 - assert response_body["details"]["current"] >= 100 - assert response_body["details"]["reset_in_seconds"] > 0 - end - - test "rate limiting should be per API key" do - # Create a second user for independent API keys - other_user = users(:family_member) - other_api_key = ApiKey.create!( - user: other_user, - name: "Other Test API Key", - scopes: [ "read" ], - display_key: "other_rate_test_#{SecureRandom.hex(8)}" - ) - - begin - # Make 50 requests with first API key - 50.times do - get "/api/v1/test", headers: { "X-Api-Key" => @plain_api_key } - assert_response :success - end - - # Should still be able to make requests with second API key - get "/api/v1/test", headers: { "X-Api-Key" => other_api_key.display_key } - assert_response :success - assert_equal "99", response.headers["X-RateLimit-Remaining"] - ensure - Redis.new.del("api_rate_limit:#{other_api_key.id}") - other_api_key.destroy - end - end - private def capture_log(&block) diff --git a/test/controllers/api/v1/transactions_controller_test.rb b/test/controllers/api/v1/transactions_controller_test.rb index b028562a632..dbd355e2d11 100644 --- a/test/controllers/api/v1/transactions_controller_test.rb +++ b/test/controllers/api/v1/transactions_controller_test.rb @@ -27,10 +27,6 @@ class Api::V1::TransactionsControllerTest < ActionDispatch::IntegrationTest display_key: "test_ro_#{SecureRandom.hex(8)}", source: "mobile" # Use different source to allow multiple keys ) - - # Clear any existing rate limit data - Redis.new.del("api_rate_limit:#{@api_key.id}") - Redis.new.del("api_rate_limit:#{@read_only_api_key.id}") end # INDEX action tests diff --git a/test/controllers/api/v1/usage_controller_test.rb b/test/controllers/api/v1/usage_controller_test.rb index 27235826139..02f38955367 100644 --- a/test/controllers/api/v1/usage_controller_test.rb +++ b/test/controllers/api/v1/usage_controller_test.rb @@ -12,14 +12,6 @@ class Api::V1::UsageControllerTest < ActionDispatch::IntegrationTest scopes: [ "read" ], display_key: "usage_test_#{SecureRandom.hex(8)}" ) - - # Clear any existing rate limit data - Redis.new.del("api_rate_limit:#{@api_key.id}") - end - - teardown do - # Clean up Redis data after each test - Redis.new.del("api_rate_limit:#{@api_key.id}") end test "should return usage information for API key authentication" do @@ -68,7 +60,6 @@ class Api::V1::UsageControllerTest < ActionDispatch::IntegrationTest response_body = JSON.parse(response.body) assert_equal "insufficient_scope", response_body["error"] ensure - Redis.new.del("api_rate_limit:#{api_key_no_read.id}") api_key_no_read.destroy end end diff --git a/test/services/api_rate_limiter_test.rb b/test/services/api_rate_limiter_test.rb deleted file mode 100644 index 8afc6bb9a8e..00000000000 --- a/test/services/api_rate_limiter_test.rb +++ /dev/null @@ -1,138 +0,0 @@ -require "test_helper" - -class ApiRateLimiterTest < ActiveSupport::TestCase - setup do - @user = users(:family_admin) - # Destroy any existing active API keys for this user - @user.api_keys.active.destroy_all - - @api_key = ApiKey.create!( - user: @user, - name: "Rate Limiter Test Key", - scopes: [ "read" ], - display_key: "rate_limiter_test_#{SecureRandom.hex(8)}" - ) - @rate_limiter = ApiRateLimiter.new(@api_key) - - # Clear any existing rate limit data - Redis.new.del("api_rate_limit:#{@api_key.id}") - end - - teardown do - # Clean up Redis data after each test - Redis.new.del("api_rate_limit:#{@api_key.id}") - end - - test "should have default rate limit" do - assert_equal 100, @rate_limiter.rate_limit - end - - test "should start with zero request count" do - assert_equal 0, @rate_limiter.current_count - end - - test "should not be rate limited initially" do - assert_not @rate_limiter.rate_limit_exceeded? - end - - test "should increment request count" do - assert_equal 0, @rate_limiter.current_count - - @rate_limiter.increment_request_count! - assert_equal 1, @rate_limiter.current_count - - @rate_limiter.increment_request_count! - assert_equal 2, @rate_limiter.current_count - end - - test "should be rate limited when exceeding limit" do - # Simulate reaching the rate limit - 100.times { @rate_limiter.increment_request_count! } - - assert_equal 100, @rate_limiter.current_count - assert @rate_limiter.rate_limit_exceeded? - end - - test "should provide correct usage info" do - 5.times { @rate_limiter.increment_request_count! } - - usage_info = @rate_limiter.usage_info - - assert_equal 5, usage_info[:current_count] - assert_equal 100, usage_info[:rate_limit] - assert_equal 95, usage_info[:remaining] - assert_equal :standard, usage_info[:tier] - assert usage_info[:reset_time] > 0 - assert usage_info[:reset_time] <= 3600 - end - - test "should calculate remaining requests correctly" do - 10.times { @rate_limiter.increment_request_count! } - - usage_info = @rate_limiter.usage_info - assert_equal 90, usage_info[:remaining] - end - - test "should have zero remaining when at limit" do - 100.times { @rate_limiter.increment_request_count! } - - usage_info = @rate_limiter.usage_info - assert_equal 0, usage_info[:remaining] - end - - test "should have zero remaining when over limit" do - 105.times { @rate_limiter.increment_request_count! } - - usage_info = @rate_limiter.usage_info - assert_equal 0, usage_info[:remaining] - end - - test "class method usage_for should work without incrementing" do - 5.times { @rate_limiter.increment_request_count! } - - usage_info = ApiRateLimiter.usage_for(@api_key) - assert_equal 5, usage_info[:current_count] - - # Should not increment when just checking usage - usage_info_again = ApiRateLimiter.usage_for(@api_key) - assert_equal 5, usage_info_again[:current_count] - end - - test "should handle multiple API keys separately" do - # Create a different user for the second API key - other_user = users(:family_member) - other_api_key = ApiKey.create!( - user: other_user, - name: "Other API Key", - scopes: [ "read_write" ], - display_key: "rate_limiter_other_#{SecureRandom.hex(8)}" - ) - - other_rate_limiter = ApiRateLimiter.new(other_api_key) - - @rate_limiter.increment_request_count! - other_rate_limiter.increment_request_count! - other_rate_limiter.increment_request_count! - - assert_equal 1, @rate_limiter.current_count - assert_equal 2, other_rate_limiter.current_count - ensure - Redis.new.del("api_rate_limit:#{other_api_key.id}") - other_api_key.destroy - end - - test "should calculate reset time correctly" do - reset_time = @rate_limiter.reset_time - - # Reset time should be within the current hour - assert reset_time > 0 - assert reset_time <= 3600 - - # Should be roughly the time until the next hour - current_time = Time.current.to_i - next_window = ((current_time / 3600) + 1) * 3600 - expected_reset = next_window - current_time - - assert_in_delta expected_reset, reset_time, 1 - end -end diff --git a/test/services/noop_api_rate_limiter_test.rb b/test/services/noop_api_rate_limiter_test.rb deleted file mode 100644 index 9c7105b12c8..00000000000 --- a/test/services/noop_api_rate_limiter_test.rb +++ /dev/null @@ -1,58 +0,0 @@ -require "test_helper" - -class NoopApiRateLimiterTest < ActiveSupport::TestCase - setup do - @user = users(:family_admin) - # Clean up any existing API keys for this user to ensure tests start fresh - @user.api_keys.destroy_all - - @api_key = ApiKey.create!( - user: @user, - name: "Noop Rate Limiter Test Key", - scopes: [ "read" ], - display_key: "noop_rate_limiter_test_#{SecureRandom.hex(8)}" - ) - @rate_limiter = NoopApiRateLimiter.new(@api_key) - end - - test "should never be rate limited" do - assert_not @rate_limiter.rate_limit_exceeded? - end - - test "should not increment request count" do - @rate_limiter.increment_request_count! - assert_equal 0, @rate_limiter.current_count - end - - test "should always have zero request count" do - assert_equal 0, @rate_limiter.current_count - end - - test "should have infinite rate limit" do - assert_equal Float::INFINITY, @rate_limiter.rate_limit - end - - test "should have zero reset time" do - assert_equal 0, @rate_limiter.reset_time - end - - test "should provide correct usage info" do - usage_info = @rate_limiter.usage_info - - assert_equal 0, usage_info[:current_count] - assert_equal Float::INFINITY, usage_info[:rate_limit] - assert_equal Float::INFINITY, usage_info[:remaining] - assert_equal 0, usage_info[:reset_time] - assert_equal :noop, usage_info[:tier] - end - - test "class method usage_for should work" do - usage_info = NoopApiRateLimiter.usage_for(@api_key) - - assert_equal 0, usage_info[:current_count] - assert_equal Float::INFINITY, usage_info[:rate_limit] - assert_equal Float::INFINITY, usage_info[:remaining] - assert_equal 0, usage_info[:reset_time] - assert_equal :noop, usage_info[:tier] - end -end From 1cb0d039de62fa1e176f12ab5589935363df2942 Mon Sep 17 00:00:00 2001 From: Tin Tran Date: Tue, 23 Sep 2025 00:15:47 +0700 Subject: [PATCH 27/30] Fix tests --- app/controllers/api/v1/auth_controller.rb | 1 - .../api/v1/usage_controller_test.rb | 62 ------------------- 2 files changed, 63 deletions(-) diff --git a/app/controllers/api/v1/auth_controller.rb b/app/controllers/api/v1/auth_controller.rb index 4f20b9346e3..06796605408 100644 --- a/app/controllers/api/v1/auth_controller.rb +++ b/app/controllers/api/v1/auth_controller.rb @@ -4,7 +4,6 @@ class AuthController < BaseController include Invitable skip_before_action :authenticate_request! - skip_before_action :check_api_key_rate_limit skip_before_action :log_api_access def signup diff --git a/test/controllers/api/v1/usage_controller_test.rb b/test/controllers/api/v1/usage_controller_test.rb index 02f38955367..e169d51b385 100644 --- a/test/controllers/api/v1/usage_controller_test.rb +++ b/test/controllers/api/v1/usage_controller_test.rb @@ -32,14 +32,6 @@ class Api::V1::UsageControllerTest < ActionDispatch::IntegrationTest assert_equal [ "read" ], response_body["api_key"]["scopes"] assert_not_nil response_body["api_key"]["last_used_at"] assert_not_nil response_body["api_key"]["created_at"] - - # Check rate limit information - assert_equal "standard", response_body["rate_limit"]["tier"] - assert_equal 100, response_body["rate_limit"]["limit"] - assert_equal 4, response_body["rate_limit"]["current_count"] # 3 test requests + 1 usage request - assert_equal 96, response_body["rate_limit"]["remaining"] - assert response_body["rate_limit"]["reset_in_seconds"] > 0 - assert_not_nil response_body["rate_limit"]["reset_at"] end test "should require read scope for usage endpoint" do @@ -70,58 +62,4 @@ class Api::V1::UsageControllerTest < ActionDispatch::IntegrationTest get "/api/v1/usage" assert_response :unauthorized end - - test "should update usage count when accessing usage endpoint" do - # Check initial state - get "/api/v1/usage", headers: { "X-Api-Key" => @api_key.display_key } - assert_response :success - - response_body = JSON.parse(response.body) - first_count = response_body["rate_limit"]["current_count"] - - # Make another usage request - get "/api/v1/usage", headers: { "X-Api-Key" => @api_key.display_key } - assert_response :success - - response_body = JSON.parse(response.body) - second_count = response_body["rate_limit"]["current_count"] - - assert_equal first_count + 1, second_count - end - - test "should include rate limit headers in usage response" do - get "/api/v1/usage", headers: { "X-Api-Key" => @api_key.display_key } - assert_response :success - - assert_not_nil response.headers["X-RateLimit-Limit"] - assert_not_nil response.headers["X-RateLimit-Remaining"] - assert_not_nil response.headers["X-RateLimit-Reset"] - - assert_equal "100", response.headers["X-RateLimit-Limit"] - assert_equal "99", response.headers["X-RateLimit-Remaining"] - end - - test "should work correctly when approaching rate limit" do - # Make 98 requests to get close to the limit - 98.times do - get "/api/v1/test", headers: { "X-Api-Key" => @api_key.display_key } - assert_response :success - end - - # Check usage - this should be request 99 - get "/api/v1/usage", headers: { "X-Api-Key" => @api_key.display_key } - assert_response :success - - response_body = JSON.parse(response.body) - assert_equal 99, response_body["rate_limit"]["current_count"] - assert_equal 1, response_body["rate_limit"]["remaining"] - - # One more request should hit the limit - get "/api/v1/test", headers: { "X-Api-Key" => @api_key.display_key } - assert_response :success - - # Now we should be rate limited - get "/api/v1/usage", headers: { "X-Api-Key" => @api_key.display_key } - assert_response :too_many_requests - end end From 5b6cebd349b032c664208075fd2aa6313e32b898 Mon Sep 17 00:00:00 2001 From: Tin Tran Date: Tue, 23 Sep 2025 00:18:59 +0700 Subject: [PATCH 28/30] Remove rate limit information from API usage response and documentation --- app/controllers/api/v1/usage_controller.rb | 9 --------- docs/api/chats.md | 1 - 2 files changed, 10 deletions(-) diff --git a/app/controllers/api/v1/usage_controller.rb b/app/controllers/api/v1/usage_controller.rb index b2a57df9e72..9d7bd4ee924 100644 --- a/app/controllers/api/v1/usage_controller.rb +++ b/app/controllers/api/v1/usage_controller.rb @@ -5,21 +5,12 @@ def show case @authentication_method when :api_key - usage_info = @rate_limiter.usage_info render_json({ api_key: { name: @api_key.name, scopes: @api_key.scopes, last_used_at: @api_key.last_used_at, created_at: @api_key.created_at - }, - rate_limit: { - tier: usage_info[:tier], - limit: usage_info[:rate_limit], - current_count: usage_info[:current_count], - remaining: usage_info[:remaining], - reset_in_seconds: usage_info[:reset_time], - reset_at: Time.current + usage_info[:reset_time].seconds } }) when :oauth diff --git a/docs/api/chats.md b/docs/api/chats.md index 159994cb155..12482258f5a 100644 --- a/docs/api/chats.md +++ b/docs/api/chats.md @@ -221,7 +221,6 @@ Common error codes: - `forbidden` - Insufficient permissions or AI not enabled - `not_found` - Resource not found - `unprocessable_entity` - Invalid request data -- `rate_limit_exceeded` - Too many requests ## Rate Limits From 203c110d7f931059c55cd8799a3f09a89d6459f0 Mon Sep 17 00:00:00 2001 From: Tin Tran Date: Tue, 23 Sep 2025 00:56:13 +0700 Subject: [PATCH 29/30] Fix the misconfigured cache database --- config/cache.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/cache.yml b/config/cache.yml index 7bfedb386fb..e87c34f40fe 100644 --- a/config/cache.yml +++ b/config/cache.yml @@ -14,4 +14,4 @@ test: production: &production <<: *default - databases: [production_cache1, production_cache2] \ No newline at end of file + databases: cache \ No newline at end of file From 8664e666e978b13055c657068ae5a337f926cef5 Mon Sep 17 00:00:00 2001 From: Tin Tran Date: Tue, 23 Sep 2025 01:06:34 +0700 Subject: [PATCH 30/30] :shrug: --- config/cache.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/cache.yml b/config/cache.yml index e87c34f40fe..fdb4f759653 100644 --- a/config/cache.yml +++ b/config/cache.yml @@ -14,4 +14,4 @@ test: production: &production <<: *default - databases: cache \ No newline at end of file + database: cache \ No newline at end of file