From 0e2cd3d749dadcdb5027d48399aea02ef74815f3 Mon Sep 17 00:00:00 2001 From: Ryuta Kamizono Date: Mon, 27 Nov 2017 11:54:59 +0900 Subject: Add new error class `QueryCanceled` which will be raised when canceling statement due to user request (#31235) This changes `StatementTimeout` to `QueryCanceled` for PostgreSQL. In MySQL, errno 1317 (`ER_QUERY_INTERRUPTED`) is only used when the query is manually cancelled. But in PostgreSQL, `QUERY_CANCELED` error code (57014) which is used `StatementTimeout` is also used when the both case. And, we can not tell which reason happened. So I decided to introduce new error class `QueryCanceled` closer to the error code name. --- activerecord/CHANGELOG.md | 5 ++++ .../connection_adapters/abstract_mysql_adapter.rb | 3 +++ .../connection_adapters/postgresql_adapter.rb | 2 +- activerecord/lib/active_record/errors.rb | 4 +++ .../test/cases/adapters/mysql2/transaction_test.rb | 27 +++++++++++++++++++ .../cases/adapters/postgresql/transaction_test.rb | 31 ++++++++++++++++++++-- 6 files changed, 69 insertions(+), 3 deletions(-) diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index ccd2aa90e1..40a26d98e1 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,8 @@ +* Add new error class `QueryCanceled` which will be raised + when canceling statement due to user request. + + *Ryuta Kamizono* + * Add `#up_only` to database migrations for code that is only relevant when migrating up, e.g. populating a new column. diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 77ccac54bf..ede8a9c1e2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -635,6 +635,7 @@ module ActiveRecord ER_CANNOT_ADD_FOREIGN = 1215 ER_CANNOT_CREATE_TABLE = 1005 ER_LOCK_WAIT_TIMEOUT = 1205 + ER_QUERY_INTERRUPTED = 1317 ER_QUERY_TIMEOUT = 3024 def translate_exception(exception, message) @@ -663,6 +664,8 @@ module ActiveRecord LockWaitTimeout.new(message) when ER_QUERY_TIMEOUT StatementTimeout.new(message) + when ER_QUERY_INTERRUPTED + QueryCanceled.new(message) else super end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 489feadb00..7b27f6b7a0 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -420,7 +420,7 @@ module ActiveRecord when LOCK_NOT_AVAILABLE LockWaitTimeout.new(message) when QUERY_CANCELED - StatementTimeout.new(message) + QueryCanceled.new(message) else super end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 89e777ffc3..efcbd44776 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -343,6 +343,10 @@ module ActiveRecord class StatementTimeout < StatementInvalid end + # QueryCanceled will be raised when canceling statement due to user request. + class QueryCanceled < StatementInvalid + end + # UnknownAttributeReference is raised when an unknown and potentially unsafe # value is passed to a query method when allow_unsafe_raw_sql is set to # :disabled. For example, passing a non column name value to a relation's diff --git a/activerecord/test/cases/adapters/mysql2/transaction_test.rb b/activerecord/test/cases/adapters/mysql2/transaction_test.rb index da4b73a0da..cb183cc54c 100644 --- a/activerecord/test/cases/adapters/mysql2/transaction_test.rb +++ b/activerecord/test/cases/adapters/mysql2/transaction_test.rb @@ -116,5 +116,32 @@ module ActiveRecord end end end + + test "raises QueryCanceled when canceling statement due to user request" do + assert_raises(ActiveRecord::QueryCanceled) do + s = Sample.create!(value: 1) + latch = Concurrent::CountDownLatch.new + + thread = Thread.new do + Sample.transaction do + Sample.lock.find(s.id) + latch.count_down + sleep(0.5) + conn = Sample.connection + pid = conn.query_value("SELECT id FROM information_schema.processlist WHERE info LIKE '% FOR UPDATE'") + conn.execute("KILL QUERY #{pid}") + end + end + + begin + Sample.transaction do + latch.wait + Sample.lock.find(s.id) + end + ensure + thread.join + end + end + end end end diff --git a/activerecord/test/cases/adapters/postgresql/transaction_test.rb b/activerecord/test/cases/adapters/postgresql/transaction_test.rb index ebba0f0c1c..c24dfeb345 100644 --- a/activerecord/test/cases/adapters/postgresql/transaction_test.rb +++ b/activerecord/test/cases/adapters/postgresql/transaction_test.rb @@ -120,8 +120,8 @@ module ActiveRecord end end - test "raises StatementTimeout when statement timeout exceeded" do - assert_raises(ActiveRecord::StatementTimeout) do + test "raises QueryCanceled when statement timeout exceeded" do + assert_raises(ActiveRecord::QueryCanceled) do s = Sample.create!(value: 1) latch1 = Concurrent::CountDownLatch.new latch2 = Concurrent::CountDownLatch.new @@ -148,6 +148,33 @@ module ActiveRecord end end + test "raises QueryCanceled when canceling statement due to user request" do + assert_raises(ActiveRecord::QueryCanceled) do + s = Sample.create!(value: 1) + latch = Concurrent::CountDownLatch.new + + thread = Thread.new do + Sample.transaction do + Sample.lock.find(s.id) + latch.count_down + sleep(0.5) + conn = Sample.connection + pid = conn.query_value("SELECT pid FROM pg_stat_activity WHERE query LIKE '% FOR UPDATE'") + conn.execute("SELECT pg_cancel_backend(#{pid})") + end + end + + begin + Sample.transaction do + latch.wait + Sample.lock.find(s.id) + end + ensure + thread.join + end + end + end + private def with_warning_suppression -- cgit v1.2.3