Skip to content

Commit 7d07bf8

Browse files
authored
Add a statement timeout to queries [PIMCORE-485] (#31)
1 parent a4f1228 commit 7d07bf8

File tree

6 files changed

+88
-11
lines changed

6 files changed

+88
-11
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# postgres-vacuum-monitor
22

3+
## v0.17.0
4+
- Increased default `monitor_max_run_time_seconds` to 60 seconds.
5+
- Added `monitor_statement_timeout_seconds` (defaults to 10 seconds) to limit query runtime.
6+
- Eagerly clear connection pools when a `ActiveRecord::StatementInvalid` or `ActiveRecord::ConnectionTimeoutError`
7+
is encountered to attempt to clear bad connections.
8+
39
## v0.16.0
410
- Add `max_attempts` and `max_run_time` to `Postgres::Vacuum::Jobs::MonitorJob` to avoid backing up the queue. The
511
defaults are 1 attempt and 10 seconds, but they can be configured with `monitor_max_attempts` and

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,10 @@ Postgres::Vacuum::Monitor.configure do |config|
3737
config.long_running_transaction_threshold_seconds = 10 * 60
3838
# Optionally change `max_attempts` of the monitor job (default 1)
3939
config.monitor_max_attempts = 3
40-
# Optionally change `max_run_time` of the monitor job (default 10 seconds)
40+
# Optionally change `max_run_time` of the monitor job (default 60 seconds)
4141
config.monitor_max_run_time_seconds = 5
42+
# Optionally change the statement timeout of queries (default 10 seconds)
43+
config.monitor_statement_timeout_seconds = 5
4244
end
4345
```
4446

lib/postgres/vacuum/configuration.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,22 @@ module Postgres
44
module Vacuum
55
class Configuration
66
DEFAULT_LONG_RUNNING_TRANSACTION_THRESHOLD_SECONDS = 5 * 60
7-
DEFAULT_MONITOR_MAX_RUN_TIME_SECONDS = 10
7+
DEFAULT_MONITOR_MAX_RUN_TIME_SECONDS = 60
88
DEFAULT_MONITOR_MAX_ATTEMPTS = 1
9+
DEFAULT_MONITOR_STATEMENT_TIMEOUT_SECONDS = 10
910

1011
attr_accessor :monitor_reporter_class_name,
1112
:long_running_transaction_threshold_seconds,
1213
:monitor_max_run_time_seconds,
13-
:monitor_max_attempts
14+
:monitor_max_attempts,
15+
:monitor_statement_timeout_seconds
1416

1517
def initialize
1618
self.monitor_reporter_class_name = nil
1719
self.long_running_transaction_threshold_seconds = DEFAULT_LONG_RUNNING_TRANSACTION_THRESHOLD_SECONDS
1820
self.monitor_max_run_time_seconds = DEFAULT_MONITOR_MAX_RUN_TIME_SECONDS
1921
self.monitor_max_attempts = DEFAULT_MONITOR_MAX_ATTEMPTS
22+
self.monitor_statement_timeout_seconds = DEFAULT_MONITOR_STATEMENT_TIMEOUT_SECONDS
2023
end
2124
end
2225
end

lib/postgres/vacuum/jobs/monitor_job.rb

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,42 @@ def with_each_db_name_and_connection
9898
databases = Set.new
9999
ActiveRecord::Base.connection_handler.connection_pools.map do |connection_pool|
100100
db_name = connection_pool.db_config.configuration_hash[:database]
101+
next unless databases.add?(db_name)
102+
103+
# ActiveRecord allocates a connection pool per call to `.establish_connection`
104+
# As a result, multiple pools might interact with the same database, so we use
105+
# the database name to dedupe.
106+
connection_pool.with_connection do |connection|
107+
original_timeout = statement_timeout(connection)
108+
set_statement_timeout(connection, "#{configured_timeout_seconds}s")
109+
110+
yield(db_name, connection)
111+
ensure
112+
set_statement_timeout(connection, original_timeout)
113+
end
101114

102-
# activerecord allocates a connection pool per call to establish_connection
103-
# multiple pools might interact with the same database so we use the
104-
# database name to dedup
105-
connection_pool.with_connection { |conn| yield(db_name, conn) } if databases.add?(db_name)
115+
# We want to avoid hanging onto a bad connection that would cause all future
116+
# jobs to fail, so we eagerly clear the pool.
117+
rescue ActiveRecord::StatementInvalid, ActiveRecord::ConnectionTimeoutError
118+
connection_pool.disconnect!
119+
raise
106120
end
107121
end
108122

123+
def statement_timeout(connection)
124+
result = connection.execute('SHOW statement_timeout').first
125+
result['statement_timeout'] if result.present?
126+
end
127+
128+
def set_statement_timeout(connection, timeout)
129+
query = ActiveRecord::Base.sanitize_sql(['SET statement_timeout = ?', timeout])
130+
connection.execute(query)
131+
end
132+
133+
def configured_timeout_seconds
134+
Postgres::Vacuum::Monitor.configuration.monitor_statement_timeout_seconds
135+
end
136+
109137
ConfigurationError = Class.new(StandardError)
110138
end
111139
end

lib/postgres/vacuum/monitor/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
module Postgres
44
module Vacuum
55
module Monitor
6-
VERSION = '0.16.0'
6+
VERSION = '0.17.0'
77
end
88
end
99
end

spec/postgres/vacuum/jobs/monitor_job_spec.rb

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ def self.report_event(name, attributes = {})
168168

169169
it "reports once for a single database." do
170170
expect(job.perform).to eq true
171-
expect(mock_connection).to have_received(:execute).exactly(5)
171+
expect(mock_connection).to have_received(:execute).exactly(8)
172172
end
173173

174174
context "to different databases" do
@@ -181,10 +181,48 @@ def self.report_event(name, attributes = {})
181181

182182
it "reports twice for two databases" do
183183
expect(job.perform).to eq true
184-
expect(mock_connection).to have_received(:execute).exactly(10)
184+
expect(mock_connection).to have_received(:execute).exactly(16)
185185
end
186186
end
187187
end
188+
189+
describe "statement timeouts" do
190+
it "sets and restored statement timeout" do
191+
original_timeout = [{ 'statement_timeout' => '20m' }]
192+
allow(mock_connection).to receive(:execute).with('SHOW statement_timeout').and_return(original_timeout)
193+
194+
job.perform
195+
196+
expect(mock_connection).to have_received(:execute).with("SET statement_timeout = '10s'").ordered
197+
expect(mock_connection).to have_received(:execute).with("SET statement_timeout = '20m'").ordered
198+
end
199+
end
200+
201+
describe "connection resetting" do
202+
it "resets the pool on statement error" do
203+
pool = ActiveRecord::Base.connection_handler.connection_pools[0]
204+
allow(pool).to receive(:disconnect!)
205+
206+
error = ActiveRecord::QueryCanceled.new
207+
query = Postgres::Vacuum::Monitor::Query.connection_idle_time
208+
allow(mock_connection).to receive(:execute).with(query).and_raise(error)
209+
210+
expect { job.perform }.to raise_error(error)
211+
expect(pool).to have_received(:disconnect!)
212+
end
213+
214+
it "resets the pool on connection timeout error" do
215+
pool = ActiveRecord::Base.connection_handler.connection_pools[0]
216+
allow(pool).to receive(:disconnect!)
217+
218+
error = ActiveRecord::ConnectionTimeoutError.new
219+
query = Postgres::Vacuum::Monitor::Query.connection_idle_time
220+
allow(mock_connection).to receive(:execute).with(query).and_raise(error)
221+
222+
expect { job.perform }.to raise_error(error)
223+
expect(pool).to have_received(:disconnect!)
224+
end
225+
end
188226
end
189227
end
190228

@@ -216,7 +254,7 @@ def self.report_event(name, attributes = {})
216254
describe "#max_run_time" do
217255
context "with default configuration" do
218256
it "times out after 10 seconds" do
219-
expect(job.max_run_time).to eq(10.seconds)
257+
expect(job.max_run_time).to eq(60.seconds)
220258
end
221259
end
222260

0 commit comments

Comments
 (0)