Skip to content

Commit 3e4d8f5

Browse files
committed
Implement MySqlCommand.Cancel.
Opens a separate connection to the server to execute 'KILL QUERY n' for the current connection.
1 parent 596a139 commit 3e4d8f5

File tree

8 files changed

+304
-12
lines changed

8 files changed

+304
-12
lines changed

src/MySqlConnector/MySqlClient/CommandExecutors/TextCommandExecutor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public virtual async Task<object> ExecuteScalarAsync(string commandText, MySqlPa
5050
public virtual async Task<DbDataReader> ExecuteReaderAsync(string commandText, MySqlParameterCollection parameterCollection,
5151
CommandBehavior behavior, IOBehavior ioBehavior, CancellationToken cancellationToken)
5252
{
53-
m_command.Connection.Session.StartQuerying();
53+
m_command.Connection.Session.StartQuerying(m_command);
5454
m_command.LastInsertedId = -1;
5555
var statementPreparerOptions = StatementPreparerOptions.None;
5656
if (m_command.Connection.AllowUserVariables || m_command.CommandType == CommandType.StoredProcedure)

src/MySqlConnector/MySqlClient/MySqlCommand.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,7 @@ public MySqlCommand(string commandText, MySqlConnection connection, MySqlTransac
4848
}
4949
}
5050

51-
public override void Cancel()
52-
{
53-
// documentation says this shouldn't throw (but just fail silently), but for now make it explicit that this doesn't work
54-
throw new NotSupportedException("Use the Async overloads with a CancellationToken.");
55-
}
51+
public override void Cancel() => Connection.Cancel(this);
5652

5753
public override int ExecuteNonQuery() =>
5854
ExecuteNonQueryAsync(IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult();

src/MySqlConnector/MySqlClient/MySqlConnection.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,37 @@ internal MySqlSession Session
209209
}
210210
}
211211

