Skip to content

Commit 18c057b

Browse files
authored
add new AddLibraryNameSuffix API for annotating connections with usage (#2659)
* add new AddLibraryNameSuffix API for annotating connections with usage * fixup test * new partial for lib-name bits * use hashing rather than array shenanigans * move to shipped; fix comment typo * comment example * reverse comment owner
1 parent a517561 commit 18c057b

File tree

8 files changed

+128
-12
lines changed

8 files changed

+128
-12
lines changed

docs/ReleaseNotes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Current package versions:
99
## Unreleased
1010

1111
- Support `HeartbeatConsistencyChecks` and `HeartbeatInterval` in `Clone()` ([#2658 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2658))
12+
- Add `AddLibraryNameSuffix` to multiplexer; allows usage-specific tokens to be appended *after connect*
1213

1314
## 2.7.23
1415

src/StackExchange.Redis/ConfigurationOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ public bool SetClientLibrary
260260
/// Gets or sets the library name to use for CLIENT SETINFO lib-name calls to Redis during handshake.
261261
/// Defaults to "SE.Redis".
262262
/// </summary>
263-
/// <remarks>If the value is null, empty or whitespace, then the value from the options-provideer is used;
263+
/// <remarks>If the value is null, empty or whitespace, then the value from the options-provider is used;
264264
/// to disable the library name feature, use <see cref="SetClientLibrary"/> instead.</remarks>
265265
public string? LibraryName { get; set; }
266266

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.Linq;
5+
using System.Threading;
6+
7+
namespace StackExchange.Redis;
8+
9+
public partial class ConnectionMultiplexer
10+
{
11+
private readonly HashSet<string> _libraryNameSuffixHash = new();
12+
private string _libraryNameSuffixCombined = "";
13+
14+
/// <inheritdoc cref="IConnectionMultiplexer.AddLibraryNameSuffix(string)" />
15+
public void AddLibraryNameSuffix(string suffix)
16+
{
17+
if (string.IsNullOrWhiteSpace(suffix)) return; // trivial
18+
19+
// sanitize and re-check
20+
suffix = ServerEndPoint.ClientInfoSanitize(suffix ?? "").Trim();
21+
if (string.IsNullOrWhiteSpace(suffix)) return; // trivial
22+
23+
lock (_libraryNameSuffixHash)
24+
{
25+
if (!_libraryNameSuffixHash.Add(suffix)) return; // already cited; nothing to do
26+
27+
_libraryNameSuffixCombined = "-" + string.Join("-", _libraryNameSuffixHash.OrderBy(_ => _));
28+
}
29+
30+
// if we get here, we *actually changed something*; we can retroactively fixup the connections
31+
var libName = GetFullLibraryName(); // note this also checks SetClientLibrary
32+
if (string.IsNullOrWhiteSpace(libName) || !CommandMap.IsAvailable(RedisCommand.CLIENT)) return; // disabled on no lib name
33+
34+
// note that during initial handshake we use raw Message; this is low frequency - no
35+
// concern over overhead of Execute here
36+
var args = new object[] { RedisLiterals.SETINFO, RedisLiterals.lib_name, libName };
37+
foreach (var server in GetServers())
38+
{
39+
try
40+
{
41+
// note we can only fixup the *interactive* channel; that's tolerable here
42+
if (server.IsConnected)
43+
{
44+
// best effort only
45+
server.Execute("CLIENT", args, CommandFlags.FireAndForget);
46+
}
47+
}
48+
catch (Exception ex)
49+
{
50+
// if an individual server trips, that's fine - best effort; note we're using
51+
// F+F here anyway, so we don't *expect* any failures
52+
Debug.WriteLine(ex.Message);
53+
}
54+
}
55+
}
56+
57+
internal string GetFullLibraryName()
58+
{
59+
var config = RawConfig;
60+
if (!config.SetClientLibrary) return ""; // disabled
61+
62+
var libName = config.LibraryName;
63+
if (string.IsNullOrWhiteSpace(libName))
64+
{
65+
// defer to provider if missing (note re null vs blank; if caller wants to disable
66+
// it, they should set SetClientLibrary to false, not set the name to empty string)
67+
libName = config.Defaults.LibraryName;
68+
}
69+
70+
libName = ServerEndPoint.ClientInfoSanitize(libName);
71+
// if no primary name, return nothing, even if suffixes exist
72+
if (string.IsNullOrWhiteSpace(libName)) return "";
73+
74+
return libName + Volatile.Read(ref _libraryNameSuffixCombined);
75+
}
76+
}

src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,5 +294,13 @@ public interface IConnectionMultiplexer : IDisposable, IAsyncDisposable
294294
/// <param name="destination">The destination stream to write the export to.</param>
295295
/// <param name="options">The options to use for this export.</param>
296296
void ExportConfiguration(Stream destination, ExportOptions options = ExportOptions.All);
297+
298+
/// <summary>
299+
/// Append a usage-specific modifier to the advertised library name; suffixes are de-duplicated
300+
/// and sorted alphabetically (so adding 'a', 'b' and 'a' will result in suffix '-a-b').
301+
/// Connections will be updated as necessary (RESP2 subscription
302+
/// connections will not show updates until those connections next connect).
303+
/// </summary>
304+
void AddLibraryNameSuffix(string suffix);
297305
}
298306
}

src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1845,4 +1845,6 @@ StackExchange.Redis.ResultType.VerbatimString = 12 -> StackExchange.Redis.Result
18451845
static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisResult![]! values, StackExchange.Redis.ResultType resultType) -> StackExchange.Redis.RedisResult!
18461846
static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.ResultType resultType) -> StackExchange.Redis.RedisResult!
18471847
virtual StackExchange.Redis.RedisResult.Length.get -> int
1848-
virtual StackExchange.Redis.RedisResult.this[int index].get -> StackExchange.Redis.RedisResult!
1848+
virtual StackExchange.Redis.RedisResult.this[int index].get -> StackExchange.Redis.RedisResult!
1849+
StackExchange.Redis.ConnectionMultiplexer.AddLibraryNameSuffix(string! suffix) -> void
1850+
StackExchange.Redis.IConnectionMultiplexer.AddLibraryNameSuffix(string! suffix) -> void

