poollint 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +226 -0
- data/docs/pool_locking.md +59 -0
- data/docs/releasing.md +21 -0
- data/lib/poollint/allowed_settings.rb +32 -0
- data/lib/poollint/configuration.rb +170 -0
- data/lib/poollint/connection_state.rb +128 -0
- data/lib/poollint/errors.rb +18 -0
- data/lib/poollint/execution_state.rb +36 -0
- data/lib/poollint/hooks/checkin_hook.rb +15 -0
- data/lib/poollint/hooks/checkout_hook.rb +13 -0
- data/lib/poollint/hooks/lifecycle.rb +16 -0
- data/lib/poollint/hooks.rb +26 -0
- data/lib/poollint/inspection_runner.rb +41 -0
- data/lib/poollint/inspectors/mysql.rb +174 -0
- data/lib/poollint/inspectors/postgresql.rb +183 -0
- data/lib/poollint/inspectors/postgresql_capture.rb +58 -0
- data/lib/poollint/inspectors.rb +16 -0
- data/lib/poollint/kannuki_lock_resolver.rb +17 -0
- data/lib/poollint/notifier.rb +24 -0
- data/lib/poollint/railtie.rb +11 -0
- data/lib/poollint/report.rb +167 -0
- data/lib/poollint/sql_watcher.rb +167 -0
- data/lib/poollint/suspicion_log.rb +43 -0
- data/lib/poollint/version.rb +5 -0
- data/lib/poollint.rb +89 -0
- metadata +112 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PoolLint
|
|
4
|
+
SettingChange = Struct.new(
|
|
5
|
+
:name,
|
|
6
|
+
:baseline,
|
|
7
|
+
:current,
|
|
8
|
+
:reset_value,
|
|
9
|
+
:comparison,
|
|
10
|
+
keyword_init: true
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
AdvisoryLock = Struct.new(
|
|
14
|
+
:database,
|
|
15
|
+
:class_id,
|
|
16
|
+
:object_key,
|
|
17
|
+
:object_sub_id,
|
|
18
|
+
:mode,
|
|
19
|
+
:human_name,
|
|
20
|
+
keyword_init: true
|
|
21
|
+
) do
|
|
22
|
+
def fingerprint
|
|
23
|
+
[database, class_id, object_key, object_sub_id, mode]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def numeric_key
|
|
27
|
+
(class_id.to_i << 32) | object_key.to_i
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
UserLevelLock = Struct.new(
|
|
32
|
+
:name,
|
|
33
|
+
:acquisition_count,
|
|
34
|
+
:mode,
|
|
35
|
+
:status,
|
|
36
|
+
:confidence,
|
|
37
|
+
keyword_init: true
|
|
38
|
+
) do
|
|
39
|
+
def fingerprint
|
|
40
|
+
[name, mode, status]
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class Report
|
|
45
|
+
attr_reader :advisory_locks,
|
|
46
|
+
:inspection_point,
|
|
47
|
+
:setting_changes,
|
|
48
|
+
:suspicions,
|
|
49
|
+
:user_level_locks
|
|
50
|
+
|
|
51
|
+
def initialize(
|
|
52
|
+
inspection_point:,
|
|
53
|
+
setting_changes:,
|
|
54
|
+
advisory_locks:,
|
|
55
|
+
suspicions:,
|
|
56
|
+
user_level_locks: []
|
|
57
|
+
)
|
|
58
|
+
@inspection_point = inspection_point
|
|
59
|
+
@setting_changes = setting_changes
|
|
60
|
+
@advisory_locks = advisory_locks
|
|
61
|
+
@suspicions = suspicions
|
|
62
|
+
@user_level_locks = user_level_locks
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def leak?
|
|
66
|
+
setting_changes.any? || advisory_locks.any? || user_level_locks.any?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def to_h
|
|
70
|
+
{
|
|
71
|
+
inspection_point: inspection_point,
|
|
72
|
+
setting_changes: setting_changes.map(&:to_h),
|
|
73
|
+
advisory_locks: advisory_locks.map(&:to_h),
|
|
74
|
+
user_level_locks: user_level_locks.map(&:to_h),
|
|
75
|
+
suspicions: suspicions.map(&:to_h)
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def to_s
|
|
80
|
+
lines = [
|
|
81
|
+
"PoolLint::LeakedSessionState",
|
|
82
|
+
"",
|
|
83
|
+
boundary_message
|
|
84
|
+
]
|
|
85
|
+
append_setting_changes(lines)
|
|
86
|
+
append_advisory_locks(lines)
|
|
87
|
+
append_user_level_locks(lines)
|
|
88
|
+
append_suspicions(lines)
|
|
89
|
+
append_remediation(lines)
|
|
90
|
+
lines.join("\n")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def append_advisory_locks(lines)
|
|
96
|
+
advisory_locks.each do |lock|
|
|
97
|
+
lines << ""
|
|
98
|
+
lines << "Leaked advisory lock:"
|
|
99
|
+
lines << " name: #{lock.human_name}" if lock.human_name
|
|
100
|
+
lines << " key: (classid=#{lock.class_id}, objid=#{lock.object_key}, mode=#{lock.mode})"
|
|
101
|
+
location = latest_suspicion(kind: :advisory_lock)&.call_site
|
|
102
|
+
lines << " acquired at: #{location}" if location
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def append_setting_changes(lines)
|
|
107
|
+
setting_changes.each do |change|
|
|
108
|
+
expected = change.comparison == :baseline ? change.baseline : change.reset_value
|
|
109
|
+
lines << ""
|
|
110
|
+
lines << "Changed session setting:"
|
|
111
|
+
lines << " #{change.name}"
|
|
112
|
+
lines << " expected: #{expected.inspect}"
|
|
113
|
+
lines << " actual: #{change.current.inspect}"
|
|
114
|
+
location = latest_suspicion(setting: change.name)&.call_site
|
|
115
|
+
lines << " last SET at: #{location}" if location
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def append_user_level_locks(lines)
|
|
120
|
+
user_level_locks.each do |lock|
|
|
121
|
+
lines << ""
|
|
122
|
+
lines << "Leaked MySQL user-level lock:"
|
|
123
|
+
lines << " name: #{lock.name}"
|
|
124
|
+
lines << " confidence: #{lock.confidence}"
|
|
125
|
+
lines << " acquisition count: #{lock.acquisition_count}" if lock.acquisition_count
|
|
126
|
+
lines << " status: #{lock.status}" if lock.status
|
|
127
|
+
location = latest_suspicion(kind: :user_level_lock, lock_name: lock.name)&.call_site
|
|
128
|
+
lines << " acquired at: #{location}" if location
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def append_suspicions(lines)
|
|
133
|
+
return if suspicions.empty?
|
|
134
|
+
|
|
135
|
+
lines << ""
|
|
136
|
+
lines << "Suspicious statements:"
|
|
137
|
+
suspicions.each do |suspicion|
|
|
138
|
+
location = suspicion.call_site ? " (#{suspicion.call_site})" : ""
|
|
139
|
+
lines << " #{suspicion.sql.to_s.strip}#{location}"
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def append_remediation(lines)
|
|
144
|
+
lines << ""
|
|
145
|
+
lines << "Reset the state before releasing the connection"
|
|
146
|
+
lines << "(for example, RESET ROLE / RESET search_path / pg_advisory_unlock_all() /"
|
|
147
|
+
lines << "RELEASE_ALL_LOCKS()),"
|
|
148
|
+
lines << "or ensure cleanup runs in an ensure block."
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def boundary_message
|
|
152
|
+
if inspection_point == :checkin
|
|
153
|
+
return "Connection was returned to the pool with modified session state."
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
"Connection was handed out with session state left over\nfrom a previous owner."
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def latest_suspicion(kind: nil, setting: nil, lock_name: nil)
|
|
160
|
+
suspicions.reverse.find do |suspicion|
|
|
161
|
+
(!kind || suspicion.kind == kind) &&
|
|
162
|
+
(!setting || suspicion.setting == setting) &&
|
|
163
|
+
(!lock_name || suspicion.lock_name == lock_name)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/notifications"
|
|
4
|
+
|
|
5
|
+
module PoolLint
|
|
6
|
+
class SqlWatcher
|
|
7
|
+
Detection = Struct.new(
|
|
8
|
+
:kind,
|
|
9
|
+
:setting,
|
|
10
|
+
:lock_operation,
|
|
11
|
+
:lock_name,
|
|
12
|
+
keyword_init: true
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
ADVISORY_PATTERN = /
|
|
16
|
+
\bpg_(?:try_)?advisory_
|
|
17
|
+
(?:lock(?:_shared)?|unlock(?:_shared|_all)?)\s*\(
|
|
18
|
+
/ix
|
|
19
|
+
LEADING_COMMENTS = %r{\A(?:\s+|/\*.*?\*/|--[^\n]*(?:\n|\z))*}m
|
|
20
|
+
MYSQL_LOCK_OPERATIONS = {
|
|
21
|
+
"GET_LOCK" => :acquire,
|
|
22
|
+
"RELEASE_LOCK" => :release,
|
|
23
|
+
"RELEASE_ALL_LOCKS" => :release_all
|
|
24
|
+
}.freeze
|
|
25
|
+
MYSQL_LOCK_PATTERN = /\b(GET_LOCK|RELEASE_LOCK|RELEASE_ALL_LOCKS)\s*\(/i
|
|
26
|
+
SETTING_PATTERN = /\A[a-z_][a-z0-9_.-]*/i
|
|
27
|
+
SPECIAL_SETTINGS = {
|
|
28
|
+
"NAMES" => "client_encoding",
|
|
29
|
+
"ROLE" => "role",
|
|
30
|
+
"SCHEMA" => "search_path",
|
|
31
|
+
"SESSION AUTHORIZATION" => "session_authorization",
|
|
32
|
+
"TIME ZONE" => "timezone",
|
|
33
|
+
"XML OPTION" => "xmloption"
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
def install!
|
|
38
|
+
return if @subscriber
|
|
39
|
+
|
|
40
|
+
@subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
|
|
41
|
+
process(args.last)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def process(payload)
|
|
46
|
+
return if ExecutionState.inspecting?
|
|
47
|
+
return if payload[:name] == "SCHEMA"
|
|
48
|
+
|
|
49
|
+
connection = payload[:connection]
|
|
50
|
+
detection = detect(payload[:sql])
|
|
51
|
+
return unless connection && detection
|
|
52
|
+
|
|
53
|
+
state_for(connection).mark_dirty(
|
|
54
|
+
kind: detection.kind,
|
|
55
|
+
setting: detection.setting,
|
|
56
|
+
sql: payload[:sql],
|
|
57
|
+
call_site: application_call_site,
|
|
58
|
+
monitor_setting: PoolLint.configuration.track_custom_gucs,
|
|
59
|
+
lock_operation: detection.lock_operation,
|
|
60
|
+
lock_name: detection.lock_name
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def detect(sql)
|
|
65
|
+
statement = sql.to_s.sub(LEADING_COMMENTS, "").strip
|
|
66
|
+
return if statement.empty?
|
|
67
|
+
return Detection.new(kind: :advisory_lock) if statement.match?(ADVISORY_PATTERN)
|
|
68
|
+
|
|
69
|
+
detect_user_level_lock(statement) || detect_set(statement) || detect_reset(statement)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def application_call_site
|
|
75
|
+
location = caller_locations.find do |candidate|
|
|
76
|
+
path = candidate.absolute_path || candidate.path
|
|
77
|
+
path &&
|
|
78
|
+
!path.include?("/lib/poollint/") &&
|
|
79
|
+
!path.include?("/gems/") &&
|
|
80
|
+
!path.include?("/active_support/")
|
|
81
|
+
end
|
|
82
|
+
return unless location
|
|
83
|
+
|
|
84
|
+
"#{location.path}:#{location.lineno}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def detect_reset(statement)
|
|
88
|
+
match = statement.match(/\ARESET\s+(.+?)(?:\s*;)?\z/im)
|
|
89
|
+
return unless match
|
|
90
|
+
|
|
91
|
+
key = normalize_setting(match[1])
|
|
92
|
+
Detection.new(kind: :reset, setting: key == "all" ? nil : key)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def detect_user_level_lock(statement)
|
|
96
|
+
match = statement.match(MYSQL_LOCK_PATTERN)
|
|
97
|
+
return unless match
|
|
98
|
+
|
|
99
|
+
function = match[1].upcase
|
|
100
|
+
Detection.new(
|
|
101
|
+
kind: :user_level_lock,
|
|
102
|
+
lock_operation: MYSQL_LOCK_OPERATIONS.fetch(function),
|
|
103
|
+
lock_name: mysql_lock_name(statement, function)
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def state_for(connection)
|
|
108
|
+
ConnectionState.fetch(
|
|
109
|
+
connection,
|
|
110
|
+
suspicion_limit: PoolLint.configuration.suspicion_limit
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def detect_set(statement)
|
|
115
|
+
return if statement.match?(/\ASET\s+(?:LOCAL|TRANSACTION)\b/i)
|
|
116
|
+
|
|
117
|
+
session_authorization = statement.match?(/\ASET\s+SESSION\s+AUTHORIZATION\b/i)
|
|
118
|
+
return Detection.new(kind: :set, setting: "session_authorization") if session_authorization
|
|
119
|
+
|
|
120
|
+
mysql_transaction = detect_mysql_session_transaction(statement)
|
|
121
|
+
return mysql_transaction if mysql_transaction
|
|
122
|
+
|
|
123
|
+
match = statement.match(/\ASET\s+(?:SESSION\s+)?(.+)\z/im)
|
|
124
|
+
return unless match
|
|
125
|
+
|
|
126
|
+
body = match[1].strip
|
|
127
|
+
return if body.match?(/\A(?:CONSTRAINTS|TRANSACTION)\b/i)
|
|
128
|
+
|
|
129
|
+
key = normalize_setting(body)
|
|
130
|
+
Detection.new(kind: :set, setting: key)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def normalize_setting(body)
|
|
134
|
+
normalized = body.to_s.strip.sub(/;\z/, "")
|
|
135
|
+
normalized = normalized.sub(/\A@@(?:SESSION\.)?/i, "")
|
|
136
|
+
normalized = normalized.upcase
|
|
137
|
+
special = SPECIAL_SETTINGS.find { |name, _| normalized.start_with?(name) }
|
|
138
|
+
return special.last if special
|
|
139
|
+
|
|
140
|
+
normalized.downcase.match(SETTING_PATTERN)&.to_s
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def mysql_lock_name(statement, function)
|
|
144
|
+
return if function == "RELEASE_ALL_LOCKS"
|
|
145
|
+
|
|
146
|
+
pattern = /\b#{function}\s*\(\s*(?:'((?:\\.|''|[^'])*)'|"((?:\\.|""|[^"])*)")/im
|
|
147
|
+
match = statement.match(pattern)
|
|
148
|
+
return unless match
|
|
149
|
+
|
|
150
|
+
name = match[1]&.gsub("''", "'") || match[2]&.gsub('""', '"')
|
|
151
|
+
name&.gsub(/\\(.)/, '\1')
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def detect_mysql_session_transaction(statement)
|
|
155
|
+
match = statement.match(/\ASET\s+SESSION\s+TRANSACTION\s+(.+)\z/im)
|
|
156
|
+
return unless match
|
|
157
|
+
|
|
158
|
+
setting = if match[1].match?(/\AISOLATION\s+LEVEL\b/i)
|
|
159
|
+
"transaction_isolation"
|
|
160
|
+
elsif match[1].match?(/\AREAD\s+(?:ONLY|WRITE)\b/i)
|
|
161
|
+
"transaction_read_only"
|
|
162
|
+
end
|
|
163
|
+
Detection.new(kind: :set, setting: setting) if setting
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PoolLint
|
|
4
|
+
Suspicion = Struct.new(
|
|
5
|
+
:kind,
|
|
6
|
+
:setting,
|
|
7
|
+
:sql,
|
|
8
|
+
:call_site,
|
|
9
|
+
:lock_operation,
|
|
10
|
+
:lock_name,
|
|
11
|
+
keyword_init: true
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
class SuspicionLog
|
|
15
|
+
SQL_LIMIT = 200
|
|
16
|
+
|
|
17
|
+
def initialize(limit:)
|
|
18
|
+
@limit = limit
|
|
19
|
+
@entries = []
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def add(suspicion)
|
|
23
|
+
@entries.shift if @entries.length >= @limit
|
|
24
|
+
@entries << normalized(suspicion)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def clear
|
|
28
|
+
@entries.clear
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def entries
|
|
32
|
+
@entries.dup
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def normalized(suspicion)
|
|
38
|
+
suspicion.dup.tap do |entry|
|
|
39
|
+
entry.sql = entry.sql.to_s.slice(0, SQL_LIMIT)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/poollint.rb
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "poollint/version"
|
|
4
|
+
require_relative "poollint/errors"
|
|
5
|
+
require_relative "poollint/execution_state"
|
|
6
|
+
require_relative "poollint/configuration"
|
|
7
|
+
require_relative "poollint/suspicion_log"
|
|
8
|
+
require_relative "poollint/connection_state"
|
|
9
|
+
require_relative "poollint/sql_watcher"
|
|
10
|
+
require_relative "poollint/report"
|
|
11
|
+
require_relative "poollint/allowed_settings"
|
|
12
|
+
require_relative "poollint/kannuki_lock_resolver"
|
|
13
|
+
require_relative "poollint/inspectors/postgresql_capture"
|
|
14
|
+
require_relative "poollint/inspectors/postgresql"
|
|
15
|
+
require_relative "poollint/inspectors/mysql"
|
|
16
|
+
require_relative "poollint/inspectors"
|
|
17
|
+
require_relative "poollint/notifier"
|
|
18
|
+
require_relative "poollint/inspection_runner"
|
|
19
|
+
require_relative "poollint/hooks/checkout_hook"
|
|
20
|
+
require_relative "poollint/hooks/checkin_hook"
|
|
21
|
+
require_relative "poollint/hooks/lifecycle"
|
|
22
|
+
require_relative "poollint/hooks"
|
|
23
|
+
|
|
24
|
+
module PoolLint
|
|
25
|
+
class << self
|
|
26
|
+
def configuration
|
|
27
|
+
@configuration ||= Configuration.new
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def configure
|
|
31
|
+
yield configuration
|
|
32
|
+
configuration.validate!
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def connection_state(connection)
|
|
36
|
+
ConnectionState.fetch(
|
|
37
|
+
connection,
|
|
38
|
+
suspicion_limit: configuration.suspicion_limit
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def install!
|
|
43
|
+
require "active_record"
|
|
44
|
+
|
|
45
|
+
SqlWatcher.install!
|
|
46
|
+
Hooks.install!
|
|
47
|
+
warn_about_checkin_in_production
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def log_warning(message)
|
|
51
|
+
logger = configuration.logger
|
|
52
|
+
logger ||= ActiveRecord::Base.logger if defined?(ActiveRecord::Base)
|
|
53
|
+
formatted = "[PoolLint] #{message}"
|
|
54
|
+
return warn_with_logger(logger, formatted) if logger
|
|
55
|
+
|
|
56
|
+
Kernel.warn(formatted)
|
|
57
|
+
rescue StandardError
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def reset_configuration!(environment: nil)
|
|
62
|
+
@configuration = Configuration.new(environment: environment)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def suppress(&)
|
|
66
|
+
ExecutionState.suppress(&)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def warn_with_logger(logger, message)
|
|
72
|
+
logger.warn(message)
|
|
73
|
+
rescue StandardError => e
|
|
74
|
+
Kernel.warn("#{message} (logger failed: #{e.class}: #{e.message})")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def warn_about_checkin_in_production
|
|
78
|
+
return unless configuration.inspection_point == :checkin
|
|
79
|
+
return if configuration.test_environment?
|
|
80
|
+
|
|
81
|
+
log_warning(
|
|
82
|
+
":checkin inspection runs while the Active Record pool mutex is held; " \
|
|
83
|
+
"use :checkout in production"
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
require_relative "poollint/railtie" if defined?(Rails::Railtie)
|
metadata
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: poollint
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Yudai Takada
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activerecord
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.1'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '9'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '7.1'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '9'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: railties
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '7.1'
|
|
39
|
+
- - "<"
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '9'
|
|
42
|
+
type: :runtime
|
|
43
|
+
prerelease: false
|
|
44
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '7.1'
|
|
49
|
+
- - "<"
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '9'
|
|
52
|
+
description: |
|
|
53
|
+
PoolLint checks Active Record connections at pool boundaries and reports
|
|
54
|
+
PostgreSQL or MySQL settings and locks that leaked from a previous borrower.
|
|
55
|
+
email:
|
|
56
|
+
- t.yudai92@gmail.com
|
|
57
|
+
executables: []
|
|
58
|
+
extensions: []
|
|
59
|
+
extra_rdoc_files: []
|
|
60
|
+
files:
|
|
61
|
+
- LICENSE.txt
|
|
62
|
+
- README.md
|
|
63
|
+
- docs/pool_locking.md
|
|
64
|
+
- docs/releasing.md
|
|
65
|
+
- lib/poollint.rb
|
|
66
|
+
- lib/poollint/allowed_settings.rb
|
|
67
|
+
- lib/poollint/configuration.rb
|
|
68
|
+
- lib/poollint/connection_state.rb
|
|
69
|
+
- lib/poollint/errors.rb
|
|
70
|
+
- lib/poollint/execution_state.rb
|
|
71
|
+
- lib/poollint/hooks.rb
|
|
72
|
+
- lib/poollint/hooks/checkin_hook.rb
|
|
73
|
+
- lib/poollint/hooks/checkout_hook.rb
|
|
74
|
+
- lib/poollint/hooks/lifecycle.rb
|
|
75
|
+
- lib/poollint/inspection_runner.rb
|
|
76
|
+
- lib/poollint/inspectors.rb
|
|
77
|
+
- lib/poollint/inspectors/mysql.rb
|
|
78
|
+
- lib/poollint/inspectors/postgresql.rb
|
|
79
|
+
- lib/poollint/inspectors/postgresql_capture.rb
|
|
80
|
+
- lib/poollint/kannuki_lock_resolver.rb
|
|
81
|
+
- lib/poollint/notifier.rb
|
|
82
|
+
- lib/poollint/railtie.rb
|
|
83
|
+
- lib/poollint/report.rb
|
|
84
|
+
- lib/poollint/sql_watcher.rb
|
|
85
|
+
- lib/poollint/suspicion_log.rb
|
|
86
|
+
- lib/poollint/version.rb
|
|
87
|
+
homepage: https://github.com/ydah/poollint
|
|
88
|
+
licenses:
|
|
89
|
+
- MIT
|
|
90
|
+
metadata:
|
|
91
|
+
allowed_push_host: https://rubygems.org
|
|
92
|
+
homepage_uri: https://github.com/ydah/poollint
|
|
93
|
+
source_code_uri: https://github.com/ydah/poollint
|
|
94
|
+
rubygems_mfa_required: 'true'
|
|
95
|
+
rdoc_options: []
|
|
96
|
+
require_paths:
|
|
97
|
+
- lib
|
|
98
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: 3.2.0
|
|
103
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
104
|
+
requirements:
|
|
105
|
+
- - ">="
|
|
106
|
+
- !ruby/object:Gem::Version
|
|
107
|
+
version: '0'
|
|
108
|
+
requirements: []
|
|
109
|
+
rubygems_version: 4.0.6
|
|
110
|
+
specification_version: 4
|
|
111
|
+
summary: Detect database session state leaked through Active Record connection pools
|
|
112
|
+
test_files: []
|