Skip to content

Commit 80a9ac7

Browse files
authored
Merge pull request #1329 from mysql-net/metrics
Add connection pool metrics.
2 parents 80bec17 + 9630b08 commit 80a9ac7

15 files changed

+719
-19
lines changed

src/MySqlConnector/Core/ConnectionPool.cs

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Collections.Concurrent;
22
using System.Diagnostics;
3+
using System.Diagnostics.Metrics;
34
using System.Net;
45
using System.Security.Authentication;
56
using Microsoft.Extensions.Logging;
@@ -50,12 +51,14 @@ public async ValueTask<ServerSession> GetSessionAsync(MySqlConnection connection
5051
{
5152
if (m_sessions.Count > 0)
5253
{
54+
// NOTE: MetricsReporter updated outside lock below
5355
session = m_sessions.First!.Value;
5456
m_sessions.RemoveFirst();
5557
}
5658
}
5759
if (session is not null)
5860
{
61+
MetricsReporter.RemoveIdle(this);
5962
Log.FoundExistingSession(m_logger, Id);
6063
bool reuseSession;
6164

@@ -96,8 +99,12 @@ public async ValueTask<ServerSession> GetSessionAsync(MySqlConnection connection
9699
m_leasedSessions.Add(session.Id, session);
97100
leasedSessionsCountPooled = m_leasedSessions.Count;
98101
}
102+
MetricsReporter.AddUsed(this);
99103
ActivitySourceHelper.CopyTags(session.ActivityTags, activity);
100104
Log.ReturningPooledSession(m_logger, Id, session.Id, leasedSessionsCountPooled);
105+
106+
session.LastLeasedTicks = unchecked((uint) Environment.TickCount);
107+
MetricsReporter.RecordWaitTime(this, unchecked(session.LastLeasedTicks - (uint) startTickCount));
101108
return session;
102109
}
103110
}
@@ -112,7 +119,11 @@ public async ValueTask<ServerSession> GetSessionAsync(MySqlConnection connection
112119
m_leasedSessions.Add(session.Id, session);
113120
leasedSessionsCountNew = m_leasedSessions.Count;
114121
}
122+
MetricsReporter.AddUsed(this);
115123
Log.ReturningNewSession(m_logger, Id, session.Id, leasedSessionsCountNew);
124+
125+
session.LastLeasedTicks = unchecked((uint) Environment.TickCount);
126+
MetricsReporter.RecordCreateTime(this, unchecked(session.LastLeasedTicks - (uint) startTickCount));
116127
return session;
117128
}
118129
catch (Exception ex)
@@ -164,12 +175,14 @@ public async ValueTask ReturnAsync(IOBehavior ioBehavior, ServerSession session)
164175
{
165176
lock (m_leasedSessions)
166177
m_leasedSessions.Remove(session.Id);
178+
MetricsReporter.RemoveUsed(this);
167179
session.OwningConnection = null;
168180
var sessionHealth = GetSessionHealth(session);
169181
if (sessionHealth == 0)
170182
{
171183
lock (m_sessions)
172184
m_sessions.AddFirst(session);
185+
MetricsReporter.AddIdle(this);
173186
}
174187
else
175188
{
@@ -224,6 +237,8 @@ public async Task ReapAsync(IOBehavior ioBehavior, CancellationToken cancellatio
224237
public void Dispose()
225238
{
226239
Log.DisposingConnectionPool(m_logger, Id);
240+
lock (s_allPools)
241+
s_allPools.Remove(this);
227242
#if NET6_0_OR_GREATER
228243
m_dnsCheckTimer?.Dispose();
229244
m_dnsCheckTimer = null;
@@ -326,12 +341,14 @@ private async Task CleanPoolAsync(IOBehavior ioBehavior, Func<ServerSession, boo
326341
{
327342
if (m_sessions.Count > 0)
328343
{
344+
// NOTE: MetricsReporter updated outside lock below
329345
session = m_sessions.Last!.Value;
330346
m_sessions.RemoveLast();
331347
}
332348
}
333349
if (session is null)
334350
return;
351+
MetricsReporter.RemoveIdle(this);
335352

336353
if (shouldCleanFn(session))
337354
{
@@ -344,6 +361,7 @@ private async Task CleanPoolAsync(IOBehavior ioBehavior, Func<ServerSession, boo
344361
// session should not be cleaned; put it back in the queue and stop iterating
345362
lock (m_sessions)
346363
m_sessions.AddLast(session);
364+
MetricsReporter.AddIdle(this);
347365
return;
348366
}
349367
}
@@ -389,6 +407,7 @@ private async Task CreateMinimumPooledSessions(MySqlConnection connection, IOBeh
389407
AdjustHostConnectionCount(session, 1);
390408
lock (m_sessions)
391409
m_sessions.AddFirst(session);
410+
MetricsReporter.AddIdle(this);
392411
}
393412
finally
394413
{
@@ -546,17 +565,18 @@ private async ValueTask<ServerSession> ConnectSessionAsync(MySqlConnection conne
546565
else if (pool != newPool)
547566
{
548567
Log.CreatedPoolWillNotBeUsed(newPool.m_logger, newPool.Id);
568+
newPool.Dispose();
549569
}
550570

551571
return pool;
552572
}
553573

554574
public static async Task ClearPoolsAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
555575
{
556-
foreach (var pool in GetAllPools())
576+
foreach (var pool in GetCachedPools())
557577
await pool.ClearAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
558578

559-
static List<ConnectionPool> GetAllPools()
579+
static List<ConnectionPool> GetCachedPools()
560580
{
561581
var pools = new List<ConnectionPool>(s_pools.Count);
562582
var uniquePools = new HashSet<ConnectionPool>();
@@ -594,8 +614,19 @@ private ConnectionPool(MySqlConnectorLoggingConfiguration loggingConfiguration,
594614
cs.LoadBalance == MySqlLoadBalance.LeastConnections ? new LeastConnectionsLoadBalancer(m_hostSessions!) :
595615
(ILoadBalancer) new RoundRobinLoadBalancer();
596616

617+
// create tag lists for reporting pool metrics
618+
var connectionString = cs.ConnectionStringBuilder.GetConnectionString(includePassword: false);
619+
m_stateTagList =
620+
[
621+
new("state", "idle"),
622+
new("pool.name", Name ?? connectionString),
623+
new("state", "used"),
624+
];
625+
597626
Id = Interlocked.Increment(ref s_poolId);
598-
Log.CreatingNewConnectionPool(m_logger, Id, cs.ConnectionStringBuilder.GetConnectionString(includePassword: false));
627+
lock (s_allPools)
628+
s_allPools.Add(this);
629+
Log.CreatingNewConnectionPool(m_logger, Id, connectionString);
599630
}
600631

601632
private void StartReaperTask()
@@ -741,6 +772,19 @@ private void AdjustHostConnectionCount(ServerSession session, int delta)
741772
}
742773
}
743774

775+
// Provides a slice of m_stateTagList that contains either the 'idle' or 'used' state tag along with the pool name.
776+
public ReadOnlySpan<KeyValuePair<string, object?>> IdleStateTagList => m_stateTagList.AsSpan(0, 2);
777+
public ReadOnlySpan<KeyValuePair<string, object?>> UsedStateTagList => m_stateTagList.AsSpan(1, 2);
778+
779+
// A slice of m_stateTagList that contains only the pool name tag.
780+
public ReadOnlySpan<KeyValuePair<string, object?>> PoolNameTagList => m_stateTagList.AsSpan(1, 1);
781+
782+
public static List<ConnectionPool> GetAllPools()
783+
{
784+
lock (s_allPools)
785+
return new(s_allPools);
786+
}
787+
744788
private sealed class LeastConnectionsLoadBalancer(Dictionary<string, int> hostSessions) : ILoadBalancer
745789
{
746790
public IReadOnlyList<string> LoadBalance(IReadOnlyList<string> hosts)
@@ -766,6 +810,7 @@ private static void OnAppDomainShutDown(object? sender, EventArgs e) =>
766810
ClearPoolsAsync(IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult();
767811

768812
private static readonly ConcurrentDictionary<string, ConnectionPool?> s_pools = new();
813+
private static readonly List<ConnectionPool> s_allPools = new();
769814
private static readonly Action<ILogger, int, string, Exception?> s_createdNewSession = LoggerMessage.Define<int, string>(
770815
LogLevel.Debug, new EventId(EventIds.PoolCreatedNewSession, nameof(EventIds.PoolCreatedNewSession)),
771816
"Pool {PoolId} has no pooled session available; created new session {SessionId}");
@@ -777,6 +822,7 @@ private static void OnAppDomainShutDown(object? sender, EventArgs e) =>
777822

778823
private readonly ILogger m_logger;
779824
private readonly ILogger m_connectionLogger;
825+
private readonly KeyValuePair<string, object?>[] m_stateTagList;
780826
private readonly SemaphoreSlim m_cleanSemaphore;
781827
private readonly SemaphoreSlim m_sessionSemaphore;
782828
private readonly LinkedList<ServerSession> m_sessions;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using System.Diagnostics.Metrics;
2+
using MySqlConnector.Utilities;
3+
4+
namespace MySqlConnector.Core;
5+
6+
internal static class MetricsReporter
7+
{
8+
public static void AddIdle(ConnectionPool pool) => s_connectionsUsageCounter.Add(1, pool.IdleStateTagList);
9+
public static void RemoveIdle(ConnectionPool pool) => s_connectionsUsageCounter.Add(-1, pool.IdleStateTagList);
10+
public static void AddUsed(ConnectionPool pool) => s_connectionsUsageCounter.Add(1, pool.UsedStateTagList);
11+
public static void RemoveUsed(ConnectionPool pool) => s_connectionsUsageCounter.Add(-1, pool.UsedStateTagList);
12+
public static void RecordCreateTime(ConnectionPool pool, uint ticks) => s_createTimeHistory.Record(ticks, pool.PoolNameTagList);
13+
public static void RecordUseTime(ConnectionPool pool, uint ticks) => s_useTimeHistory.Record(ticks, pool.PoolNameTagList);
14+
public static void RecordWaitTime(ConnectionPool pool, uint ticks) => s_waitTimeHistory.Record(ticks, pool.PoolNameTagList);
15+
16+
public static void AddPendingRequest(ConnectionPool? pool)
17+
{
18+
if (pool is not null)
19+
s_pendingRequestsCounter.Add(1, pool.PoolNameTagList);
20+
}
21+
22+
public static void RemovePendingRequest(ConnectionPool? pool)
23+
{
24+
if (pool is not null)
25+
s_pendingRequestsCounter.Add(-1, pool.PoolNameTagList);
26+
}
27+
28+
static MetricsReporter()
29+
{
30+
ActivitySourceHelper.Meter.CreateObservableUpDownCounter<int>("db.client.connections.idle.max",
31+
observeValues: GetMaximumConnections, unit: "{connection}",
32+
description: "The maximum number of idle open connections allowed; this corresponds to MaximumPoolSize in the connection string.");
33+
ActivitySourceHelper.Meter.CreateObservableUpDownCounter<int>("db.client.connections.idle.min",
34+
observeValues: GetMinimumConnections, unit: "{connection}",
35+
description: "The minimum number of idle open connections allowed; this corresponds to MinimumPoolSize in the connection string.");
36+
ActivitySourceHelper.Meter.CreateObservableUpDownCounter<int>("db.client.connections.max",
37+
observeValues: GetMaximumConnections, unit: "{connection}",
38+
description: "The maximum number of open connections allowed; this corresponds to MaximumPoolSize in the connection string.");
39+
40+
static IEnumerable<Measurement<int>> GetMaximumConnections() =>
41+
ConnectionPool.GetAllPools().Select(x => new Measurement<int>(x.ConnectionSettings.MaximumPoolSize, x.PoolNameTagList));
42+
43+
static IEnumerable<Measurement<int>> GetMinimumConnections() =>
44+
ConnectionPool.GetAllPools().Select(x => new Measurement<int>(x.ConnectionSettings.MinimumPoolSize, x.PoolNameTagList));
45+
}
46+
47+
private static readonly UpDownCounter<int> s_connectionsUsageCounter = ActivitySourceHelper.Meter.CreateUpDownCounter<int>("db.client.connections.usage",
48+
unit: "{connection}", description: "The number of connections that are currently in the state described by the state tag.");
49+
private static readonly UpDownCounter<int> s_pendingRequestsCounter = ActivitySourceHelper.Meter.CreateUpDownCounter<int>("db.client.connections.pending_requests",
50+
unit: "{request}", description: "The number of pending requests for an open connection, cumulative for the entire pool.");
51+
private static readonly Histogram<float> s_createTimeHistory = ActivitySourceHelper.Meter.CreateHistogram<float>("db.client.connections.create_time",
52+
unit: "ms", description: "The time it took to create a new connection.");
53+
private static readonly Histogram<float> s_useTimeHistory = ActivitySourceHelper.Meter.CreateHistogram<float>("db.client.connections.use_time",
54+
unit: "ms", description: "The time between borrowing a connection and returning it to the pool.");
55+
private static readonly Histogram<float> s_waitTimeHistory = ActivitySourceHelper.Meter.CreateHistogram<float>("db.client.connections.wait_time",
56+
unit: "ms", description: "The time it took to obtain an open connection from the pool.");
57+
}

src/MySqlConnector/Core/ServerSession.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Buffers.Text;
22
using System.ComponentModel;
33
using System.Diagnostics;
4+
using System.Diagnostics.Metrics;
45
using System.Globalization;
56
using System.IO.Pipes;
67
using System.Net;
@@ -56,6 +57,7 @@ public ServerSession(ILogger logger, ConnectionPool? pool, int poolGeneration, i
5657
public uint CreatedTicks { get; }
5758
public ConnectionPool? Pool { get; }
5859
public int PoolGeneration { get; }
60+
public uint LastLeasedTicks { get; set; }
5961
public uint LastReturnedTicks { get; private set; }
6062
public string? DatabaseOverride { get; set; }
6163
public string HostName { get; private set; }
@@ -74,7 +76,11 @@ public ValueTask ReturnToPoolAsync(IOBehavior ioBehavior, MySqlConnection? ownin
7476
{
7577
Log.ReturningToPool(m_logger, Id, Pool?.Id ?? 0);
7678
LastReturnedTicks = unchecked((uint) Environment.TickCount);
77-
return Pool is null ? default : Pool.ReturnAsync(ioBehavior, this);
79+
if (Pool is null)
80+
return default;
81+
MetricsReporter.RecordUseTime(Pool, unchecked(LastReturnedTicks - LastLeasedTicks));
82+
LastLeasedTicks = 0;
83+
return Pool.ReturnAsync(ioBehavior, this);
7884
}
7985

8086
public bool IsConnected

src/MySqlConnector/MySqlConnection.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,8 @@ private async ValueTask<bool> PingAsync(IOBehavior ioBehavior, CancellationToken
384384

385385
internal async Task OpenAsync(IOBehavior? ioBehavior, CancellationToken cancellationToken)
386386
{
387+
var openStartTickCount = Environment.TickCount;
388+
387389
VerifyNotDisposed();
388390
cancellationToken.ThrowIfCancellationRequested();
389391
if (State != ConnectionState.Closed)
@@ -392,8 +394,6 @@ internal async Task OpenAsync(IOBehavior? ioBehavior, CancellationToken cancella
392394
using var activity = ActivitySourceHelper.StartActivity(ActivitySourceHelper.OpenActivityName);
393395
try
394396
{
395-
var openStartTickCount = Environment.TickCount;
396-
397397
SetState(ConnectionState.Connecting);
398398

399399
var pool = m_dataSource?.Pool ??
@@ -896,6 +896,7 @@ internal void FinishQuerying(bool hasWarnings)
896896

897897
private async ValueTask<ServerSession> CreateSessionAsync(ConnectionPool? pool, int startTickCount, Activity? activity, IOBehavior? ioBehavior, CancellationToken cancellationToken)
898898
{
899+
MetricsReporter.AddPendingRequest(pool);
899900
var connectionSettings = GetInitializedConnectionSettings();
900901
var actualIOBehavior = ioBehavior ?? (connectionSettings.ForceSynchronous ? IOBehavior.Synchronous : IOBehavior.Asynchronous);
901902

@@ -949,6 +950,7 @@ private async ValueTask<ServerSession> CreateSessionAsync(ConnectionPool? pool,
949950
}
950951
finally
951952
{
953+
MetricsReporter.RemovePendingRequest(pool);
952954
linkedSource?.Dispose();
953955
timeoutSource?.Dispose();
954956
}

src/MySqlConnector/MySqlConnector.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
<PackageReference Include="System.Threading.Tasks.Extensions" />
3030
</ItemGroup>
3131

32-
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' != '.NETCoreApp' ">
32+
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' != '.NETCoreApp' OR '$(TargetFramework)' == 'net6.0' ">
3333
<PackageReference Include="System.Diagnostics.DiagnosticSource" />
3434
</ItemGroup>
3535

src/MySqlConnector/Utilities/ActivitySourceHelper.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Diagnostics;
2+
using System.Diagnostics.Metrics;
23
using System.Globalization;
34
using System.Reflection;
45

@@ -55,12 +56,10 @@ public static void CopyTags(IEnumerable<KeyValuePair<string, object?>> tags, Act
5556
}
5657
}
5758

58-
private static ActivitySource ActivitySource { get; } = CreateActivitySource();
59+
public static Meter Meter { get; } = new("MySqlConnector", GetVersion());
5960

60-
private static ActivitySource CreateActivitySource()
61-
{
62-
var assembly = typeof(ActivitySourceHelper).Assembly;
63-
var version = assembly.GetCustomAttribute<AssemblyFileVersionAttribute>()!.Version;
64-
return new("MySqlConnector", version);
65-
}
61+
private static ActivitySource ActivitySource { get; } = new("MySqlConnector", GetVersion());
62+
63+
private static string GetVersion() =>
64+
typeof(ActivitySourceHelper).Assembly.GetCustomAttribute<AssemblyFileVersionAttribute>()!.Version;
6665
}

tests/MySqlConnector.Tests/ConnectionTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ public void PingWhenClosed()
179179
[Fact]
180180
public void ConnectionTimeout()
181181
{
182-
m_server.BlockOnConnect = true;
182+
m_server.ConnectDelay = TimeSpan.FromSeconds(10);
183183
var csb = new MySqlConnectionStringBuilder(m_csb.ConnectionString);
184184
csb.ConnectionTimeout = 4;
185185
using var connection = new MySqlConnection(csb.ConnectionString);

tests/MySqlConnector.Tests/FakeMySqlServer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public void Stop()
5252

5353
public bool SuppressAuthPluginNameTerminatingNull { get; set; }
5454
public bool SendIncompletePostHandshakeResponse { get; set; }
55-
public bool BlockOnConnect { get; set; }
55+
public TimeSpan? ConnectDelay { get; set; }
5656
public TimeSpan? ResetDelay { get; set; }
5757

5858
internal void CancelQuery(int connectionId)

tests/MySqlConnector.Tests/FakeMySqlServerConnection.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ public async Task RunAsync(TcpClient client, CancellationToken token)
2525
using (client)
2626
using (var stream = client.GetStream())
2727
{
28-
if (m_server.BlockOnConnect)
29-
Thread.Sleep(TimeSpan.FromSeconds(10));
28+
if (m_server.ConnectDelay is { } connectDelay)
29+
await Task.Delay(connectDelay);
3030

3131
await SendAsync(stream, 0, WriteInitialHandshake);
3232
await ReadPayloadAsync(stream, token); // handshake response

0 commit comments

Comments
 (0)