212+
internal void Cancel(MySqlCommand command)
213+
{
214+
var session = Session;
215+
if (!session.TryStartCancel(command))
216+
return;
217+
218+
try
219+
{
220+
// open a dedicated connection to the server to kill the active query
221+
var csb = new MySqlConnectionStringBuilder(m_connectionStringBuilder.GetConnectionString(includePassword: true));
222+
csb.Pooling = false;
223+
if (m_session.IPAddress != null)
224+
csb.Server = m_session.IPAddress.ToString();
225+
csb.ConnectionTimeout = 3u;
226+
227+
using (var connection = new MySqlConnection(csb.ConnectionString))
228+
{
229+
connection.Open();
230+
using (var killCommand = new MySqlCommand("KILL QUERY {0}".FormatInvariant(command.Connection.ServerThread), connection))
231+
{
232+
session.DoCancel(command, killCommand);
233+
}
234+
}
235+
}
236+
catch (MySqlException)
237+
{
238+
// cancelling the query failed; setting the state back to 'Querying' will allow another call to 'Cancel' to try again
239+
session.AbortCancel(command);
240+
}
241+
}
242+
212243
internal async Task<CachedProcedure> GetCachedProcedure(IOBehavior ioBehavior, string name, CancellationToken cancellationToken)
213244
{
214245
if (State != ConnectionState.Open)

src/MySqlConnector/MySqlClient/MySqlDataReader.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,9 +257,17 @@ private void DoClose()
257257
{
258258
if (Command != null)
259259
{
260-
while (NextResult())
260+
try
261261
{
262+
while (NextResult())
263+
{
264+
}
262265
}
266+
catch (MySqlException ex) when (ex.Number == (int) MySqlErrorCode.QueryInterrupted)
267+
{
268+
// ignore "Query execution was interrupted" exceptions when closing a data reader
269+
}
270+
263271
m_resultSet = null;
264272
m_resultSetBuffered = null;
265273
m_nextResultSetBuffer.Clear();

src/MySqlConnector/MySqlClient/MySqlErrorCode.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,10 @@ public enum MySqlErrorCode
99
/// You have an error in your SQL syntax (ER_PARSE_ERROR).
1010
/// </summary>
1111
ParseError = 1064,
12+
13+
/// <summary>
14+
/// Query execution was interrupted (ER_QUERY_INTERRUPTED).
15+
/// </summary>
16+
QueryInterrupted = 1317,
1217
}
1318
}

src/MySqlConnector/MySqlClient/Results/ResultSet.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,18 @@ private ValueTask<Row> ScanRowAsync(IOBehavior ioBehavior, Row row, Cancellation
167167
? new ValueTask<Row>(ScanRowAsyncRemainder(payloadValueTask.Result))
168168
: new ValueTask<Row>(ScanRowAsyncAwaited(payloadValueTask.AsTask()));
169169

170-
async Task<Row> ScanRowAsyncAwaited(Task<PayloadData> payloadTask) => ScanRowAsyncRemainder(await payloadTask.ConfigureAwait(false));
170+
async Task<Row> ScanRowAsyncAwaited(Task<PayloadData> payloadTask)
171+
{
172+
try
173+
{
174+
return ScanRowAsyncRemainder(await payloadTask.ConfigureAwait(false));
175+
}
176+
catch (MySqlException ex) when (ex.Number == (int) MySqlErrorCode.QueryInterrupted)
177+
{
178+
BufferState = State = ResultSetState.NoMoreData;
179+
throw;
180+
}
181+
}
171182

172183
Row ScanRowAsyncRemainder(PayloadData payload)
173184
{

src/MySqlConnector/Serialization/MySqlSession.cs

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public MySqlSession(ConnectionPool pool, int poolGeneration)
3636
public ConnectionPool Pool { get; }
3737
public int PoolGeneration { get; }
3838
public string DatabaseOverride { get; set; }
39+
public IPAddress IPAddress => (m_tcpClient?.Client.RemoteEndPoint as IPEndPoint)?.Address;
3940

4041
public void ReturnToPool() => Pool?.Return(this);
4142

@@ -48,23 +49,66 @@ public bool IsConnected
4849
}
4950
}
5051

51-
public void StartQuerying()
52+
public bool TryStartCancel(MySqlCommand command)
5253
{
5354
lock (m_lock)
5455
{
55-
if (m_state == State.Querying)
56+
if (m_activeCommand != command)
57+
return false;
58+
VerifyState(State.Querying, State.CancelingQuery);
59+
if (m_state != State.Querying)
60+
return false;
61+
m_state = State.CancelingQuery;
62+
}
63+
64+
return true;
65+
}
66+
67+
public void DoCancel(MySqlCommand commandToCancel, MySqlCommand killCommand)
68+
{
69+
lock (m_lock)
70+
{
71+
if (m_activeCommand != commandToCancel)
72+
return;
73+
74+
// NOTE: This command is executed while holding the lock to prevent race conditions during asynchronous cancellation.
75+
// For example, if the lock weren't held, the current command could finish and the other thread could set m_activeCommand
76+
// to null, then start executing a new command. By the time this "KILL QUERY" command reached the server, the wrong
77+
// command would be killed (because "KILL QUERY" specifies the connection whose command should be killed, not
78+
// a unique identifier of the command itself). As a mitigation, we set the CommandTimeout to a low value to avoid
79+
// blocking the other thread for an extended duration.
80+
killCommand.CommandTimeout = 3;
81+
killCommand.ExecuteNonQuery();
82+
}
83+
}
84+
85+
public void AbortCancel(MySqlCommand command)
86+
{
87+
lock (m_lock)
88+
{
89+
if (m_activeCommand == command && m_state == State.CancelingQuery)
90+
m_state = State.Querying;
91+
}
92+
}
93+
94+
public void StartQuerying(MySqlCommand command)
95+
{
96+
lock (m_lock)
97+
{
98+
if (m_state == State.Querying || m_state == State.CancelingQuery)
5699
throw new MySqlException("There is already an open DataReader associated with this Connection which must be closed first.");
57100

58101
VerifyState(State.Connected);
59102
m_state = State.Querying;
103+
m_activeCommand = command;
60104
}
61105
}
62106

63107
public MySqlDataReader ActiveReader => m_activeReader;
64108

65109
public void SetActiveReader(MySqlDataReader dataReader)
66110
{
67-
VerifyState(State.Querying);
111+
VerifyState(State.Querying, State.CancelingQuery);
68112
if (dataReader == null)
69113
throw new ArgumentNullException(nameof(dataReader));
70114
if (m_activeReader != null)
@@ -76,9 +120,10 @@ public void FinishQuerying()
76120
{
77121
lock (m_lock)
78122
{
79-
VerifyState(State.Querying);
123+
VerifyState(State.Querying, State.CancelingQuery);
80124
m_state = State.Connected;
81125
m_activeReader = null;
126+
m_activeCommand = null;
82127
}
83128
}
84129

@@ -634,6 +679,7 @@ private enum State
634679
Socket m_socket;
635680
NetworkStream m_networkStream;
636681
IPayloadHandler m_payloadHandler;
682+
MySqlCommand m_activeCommand;
637683
MySqlDataReader m_activeReader;
638684
}
639685
}

0 commit comments

Comments
 (0)