Compartir tecnología

[ruby on rails] Razones y soluciones para los errores ActiveRecord::PreparedStatementCacheExpired durante la implementación

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

1 pregunta:

  • A veces, al implementar una aplicación Rails en Postgres, es posible que vea un error ActiveRecord::PreparedStatementCacheExpired. Esto solo sucede cuando se ejecutan migraciones en una implementación.
  • Esto sucede porque Rails aprovecha la función de caché de declaraciones preparadas de Postgres (PreparedStatementCache) para mejorar el rendimiento. Esta característica está habilitada de forma predeterminada en Rails.

2. Recurrencia del problema:

  • Podemos usar rspec para reproducir este error.
 it 'not raise ActiveRecord::PreparedStatementCacheExpired' do
    create(:user)
    User.first
    User.find_by_sql('ALTER TABLE users ADD new_metric_column integer;')
    ActiveRecord::Base.transaction { User.first }
  end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Insertar descripción de la imagen aquí

3. Principio de producción:

  • Declaraciones de consulta de Rails comoUser.allpor active_record Analizar en declaración SQLDespués de eso, envíelo a la base de datos,Ejecute PREPARE primero Declaraciones preparadas, las declaraciones SQL se analizarán, optimizarán y reescribirán.Cuando el seguimientoEmitir un comando EJECUTAR, la declaración preparada será planificada y ejecutada.
  • Rails guardará la declaración de consulta enpg_prepared_statementspara facilitar la próxima vez que llame a declaraciones similares.ejecutar directamente declaraciones, en lugar de analizar, analizar y optimizar, evitando la duplicación de trabajo y mejorando la eficiencia.
