Skip to content

Commit

Permalink
Similar to the mysql2 patch, this workaround is for PG (Postgresql) u…
Browse files Browse the repository at this point in the history
…sers who are experiencing conflict between rack-mini-profiler and another gem that does the same patching.
  • Loading branch information
okarsono committed May 1, 2023
1 parent b6cf1b5 commit b1359dc
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 119 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
123 changes: 4 additions & 119 deletions lib/patches/db/pg.rb
Original file line number Diff line number Diff line change
@@ -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
123 changes: 123 additions & 0 deletions lib/patches/db/pg/alias_method.rb
Original file line number Diff line number Diff line change
@@ -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
34 changes: 34 additions & 0 deletions lib/patches/db/pg/prepend.rb
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions lib/prepend_pg_patch.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true

module Rack
MINI_PROFILER_PREPEND_PG_PATCH = true
end

0 comments on commit b1359dc

Please sign in to comment.