From b1359dc84aa3c1b07c2aaee1eff33a5111ff71d6 Mon Sep 17 00:00:00 2001 From: okarsono Date: Sat, 25 Mar 2023 14:33:55 +1100 Subject: [PATCH] Similar to the mysql2 patch, this workaround is for PG (Postgresql) users who are experiencing conflict between rack-mini-profiler and another gem that does the same patching. --- README.md | 14 ++++ lib/patches/db/pg.rb | 123 +----------------------------- lib/patches/db/pg/alias_method.rb | 123 ++++++++++++++++++++++++++++++ lib/patches/db/pg/prepend.rb | 34 +++++++++ lib/prepend_pg_patch.rb | 5 ++ 5 files changed, 180 insertions(+), 119 deletions(-) create mode 100644 lib/patches/db/pg/alias_method.rb create mode 100644 lib/patches/db/pg/prepend.rb create mode 100644 lib/prepend_pg_patch.rb diff --git a/README.md b/README.md index d0fff3c9..c3be29e1 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,20 @@ gem 'rack-mini-profiler', require: ['prepend_mysql2_patch', 'rack-mini-profiler' This should not be necessary with Rails < 5 because peek-mysql2 hooks into mysql2 gem in different ways depending on your Rails version. +#### `pg` stack level too deep errors + +If you encounter `SystemStackError (stack level too deep)` from PG, you'll need to use this gem spec in your Gemfile: + +```ruby +gem 'rack-mini-profiler', require: ['prepend_pg_patch', 'rack-mini-profiler'] +``` + +Or if you initially have `require: false`, then use + +```ruby +gem 'rack-mini-profiler', require: ['prepend_pg_patch'] +``` + #### Rails and manual initialization In case you need to make sure rack_mini_profiler is initialized after all other gems, or you want to execute some code before rack_mini_profiler required: diff --git a/lib/patches/db/pg.rb b/lib/patches/db/pg.rb index b355b2a8..fa2a148c 100644 --- a/lib/patches/db/pg.rb +++ b/lib/patches/db/pg.rb @@ -1,122 +1,7 @@ # frozen_string_literal: true -# PG patches, keep in mind exec and async_exec have a exec{|r| } semantics that is yet to be implemented -class PG::Result - alias_method :each_without_profiling, :each - alias_method :values_without_profiling, :values - - def values(*args, &blk) - return values_without_profiling(*args, &blk) unless defined?(@miniprofiler_sql_id) - mp_report_sql do - values_without_profiling(*args , &blk) - end - end - - def each(*args, &blk) - return each_without_profiling(*args, &blk) unless defined?(@miniprofiler_sql_id) - mp_report_sql do - each_without_profiling(*args, &blk) - end - end - - def mp_report_sql(&block) - start = Process.clock_gettime(Process::CLOCK_MONOTONIC) - result = yield - elapsed_time = SqlPatches.elapsed_time(start) - @miniprofiler_sql_id.report_reader_duration(elapsed_time) - result - end -end - -class PG::Connection - alias_method :exec_without_profiling, :exec - alias_method :async_exec_without_profiling, :async_exec - alias_method :exec_prepared_without_profiling, :exec_prepared - alias_method :send_query_prepared_without_profiling, :send_query_prepared - alias_method :prepare_without_profiling, :prepare - - if Gem::Version.new(PG::VERSION) >= Gem::Version.new("1.1.0") - alias_method :exec_params_without_profiling, :exec_params - end - - def prepare(*args, &blk) - # we have no choice but to do this here, - # if we do the check for profiling first, our cache may miss critical stuff - - @prepare_map ||= {} - @prepare_map[args[0]] = args[1] - # dont leak more than 10k ever - @prepare_map = {} if @prepare_map.length > 1000 - - return prepare_without_profiling(*args, &blk) unless SqlPatches.should_measure? - prepare_without_profiling(*args, &blk) - end - - def exec(*args, &blk) - return exec_without_profiling(*args, &blk) unless SqlPatches.should_measure? - - start = Process.clock_gettime(Process::CLOCK_MONOTONIC) - result = exec_without_profiling(*args, &blk) - elapsed_time = SqlPatches.elapsed_time(start) - record = ::Rack::MiniProfiler.record_sql(args[0], elapsed_time) - result.instance_variable_set("@miniprofiler_sql_id", record) if result - - result - end - - if Gem::Version.new(PG::VERSION) >= Gem::Version.new("1.1.0") - def exec_params(*args, &blk) - return exec_params_without_profiling(*args, &blk) unless SqlPatches.should_measure? - - start = Process.clock_gettime(Process::CLOCK_MONOTONIC) - result = exec_params_without_profiling(*args, &blk) - elapsed_time = SqlPatches.elapsed_time(start) - record = ::Rack::MiniProfiler.record_sql(args[0], elapsed_time) - result.instance_variable_set("@miniprofiler_sql_id", record) if result - - result - end - end - - def exec_prepared(*args, &blk) - return exec_prepared_without_profiling(*args, &blk) unless SqlPatches.should_measure? - - start = Process.clock_gettime(Process::CLOCK_MONOTONIC) - result = exec_prepared_without_profiling(*args, &blk) - elapsed_time = SqlPatches.elapsed_time(start) - mapped = args[0] - mapped = @prepare_map[mapped] || args[0] if @prepare_map - record = ::Rack::MiniProfiler.record_sql(mapped, elapsed_time) - result.instance_variable_set("@miniprofiler_sql_id", record) if result - - result - end - - def send_query_prepared(*args, &blk) - return send_query_prepared_without_profiling(*args, &blk) unless SqlPatches.should_measure? - - start = Process.clock_gettime(Process::CLOCK_MONOTONIC) - result = send_query_prepared_without_profiling(*args, &blk) - elapsed_time = SqlPatches.elapsed_time(start) - mapped = args[0] - mapped = @prepare_map[mapped] || args[0] if @prepare_map - record = ::Rack::MiniProfiler.record_sql(mapped, elapsed_time) - result.instance_variable_set("@miniprofiler_sql_id", record) if result - - result - end - - def async_exec(*args, &blk) - return async_exec_without_profiling(*args, &blk) unless SqlPatches.should_measure? - - start = Process.clock_gettime(Process::CLOCK_MONOTONIC) - result = exec_without_profiling(*args, &blk) - elapsed_time = SqlPatches.elapsed_time(start) - record = ::Rack::MiniProfiler.record_sql(args[0], elapsed_time) - result.instance_variable_set("@miniprofiler_sql_id", record) if result - - result - end - - alias_method :query, :exec +if defined?(Rack::MINI_PROFILER_PREPEND_PG_PATCH) + require "patches/db/pg/prepend" +else + require "patches/db/pg/alias_method" end diff --git a/lib/patches/db/pg/alias_method.rb b/lib/patches/db/pg/alias_method.rb new file mode 100644 index 00000000..d2839f01 --- /dev/null +++ b/lib/patches/db/pg/alias_method.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +# The best kind of instrumentation is in the actual db provider, however we don't want to double instrument + +class PG::Result + alias_method :each_without_profiling, :each + alias_method :values_without_profiling, :values + + def values(*args, &blk) + return values_without_profiling(*args, &blk) unless defined?(@miniprofiler_sql_id) + mp_report_sql do + values_without_profiling(*args , &blk) + end + end + + def each(*args, &blk) + return each_without_profiling(*args, &blk) unless defined?(@miniprofiler_sql_id) + mp_report_sql do + each_without_profiling(*args, &blk) + end + end + + def mp_report_sql(&block) + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + result = yield + elapsed_time = SqlPatches.elapsed_time(start) + @miniprofiler_sql_id.report_reader_duration(elapsed_time) + result + end +end + +class PG::Connection + alias_method :exec_without_profiling, :exec + alias_method :async_exec_without_profiling, :async_exec + alias_method :exec_prepared_without_profiling, :exec_prepared + alias_method :send_query_prepared_without_profiling, :send_query_prepared + alias_method :prepare_without_profiling, :prepare + + if Gem::Version.new(PG::VERSION) >= Gem::Version.new("1.1.0") + alias_method :exec_params_without_profiling, :exec_params + end + + def prepare(*args, &blk) + # we have no choice but to do this here, + # if we do the check for profiling first, our cache may miss critical stuff + + @prepare_map ||= {} + @prepare_map[args[0]] = args[1] + # dont leak more than 10k ever + @prepare_map = {} if @prepare_map.length > 1000 + + return prepare_without_profiling(*args, &blk) unless SqlPatches.should_measure? + prepare_without_profiling(*args, &blk) + end + + def exec(*args, &blk) + return exec_without_profiling(*args, &blk) unless SqlPatches.should_measure? + + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + result = exec_without_profiling(*args, &blk) + elapsed_time = SqlPatches.elapsed_time(start) + record = ::Rack::MiniProfiler.record_sql(args[0], elapsed_time) + result.instance_variable_set("@miniprofiler_sql_id", record) if result + + result + end + + if Gem::Version.new(PG::VERSION) >= Gem::Version.new("1.1.0") + def exec_params(*args, &blk) + return exec_params_without_profiling(*args, &blk) unless SqlPatches.should_measure? + + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + result = exec_params_without_profiling(*args, &blk) + elapsed_time = SqlPatches.elapsed_time(start) + record = ::Rack::MiniProfiler.record_sql(args[0], elapsed_time) + result.instance_variable_set("@miniprofiler_sql_id", record) if result + + result + end + end + + def exec_prepared(*args, &blk) + return exec_prepared_without_profiling(*args, &blk) unless SqlPatches.should_measure? + + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + result = exec_prepared_without_profiling(*args, &blk) + elapsed_time = SqlPatches.elapsed_time(start) + mapped = args[0] + mapped = @prepare_map[mapped] || args[0] if @prepare_map + record = ::Rack::MiniProfiler.record_sql(mapped, elapsed_time) + result.instance_variable_set("@miniprofiler_sql_id", record) if result + + result + end + + def send_query_prepared(*args, &blk) + return send_query_prepared_without_profiling(*args, &blk) unless SqlPatches.should_measure? + + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + result = send_query_prepared_without_profiling(*args, &blk) + elapsed_time = SqlPatches.elapsed_time(start) + mapped = args[0] + mapped = @prepare_map[mapped] || args[0] if @prepare_map + record = ::Rack::MiniProfiler.record_sql(mapped, elapsed_time) + result.instance_variable_set("@miniprofiler_sql_id", record) if result + + result + end + + def async_exec(*args, &blk) + return async_exec_without_profiling(*args, &blk) unless SqlPatches.should_measure? + + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + result = exec_without_profiling(*args, &blk) + elapsed_time = SqlPatches.elapsed_time(start) + record = ::Rack::MiniProfiler.record_sql(args[0], elapsed_time) + result.instance_variable_set("@miniprofiler_sql_id", record) if result + + result + end + + alias_method :query, :exec +end diff --git a/lib/patches/db/pg/prepend.rb b/lib/patches/db/pg/prepend.rb new file mode 100644 index 00000000..abdb3cab --- /dev/null +++ b/lib/patches/db/pg/prepend.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class PG::Result + module MiniProfiler + def each(*args, &blk) + return super unless defined?(@miniprofiler_sql_id) + + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + result = super + elapsed_time = SqlPatches.elapsed_time(start) + + @miniprofiler_sql_id.report_reader_duration(elapsed_time) + result + end + end + + prepend MiniProfiler +end + +class PG::Connection + module MiniProfiler + def query(*args, &blk) + return super unless SqlPatches.should_measure? + + result, record = SqlPatches.record_sql(args[0]) do + super + end + result.instance_variable_set("@miniprofiler_sql_id", record) if result + result + end + end + + prepend MiniProfiler +end diff --git a/lib/prepend_pg_patch.rb b/lib/prepend_pg_patch.rb new file mode 100644 index 00000000..3a48a618 --- /dev/null +++ b/lib/prepend_pg_patch.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module Rack + MINI_PROFILER_PREPEND_PG_PATCH = true +end