The project is in a healthy, maintained state
Framework de infraestructura para Rails 7.1+ que automatiza el particionamiento nativo (List Partitioning). Incluye soporte para Composite Primary Keys, orquestación de tenants y migraciones zero-downtime.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

 Project Readme

TenantPartition

TenantPartition es un framework de infraestructura para Ruby on Rails (7.1+) diseñado para simplificar y automatizar la gestión de Particionamiento por Lista (List Partitioning) nativo de PostgreSQL.

Específicamente construido para arquitecturas Multi-tenant, este framework resuelve la complejidad de:

  1. Composite Primary Keys (CPK): Configuración automática de claves compuestas (id + partition_key) requeridas por ActiveRecord para soportar particionamiento.
  2. Orquestación Centralizada: Una única interfaz para crear y eliminar particiones en todos los modelos de la aplicación simultáneamente.
  3. Mantenimiento Zero-Downtime: Migración atómica de datos "huérfanos" (que cayeron en la tabla DEFAULT) hacia sus particiones correctas sin perder servicio.

🚀 Características Principales

  • Fachada de Servicio: API unificada (TenantPartition.create!, destroy!, audit, cleanup!) para gestionar el ciclo de vida completo de los tenants.
  • Introspección Inteligente: Los modelos de negocio detectan automáticamente su configuración de infraestructura por convención (ej: User -> Partition::User).
  • Migration DSL: Helper create_partitioned_table para definir tablas particionadas y sus tablas DEFAULT en una sola instrucción.
  • Seguridad Estricta: Concern para controladores que valida Headers HTTP (X-Tenant-ID) para asegurar el contexto del tenant.
  • Observabilidad: Instrumentación integrada con ActiveSupport::Notifications.

📋 Requisitos

  • Ruby: >= 3.2
  • Ruby on Rails: >= 7.1
  • PostgreSQL: >= 13.0 (Requerido para gen_random_uuid() nativo)

📦 Instalación

Agrega esto a tu Gemfile:

gem 'tenant_partition'

Luego ejecuta:

bundle install

⚙️ Configuración

Crea un inicializador en config/initializers/tenant_partition.rb. Es obligatorio definir la clave de partición.

TenantPartition.configure do |config|
  # 1. La columna que discrimina los tenants (ej: :isp_id, :account_id, :tenant_id)
  config.partition_key = :isp_id

  # 2. El Header HTTP para la seguridad en controladores API
  config.header_name   = 'X-Tenant-ID'
end

🛠 Guía de Uso

1. Migraciones (Crear las Tablas)

Usa el helper create_partitioned_table. Este método deshabilita el ID automático simple y configura una Primary Key Compuesta ([:id, :partition_key]) necesaria para que PostgreSQL permita el particionamiento.

class CreateConversations < ActiveRecord::Migration[7.1]
  def change
    create_partitioned_table :conversations do |t|
      # No definas t.primary_key. La gema crea (id, isp_id) automáticamente.
      t.string :topic
      t.timestamps
    end
  end
end

2. Capa de Infraestructura (Modelos Partition)

Define modelos que hereden de TenantPartition::Base. Estos modelos son responsables de las operaciones DDL (Create/Drop tables). Por convención, se recomienda usar el namespace Partition::.

# app/models/partition/conversation.rb
module Partition
  class Conversation < TenantPartition::Base
    # Hereda la configuración global (:isp_id) automáticamente.
  end
end

3. Capa de Negocio (Modelos Rails)

En tus modelos estándar (ApplicationRecord), incluye el concern Partitioned.

Magia de Introspección: Al incluir el concern, la gema busca automáticamente si existe un modelo de infraestructura asociado (ej: Partition::Conversation) y hereda su configuración.

# app/models/conversation.rb
class Conversation < ApplicationRecord
  include TenantPartition::Concerns::Partitioned

  # ¡Listo! Rails ahora sabe que la Primary Key es [:id, :isp_id]
  # y aplica scopes automáticos.
end

4. Orquestación (Ciclo de Vida del Tenant)

Ya no necesitas crear particiones tabla por tabla. Usa la Fachada TenantPartition para gestionar la infraestructura de un tenant en todos los modelos registrados simultáneamente.

Crear un nuevo Tenant (Provisioning): Ideal para usar en tu RegistrationService o AfterCommit de la creación del tenant.

# En tu Service Object o Controller de registro
def create_tenant
  isp = Isp.create!(params)

  # Busca TODOS los modelos particionados y crea las tablas físicas para este ID.
  # Es idempotente: si alguna ya existe, la salta sin error.
  TenantPartition.create!(isp.id)
end

Eliminar un Tenant (Deprovisioning):

# Esta operación realiza DETACH + DROP de las tablas físicas.
# ¡Es destructiva e irreversible!
TenantPartition.destroy!(old_isp.id)

5. Seguridad en Controladores

Protege tus API endpoints asegurando que siempre reciban el ID del tenant.

class ApiController < ActionController::API
  include TenantPartition::Concerns::Controller

  # Valida que el request traiga el header 'X-Tenant-ID'.
  # Devuelve 400 Bad Request si falta.
  before_action :require_partition_key!

  def index
    # current_partition_id contiene el valor seguro del Header
    @chats = Conversation.for_partition(current_partition_id).all
    render json: @chats
  end
end

🛡 Mantenimiento y Recuperación

TenantPartition maneja el escenario de "Race Condition" donde llegan datos antes de que la partición exista. Esos datos caen automáticamente en la tabla _default.

Auditoría

Verifica si tienes datos "fugados" en las tablas default:

# Desde consola Rails
report = TenantPartition.audit
# => { "Partition::Conversation" => 14, "Partition::Message" => 0 }

O vía Rake task:

bundle exec rails tenant_partition:audit

Limpieza (Cleanup)

Mueve los datos huérfanos a sus particiones correspondientes de forma atómica y segura.

# Ruby API (Ideal para Jobs nocturnos)
TenantPartition.cleanup!

O vía Rake task:

bundle exec rails tenant_partition:cleanup

📊 Observabilidad

Puedes suscribirte a los eventos para enviar métricas a tu sistema de monitoreo (Datadog, Prometheus, NewRelic).

# config/initializers/notifications.rb
ActiveSupport::Notifications.subscribe(/tenant_partition/) do |name, start, finish, id, payload|
  duration = (finish - start) * 1000

  case name
  when "create.tenant_partition"
    Rails.logger.info "📦 Partición creada: #{payload[:table]} para #{payload[:value]}"
  when "populate.tenant_partition"
    Rails.logger.info "🧹 Limpieza: #{payload[:count]} registros movidos en #{duration.round(2)}ms"
  end
end

📄 Licencia

Este proyecto está disponible como código abierto bajo los términos de la Licencia MIT.