Skip to content

Commit b78e43f

Browse files
authored
Add ConnectionOpenedCallback. Fixes #1508 (#1515)
This differs from the existing StateChanged event in that: - it supports an async callback - it's only invoked when a connection is opened - it provides information about new vs existing and whether the connection was reset Signed-off-by: Bradley Grainger <[email protected]>
1 parent 633a65b commit b78e43f

10 files changed

+273
-4
lines changed

src/MySqlConnector/Core/ServerSession.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,12 @@ public ServerSession(ILogger logger, IConnectionPoolMetadata pool)
6565
public bool ProcAccessDenied { get; set; }
6666
public ICollection<KeyValuePair<string, object?>> ActivityTags => m_activityTags;
6767
public MySqlDataReader DataReader { get; set; }
68+
public MySqlConnectionOpenedConditions Conditions { get; private set; }
6869

6970
public ValueTask ReturnToPoolAsync(IOBehavior ioBehavior, MySqlConnection? owningConnection)
7071
{
7172
Log.ReturningToPool(m_logger, Id, Pool?.Id ?? 0);
73+
Conditions = MySqlConnectionOpenedConditions.None;
7274
LastReturnedTimestamp = Stopwatch.GetTimestamp();
7375
if (Pool is null)
7476
return default;
@@ -414,6 +416,7 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
414416
}
415417
}
416418