User.first
User.all
# 执行上面的2个查询后,用connection.instance_variable_get(:@statements)就可以看到缓存的准备语句
ActiveRecord::Base.connection.instance_variable_get(:@statements)
==> <ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::StatementPool:0x00000001086b13c8 
@cache={78368=>{""$user", public-SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 
$1"=>"a7", ""$user", public-SELECT "users".* FROM "users" /* loading for inspect */ LIMIT $1"=>"a8"}},
@statement_limit=1000, @connection=#<PG::Connection:0x00000001086b31a0>, @counter=8>

# 这个也可以看到,会在数据库中去查询
ActiveRecord::Base.connection.execute('select * from pg_prepared_statements').values
(0.5ms) select * from pg_prepared_statements
==> [["a7", "SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1", "2024-07-
11T07:03:06.891+00:00", "{bigint}", false], ["a8", "SELECT "users".* FROM "users" /* loading for inspect 
*/ LIMIT $1", "2024-07-11T07:04:47.772+00:00", "{bigint}", false]]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • En Postgres, el caché de declaraciones preparadas se invalidará si el esquema de la tabla cambia y afecta los resultados devueltos. Específicamente, se trata de operaciones DDL, como agregar y eliminar campos a la tabla, o modificar el tipo y la longitud de los campos.

Como en el siguiente ejemplo, al ejecutar SELECT después de agregar o eliminar campos,página de base de datostirarácached plan must not change result type, en rielesregistro activoRecibe este error y luego lanzaActiveRecord::PreparedStatementCacheExpired

ALTER TABLE users ADD COLUMN new_column integer;
ALTER TABLE users DROP COLUMN old_column;
添加或删除列,然后执行 SELECT *
删除 old_column 列然后执行 SELECT users.old_column
  • 1
  • 2
  • 3
  • 4
  • Al ejecutar la migración de agregar, restar o modificar campos en el servicio de implementación, la declaración de consulta emitida por el usuario recuperará directamente el SQL del caché de declaraciones preparado y ejecutará la ejecución directamente. Sin embargo, debido a que la estructura de la tabla cambia en este momento, el caché de declaraciones preparado dejará de ser válido.página de base de datostirarácached plan must not change result typeerror
  • Ver el código fuente de active_recordexec_cacheMétodo, descubrí que el método de manejo de errores de Rails para pg es:
    1. En la transacción, se arrojará directamente. raise ActiveRecord::PreparedStatementCacheExpired.new(e.cause.message)
    2. Aquellos fuera de la transacción almacenarán en caché @statementsElimina esta frase y prueba.Después de volver a intentarlo, la declaración SQL se volverá a analizar, analizar, optimizar y ejecutar.prepare_statementmétodo en el caché de declaraciones preparadas
module ActiveRecord
  module ConnectionHandling
    def exec_cache(sql, name, binds)
      materialize_transactions
      mark_transaction_written_if_write(sql)
      update_typemap_for_default_timezone

      stmt_key = prepare_statement(sql, binds)
      type_casted_binds = type_casted_binds(binds)

      log(sql, name, binds, type_casted_binds, stmt_key) do
        ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
          @connection.exec_prepared(stmt_key, type_casted_binds)
        end
      end
    rescue ActiveRecord::StatementInvalid => e
      raise unless is_cached_plan_failure?(e)

      # Nothing we can do if we are in a transaction because all commands
      # will raise InFailedSQLTransaction
      if in_transaction?
        raise ActiveRecord::PreparedStatementCacheExpired.new(e.cause.message)
      else
        @lock.synchronize do
          # outside of transactions we can simply flush this query and retry
          @statements.delete sql_key(sql)
        end
        retry
      end
    end
  end
end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • Por lo tanto, este error que aparece en la transacción hará que la transacción se revierta para la empresa, lo que significa que la solicitud falló y debemos manejarla nosotros mismos.

4. Solución:

1. Deshabilite la función de declaración preparada en caché (no recomendado)

Rails6 y superiores pueden deshabilitar esta función estableciendo declaraciones_preparadas en la base de datos en falso.

default: &default
  adapter: postgresql
  encoding: unicode
  prepared_statements: false
  • 1
  • 2
  • 3
  • 4

Rails6 y versiones inferiores no se han probado. Si lo anterior no funciona, puede intentar crear un nuevo archivo de inicialización.

# config/initializers/disable_prepared_statements.rb:
db_configuration = ActiveRecord::Base.configurations[Rails.env]
db_configuration.merge!('prepared_statements' => false)
ActiveRecord::Base.establish_connection(db_configuration)
  • 1
  • 2
  • 3
  • 4

verificar:

User.all
ActiveRecord::Base.connection.execute('select * from pg_prepared_statements').values
==> []
  • 1
  • 2
  • 3

Conclusión: en proyectos pequeños, no importa si deshabilita esta función y el rendimiento casi no se verá afectado. Sin embargo, en proyectos grandes, cuantos más usuarios y declaraciones de consulta más complejas, mayores serán los beneficios que traerá esta función. para que pueda decidir si desea desactivarlo según la situación real.

2. hacerselect * convertirseselect id, nameCampos tan específicos, en Rails7Solución oficialEso es todo

  • Establezca enumerate_columns_in_select_statements en verdadero en Rails7
# config/application.rb
module MyApp
  class Application < Rails::Application
    config.active_record.enumerate_columns_in_select_statements = true
  end
end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • No existe tal configuración debajo de Rails7, puedes usar ignore_columns para lograrlo
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
  #__fake_column__是自定义的,不要是某个表中的字段就行,如果是[:id],那么 User.all就会被解析为select name from users,没有id了
  self.ignored_columns = [:__fake_column__] 
end
  • 1
  • 2
  • 3
  • 4
  • 5

Conclusión: El problema con esta solución es que agregar campos se puede resolver perfectamente, pero eliminar campos aún causará errores. Por ejemplo, después de eliminar el campo de nombre, si el nombre en la declaración preparada selecciona id, el nombre de los usuarios no existe. Se informará un error. La solución oficial de Rails7 también tiene este problema.

3. Reinicie la aplicación Rails.

  • El ciclo de vida de la memoria caché de declaraciones preparadas solo existe en una sesión de base de datos. Si se cierra la conexión de la base de datos (al reiniciar la aplicación se cerrará la conexión original y se restablecerá una nueva), la memoria caché de declaraciones preparadas original se borrará y la memoria caché de declaraciones preparadas se borrará. La solicitud SQL después del reinicio se reiniciará. Almacene en caché la declaración preparada y podrá obtener los datos normalmente.

Conclusión: reiniciar la aplicación provocará un servicio temporal 502 no disponible. Por supuesto, al implementar la aplicación, también debe reiniciar el servicio y también aparecerá 502, por lo que es mejor implementarlo cuando nadie acceda a él (en el). media noche?), para que haya la menor cantidad de errores posible.PreparedStatementCacheExpiredInformar un error

4. Reescribir transaction método

class ApplicationRecord < ActiveRecord::Base
  class << self
    def transaction(*args, &block)
      retried ||= false
      super
    rescue ActiveRecord::PreparedStatementCacheExpired
      if retried
        raise
      else
        retried = true
        retry
      end
    end
  end
end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • Después de reescribir, el lugar donde se escriben las transacciones en el código se cambia para usar ApplicationRecord.transaction do ... end oMyModel.transaction

Conclusión: Nota importante: si envía correos electrónicos, publica en API o realiza otras operaciones que interactúan con el mundo exterior como parte de una transacción, esto puede causar que algunas de estas operaciones ocurran ocasionalmente dos veces. Es por eso que Rails oficialmente no realiza reintentos automáticamente, sino que lo deja en manos de los desarrolladores de aplicaciones.

&gt;&gt;&gt;&gt;&gt;&gt;&gt;Cuando pruebo este método yo mismo, todavía recibo errores.

5. Borre manualmente el caché de extractos preparados.

 ActiveRecord::Base.connection.clear_cache!
  • 1

5. Respuesta final

No encontré una solución perfecta.