Los cambios en el pool de conexiones de Rails 7.2 pueden ralentizar tu app

Después de actualizar a Rails 7.2, podrías notar que tu app está un poco más lenta. No dramáticamente, pero mediblemente—alrededor del 5-6% en benchmarks del mundo real. Todo se reduce a cómo ActiveRecord ahora gestiona las conexiones de base de datos.

El Cambio

En Rails 7.1, cada hilo mantenía su conexión de base de datos durante toda la duración de una petición.

Rails 7.2 cambió esto: las conexiones ahora se toman prestadas para cada consulta y se devuelven inmediatamente después. El objetivo era mejorar el uso compartido de conexiones en entornos multi-hilo donde las conexiones son escasas.

El cambio se introdujo en PR #50793, reemplazando ActiveRecord::Base.connection con ActiveRecord::Base.with_connection para acceso temporal y lease_connection para mantenerlas más tiempo.

Por Qué Esto Causa Ralentizaciones

El ciclo de tomar/devolver no es gratis. Cada vez que una conexión vuelve al pool, se ejecutan callbacks checkin y el sistema de callbacks asigna objetos. Una petición con 10 consultas ahora hace 10 check-ins en lugar de cero.

Los benchmarks del issue #55728 muestran el impacto:

Rails 7.1:              98.55 req/sec
Rails 7.2:              92.45 req/sec  (6% más lento)
Rails 7.2 + workaround: 99.99 req/sec  (vuelve a la normalidad)

Sentirás esto más si estás ejecutando workers de un solo hilo como Unicorn, tienes una base de datos local o de baja latencia, y ejecutas muchas consultas por petición.

La Solución

Para workers de un solo hilo donde el uso compartido de conexiones no importa, toma prestada una conexión al inicio de cada petición:

class ApplicationController < ActionController::Base
  before_action :lease_database_connection

  private

  def lease_database_connection
    ActiveRecord::Base.lease_connection
  end
end

Esto mantiene la conexión durante toda la petición, evitando el ciclo de tomar/devolver.

Para trabajos en segundo plano, la misma idea:

class ApplicationJob < ActiveJob::Base
  before_perform do
    ActiveRecord::Base.lease_connection
  end
end

Cuándo No Aplicar Esto

Si estás ejecutando Puma multi-hilo con más hilos que conexiones de base de datos, el nuevo comportamiento es realmente lo que quieres—permite que los hilos compartan conexiones. Lo mismo aplica para peticiones de larga duración que no tocan mucho la base de datos. Mantener conexiones que no necesitas priva a otros hilos.

Bajo el Capó

La mayor parte de la sobrecarga está en run_callbacks(:checkin). Incluso sin callbacks personalizados, Rails ejecuta toda la maquinaria de callbacks—asignación de objetos, dispatch de métodos, todo.

Jean Boussier (byroot) ha estado trabajando en optimizaciones para Rails main, pero estas no serán portadas a 7.2 ya que son ajustes de rendimiento en lugar de correcciones de bugs.

Midiendo Tu App

Antes de cambiar nada, verifica si esto te afecta:

# config/initializers/connection_benchmark.rb
if Rails.env.development?
  ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
    event = ActiveSupport::Notifications::Event.new(*args)
    Rails.logger.debug "SQL (#{event.duration.round(2)}ms)"
  end
end

Compara los tiempos de petición con y sin lease_connection. Si no ves diferencia, no te molestes con la solución.

Para despliegues con Unicorn o Puma de un solo hilo que experimentan respuestas más lentas después de actualizar, prueba lease_connection. Es seguro y te devuelve al comportamiento de 7.1.

Sigue el issue #55728 si quieres seguir el trabajo de optimización en curso.