Implementación Avanzada de Multi-Tenancy en Rails con activerecord-multi-tenant

La gem activerecord-multi-tenant facilita la implementación de arquitecturas multi-tenant en aplicaciones Ruby on Rails, especialmente cuando se utiliza PostgreSQL con la extensión Citus. A continuación, se detallan técnicas avanzadas para gestionar el contexto del tanant en controladores, integrar tareas en segundo plano con Sidekiq y ejecutar migraciones de datos progresivas mediante el modo de solo escritura.

Gestión del Contexto en Controladores

Para asegurar que cada solicitud HTTP opere dentro del aislamiento correcto, es fundamental configurar el tenant actual a nivel de controlador. Esto se logra mediante filtros que interceptan la petición y establecen el contexto global para ActiveRecord.

class BaseController < ActionController::Base
  set_current_tenant_through_filter
  before_action :resolve_and_set_organization

  private

  def resolve_and_set_organization
    # Extraer el identificador de la sesión o del subdominio
    org_id = session[:active_org_id] || extract_from_subdomain
    organization = Organization.find(org_id)
    set_current_tenant(organization)
  end
end

Al invocar set_current_tenant, la gema almacena la referencia en una variable de hilo (MultiTenant.current_tenant). A partir de ese momento, cualquier consulta generada por ActiveRecord inyectará automáticamente la cláusula WHERE tenant_id = ?.

En paneles de administración donde se requiere acceso transversal, el contexto puede alternarse dinámicamente:

class SuperAdmin::DashboardController < BaseController
  skip_before_action :resolve_and_set_organization
  before_action :impersonate_tenant

  private

  def impersonate_tenant
    if params[:target_tenant_id].present?
      target = Organization.find(params[:target_tenant_id])
      set_current_tenant(target)
    end
  end
end

Propagación de Contexto en Sidekiq

Las tareas asíncronas se ejecutan fuera del ciclo de vida de la solicitud HTTP, por lo que el contexto del tenant se pierde. Para solucionarlo, activerecord-multi-tenant proporciona middlewares para Sidekiq que serializan y deserializan el identificador del tenant en la cola de Redis.

# config/initializers/sidekiq.rb
Sidekiq.configure_server do |cfg|
  cfg.server_middleware do |chain|
    chain.add Sidekiq::Middleware::MultiTenant::Server
  end
  cfg.client_middleware do |chain|
    chain.add Sidekiq::Middleware::MultiTenant::Client
  end
end

Sidekiq.configure_client do |cfg|
  cfg.client_middleware do |chain|
    chain.add Sidekiq::Middleware::MultiTenant::Client
  end
end

Con esta configuración, los trabajos encolados heredarán el tenant activo:

class DataExportWorker
  include Sidekiq::Worker

  def perform(record_id)
    # El contexto del tenant ya está restaurado automáticamente
    record = FinancialRecord.find(record_id)
    ExportService.new(record).process
  end
end

# Encolar el trabajo manteniendo el aislamiento
MultiTenant.with(current_org) do
  DataExportWorker.perform_async(target_record.id)
end

Para operaciones masivas que involucran múltiples tenants, se puede utilizar la API de inserción por lotes:

bulk_payload = {
  'class' => 'DataExportWorker',
  'jobs' => [
    { 'args' => [101], 'tenant_id' => 50 },
    { 'args' => [102], 'tenant_id' => 51 },
    { 'args' => [103], 'tenant_id' => 52 }
  ]
}

Sidekiq::Client.push_bulk_with_tenants(bulk_payload)

Migraciones Progresivas con el Modo de Solo Escritura

Adoptar una arquitectura multi-tenant en una base de datos existente requiere una transición cuidadosa. El modo de solo escritura (write-only mode) permite que las nuevas insecriones incluyan el tenant_id, mientras que las lecturas y actualizaciones ignoran temporalmente este filtro, evitando romper la aplicación durante el backfill de datos.

# config/initializers/multi_tenant_setup.rb
MultiTenant.enable_write_only_mode

Comportamiento comparativo:

  • Modo Estándar: Inyecta tenant_id en SELECT, INSERT, UPDATE y DELETE.
  • Modo Solo Escritura: Solo inyecta tenant_id en operaciones INSERT. Las lecturas y modificaciones operan sobre todo el conjunto de datos.

Flujo de migración recomendado:

  1. Activar el modo de solo escritura en producción.
  2. Ejecutar un script de backfill para asignar el tenant_id a los registros históricos.
  3. Validar que no existan registros huérfanos (con tenant_id nulo).
  4. Desactivar el modo de solo escritura y aplicar restricciones de base de datos (NOT NULL).
class HistoricalDataBackfill
  def self.execute
    Organization.find_each do |org|
      MultiTenant.with(org) do
        # Actualizar registros legacy sin disparar callbacks
        LegacyTransaction.where(tenant_id: nil).update_all(tenant_id: org.id)
        AuditLog.where(tenant_id: nil).update_all(tenant_id: org.id)
      end
    end
  end
end

Optimización de Rendimiento y Monitoreo

La inyección automática de cláusulas WHERE puede afectar los planes de ejecución si no se indexa correctamente. Es obligatorio crear índices compuestos que prioricen el identificador del tenant.

class AddTenantIndexes < ActiveRecord::Migration[7.0]
  def change
    # El tenant_id debe ser la primera columna en el índice compuesto
    add_index :financial_records, [:tenant_id, :status, :created_at]
    add_index :audit_logs, [:tenant_id, :user_id]
  end
end

Para diagnosticar consultas que no están siendo filtradas correctamente o para auditar el comportamiento de la gema, se puede habilitar el registro de consultas:

# Activar en entornos de desarrollo o staging
MultiTenant.enable_query_monitor = true

# Inspeccionar las consultas interceptadas
Rails.logger.info MultiTenant.current_tenant_query_log

Adicionalmente, integrar estrategias de caché a nivel de modelo reduce la carga en la base de datos distribuida:

class Workspace < ApplicationRecord
  multi_tenant :organization

  def active_users_count
    Rails.cache.fetch("ws_#{id}_active_users", expires_in: 10.minutes) do
      users.where(status: 'active').count
    end
  end
end

Etiquetas: ruby-on-rails activerecord multi-tenant sidekiq PostgreSQL

Publicado el 9-14 03:31