diff --git a/src/MySqlConnector/MySqlClient/MySqlCommand.cs b/src/MySqlConnector/MySqlClient/MySqlCommand.cs index 5de8a9100..5984c7167 100644 --- a/src/MySqlConnector/MySqlClient/MySqlCommand.cs +++ b/src/MySqlConnector/MySqlClient/MySqlCommand.cs @@ -66,7 +66,6 @@ public override void Prepare() public override string CommandText { get; set; } public override int CommandTimeout { get; set; } - public bool IsCanceled { get; internal set; } public override CommandType CommandType { diff --git a/src/MySqlConnector/MySqlClient/MySqlDataReader.cs b/src/MySqlConnector/MySqlClient/MySqlDataReader.cs index 09c5d609a..cb67f5aa3 100644 --- a/src/MySqlConnector/MySqlClient/MySqlDataReader.cs +++ b/src/MySqlConnector/MySqlClient/MySqlDataReader.cs @@ -288,15 +288,6 @@ private void DoClose() if (!connection.BufferResultSets) connection.Session.FinishQuerying(); - if (Command.IsCanceled) - { - // KILL QUERY will kill a subsequent query if the command it was intended to cancel has already completed. - // In order to handle this case, we issue a dummy query to catch the QueryInterrupted exception. - // See https://bugs.mysql.com/bug.php?id=45679 - var killClearCommand = new MySqlCommand("DO SLEEP(0);", connection); - killClearCommand.ExecuteNonQuery(); - } - Command.ReaderClosed(); if ((m_behavior & CommandBehavior.CloseConnection) != 0) { diff --git a/src/MySqlConnector/Serialization/MySqlSession.cs b/src/MySqlConnector/Serialization/MySqlSession.cs index 1bf1a27b1..c78738198 100644 --- a/src/MySqlConnector/Serialization/MySqlSession.cs +++ b/src/MySqlConnector/Serialization/MySqlSession.cs @@ -84,8 +84,6 @@ public void DoCancel(MySqlCommand commandToCancel, MySqlCommand killCommand) // blocking the other thread for an extended duration. killCommand.CommandTimeout = 3; killCommand.ExecuteNonQuery(); - - commandToCancel.IsCanceled = true; } } @@ -125,9 +123,30 @@ public void SetActiveReader(MySqlDataReader dataReader) public void FinishQuerying() { + bool clearConnection = false; lock (m_lock) { - VerifyState(State.Querying, State.CancelingQuery); + if (m_state == State.CancelingQuery) + { + m_state = State.ClearingPendingCancellation; + clearConnection = true; + } + } + + if (clearConnection) + { + // KILL QUERY will kill a subsequent query if the command it was intended to cancel has already completed. + // In order to handle this case, we issue a dummy query that will consume the pending cancellation. + // See https://bugs.mysql.com/bug.php?id=45679 + var payload = new PayloadData(new ArraySegment(PayloadUtilities.CreateEofStringPayload(CommandKind.Query, "DO SLEEP(0);"))); + SendAsync(payload, IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult(); + payload = ReceiveReplyAsync(IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult(); + OkPayload.Create(payload); + } + + lock (m_lock) + { + VerifyState(State.Querying, State.ClearingPendingCancellation); m_state = State.Connected; m_activeReader = null; m_activeCommand = null; @@ -313,7 +332,7 @@ private void VerifyConnected() { if (m_state == State.Closed) throw new ObjectDisposedException(nameof(MySqlSession)); - if (m_state != State.Connected && m_state != State.Querying && m_state != State.CancelingQuery && m_state != State.Closing) + if (m_state != State.Connected && m_state != State.Querying && m_state != State.CancelingQuery && m_state != State.ClearingPendingCancellation && m_state != State.Closing) throw new InvalidOperationException("MySqlSession is not connected."); } } @@ -669,6 +688,9 @@ private enum State // The session is connected to a server and the active query is being cancelled. CancelingQuery, + // A cancellation is pending on the server and needs to be cleared. + ClearingPendingCancellation, + // The session is closing. Closing, diff --git a/tests/SideBySide/CancelTests.cs b/tests/SideBySide/CancelTests.cs index 412edb810..771889a28 100644 --- a/tests/SideBySide/CancelTests.cs +++ b/tests/SideBySide/CancelTests.cs @@ -251,6 +251,28 @@ value varchar(45) await cmd.ExecuteNonQueryAsync().ConfigureAwait(false); } + + using (var cmd = m_database.Connection.CreateCommand()) + { + cmd.CommandText = "select value from cancel_completed_command where id = 1;"; + var value = (string) await cmd.ExecuteScalarAsync(); + Assert.Equal("value", value); + } + } + + [Fact] + public void ImplicitCancelWithDapper() + { + m_database.Connection.Execute(@"drop table if exists cancel_completed_command; +create table cancel_completed_command(id integer not null primary key, value text null);"); + + // a query that returns 0 fields will cause Dapper to cancel the command + m_database.Connection.Query("insert into cancel_completed_command(id, value) values (1, null);"); + + m_database.Connection.Execute("update cancel_completed_command set value = 'value' where id = 1;"); + + var value = m_database.Connection.Query(@"select value from cancel_completed_command where id = 1").FirstOrDefault(); + Assert.Equal("value", value); } [UnbufferedResultSetsFact]