Skip to content

Commit e9ba35f

Browse files
authored
Merge pull request #1089 from bgrainger/pipelining
Implement pipelining.
2 parents c9abddc + 1b5f4db commit e9ba35f

File tree

10 files changed

+125
-22
lines changed

10 files changed

+125
-22
lines changed

docs/content/connection-options.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
lastmod: 2021-02-06
2+
lastmod: 2021-11-25
33
date: 2016-10-16
44
title: Connection Options
55
customtitle: MySQL Connection String for C# .NET Core Programs
@@ -390,6 +390,11 @@ These are the other options that MySqlConnector supports. They are set to sensib
390390
<td>false</td>
391391
<td>When set to <code>false</code> or no (strongly recommended), security-sensitive information, such as the password, is not returned as part of the connection string if the connection is open or has ever been in an open state. Resetting the connection string resets all connection string values, including the password. Recognized values are true, false, yes, and no.</td>
392392
</tr>
393+
<tr id="Pipelining">
394+
<td>Pipelining</td>
395+
<td>true</td>
396+
<td>When set to <code>true</code>, queries will be "pipelined" (when possible) by sending multiple packets to the server before waiting for a response. This improves performance (by reducing latency) but is not compatible with some servers (most notably Amazon Aurora RDS). Set to <code>false</code> to disable this behavior.</td>
397+
</tr>
393398
<tr id="ServerRedirectionMode">
394399
<td>Server Redirection Mode, ServerRedirectionMode</td>
395400
<td>Disabled</td>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
date: 2021-11-25
3+
title: Aurora Freeze
4+
customtitle: "MySqlConnection.Open Freezes with Amazon Aurora"
5+
weight: 5
6+
menu:
7+
main:
8+
parent: troubleshooting
9+
---
10+
11+
# MySqlConnection.Open Freezes with Amazon Aurora
12+
13+
When using Amazon Aurora RDS, you may experience a hang when calling `MySqlConnection.Open()`. The immediate prior log statement will be:
14+
15+
```
16+
[TRACE] ServerSession Session0.0 ServerVersion=5.7.12 supports reset connection and pipelining; sending pipelined reset connection request
17+
```
18+
19+
The cause of this problem is Amazon Aurora not correctly supporting pipelining in the MySQL protocol. This is known to be a problem with 2.x versions of Aurora (that implement MySQL 5.7), but not with 3.x versions (that implement MySQL 8.0).
20+
21+
To work around it, add `Pipelining = False;` to your connection string to disable the pipelining feature.