src/StackExchange.Redis/ServerEndPoint.cs

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -930,7 +930,7 @@ private async Task HandshakeAsync(PhysicalConnection connection, ILogger? log)
930930
var config = Multiplexer.RawConfig;
931931
string? user = config.User;
932932
string password = config.Password ?? "";
933-
933+
934934
string clientName = Multiplexer.ClientName;
935935
if (!string.IsNullOrWhiteSpace(clientName))
936936
{
@@ -1017,15 +1017,7 @@ private async Task HandshakeAsync(PhysicalConnection connection, ILogger? log)
10171017
// server version, so we will use this speculatively and hope for the best
10181018
log?.LogInformation($"{Format.ToString(this)}: Setting client lib/ver");
10191019

1020-
var libName = config.LibraryName;
1021-
if (string.IsNullOrWhiteSpace(libName))
1022-
{
1023-
// defer to provider if missing (note re null vs blank; if caller wants to disable
1024-
// it, they should set SetClientLibrary to false, not set the name to empty string)
1025-
libName = config.Defaults.LibraryName;
1026-
}
1027-
1028-
libName = ClientInfoSanitize(libName);
1020+
var libName = Multiplexer.GetFullLibraryName();
10291021
if (!string.IsNullOrWhiteSpace(libName))
10301022
{
10311023
msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.CLIENT,

tests/StackExchange.Redis.Tests/ConfigTests.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,41 @@ public void ClientName()
279279
Assert.Equal("TestRig", name);
280280
}
281281

282+
[Fact]
283+
public async Task ClientLibraryName()
284+
{
285+
using var conn = Create(allowAdmin: true, shared: false);
286+
var server = GetAnyPrimary(conn);
287+
288+
await server.PingAsync();
289+
var possibleId = conn.GetConnectionId(server.EndPoint, ConnectionType.Interactive);
290+
291+
if (possibleId is null)
292+
{
293+
Log("(client id not available)");
294+
return;
295+
}
296+
var id = possibleId.Value;
297+
var libName = server.ClientList().Single(x => x.Id == id).LibraryName;
298+
if (libName is not null) // server-version dependent
299+
{
300+
Log("library name: {0}", libName);
301+
Assert.Equal("SE.Redis", libName);
302+
303+
conn.AddLibraryNameSuffix("foo");
304+
conn.AddLibraryNameSuffix("bar");
305+
conn.AddLibraryNameSuffix("foo");
306+
307+
libName = (await server.ClientListAsync()).Single(x => x.Id == id).LibraryName;
308+
Log("library name: {0}", libName);
309+
Assert.Equal("SE.Redis-bar-foo", libName);
310+
}
311+
else
312+
{
313+
Log("(library name not available)");
314+
}
315+
}
316+
282317
[Fact]
283318
public void DefaultClientName()
284319
{

tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ public bool IgnoreConnect
9898
public int GetSubscriptionsCount() => _inner.GetSubscriptionsCount();
9999
public ConcurrentDictionary<RedisChannel, ConnectionMultiplexer.Subscription> GetSubscriptions() => _inner.GetSubscriptions();
100100

101+
public void AddLibraryNameSuffix(string suffix) => _inner.AddLibraryNameSuffix(suffix);
102+
101103
public string ClientName => _inner.ClientName;
102104

103105
public string Configuration => _inner.Configuration;

0 commit comments

Comments
 (0)