Search code examples
ruby-on-railsrubypostgresql

How to auto reconnect Rails 6 PostgreSQL connection?


I have a rails 6 application with some worker processes. The app uses PostgreSQL as a DB. Sometimes the DB reboots (e.g. minor version upgrade) and workers lose their connection. I'd expect them to auto-reconnect but it doesn't happen.

I tried to use reconnect: true flag in database.yml. The same story. I still receive PG::UnableToSend: no connection to the server. The option is not even available in PostgresqlAdapter. I guess it's only the MySQL adapter option.

The workers are simple service classes I run with rails runner

What could be done? I believe the answer must be simple.


Solution

  • I made an ActiveRecord patch for PG auto-reconnection.

    Work with exceptions could be optimised but I had some weird mixed of PG::UnableToSend, PG::ConnectionBad and PG::Error, so I compare exception names instead.

    module PostgreSQLAdapterAutoReconnectPatch
      QUERY_EXCEPTIONS = [
        "SSL connection has been closed unexpectedly",
        "server closed the connection unexpectedly",
        "no connection to the server",
      ].freeze
    
      CONNECTION_EXCEPTIONS = [
        "could not connect to server",
        "the database system is starting up",
      ].freeze
    
      def exec_query(*args)
        super(*args)
      rescue ActiveRecord::StatementInvalid => e
        raise unless recoverable_query?(e.message)
    
        in_transaction = transaction_manager.current_transaction.open?
        try_reconnect
        in_transaction ? raise : retry
      end
    
      private
    
      def recoverable_query?(error_message)
        QUERY_EXCEPTIONS.any? { |e| error_message.include?(e) }
      end
    
      def recoverable_connection?(error_message)
        CONNECTION_EXCEPTIONS.any? { |e| error_message.include?(e) }
      end
    
      def try_reconnect
        sleep_times = [0.1, 0.5, 1, 2, 4, 8, 16, 32]
    
        begin
          reconnect!
        rescue PG::Error => e
          sleep_time = sleep_times.shift
          if sleep_time && recoverable_connection?(e.message)
            logger.warn("DB Server timed out, retrying in #{sleep_time} sec")
            sleep sleep_time
            retry
          else
            logger.error(e)
            raise
          end
        end
      end
    end
    
    require "active_record/connection_adapters/postgresql_adapter"
    ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(PostgreSQLAdapterAutoReconnectPatch)
    

    Inspired by