src/MySqlConnector/Core/ConnectionPool.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public async ValueTask<ServerSession> GetSessionAsync(MySqlConnection connection
6262
else
6363
{
6464
if (ConnectionSettings.ConnectionReset || session.DatabaseOverride is not null)
65-
reuseSession = await session.TryResetConnectionAsync(ConnectionSettings, connection, false, ioBehavior, cancellationToken).ConfigureAwait(false);
65+
reuseSession = await session.TryResetConnectionAsync(ConnectionSettings, connection, ioBehavior, cancellationToken).ConfigureAwait(false);
6666
else
6767
reuseSession = true;
6868
}

src/MySqlConnector/Core/ConnectionSettings.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ public ConnectionSettings(MySqlConnectionStringBuilder csb)
140140
Keepalive = csb.Keepalive;
141141
NoBackslashEscapes = csb.NoBackslashEscapes;
142142
PersistSecurityInfo = csb.PersistSecurityInfo;
143+
Pipelining = csb.ContainsKey("Pipelining") ? csb.Pipelining : default(bool?);
143144
ServerRedirectionMode = csb.ServerRedirectionMode;
144145
ServerRsaPublicKeyFile = csb.ServerRsaPublicKeyFile;
145146
ServerSPN = csb.ServerSPN;
@@ -233,6 +234,7 @@ private static MySqlGuidFormat GetEffectiveGuidFormat(MySqlGuidFormat guidFormat
233234
public uint Keepalive { get; }
234235
public bool NoBackslashEscapes { get; }
235236
public bool PersistSecurityInfo { get; }
237+
public bool? Pipelining { get; }
236238
public MySqlServerRedirectionMode ServerRedirectionMode { get; }
237239
public string ServerRsaPublicKeyFile { get; }
238240
public string ServerSPN { get; }
@@ -316,6 +318,7 @@ private ConnectionSettings(ConnectionSettings other, string host, int port, stri
316318
Keepalive = other.Keepalive;
317319
NoBackslashEscapes = other.NoBackslashEscapes;
318320
PersistSecurityInfo = other.PersistSecurityInfo;
321+
Pipelining = other.Pipelining;
319322
ServerRedirectionMode = other.ServerRedirectionMode;
320323
ServerRsaPublicKeyFile = other.ServerRsaPublicKeyFile;
321324
ServerSPN = other.ServerSPN;

src/MySqlConnector/Core/ServerSession.cs

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -468,9 +468,36 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
468468
m_characterSet = ServerVersion.Version >= ServerVersions.SupportsUtf8Mb4 ? CharacterSet.Utf8Mb4GeneralCaseInsensitive : CharacterSet.Utf8GeneralCaseInsensitive;
469469
m_setNamesPayload = ServerVersion.Version >= ServerVersions.SupportsUtf8Mb4 ? s_setNamesUtf8mb4Payload : s_setNamesUtf8Payload;
470470

471-
Log.Debug("Session{0} made connection; ServerVersion={1}; ConnectionId={2}; Compression={3}; Attributes={4}; DeprecateEof={5}; Ssl={6}; SessionTrack={7}",
471+
// disable pipelining for RDS MySQL 5.7 (assuming Aurora); otherwise take it from the connection string or default to true
472+
if (!cs.Pipelining.HasValue && ServerVersion.Version.Major == 5 && ServerVersion.Version.Minor == 7 && HostName.EndsWith(".rds.amazonaws.com", StringComparison.OrdinalIgnoreCase))
473+
{
474+
m_logArguments[1] = HostName;
475+
Log.Debug("Session{0} auto-detected Aurora 5.7 at '{1}'; disabling pipelining", m_logArguments);
476+
m_supportsPipelining = false;
477+
}
478+
else
479+
{
480+
// pipelining is not currently compatible with compression
481+
m_supportsPipelining = !cs.UseCompression && (cs.Pipelining ?? true);
482+
483+
// for pipelining, concatenate reset connection and SET NAMES query into one buffer
484+
if (m_supportsPipelining)
485+
{
486+
m_pipelinedResetConnectionBytes = new byte[m_setNamesPayload.Span.Length + 9];
487+
488+
// first packet: reset connection
489+
m_pipelinedResetConnectionBytes[0] = 1;
490+
m_pipelinedResetConnectionBytes[4] = (byte) CommandKind.ResetConnection;
491+
492+
// second packet: SET NAMES query
493+
m_pipelinedResetConnectionBytes[5] = (byte) m_setNamesPayload.Span.Length;
494+
m_setNamesPayload.Span.CopyTo(m_pipelinedResetConnectionBytes.AsSpan().Slice(9));
495+
}
496+
}
497+
498+
Log.Debug("Session{0} made connection; ServerVersion={1}; ConnectionId={2}; Compression={3}; Attributes={4}; DeprecateEof={5}; Ssl={6}; SessionTrack={7}; Pipelining={8}",
472499
m_logArguments[0], ServerVersion.OriginalString, ConnectionId,
473-
m_useCompression, m_supportsConnectionAttributes, m_supportsDeprecateEof, serverSupportsSsl, m_supportsSessionTrack);
500+
m_useCompression, m_supportsConnectionAttributes, m_supportsDeprecateEof, serverSupportsSsl, m_supportsSessionTrack, m_supportsPipelining);
474501

475502
if (cs.SslMode != MySqlSslMode.None && (cs.SslMode != MySqlSslMode.Preferred || serverSupportsSsl))
476503
{
@@ -555,11 +582,10 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
555582
return statusInfo;
556583
}
557584

558-
public async Task<bool> TryResetConnectionAsync(ConnectionSettings cs, MySqlConnection connection, bool returnToPool, IOBehavior ioBehavior, CancellationToken cancellationToken)
585+
public async Task<bool> TryResetConnectionAsync(ConnectionSettings cs, MySqlConnection connection, IOBehavior ioBehavior, CancellationToken cancellationToken)
559586
{
560587
VerifyState(State.Connected);
561588

562-
var success = false;
563589
try
564590
{
565591
// clear all prepared statements; resetting the connection will clear them on the server
@@ -569,6 +595,26 @@ public async Task<bool> TryResetConnectionAsync(ConnectionSettings cs, MySqlConn
569595
if (DatabaseOverride is null && (ServerVersion.Version.CompareTo(ServerVersions.SupportsResetConnection) >= 0 || ServerVersion.MariaDbVersion?.CompareTo(ServerVersions.MariaDbSupportsResetConnection) >= 0))
570596
{
571597
m_logArguments[1] = ServerVersion.OriginalString;
598+
599+
if (m_supportsPipelining)
600+
{
601+
Log.Trace("Session{0} ServerVersion={1} supports reset connection and pipelining; sending pipelined reset connection request", m_logArguments);
602+
603+
// send both packets at once
604+
await m_payloadHandler!.ByteHandler.WriteBytesAsync(m_pipelinedResetConnectionBytes!, ioBehavior).ConfigureAwait(false);
605+
606+
// read two OK replies
607+
m_payloadHandler.SetNextSequenceNumber(1);
608+
payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
609+
OkPayload.Create(payload.Span, SupportsDeprecateEof, SupportsSessionTrack);
610+
611+
m_payloadHandler.SetNextSequenceNumber(1);
612+
payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
613+
OkPayload.Create(payload.Span, SupportsDeprecateEof, SupportsSessionTrack);
614+
615+
return true;
616+
}
617+
572618
Log.Trace("Session{0} ServerVersion={1} supports reset connection; sending reset connection request", m_logArguments);
573619
await SendAsync(ResetConnectionPayload.Instance, ioBehavior, cancellationToken).ConfigureAwait(false);
574620
payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
@@ -606,7 +652,7 @@ public async Task<bool> TryResetConnectionAsync(ConnectionSettings cs, MySqlConn
606652
payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
607653
OkPayload.Create(payload.Span, SupportsDeprecateEof, SupportsSessionTrack);
608654

609-
success = true;
655+
return true;
610656
}
611657
catch (IOException ex)
612658
{
@@ -625,12 +671,7 @@ public async Task<bool> TryResetConnectionAsync(ConnectionSettings cs, MySqlConn
625671
Log.Trace(ex, "Session{0} ignoring SocketException in TryResetConnectionAsync", m_logArguments);
626672
}
627673

628-
if (returnToPool && Pool is not null)
629-
{
630-
await Pool.ReturnAsync(ioBehavior, this).ConfigureAwait(false);
631-
}
632-
633-
return success;
674+
return false;
634675
}
635676

636677
private async Task<PayloadData> SwitchAuthenticationAsync(ConnectionSettings cs, string password, PayloadData payload, IOBehavior ioBehavior, CancellationToken cancellationToken)
@@ -1888,7 +1929,9 @@ protected override void OnStatementBegin(int index)
18881929
bool m_supportsConnectionAttributes;
18891930
bool m_supportsDeprecateEof;
18901931
bool m_supportsSessionTrack;
1932+
bool m_supportsPipelining;
18911933
CharacterSet m_characterSet;
18921934
PayloadData m_setNamesPayload;
1935+
byte[]? m_pipelinedResetConnectionBytes;
18931936
Dictionary<string, PreparedStatements>? m_preparedStatements;
18941937
}

src/MySqlConnector/MySqlConnectionStringBuilder.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -678,8 +678,21 @@ public bool PersistSecurityInfo
678678
}
679679

680680
/// <summary>
681-
/// Whether to use server redirection.
681+
/// Enables query pipelining.
682682
/// </summary>
683+
[Category("Other")]
684+
[DefaultValue(true)]
685+
[Description("Enables query pipelining.")]
686+
[DisplayName("Pipelining")]
687+
public bool Pipelining
688+
{
689+
get => MySqlConnectionStringOption.Pipelining.GetValue(this);
690+
set => MySqlConnectionStringOption.Pipelining.SetValue(this, value);
691+
}
692+
693+
/// <summary>
694+
/// Whether to use server redirection.
695+
/// </summary>
683696
[Category("Connection")]
684697
[DefaultValue(MySqlServerRedirectionMode.Disabled)]
685698
[Description("Whether to use server redirection.")]
@@ -911,6 +924,7 @@ internal abstract class MySqlConnectionStringOption
911924
public static readonly MySqlConnectionStringValueOption<bool> NoBackslashEscapes;
912925
public static readonly MySqlConnectionStringValueOption<bool> OldGuids;
913926
public static readonly MySqlConnectionStringValueOption<bool> PersistSecurityInfo;
927+
public static readonly MySqlConnectionStringValueOption<bool> Pipelining;
914928
public static readonly MySqlConnectionStringValueOption<MySqlServerRedirectionMode> ServerRedirectionMode;
915929
public static readonly MySqlConnectionStringReferenceOption<string> ServerRsaPublicKeyFile;
916930
public static readonly MySqlConnectionStringReferenceOption<string> ServerSPN;
@@ -1178,6 +1192,10 @@ static MySqlConnectionStringOption()
11781192
keys: new[] { "Persist Security Info", "PersistSecurityInfo" },
11791193
defaultValue: false));
11801194

1195+
AddOption(Pipelining = new(
1196+
keys: new[] { "Pipelining" },
1197+
defaultValue: true));
1198+
11811199
AddOption(ServerRedirectionMode = new(
11821200
keys: new[] { "Server Redirection Mode", "ServerRedirectionMode" },
11831201
defaultValue: MySqlServerRedirectionMode.Disabled));

src/MySqlConnector/Protocol/Serialization/CompressedPayloadHandler.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ public void Dispose()
2121
Utility.Dispose(ref m_uncompressedStream);
2222
}
2323

24-
public void StartNewConversation()
25-
{
26-
m_compressedSequenceNumber = 0;
27-
m_uncompressedSequenceNumber = 0;
28-
}
24+
public void StartNewConversation() =>
25+
m_compressedSequenceNumber = m_uncompressedSequenceNumber = 0;
26+
27+
public void SetNextSequenceNumber(int sequenceNumber) =>
28+
throw new NotSupportedException();
2929

3030
public IByteHandler ByteHandler
3131
{

src/MySqlConnector/Protocol/Serialization/IPayloadHandler.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ internal interface IPayloadHandler : IDisposable
88
/// </summary>
99
void StartNewConversation();
1010

11+
/// <summary>
12+
/// Forces the next sequence number to be the specified value.
13+
/// </summary>
14+
/// <param name="sequenceNumber">The next sequence number.</param>
15+
/// <remarks>This should only be used in advanced scenarios.</remarks>
16+
void SetNextSequenceNumber(int sequenceNumber);
17+
1118
/// <summary>
1219
/// Gets or sets the underlying <see cref="IByteHandler"/> that data is read from and written to.
1320
/// </summary>

src/MySqlConnector/Protocol/Serialization/StandardPayloadHandler.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ public void Dispose()
1616
Utility.Dispose(ref m_byteHandler);
1717
}
1818

19-
public void StartNewConversation()
20-
{
19+
public void StartNewConversation() =>
2120
m_sequenceNumber = 0;
22-
}
21+
22+
public void SetNextSequenceNumber(int sequenceNumber) =>
23+
m_sequenceNumber = (byte) sequenceNumber;
2324

2425
public IByteHandler ByteHandler
2526
{

tests/MySqlConnector.Tests/MySqlConnectionStringBuilderTests.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ public void Defaults()
7171
#endif
7272
Assert.False(csb.OldGuids);
7373
Assert.False(csb.PersistSecurityInfo);
74+
#if !BASELINE
75+
Assert.True(csb.Pipelining);
76+
#endif
7477
Assert.True(csb.Pooling);
7578
Assert.Equal(3306u, csb.Port);
7679
Assert.Equal("", csb.Server);
@@ -135,6 +138,7 @@ public void ParseConnectionString()
135138
"load balance=random;" +
136139
"guidformat=timeswapbinary16;" +
137140
"nobackslashescapes=true;" +
141+
"pipelining=false;" +
138142
"server redirection mode=required;" +
139143
"server spn=mariadb/host.example.com@EXAMPLE.COM;" +
140144
"use xa transactions=false;" +
@@ -197,6 +201,7 @@ public void ParseConnectionString()
197201
Assert.Equal(MySqlLoadBalance.Random, csb.LoadBalance);
198202
Assert.Equal(MySqlGuidFormat.TimeSwapBinary16, csb.GuidFormat);
199203
Assert.True(csb.NoBackslashEscapes);
204+
Assert.False(csb.Pipelining);
200205
Assert.Equal(MySqlServerRedirectionMode.Required, csb.ServerRedirectionMode);
201206
Assert.Equal("mariadb/host.example.com@EXAMPLE.COM", csb.ServerSPN);
202207
Assert.False(csb.UseXaTransactions);

0 commit comments

Comments
 (0)