Skip to content

Commit 5a97a56

Browse files
committed
Remove strong references. Fixes #306
The code to recover leaked sessions depends on MySqlConnection being GCed. If MySqlSession holds strong references to MySqlConnection (via MySqlCommand or MySqlDataReader), then failure to dispose a MySqlDataReader will leak the connection.
1 parent b12e210 commit 5a97a56

File tree

5 files changed

+59
-27
lines changed

5 files changed

+59
-27
lines changed

src/MySqlConnector/MySqlClient/MySqlCommand.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public MySqlCommand(string commandText, MySqlConnection connection)
3232

3333
public MySqlCommand(string commandText, MySqlConnection connection, MySqlTransaction transaction)
3434
{
35+
CommandId = Interlocked.Increment(ref s_commandId);
3536
CommandText = commandText;
3637
DbConnection = connection;
3738
DbTransaction = transaction;
@@ -166,6 +167,8 @@ internal IDisposable RegisterCancel(CancellationToken token)
166167
return token.Register(m_cancelAction);
167168
}
168169

170+
internal int CommandId { get; }
171+
169172
private void VerifyNotDisposed()
170173
{
171174
if (m_parameterCollection == null)
@@ -190,6 +193,8 @@ private bool IsValid(out Exception exception)
190193

191194
internal void ReaderClosed() => (m_commandExecutor as StoredProcedureCommandExecutor)?.SetParams();
192195

196+
static int s_commandId = 1;
197+
193198
MySqlParameterCollection m_parameterCollection;
194199
CommandType m_commandType;
195200
ICommandExecutor m_commandExecutor;

src/MySqlConnector/MySqlClient/MySqlConnection.cs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,14 +290,28 @@ internal async Task<CachedProcedure> GetCachedProcedure(IOBehavior ioBehavior, s
290290
}
291291

292292
internal MySqlTransaction CurrentTransaction { get; set; }
293-
internal MySqlDataReader ActiveReader => m_session.ActiveReader;
294293
internal bool AllowUserVariables => m_connectionSettings.AllowUserVariables;
295294
internal bool BufferResultSets => m_connectionSettings.BufferResultSets;
296295
internal bool ConvertZeroDateTime => m_connectionSettings.ConvertZeroDateTime;
297296
internal bool OldGuids => m_connectionSettings.OldGuids;
298297
internal bool TreatTinyAsBoolean => m_connectionSettings.TreatTinyAsBoolean;
299298
internal IOBehavior AsyncIOBehavior => m_connectionSettings.ForceSynchronous ? IOBehavior.Synchronous : IOBehavior.Asynchronous;
300299

300+
internal void SetActiveReader(MySqlDataReader dataReader)
301+
{
302+
if (dataReader == null)
303+
throw new ArgumentNullException(nameof(dataReader));
304+
if (m_activeReader != null)
305+
throw new InvalidOperationException("Can't replace active reader.");
306+
m_activeReader = dataReader;
307+
}
308+
309+
internal void FinishQuerying()
310+
{
311+
m_session.FinishQuerying();
312+
m_activeReader = null;
313+
}
314+
301315
private async Task<MySqlSession> CreateSessionAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
302316
{
303317
var connectTimeout = m_connectionSettings.ConnectionTimeout == 0 ? Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(checked((int) m_connectionSettings.ConnectionTimeout));
@@ -382,7 +396,7 @@ private void DoClose()
382396
private void CloseDatabase()
383397
{
384398
m_cachedProcedures = null;
385-
Session?.ActiveReader?.Dispose();
399+
m_activeReader?.Dispose();
386400
if (CurrentTransaction != null && m_session.IsConnected)
387401
{
388402
CurrentTransaction.Dispose();
@@ -398,5 +412,6 @@ private void CloseDatabase()
398412
bool m_isDisposed;
399413
bool m_shouldCloseWhenUnenlisted;
400414
Dictionary<string, CachedProcedure> m_cachedProcedures;
415+
MySqlDataReader m_activeReader;
401416
}
402417
}

src/MySqlConnector/MySqlClient/MySqlDataReader.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ protected override void Dispose(bool disposing)
227227
internal static async Task<MySqlDataReader> CreateAsync(MySqlCommand command, CommandBehavior behavior, IOBehavior ioBehavior)
228228
{
229229
var dataReader = new MySqlDataReader(command, behavior);
230-
command.Connection.Session.SetActiveReader(dataReader);
230+
command.Connection.SetActiveReader(dataReader);
231231

232232
try
233233
{
@@ -248,7 +248,7 @@ internal static async Task<MySqlDataReader> CreateAsync(MySqlCommand command, Co
248248
finally
249249
{
250250
if (command.Connection.BufferResultSets)
251-
command.Connection.Session.FinishQuerying();
251+
command.Connection.FinishQuerying();
252252
}
253253
}
254254

@@ -289,7 +289,7 @@ private void DoClose()
289289

290290
var connection = Command.Connection;
291291
if (!connection.BufferResultSets)
292-
connection.Session.FinishQuerying();
292+
connection.FinishQuerying();
293293

294294
Command.ReaderClosed();
295295
if ((m_behavior & CommandBehavior.CloseConnection) != 0)

src/MySqlConnector/Serialization/MySqlSession.cs

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public bool TryStartCancel(MySqlCommand command)
6666
{
6767
lock (m_lock)
6868
{
69-
if (m_activeCommand != command)
69+
if (m_activeCommandId != command.CommandId)
7070
return false;
7171
VerifyState(State.Querying, State.CancelingQuery);
7272
if (m_state != State.Querying)
@@ -81,12 +81,12 @@ public void DoCancel(MySqlCommand commandToCancel, MySqlCommand killCommand)
8181
{
8282
lock (m_lock)
8383
{
84-
if (m_activeCommand != commandToCancel)
84+
if (m_activeCommandId != commandToCancel.CommandId)
8585
return;
8686

8787
// NOTE: This command is executed while holding the lock to prevent race conditions during asynchronous cancellation.
88-
// For example, if the lock weren't held, the current command could finish and the other thread could set m_activeCommand
89-
// to null, then start executing a new command. By the time this "KILL QUERY" command reached the server, the wrong
88+
// For example, if the lock weren't held, the current command could finish and the other thread could set m_activeCommandId
89+
// to zero, then start executing a new command. By the time this "KILL QUERY" command reached the server, the wrong
9090
// command would be killed (because "KILL QUERY" specifies the connection whose command should be killed, not
9191
// a unique identifier of the command itself). As a mitigation, we set the CommandTimeout to a low value to avoid
9292
// blocking the other thread for an extended duration.
@@ -99,7 +99,7 @@ public void AbortCancel(MySqlCommand command)
9999
{
100100
lock (m_lock)
101101
{
102-
if (m_activeCommand == command && m_state == State.CancelingQuery)
102+
if (m_activeCommandId == command.CommandId && m_state == State.CancelingQuery)
103103
m_state = State.Querying;
104104
}
105105
}
@@ -113,22 +113,10 @@ public void StartQuerying(MySqlCommand command)
113113

114114
VerifyState(State.Connected);
115115
m_state = State.Querying;
116-
m_activeCommand = command;
116+
m_activeCommandId = command.CommandId;
117117
}
118118
}
119119

120-
public MySqlDataReader ActiveReader => m_activeReader;
121-
122-
public void SetActiveReader(MySqlDataReader dataReader)
123-
{
124-
VerifyState(State.Querying, State.CancelingQuery);
125-
if (dataReader == null)
126-
throw new ArgumentNullException(nameof(dataReader));
127-
if (m_activeReader != null)
128-
throw new InvalidOperationException("Can't replace active reader.");
129-
m_activeReader = dataReader;
130-
}
131-
132120
public void FinishQuerying()
133121
{
134122
bool clearConnection = false;
@@ -156,8 +144,7 @@ public void FinishQuerying()
156144
{
157145
VerifyState(State.Querying, State.ClearingPendingCancellation);
158146
m_state = State.Connected;
159-
m_activeReader = null;
160-
m_activeCommand = null;
147+
m_activeCommandId = 0;
161148
}
162149
}
163150

@@ -879,8 +866,7 @@ private enum State
879866
IDisposable m_serverCertificate;
880867
#endif
881868
IPayloadHandler m_payloadHandler;
882-
MySqlCommand m_activeCommand;
883-
MySqlDataReader m_activeReader;
869+
int m_activeCommandId;
884870
bool m_useCompression;
885871
bool m_isSecureConnection;
886872
bool m_supportsConnectionAttributes;

tests/SideBySide/ConnectionPool.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,32 @@ public void LeakConnections()
149149
}
150150
}
151151

152+
#if BASELINE
153+
[Fact(Skip = "Throws MySqlException - error connecting: Timeout expired")]
154+
#else
155+
[Fact]
156+
#endif
157+
public void LeakReaders()
158+
{
159+
var csb = AppConfig.CreateConnectionStringBuilder();
160+
csb.Pooling = true;
161+
csb.MinimumPoolSize = 0;
162+
csb.MaximumPoolSize = 6;
163+
csb.ConnectionTimeout = 3u;
164+
165+
for (int i = 0; i < csb.MaximumPoolSize + 2; i++)
166+
{
167+
var connection = new MySqlConnection(csb.ConnectionString);
168+
connection.Open();
169+
var cmd = connection.CreateCommand();
170+
cmd.CommandText = "SELECT 1";
171+
var reader = cmd.ExecuteReader();
172+
Assert.True(reader.Read());
173+
174+
// have to GC for leaked connections to be removed from the pool
175+
GC.Collect();
176+
}
177+
}
152178

153179
[Fact]
154180
public async Task WaitTimeout()

0 commit comments

Comments
 (0)