Skip to content

Commit 4832251

Browse files
committed
Merge pull request #248 from adam-poit/fix-kill-query
Handle QueryInterrupted exceptions when canceling completed queries.
2 parents d9780e4 + 13cfd38 commit 4832251

File tree

3 files changed

+74
-2
lines changed

3 files changed

+74
-2
lines changed

src/MySqlConnector/MySqlClient/MySqlDataReader.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ private void DoClose()
287287
var connection = Command.Connection;
288288
if (!connection.BufferResultSets)
289289
connection.Session.FinishQuerying();
290+
290291
Command.ReaderClosed();
291292
if ((m_behavior & CommandBehavior.CloseConnection) != 0)
292293
{

src/MySqlConnector/Serialization/MySqlSession.cs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,30 @@ public void SetActiveReader(MySqlDataReader dataReader)
123123

124124
public void FinishQuerying()
125125
{
126+
bool clearConnection = false;
126127
lock (m_lock)
127128
{
128-
VerifyState(State.Querying, State.CancelingQuery);
129+
if (m_state == State.CancelingQuery)
130+
{
131+
m_state = State.ClearingPendingCancellation;
132+
clearConnection = true;
133+
}
134+
}
135+
136+
if (clearConnection)
137+
{
138+
// KILL QUERY will kill a subsequent query if the command it was intended to cancel has already completed.
139+
// In order to handle this case, we issue a dummy query that will consume the pending cancellation.
140+
// See https://bugs.mysql.com/bug.php?id=45679
141+
var payload = new PayloadData(new ArraySegment<byte>(PayloadUtilities.CreateEofStringPayload(CommandKind.Query, "DO SLEEP(0);")));
142+
SendAsync(payload, IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult();
143+
payload = ReceiveReplyAsync(IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult();
144+
OkPayload.Create(payload);
145+
}
146+
147+
lock (m_lock)
148+
{
149+
VerifyState(State.Querying, State.ClearingPendingCancellation);
129150
m_state = State.Connected;
130151
m_activeReader = null;
131152
m_activeCommand = null;
@@ -311,7 +332,7 @@ private void VerifyConnected()
311332
{
312333
if (m_state == State.Closed)
313334
throw new ObjectDisposedException(nameof(MySqlSession));
314-
if (m_state != State.Connected && m_state != State.Querying && m_state != State.CancelingQuery && m_state != State.Closing)
335+
if (m_state != State.Connected && m_state != State.Querying && m_state != State.CancelingQuery && m_state != State.ClearingPendingCancellation && m_state != State.Closing)
315336
throw new InvalidOperationException("MySqlSession is not connected.");
316337
}
317338
}
@@ -667,6 +688,9 @@ private enum State
667688
// The session is connected to a server and the active query is being cancelled.
668689
CancelingQuery,
669690

691+
// A cancellation is pending on the server and needs to be cleared.
692+
ClearingPendingCancellation,
693+
670694
// The session is closing.
671695
Closing,
672696

tests/SideBySide/CancelTests.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,53 @@ public async Task CancelCommandWithTokenBeforeExecuteReader()
228228
}
229229
}
230230

231+
[Fact]
232+
public async Task CancelCompletedCommand()
233+
{
234+
await m_database.Connection.ExecuteAsync(@"drop table if exists cancel_completed_command;
235+
create table cancel_completed_command (
236+
id bigint unsigned,
237+
value varchar(45)
238+
);").ConfigureAwait(false);
239+
240+
using (var cmd = m_database.Connection.CreateCommand())
241+
{
242+
cmd.CommandText = @"insert into cancel_completed_command (id, value) values (1, null);";
243+
244+
using (await cmd.ExecuteReaderAsync().ConfigureAwait(false))
245+
cmd.Cancel();
246+
}
247+
248+
using (var cmd = m_database.Connection.CreateCommand())
249+
{
250+
cmd.CommandText = @"update cancel_completed_command SET value = ""value"" where id = 1;";
251+
252+
await cmd.ExecuteNonQueryAsync().ConfigureAwait(false);
253+
}
254+
255+
using (var cmd = m_database.Connection.CreateCommand())
256+
{
257+
cmd.CommandText = "select value from cancel_completed_command where id = 1;";
258+
var value = (string) await cmd.ExecuteScalarAsync();
259+
Assert.Equal("value", value);
260+
}
261+
}
262+
263+
[Fact]
264+
public void ImplicitCancelWithDapper()
265+
{
266+
m_database.Connection.Execute(@"drop table if exists cancel_completed_command;
267+
create table cancel_completed_command(id integer not null primary key, value text null);");
268+
269+
// a query that returns 0 fields will cause Dapper to cancel the command
270+
m_database.Connection.Query<int>("insert into cancel_completed_command(id, value) values (1, null);");
271+
272+
m_database.Connection.Execute("update cancel_completed_command set value = 'value' where id = 1;");
273+
274+
var value = m_database.Connection.Query<string>(@"select value from cancel_completed_command where id = 1").FirstOrDefault();
275+
Assert.Equal("value", value);
276+
}
277+
231278
[UnbufferedResultSetsFact]
232279
public async Task CancelHugeQueryWithTokenAfterExecuteReader()
233280
{

0 commit comments

Comments
 (0)