419+
Conditions = MySqlConnectionOpenedConditions.New;
417420
var connected = cs.ConnectionProtocol switch
418421
{
419422
MySqlConnectionProtocol.Sockets => await OpenTcpSocketAsync(cs, loadBalancer ?? throw new ArgumentNullException(nameof(loadBalancer)), activity, ioBehavior, cancellationToken).ConfigureAwait(false),
@@ -747,6 +750,7 @@ public static async ValueTask<ServerSession> ConnectAndRedirectAsync(ILogger con
747750
public async Task<bool> TryResetConnectionAsync(ConnectionSettings cs, MySqlConnection connection, IOBehavior ioBehavior, CancellationToken cancellationToken)
748751
{
749752
VerifyState(State.Connected);
753+
Conditions |= MySqlConnectionOpenedConditions.Reset;
750754

751755
try
752756
{
@@ -829,6 +833,7 @@ public async Task<bool> TryResetConnectionAsync(ConnectionSettings cs, MySqlConn
829833
Log.IgnoringFailureInTryResetConnectionAsync(m_logger, ex, Id, "SocketException");
830834
}
831835

836+
Conditions &= ~MySqlConnectionOpenedConditions.Reset;
832837
return false;
833838
}
834839

src/MySqlConnector/MySqlConnection.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,13 @@ internal async Task OpenAsync(IOBehavior? ioBehavior, CancellationToken cancella
551551
ActivitySourceHelper.CopyTags(m_session!.ActivityTags, activity);
552552
m_hasBeenOpened = true;
553553
SetState(ConnectionState.Open);
554+
555+
if (ConnectionOpenedCallback is { } autoEnlistConnectionOpenedCallback)
556+
{
557+
cancellationToken.ThrowIfCancellationRequested();
558+
await autoEnlistConnectionOpenedCallback(new(this, MySqlConnectionOpenedConditions.None), cancellationToken).ConfigureAwait(false);
559+
}
560+
554561
return;
555562
}
556563
}
@@ -582,6 +589,12 @@ internal async Task OpenAsync(IOBehavior? ioBehavior, CancellationToken cancella
582589

583590
if (m_connectionSettings.AutoEnlist && System.Transactions.Transaction.Current is not null)
584591
EnlistTransaction(System.Transactions.Transaction.Current);
592+
593+
if (ConnectionOpenedCallback is { } connectionOpenedCallback)
594+
{
595+
cancellationToken.ThrowIfCancellationRequested();
596+
await connectionOpenedCallback(new(this, m_session.Conditions), cancellationToken).ConfigureAwait(false);
597+
}
585598
}
586599
catch (Exception ex) when (activity is { IsAllDataRequested: true })
587600
{
@@ -917,6 +930,11 @@ internal void Cancel(ICancellableCommand command, int commandId, bool isCancel)
917930

918931
using var connection = CloneWith(csb.ConnectionString);
919932
connection.m_connectionSettings = connectionSettings;
933+
934+
// clear the callback because this is not intended to be a user-visible MySqlConnection that will execute setup logic; it's a
935+
// non-pooled connection that will execute "KILL QUERY" then immediately be closed
936+
connection.ConnectionOpenedCallback = null;
937+
920938
connection.Open();
921939
#if NET6_0_OR_GREATER
922940
var killQuerySql = string.Create(CultureInfo.InvariantCulture, $"KILL QUERY {command.Connection!.ServerThread}");
@@ -992,6 +1010,7 @@ internal void Cancel(ICancellableCommand command, int commandId, bool isCancel)
9921010
internal MySqlTransaction? CurrentTransaction { get; set; }
9931011
internal MySqlConnectorLoggingConfiguration LoggingConfiguration { get; }
9941012
internal ZstandardPlugin? ZstandardPlugin { get; set; }
1013+
internal MySqlConnectionOpenedCallback? ConnectionOpenedCallback { get; set; }
9951014
internal bool AllowLoadLocalInfile => GetInitializedConnectionSettings().AllowLoadLocalInfile;
9961015
internal bool AllowUserVariables => GetInitializedConnectionSettings().AllowUserVariables;
9971016
internal bool AllowZeroDateTime => GetInitializedConnectionSettings().AllowZeroDateTime;
@@ -1142,6 +1161,7 @@ private MySqlConnection(MySqlConnection other, MySqlDataSource? dataSource, stri
11421161
ProvideClientCertificatesCallback = other.ProvideClientCertificatesCallback;
11431162
ProvidePasswordCallback = other.ProvidePasswordCallback;
11441163
RemoteCertificateValidationCallback = other.RemoteCertificateValidationCallback;
1164+
ConnectionOpenedCallback = other.ConnectionOpenedCallback;
11451165
}
11461166

11471167
private void VerifyNotDisposed()
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace MySqlConnector;
2+
3+
/// <summary>
4+
/// A callback that is invoked when a new <see cref="MySqlConnection"/> is opened.
5+
/// </summary>
6+
/// <param name="context">A <see cref="MySqlConnectionOpenedContext"/> giving information about the connection being opened.</param>
7+
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the asynchronous operation.</param>
8+
/// <returns>A <see cref="ValueTask"/> representing the result of the possibly-asynchronous operation.</returns>
9+
public delegate ValueTask MySqlConnectionOpenedCallback(MySqlConnectionOpenedContext context, CancellationToken cancellationToken);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
namespace MySqlConnector;
2+
3+
/// <summary>
4+
/// Bitflags giving the conditions under which a connection was opened.
5+
/// </summary>
6+
[Flags]
7+
public enum MySqlConnectionOpenedConditions
8+
{
9+
/// <summary>
10+
/// No specific conditions apply. This value may be used when an existing pooled connection is reused without being reset.
11+
/// </summary>
12+
None = 0,
13+
14+
/// <summary>
15+
/// A new physical connection to a MySQL Server was opened. This value is mutually exclusive with <see cref="Reset"/>.
16+
/// </summary>
17+
New = 1,
18+
19+
/// <summary>
20+
/// An existing pooled connection to a MySQL Server was reset. This value is mutually exclusive with <see cref="New"/>.
21+
/// </summary>
22+
Reset = 2,
23+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
namespace MySqlConnector;
2+
3+
/// <summary>
4+
/// Contains information passed to <see cref="MySqlConnectionOpenedCallback"/> when a new <see cref="MySqlConnection"/> is opened.
5+
/// </summary>
6+
public sealed class MySqlConnectionOpenedContext
7+
{
8+
/// <summary>
9+
/// The <see cref="MySqlConnection"/> that was opened.
10+
/// </summary>
11+
public MySqlConnection Connection { get; }
12+
13+
/// <summary>
14+
/// Bitflags giving the conditions under which a connection was opened.
15+
/// </summary>
16+
public MySqlConnectionOpenedConditions Conditions { get; }
17+
18+
internal MySqlConnectionOpenedContext(MySqlConnection connection, MySqlConnectionOpenedConditions conditions)
19+
{
20+
Connection = connection;
21+
Conditions = conditions;
22+
}
23+
}

src/MySqlConnector/MySqlDataSource.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public sealed class MySqlDataSource : DbDataSource
1919
/// <param name="connectionString">The connection string for the MySQL Server. This parameter is required.</param>
2020
/// <exception cref="ArgumentNullException">Thrown if <paramref name="connectionString"/> is <c>null</c>.</exception>
2121
public MySqlDataSource(string connectionString)
22-
: this(connectionString ?? throw new ArgumentNullException(nameof(connectionString)), MySqlConnectorLoggingConfiguration.NullConfiguration, null, null, null, null, default, default, default)
22+
: this(connectionString ?? throw new ArgumentNullException(nameof(connectionString)), MySqlConnectorLoggingConfiguration.NullConfiguration, null, null, null, null, default, default, default, default)
2323
{
2424
}
2525

@@ -31,7 +31,8 @@ internal MySqlDataSource(string connectionString,
3131
Func<MySqlProvidePasswordContext, CancellationToken, ValueTask<string>>? periodicPasswordProvider,
3232
TimeSpan periodicPasswordProviderSuccessRefreshInterval,
3333
TimeSpan periodicPasswordProviderFailureRefreshInterval,
34-
ZstandardPlugin? zstandardPlugin)
34+
ZstandardPlugin? zstandardPlugin,
35+
MySqlConnectionOpenedCallback? connectionOpenedCallback)
3536
{
3637
m_connectionString = connectionString;
3738
LoggingConfiguration = loggingConfiguration;
@@ -40,6 +41,7 @@ internal MySqlDataSource(string connectionString,
4041
m_remoteCertificateValidationCallback = remoteCertificateValidationCallback;
4142
m_logger = loggingConfiguration.DataSourceLogger;
4243
m_zstandardPlugin = zstandardPlugin;
44+
m_connectionOpenedCallback = connectionOpenedCallback;
4345

4446
Pool = ConnectionPool.CreatePool(m_connectionString, LoggingConfiguration, name);
4547
m_id = Interlocked.Increment(ref s_lastId);
@@ -142,6 +144,7 @@ protected override DbConnection CreateDbConnection()
142144
ProvideClientCertificatesCallback = m_clientCertificatesCallback,
143145
ProvidePasswordCallback = m_providePasswordCallback,
144146
RemoteCertificateValidationCallback = m_remoteCertificateValidationCallback,
147+
ConnectionOpenedCallback = m_connectionOpenedCallback,
145148
};
146149
}
147150

@@ -225,6 +228,7 @@ private string ProvidePasswordFromInitialRefreshTask(MySqlProvidePasswordContext
225228
private readonly TimeSpan m_periodicPasswordProviderSuccessRefreshInterval;
226229
private readonly TimeSpan m_periodicPasswordProviderFailureRefreshInterval;
227230
private readonly ZstandardPlugin? m_zstandardPlugin;
231+
private readonly MySqlConnectionOpenedCallback? m_connectionOpenedCallback;
228232
private readonly MySqlProvidePasswordContext? m_providePasswordContext;
229233
private readonly CancellationTokenSource? m_passwordProviderTimerCancellationTokenSource;
230234
private readonly Timer? m_passwordProviderTimer;

src/MySqlConnector/MySqlDataSourceBuilder.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,17 @@ public MySqlDataSourceBuilder UseRemoteCertificateValidationCallback(RemoteCerti
8989
return this;
9090
}
9191

92+
/// <summary>
93+
/// Adds a callback that is invoked when a new <see cref="MySqlConnection"/> is opened.
94+
/// </summary>
95+
/// <param name="callback">The callback to invoke.</param>
96+
/// <returns>This builder, so that method calls can be chained.</returns>
97+
public MySqlDataSourceBuilder UseConnectionOpenedCallback(MySqlConnectionOpenedCallback callback)
98+
{
99+
m_connectionOpenedCallback += callback;
100+
return this;
101+
}
102+
92103
/// <summary>
93104
/// Builds a <see cref="MySqlDataSource"/> which is ready for use.
94105
/// </summary>
@@ -104,7 +115,8 @@ public MySqlDataSource Build()
104115
m_periodicPasswordProvider,
105116
m_periodicPasswordProviderSuccessRefreshInterval,
106117
m_periodicPasswordProviderFailureRefreshInterval,
107-
ZstandardPlugin
118+
ZstandardPlugin,
119+
m_connectionOpenedCallback
108120
);
109121
}
110122

@@ -122,4 +134,5 @@ public MySqlDataSource Build()
122134
private Func<MySqlProvidePasswordContext, CancellationToken, ValueTask<string>>? m_periodicPasswordProvider;
123135
private TimeSpan m_periodicPasswordProviderSuccessRefreshInterval;
124136
private TimeSpan m_periodicPasswordProviderFailureRefreshInterval;
137+
private MySqlConnectionOpenedCallback? m_connectionOpenedCallback;
125138
}

tests/IntegrationTests/TransactionScopeTests.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -886,6 +886,38 @@ public void Bug1348()
886886

887887
Assert.True(rollbacked, $"First branch transaction '{xid}1' not rolled back");
888888
}
889+
890+
[Fact]
891+
public void ConnectionOpenedCallbackAutoEnlistInTransaction()
892+
{
893+
var connectionOpenedCallbackCount = 0;
894+
var connectionOpenedConditions = MySqlConnectionOpenedConditions.None;
895+
using var dataSource = new MySqlDataSourceBuilder(AppConfig.ConnectionString)
896+
.UseConnectionOpenedCallback((ctx, token) =>
897+
{
898+
connectionOpenedCallbackCount++;
899+
connectionOpenedConditions = ctx.Conditions;
900+
return default;
901+
})
902+
.Build();
903+
904+
using (var transactionScope = new TransactionScope())
905+
{
906+
using (var conn = dataSource.OpenConnection())
907+
{
908+
Assert.Equal(1, connectionOpenedCallbackCount);
909+
Assert.Equal(MySqlConnectionOpenedConditions.New, connectionOpenedConditions);
910+
}
911+
912+
using (var conn = dataSource.OpenConnection())
913+
{
914+
Assert.Equal(2, connectionOpenedCallbackCount);
915+
Assert.Equal(MySqlConnectionOpenedConditions.None, connectionOpenedConditions);
916+
}
917+
918+
transactionScope.Complete();
919+
}
920+
}
889921
#endif
890922

891923
readonly DatabaseFixture m_database;

0 commit comments

Comments
 (0)