# frozen_string_literal: true

# Setting TRACE_PG_CONNECTIONS=1 will cause all pg connections
# to be streamed to files for debugging. The filenames are formatted
# like tmp/pgtrace/{{PID}}_{{CONNECTION_OBJECT_ID}}.txt
#
# Setting TRACE_PG_CONNECTIONS=SIDEKIQ will only trace connections
# on in sidekiq (safer, because there will be minimal user-facing perf impact)
#
# Files will be automatically deleted when the connection is closed gracefully
# (e.g. when activerecord closes it after a period of inactivity)
# Files will not be automatically deleted when closed abruptly
# (e.g. terminating/restarting the app process)
#
# Warning: this could create some very large files!

if ENV["TRACE_PG_CONNECTIONS"]
  PG::Connection.prepend(Module.new do
    TRACE_DIR = "tmp/pgtrace"

    def initialize(*args)
      super(*args).tap do
        next if ENV["TRACE_PG_CONNECTIONS"] == "SIDEKIQ" && !Sidekiq.server?
        FileUtils.mkdir_p(TRACE_DIR)
        @trace_filename = "#{TRACE_DIR}/#{Process.pid}_#{self.object_id}.txt"
        trace File.new(@trace_filename, "w")
      end
      @access_log_mutex = Mutex.new
      @accessor_thread = nil
    end

    def close
      super.tap do
        next if ENV["TRACE_PG_CONNECTIONS"] == "SIDEKIQ" && !Sidekiq.server?
        File.delete(@trace_filename)
      end
    end

    def log_access(&blk)
      @access_log_mutex.synchronize do
        if !@accessor_thread.nil?
          Rails.logger.error <<~STRING
            PG Clash: A connection is being accessed from two locations

            #{@accessor_thread} was using the connection. Backtrace:

            #{@accessor_thread&.backtrace&.join("\n")}

            #{Thread.current} is now attempting to use the connection. Backtrace:

            #{Thread.current&.backtrace&.join("\n")}
          STRING

          if ENV["ON_PG_CLASH"] == "byebug"
            require "byebug"
            byebug # rubocop:disable Lint/Debugger
          end
        end
        @accessor_thread = Thread.current
      end
      yield
    ensure
      @access_log_mutex.synchronize do
        @accessor_thread = nil
      end
    end

  end)

  class PG::Connection
    LOG_ACCESS_METHODS = [:exec, :sync_exec, :async_exec,
                          :sync_exec_params, :async_exec_params,
                          :sync_prepare, :async_prepare,
                          :sync_exec_prepared, :async_exec_prepared,
                        ]

    LOG_ACCESS_METHODS.each do |method|
      new_method = "#{method}_without_logging".to_sym
      alias_method new_method, method

      define_method(method) do |*args, &blk|
        log_access { send(new_method, *args, &blk) }
      end
    end
  end

end
