Skip to content

Commit c9b6bda

Browse files
committed
Fix connection timeout when server doesn't respond. Fixes #739
1 parent f03e85f commit c9b6bda

File tree

6 files changed

+41
-9
lines changed

6 files changed

+41
-9
lines changed

src/MySqlConnector/Core/ConnectionPool.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ internal sealed class ConnectionPool
2020

2121
public SslProtocols SslProtocols { get; set; }
2222

23-
public async ValueTask<ServerSession> GetSessionAsync(MySqlConnection connection, IOBehavior ioBehavior, CancellationToken cancellationToken)
23+
public async ValueTask<ServerSession> GetSessionAsync(MySqlConnection connection, int startTickCount, IOBehavior ioBehavior, CancellationToken cancellationToken)
2424
{
2525
cancellationToken.ThrowIfCancellationRequested();
2626

@@ -108,7 +108,7 @@ public async ValueTask<ServerSession> GetSessionAsync(MySqlConnection connection
108108
session = new ServerSession(this, m_generation, Interlocked.Increment(ref m_lastSessionId));
109109
if (Log.IsInfoEnabled())
110110
Log.Info("Pool{0} no pooled session available; created new Session{1}", m_logArguments[0], session.Id);
111-
await session.ConnectAsync(ConnectionSettings, m_loadBalancer, ioBehavior, cancellationToken).ConfigureAwait(false);
111+
await session.ConnectAsync(ConnectionSettings, startTickCount, m_loadBalancer, ioBehavior, cancellationToken).ConfigureAwait(false);
112112
AdjustHostConnectionCount(session, 1);
113113
session.OwningConnection = new WeakReference<MySqlConnection>(connection);
114114
int leasedSessionsCountNew;
@@ -353,7 +353,7 @@ private async Task CreateMinimumPooledSessions(IOBehavior ioBehavior, Cancellati
353353
{
354354
var session = new ServerSession(this, m_generation, Interlocked.Increment(ref m_lastSessionId));
355355
Log.Info("Pool{0} created Session{1} to reach minimum pool size", m_logArguments[0], session.Id);
356-
await session.ConnectAsync(ConnectionSettings, m_loadBalancer, ioBehavior, cancellationToken).ConfigureAwait(false);
356+
await session.ConnectAsync(ConnectionSettings, Environment.TickCount, m_loadBalancer, ioBehavior, cancellationToken).ConfigureAwait(false);
357357
AdjustHostConnectionCount(session, 1);
358358
lock (m_sessions)
359359
m_sessions.AddFirst(session);

src/MySqlConnector/Core/ServerSession.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
309309
m_state = State.Closed;
310310
}
311311

312-
public async Task ConnectAsync(ConnectionSettings cs, ILoadBalancer? loadBalancer, IOBehavior ioBehavior, CancellationToken cancellationToken)
312+
public async Task ConnectAsync(ConnectionSettings cs, int startTickCount, ILoadBalancer? loadBalancer, IOBehavior ioBehavior, CancellationToken cancellationToken)
313313
{
314314
try
315315
{
@@ -349,6 +349,8 @@ public async Task ConnectAsync(ConnectionSettings cs, ILoadBalancer? loadBalance
349349
}
350350

351351
var byteHandler = m_socket is null ? new StreamByteHandler(m_stream!) : (IByteHandler) new SocketByteHandler(m_socket);
352+
if (cs.ConnectionTimeout != 0)
353+
byteHandler.RemainingTimeout = Math.Max(1, cs.ConnectionTimeoutMilliseconds - unchecked(Environment.TickCount - startTickCount));
352354
m_payloadHandler = new StandardPayloadHandler(byteHandler);
353355

354356
payload = await ReceiveAsync(ioBehavior, cancellationToken).ConfigureAwait(false);

src/MySqlConnector/MySql.Data.MySqlClient/MySqlConnection.cs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,8 @@ private async Task OpenAsync(IOBehavior? ioBehavior, CancellationToken cancellat
287287
if (State != ConnectionState.Closed)
288288
throw new InvalidOperationException("Cannot Open when State is {0}.".FormatInvariant(State));
289289

290+
var openStartTickCount = Environment.TickCount;
291+
290292
SetState(ConnectionState.Connecting);
291293

292294
var pool = ConnectionPool.GetPool(m_connectionString);
@@ -309,7 +311,7 @@ private async Task OpenAsync(IOBehavior? ioBehavior, CancellationToken cancellat
309311

310312
try
311313
{
312-
m_session = await CreateSessionAsync(pool, ioBehavior, cancellationToken).ConfigureAwait(false);
314+
m_session = await CreateSessionAsync(pool, openStartTickCount, ioBehavior, cancellationToken).ConfigureAwait(false);
313315

314316
m_hasBeenOpened = true;
315317
SetState(ConnectionState.Open);
@@ -621,7 +623,7 @@ internal void FinishQuerying(bool hasWarnings)
621623
}
622624
}
623625

624-
private async ValueTask<ServerSession> CreateSessionAsync(ConnectionPool? pool, IOBehavior? ioBehavior, CancellationToken cancellationToken)
626+
private async ValueTask<ServerSession> CreateSessionAsync(ConnectionPool? pool, int startTickCount, IOBehavior? ioBehavior, CancellationToken cancellationToken)
625627
{
626628
var connectionSettings = GetInitializedConnectionSettings();
627629
var actualIOBehavior = ioBehavior ?? (connectionSettings.ForceSynchronous ? IOBehavior.Synchronous : IOBehavior.Asynchronous);
@@ -633,7 +635,7 @@ private async ValueTask<ServerSession> CreateSessionAsync(ConnectionPool? pool,
633635
// the cancellation token for connection is controlled by 'cancellationToken' (if it can be cancelled), ConnectionTimeout
634636
// (from the connection string, if non-zero), or a combination of both
635637
if (connectionSettings.ConnectionTimeout != 0)
636-
timeoutSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(connectionSettings.ConnectionTimeoutMilliseconds));
638+
timeoutSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(Math.Max(1, connectionSettings.ConnectionTimeoutMilliseconds - unchecked(Environment.TickCount - startTickCount))));
637639
if (cancellationToken.CanBeCanceled && timeoutSource is object)
638640
linkedSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutSource.Token);
639641
var connectToken = linkedSource?.Token ?? timeoutSource?.Token ?? cancellationToken;
@@ -642,7 +644,7 @@ private async ValueTask<ServerSession> CreateSessionAsync(ConnectionPool? pool,
642644
if (pool is object)
643645
{
644646
// this returns an open session
645-
return await pool.GetSessionAsync(this, actualIOBehavior, connectToken).ConfigureAwait(false);
647+
return await pool.GetSessionAsync(this, startTickCount, actualIOBehavior, connectToken).ConfigureAwait(false);
646648
}
647649
else
648650
{
@@ -653,7 +655,7 @@ private async ValueTask<ServerSession> CreateSessionAsync(ConnectionPool? pool,
653655
var session = new ServerSession();
654656
session.OwningConnection = new WeakReference<MySqlConnection>(this);
655657
Log.Info("Created new non-pooled Session{0}", session.Id);
656-
await session.ConnectAsync(connectionSettings, loadBalancer, actualIOBehavior, connectToken).ConfigureAwait(false);
658+
await session.ConnectAsync(connectionSettings, startTickCount, loadBalancer, actualIOBehavior, connectToken).ConfigureAwait(false);
657659
return session;
658660
}
659661
}
@@ -662,6 +664,10 @@ private async ValueTask<ServerSession> CreateSessionAsync(ConnectionPool? pool,
662664
var messageSuffix = (pool?.IsEmpty ?? false) ? " All pooled connections are in use." : "";
663665
throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "Connect Timeout expired." + messageSuffix, ex);
664666
}
667+
catch (MySqlException ex) when (timeoutSource?.IsCancellationRequested ?? false)
668+
{
669+
throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "Connect Timeout expired.", ex);
670+
}
665671
finally
666672
{
667673
linkedSource?.Dispose();

tests/MySqlConnector.Tests/ConnectionTests.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,26 @@ public void PingWhenClosed()
184184
Assert.Equal(ConnectionState.Closed, connection.State);
185185
}
186186

187+
[Fact]
188+
public void ConnectionTimeout()
189+
{
190+
m_server.BlockOnConnect = true;
191+
var csb = new MySqlConnectionStringBuilder(m_csb.ConnectionString);
192+
csb.ConnectionTimeout = 4;
193+
using var connection = new MySqlConnection(csb.ConnectionString);
194+
var stopwatch = Stopwatch.StartNew();
195+
try
196+
{
197+
connection.Open();
198+
Assert.False(true);
199+
}
200+
catch (MySqlException ex)
201+
{
202+
Assert.InRange(stopwatch.ElapsedMilliseconds, 3900, 4100);
203+
Assert.Equal(MySqlErrorCode.UnableToConnectToHost, (MySqlErrorCode) ex.Number);
204+
}
205+
}
206+
187207
private static async Task WaitForConditionAsync<T>(T expected, Func<T> getValue)
188208
{
189209
var sw = Stopwatch.StartNew();

tests/MySqlConnector.Tests/FakeMySqlServer.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public void Stop()
5050

5151
public bool SuppressAuthPluginNameTerminatingNull { get; set; }
5252
public bool SendIncompletePostHandshakeResponse { get; set; }
53+
public bool BlockOnConnect { get; set; }
5354

5455
internal void ClientDisconnected() => Interlocked.Decrement(ref m_activeConnections);
5556

tests/MySqlConnector.Tests/FakeMySqlServerConnection.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ public async Task RunAsync(TcpClient client, CancellationToken token)
2626
using (client)
2727
using (var stream = client.GetStream())
2828
{
29+
if (m_server.BlockOnConnect)
30+
Thread.Sleep(TimeSpan.FromSeconds(10));
31+
2932
await SendAsync(stream, 0, WriteInitialHandshake);
3033
await ReadPayloadAsync(stream, token); // handshake response
3134

0 commit comments

Comments
 (0)