From 4d3c26504262337fc25d79e72de32950c552bef2 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Mon, 10 Nov 2025 07:12:59 -0500 Subject: [PATCH] Rip out activerecord-tenanting Co-authored-by: Stanko K.R. --- Gemfile | 1 - Gemfile.lock | 50 +++--- app/jobs/card/auto_postpone_all_due_job.rb | 4 +- app/jobs/delete_unused_tags_job.rb | 6 +- .../notification/bundle/deliver_all_job.rb | 4 +- app/jobs/sqlite_backups_job.rb | 155 ------------------ app/jobs/webhook/cleanup_deliveries_job.rb | 4 +- app/models/application_record.rb | 1 - bin/dev | 7 +- config/database.yml | 48 ++---- config/initializers/tenanting/account_slug.rb | 22 +-- .../initializers/tenanting/active_storage.rb | 19 --- .../initializers/tenanting/default_tenant.rb | 6 - config/initializers/tenanting/logging.rb | 7 +- config/initializers/tenanting/turbo.rb | 33 ++-- config/recurring.yml | 3 - 16 files changed, 61 insertions(+), 309 deletions(-) delete mode 100644 app/jobs/sqlite_backups_job.rb delete mode 100644 config/initializers/tenanting/active_storage.rb delete mode 100644 config/initializers/tenanting/default_tenant.rb diff --git a/Gemfile b/Gemfile index 5932188ce..5df3f6d74 100644 --- a/Gemfile +++ b/Gemfile @@ -10,7 +10,6 @@ gem "stimulus-rails" gem "turbo-rails" # Deployment and drivers -gem "activerecord-tenanted", bc: "activerecord-tenanted" gem "bootsnap", require: false gem "kamal", require: false gem "puma", ">= 5.0" diff --git a/Gemfile.lock b/Gemfile.lock index c16b85b63..b0066fd78 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,12 +1,3 @@ -GIT - remote: https://github.com/basecamp/activerecord-tenanted - revision: 1e92cb34d0b7b02875040826330c1918abb06e9d - specs: - activerecord-tenanted (0.6.0) - activerecord (>= 8.1.beta) - railties (>= 8.1.beta) - zeitwerk - GIT remote: https://github.com/basecamp/queenbee-plugin revision: eb01c697de1ad028afc65cc7d9b5345a7a8e849f @@ -161,10 +152,10 @@ GEM anyway_config (2.7.2) ruby-next-core (~> 1.0) ast (2.4.3) - autotuner (1.1.0) + autotuner (1.0.2) aws-eventstream (1.4.0) - aws-partitions (1.1181.0) - aws-sdk-core (3.236.0) + aws-partitions (1.1175.0) + aws-sdk-core (3.234.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -172,10 +163,10 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.117.0) + aws-sdk-kms (1.115.0) aws-sdk-core (~> 3, >= 3.234.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.203.0) + aws-sdk-s3 (1.201.0) aws-sdk-core (~> 3, >= 3.234.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) @@ -189,7 +180,7 @@ GEM bindex (0.8.1) bootsnap (1.18.6) msgpack (~> 1.2) - brakeman (7.1.1) + brakeman (7.1.0) racc builder (3.3.0) bundler-audit (0.9.2) @@ -259,7 +250,7 @@ GEM actionview (>= 7.0.0) activesupport (>= 7.0.0) jmespath (1.6.2) - json (2.16.0) + json (2.15.2) jwt (3.1.2) base64 kamal (2.8.2) @@ -299,7 +290,7 @@ GEM logger mini_mime (1.1.5) mini_portile2 (2.8.9) - minitest (5.26.1) + minitest (5.26.0) mission_control-jobs (1.1.0) actioncable (>= 7.1) actionpack (>= 7.1) @@ -417,7 +408,7 @@ GEM tsort redcarpet (3.6.1) regexp_parser (2.11.3) - reline (0.6.3) + reline (0.6.2) io-console (~> 0.5) rexml (3.4.4) rouge (4.6.1) @@ -467,10 +458,10 @@ GEM rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 4.0) websocket (~> 1.0) - sentry-rails (6.1.0) + sentry-rails (6.0.0) railties (>= 5.2.0) - sentry-ruby (~> 6.1.0) - sentry-ruby (6.1.0) + sentry-ruby (~> 6.0.0) + sentry-ruby (6.0.0) bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) sniffer (0.5.0) @@ -481,23 +472,23 @@ GEM activejob (>= 7.2) activerecord (>= 7.2) railties (>= 7.2) - solid_cache (1.0.10) + solid_cache (1.0.8) activejob (>= 7.2) activerecord (>= 7.2) railties (>= 7.2) - solid_queue (1.2.4) + solid_queue (1.2.2) activejob (>= 7.1) activerecord (>= 7.1) concurrent-ruby (>= 1.3.1) fugit (~> 1.11) railties (>= 7.1) thor (>= 1.3.1) - sqlite3 (2.8.0) + sqlite3 (2.7.4) mini_portile2 (~> 2.8.0) - sqlite3 (2.8.0-arm64-darwin) - sqlite3 (2.8.0-x86_64-darwin) - sqlite3 (2.8.0-x86_64-linux-gnu) - sqlite3 (2.8.0-x86_64-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) sshkit (1.24.0) base64 logger @@ -536,7 +527,7 @@ GEM web-push (3.0.2) jwt (~> 3.0) openssl (~> 3.0) - webmock (3.26.1) + webmock (3.26.0) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) @@ -586,7 +577,6 @@ PLATFORMS x86_64-linux-musl DEPENDENCIES - activerecord-tenanted! activeresource autotuner aws-sdk-s3 diff --git a/app/jobs/card/auto_postpone_all_due_job.rb b/app/jobs/card/auto_postpone_all_due_job.rb index 2b75f0736..684e2cbe2 100644 --- a/app/jobs/card/auto_postpone_all_due_job.rb +++ b/app/jobs/card/auto_postpone_all_due_job.rb @@ -1,7 +1,5 @@ class Card::AutoPostponeAllDueJob < ApplicationJob def perform - ApplicationRecord.with_each_tenant do |tenant| - Card.auto_postpone_all_due - end + Card.auto_postpone_all_due end end diff --git a/app/jobs/delete_unused_tags_job.rb b/app/jobs/delete_unused_tags_job.rb index a733a0e75..375984ba6 100644 --- a/app/jobs/delete_unused_tags_job.rb +++ b/app/jobs/delete_unused_tags_job.rb @@ -1,9 +1,7 @@ class DeleteUnusedTagsJob < ApplicationJob def perform - ApplicationRecord.with_each_tenant do |tenant| - Tag.unused.find_each do |tag| - tag.destroy! - end + Tag.unused.find_each do |tag| + tag.destroy! end end end diff --git a/app/jobs/notification/bundle/deliver_all_job.rb b/app/jobs/notification/bundle/deliver_all_job.rb index 250b53b6e..79aeaed16 100644 --- a/app/jobs/notification/bundle/deliver_all_job.rb +++ b/app/jobs/notification/bundle/deliver_all_job.rb @@ -2,8 +2,6 @@ class Notification::Bundle::DeliverAllJob < ApplicationJob queue_as :backend def perform - ApplicationRecord.with_each_tenant do |tenant| - Notification::Bundle.deliver_all - end + Notification::Bundle.deliver_all end end diff --git a/app/jobs/sqlite_backups_job.rb b/app/jobs/sqlite_backups_job.rb deleted file mode 100644 index 5daea366c..000000000 --- a/app/jobs/sqlite_backups_job.rb +++ /dev/null @@ -1,155 +0,0 @@ -require "find" - -# -# This job backs up all the tenant databases using the SQLite Backup API, which should allow the -# application to continue running against the database while it is backed up. -# -# ref: https://www.sqlite.org/c3ref/backup_finish.html -# -# It will keep N files around, like this: -# -# storage/tenants/development/12345678/db/ -# ├─ main.sqlite3 -# ├─ main.sqlite3.1 -# ├─ main.sqlite3.2 -# ├─ main.sqlite3.3 -# ├─ main.sqlite3.4 -# └─ main.sqlite3.5 -# -# On some systems, notably in production, we have an NFS-mounted filesystem into which the -# application copies the backup files for disaster recovery. We copy into an environment- and -# tenant-specific directory. The file, when copied, will be renamed with the timestamp of the file -# creation time. For example: -# -# /backups/production/12345678/main.sqlite3.20251014194804 -# -# It will also clean up old backups in the NFS-mounted filesystem following our data retention policy. -# -class SQLiteBackupsJob < ApplicationJob - DEFAULT_NUMBER_OF_BACKUPS = 5 - DEFAULT_STEP_PAGES = 1024 - DEFAULT_SWEEP_DIR = "/backups" - DEFAULT_SWEEP_RETENTION = 30.days # https://37signals.com/policies/privacy - - def perform(keep: DEFAULT_NUMBER_OF_BACKUPS, step: DEFAULT_STEP_PAGES, sweep_dir: DEFAULT_SWEEP_DIR, sweep_retention: DEFAULT_SWEEP_RETENTION) - @failures = [] - - ApplicationRecord.with_each_tenant do |tenant| - perform_file_rollover(tenant, keep:) - perform_backup(tenant, step:) - perform_sweep(tenant, sweep_dir:) - enforce_retention(tenant, sweep_dir:, sweep_retention:) - end - - if @failures.present? - raise "SQLiteBackupsJob: failed to backup tenants: #{@failures.join(", ")}" - end - end - - private - def perform_file_rollover(tenant, keep:) - keep.downto(2) do |j| - fresher = backup_path(tenant, j - 1) - staler = backup_path(tenant, j) - - if j == keep && File.exist?(staler) - FileUtils.rm(staler) - end - - if File.exist?(fresher) - # TODO: It may be worth benchmarking whether backing up into the previous backup is faster - # than backing up into an empty file. - FileUtils.mv(fresher, staler) - end - end - end - - def perform_backup(tenant, step:) - ApplicationRecord.with_connection do |conn| - current_adapter = conn.raw_connection - backup_db = backup_path(tenant, 1) - backup_adapter = SQLite3::Database.new(backup_db) - backup = SQLite3::Backup.new(backup_adapter, "main", current_adapter, "main") - - pages = 0 - elapsed = ActiveSupport::Benchmark.realtime(:float_millisecond) do - loop do - status = backup.step(step) - case status - when SQLite3::Constants::ErrorCode::DONE - break - when SQLite3::Constants::ErrorCode::OK - total = backup.pagecount - progress = total - backup.remaining - log(tenant, :debug) { "Wrote #{progress} of #{total} pages." } - when SQLite3::Constants::ErrorCode::BUSY, SQLite3::Constants::ErrorCode::LOCKED - log(tenant, :debug) { "Busy, retrying." } - else - log(tenant, :error) { "Failed with status #{status}." } - @failures << tenant - end - end - - pages = backup.pagecount - backup.finish - end - - log(tenant) { sprintf("Backup complete in %.1f ms. Wrote %{pages} pages to %{path}", path: backup_db.inspect, pages: pages, elapsed: elapsed) } - end - end - - def perform_sweep(tenant, sweep_dir:) - unless File.directory?(sweep_dir) && File.writable?(sweep_dir) - log(tenant, :warn) { "Skipping sweep, #{sweep_dir.inspect} does not exist or is not writable." } - return - end - - backup_file = backup_path(tenant, 1) - sweep_path = File.join(sweep_dir, Rails.env, tenant) - FileUtils.mkdir_p(sweep_path) - - if File.exist?(backup_file) - timestamp = File.ctime(backup_file).utc.strftime("%Y%m%d%H%M%S") - swept_file = File.join(sweep_path, File.basename(db_path(tenant)) + ".#{timestamp}") - FileUtils.cp(backup_file, swept_file) - log(tenant) { "Swept backup to #{swept_file.inspect}." } - else - log(tenant, :warn) { "No backup file found at #{backup_file.inspect} to sweep." } - end - end - - def enforce_retention(tenant, sweep_dir:, sweep_retention:) - unless File.directory?(sweep_dir) && File.writable?(sweep_dir) - log(tenant, :warn) { "Skipping retention enforcement, #{sweep_dir.inspect} does not exist or is not writable." } - return - end - - sweep_path = File.join(sweep_dir, Rails.env, tenant) - if File.directory?(sweep_path) - cutoff_time = Time.now - sweep_retention - Find.find(sweep_path) do |path| - if File.file?(path) && File.ctime(path) < cutoff_time - FileUtils.rm(path) - log(tenant) { "Removed old swept backup #{path.inspect}." } - end - end - end - end - - def backup_path(tenant, index) - db_path(tenant) + ".#{index}" - end - - def db_path(tenant) - db_config.config_adapter.database_path - end - - def db_config - ApplicationRecord.connection_pool.db_config - end - - def log(tenant, level = :info, &block) - message = block.call - Rails.logger.send(level, "[tenant=#{tenant}] SQLiteBackupsJob: #{message}") - end -end diff --git a/app/jobs/webhook/cleanup_deliveries_job.rb b/app/jobs/webhook/cleanup_deliveries_job.rb index 63f87ead5..37439de91 100644 --- a/app/jobs/webhook/cleanup_deliveries_job.rb +++ b/app/jobs/webhook/cleanup_deliveries_job.rb @@ -1,7 +1,5 @@ class Webhook::CleanupDeliveriesJob < ApplicationJob def perform - ApplicationRecord.with_each_tenant do - Webhook::Delivery.cleanup - end + Webhook::Delivery.cleanup end end diff --git a/app/models/application_record.rb b/app/models/application_record.rb index cb75f5d41..b63caeb8a 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -1,4 +1,3 @@ class ApplicationRecord < ActiveRecord::Base primary_abstract_class - tenanted end diff --git a/bin/dev b/bin/dev index 1c3b7427a..a798bc227 100755 --- a/bin/dev +++ b/bin/dev @@ -1,12 +1,7 @@ #!/usr/bin/env sh bin/rails runner - < 0 - puts " - #{Account.sole.name}: http://fizzy.localhost:3006#{Account.sole.slug}" - end + puts "Login with david@37signals.com to:" EOF if [ -f tmp/solid-queue.txt ]; then diff --git a/config/database.yml b/config/database.yml index a1111d8f2..79ae7d99c 100644 --- a/config/database.yml +++ b/config/database.yml @@ -1,36 +1,25 @@ -# SQLite. Versions 3.8.0 and up are supported. -# gem install sqlite3 -# -# Ensure the SQLite 3 gem is defined in your Gemfile -# gem "sqlite3" -# default: &default - adapter: sqlite3 + adapter: trilogy + host: localhost:33384 + user: root pool: 50 timeout: 5000 development: primary: <<: *default - adapter: sqlite3 - database: storage/tenants/<%= Rails.env %>/%{tenant}/db/main.sqlite3 - tenanted: true - untenanted: - <<: *default - adapter: sqlite3 - database: storage/untenanted/development.sqlite3 - migrations_paths: db/untenanted_migrate + database: fizzy_development cable: <<: *default - database: storage/development_cable.sqlite3 + database: fizzy_development_cable migrations_paths: db/cable_migrate cache: <<: *default - database: storage/development_cache.sqlite3 + database: fizzy_development_cache migrations_paths: db/cache_migrate queue: <<: *default - database: storage/development_queue.sqlite3 + database: fizzy_development_queue migrations_paths: db/queue_migrate # Warning: The database defined as "test" will be erased and @@ -39,35 +28,24 @@ development: test: primary: <<: *default - database: storage/tenants/<%= Rails.env %>/%{tenant}/db/main.sqlite3 - tenanted: true - untenanted: - <<: *default - database: storage/untenanted/test.sqlite3 - migrations_paths: db/untenanted_migrate + database: fizzy_test production: &production primary: <<: *default - adapter: sqlite3 - database: storage/tenants/<%= Rails.env %>/%{tenant}/db/main.sqlite3 - tenanted: true - untenanted: - <<: *default - adapter: sqlite3 - database: storage/untenanted/<%= Rails.env %>.sqlite3 - migrations_paths: db/untenanted_migrate + adapter: beamer + database: fizzy_<%= Rails.env %> cable: <<: *default - database: storage/production_cable.sqlite3 + database: fizzy_<% Rails.env %>_cable.sqlite3 migrations_paths: db/cable_migrate cache: <<: *default - database: storage/production_cache.sqlite3 + database: fizzy_<% Rails.env %>_cache.sqlite3 migrations_paths: db/cache_migrate queue: <<: *default - database: storage/production_queue.sqlite3 + database: fizzy_<% Rails.env %>_queue.sqlite3 migrations_paths: db/queue_migrate beta: *production diff --git a/config/initializers/tenanting/account_slug.rb b/config/initializers/tenanting/account_slug.rb index b2c0cdb40..6a4f83eb9 100644 --- a/config/initializers/tenanting/account_slug.rb +++ b/config/initializers/tenanting/account_slug.rb @@ -15,31 +15,11 @@ module AccountSlug request.engine_script_name = request.script_name = $1 request.path_info = $'.empty? ? "/" : $' - # Return the account id for tenanting. + # Return the account id AccountSlug.decode($2) end end def self.decode(slug) slug.to_i end def self.encode(id) FORMAT % id end - - def self.creation_request?(request) - if defined?(Fizzy::Saas) && request.post? - path = Fizzy::Saas::Engine.routes.url_helpers.signup_completion_path - request.path_info =~ /\A\/#{PATTERN}#{Regexp.escape(path)}\Z/ - else - false - end - end -end - -Rails.application.config.after_initialize do - Rails.application.config.active_record_tenanted.tenant_resolver = ->(request) do - if AccountSlug.creation_request?(request) - AccountSlug.extract(request) - nil - else - AccountSlug.extract(request) - end - end end diff --git a/config/initializers/tenanting/active_storage.rb b/config/initializers/tenanting/active_storage.rb deleted file mode 100644 index f57cdc304..000000000 --- a/config/initializers/tenanting/active_storage.rb +++ /dev/null @@ -1,19 +0,0 @@ -module ActiveStorageControllerExtensions - extend ActiveSupport::Concern - - included do - before_action do - # Add script_name so that Disk Service will generate correct URLs for uploads - ActiveStorage::Current.url_options = { - protocol: request.protocol, - host: request.host, - port: request.port, - script_name: request.script_name - } - end - end -end - -Rails.application.config.to_prepare do - ActiveStorage::BaseController.include ActiveStorageControllerExtensions -end diff --git a/config/initializers/tenanting/default_tenant.rb b/config/initializers/tenanting/default_tenant.rb deleted file mode 100644 index 2066f61d6..000000000 --- a/config/initializers/tenanting/default_tenant.rb +++ /dev/null @@ -1,6 +0,0 @@ -Rails.application.configure do - # In test environment, default tenant is set in test_helper.rb - if Rails.env.development? - config.active_record_tenanted.default_tenant = "686465299" # Honcho - end -end diff --git a/config/initializers/tenanting/logging.rb b/config/initializers/tenanting/logging.rb index d6675942e..08733395b 100644 --- a/config/initializers/tenanting/logging.rb +++ b/config/initializers/tenanting/logging.rb @@ -1,3 +1,4 @@ -ActiveSupport.on_load(:action_controller_base) do - before_action { logger.struct tenant: ApplicationRecord.current_tenant } -end +# # TODO:PLANB: we should log account id +# ActiveSupport.on_load(:action_controller_base) do +# before_action { logger.struct tenant: ApplicationRecord.current_tenant } +# end diff --git a/config/initializers/tenanting/turbo.rb b/config/initializers/tenanting/turbo.rb index d740ff8f6..67447a58b 100644 --- a/config/initializers/tenanting/turbo.rb +++ b/config/initializers/tenanting/turbo.rb @@ -1,18 +1,19 @@ -module TurboStreamsJobExtensions - extend ActiveSupport::Concern +# # TODO:PLANB: we probably still need to set script_name properly +# module TurboStreamsJobExtensions +# extend ActiveSupport::Concern - class_methods do - def render_format(format, **rendering) - if ApplicationRecord.current_tenant - script_name = "/#{ApplicationRecord.current_tenant}" - ApplicationController.renderer.new(script_name: script_name).render(formats: [ format ], **rendering) - else - super - end - end - end -end +# class_methods do +# def render_format(format, **rendering) +# if ApplicationRecord.current_tenant +# script_name = "/#{ApplicationRecord.current_tenant}" +# ApplicationController.renderer.new(script_name: script_name).render(formats: [ format ], **rendering) +# else +# super +# end +# end +# end +# end -Rails.application.config.after_initialize do - Turbo::StreamsChannel.prepend TurboStreamsJobExtensions -end +# Rails.application.config.after_initialize do +# Turbo::StreamsChannel.prepend TurboStreamsJobExtensions +# end diff --git a/config/recurring.yml b/config/recurring.yml index 15067065b..eec4772d5 100644 --- a/config/recurring.yml +++ b/config/recurring.yml @@ -25,9 +25,6 @@ production: &production cleanup_magic_links: command: "MagicLink.cleanup" schedule: every 4 hours - sqlite_backups: - class: SQLiteBackupsJob - schedule: every day at 05:02 # Metrics yabeda_actioncable: