diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 63bd07b0a..c144d9235 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -9,6 +9,7 @@ Current package versions: ## Unreleased - Support `HeartbeatConsistencyChecks` and `HeartbeatInterval` in `Clone()` ([#2658 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2658)) +- Add `AddLibraryNameSuffix` to multiplexer; allows usage-specific tokens to be appended *after connect* ## 2.7.23 diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index 4f025d218..2c47d034d 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -260,7 +260,7 @@ public bool SetClientLibrary /// Gets or sets the library name to use for CLIENT SETINFO lib-name calls to Redis during handshake. /// Defaults to "SE.Redis". /// - /// If the value is null, empty or whitespace, then the value from the options-provideer is used; + /// If the value is null, empty or whitespace, then the value from the options-provider is used; /// to disable the library name feature, use instead. public string? LibraryName { get; set; } diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.LibraryName.cs b/src/StackExchange.Redis/ConnectionMultiplexer.LibraryName.cs new file mode 100644 index 000000000..2c79f80c5 --- /dev/null +++ b/src/StackExchange.Redis/ConnectionMultiplexer.LibraryName.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; + +namespace StackExchange.Redis; + +public partial class ConnectionMultiplexer +{ + private readonly HashSet _libraryNameSuffixHash = new(); + private string _libraryNameSuffixCombined = ""; + + /// + public void AddLibraryNameSuffix(string suffix) + { + if (string.IsNullOrWhiteSpace(suffix)) return; // trivial + + // sanitize and re-check + suffix = ServerEndPoint.ClientInfoSanitize(suffix ?? "").Trim(); + if (string.IsNullOrWhiteSpace(suffix)) return; // trivial + + lock (_libraryNameSuffixHash) + { + if (!_libraryNameSuffixHash.Add(suffix)) return; // already cited; nothing to do + + _libraryNameSuffixCombined = "-" + string.Join("-", _libraryNameSuffixHash.OrderBy(_ => _)); + } + + // if we get here, we *actually changed something*; we can retroactively fixup the connections + var libName = GetFullLibraryName(); // note this also checks SetClientLibrary + if (string.IsNullOrWhiteSpace(libName) || !CommandMap.IsAvailable(RedisCommand.CLIENT)) return; // disabled on no lib name + + // note that during initial handshake we use raw Message; this is low frequency - no + // concern over overhead of Execute here + var args = new object[] { RedisLiterals.SETINFO, RedisLiterals.lib_name, libName }; + foreach (var server in GetServers()) + { + try + { + // note we can only fixup the *interactive* channel; that's tolerable here + if (server.IsConnected) + { + // best effort only + server.Execute("CLIENT", args, CommandFlags.FireAndForget); + } + } + catch (Exception ex) + { + // if an individual server trips, that's fine - best effort; note we're using + // F+F here anyway, so we don't *expect* any failures + Debug.WriteLine(ex.Message); + } + } + } + + internal string GetFullLibraryName() + { + var config = RawConfig; + if (!config.SetClientLibrary) return ""; // disabled + + var libName = config.LibraryName; + if (string.IsNullOrWhiteSpace(libName)) + { + // defer to provider if missing (note re null vs blank; if caller wants to disable + // it, they should set SetClientLibrary to false, not set the name to empty string) + libName = config.Defaults.LibraryName; + } + + libName = ServerEndPoint.ClientInfoSanitize(libName); + // if no primary name, return nothing, even if suffixes exist + if (string.IsNullOrWhiteSpace(libName)) return ""; + + return libName + Volatile.Read(ref _libraryNameSuffixCombined); + } +} diff --git a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs index 0c1494641..25d2f7099 100644 --- a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs @@ -294,5 +294,13 @@ public interface IConnectionMultiplexer : IDisposable, IAsyncDisposable /// The destination stream to write the export to. /// The options to use for this export. void ExportConfiguration(Stream destination, ExportOptions options = ExportOptions.All); + + /// + /// Append a usage-specific modifier to the advertised library name; suffixes are de-duplicated + /// and sorted alphabetically (so adding 'a', 'b' and 'a' will result in suffix '-a-b'). + /// Connections will be updated as necessary (RESP2 subscription + /// connections will not show updates until those connections next connect). + /// + void AddLibraryNameSuffix(string suffix); } } diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 906130e7a..1bcc6c66d 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -1845,4 +1845,6 @@ StackExchange.Redis.ResultType.VerbatimString = 12 -> StackExchange.Redis.Result static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisResult![]! values, StackExchange.Redis.ResultType resultType) -> StackExchange.Redis.RedisResult! static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.ResultType resultType) -> StackExchange.Redis.RedisResult! virtual StackExchange.Redis.RedisResult.Length.get -> int -virtual StackExchange.Redis.RedisResult.this[int index].get -> StackExchange.Redis.RedisResult! \ No newline at end of file +virtual StackExchange.Redis.RedisResult.this[int index].get -> StackExchange.Redis.RedisResult! +StackExchange.Redis.ConnectionMultiplexer.AddLibraryNameSuffix(string! suffix) -> void +StackExchange.Redis.IConnectionMultiplexer.AddLibraryNameSuffix(string! suffix) -> void \ No newline at end of file diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index 6191da28c..2cbe36920 100644 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -930,7 +930,7 @@ private async Task HandshakeAsync(PhysicalConnection connection, ILogger? log) var config = Multiplexer.RawConfig; string? user = config.User; string password = config.Password ?? ""; - + string clientName = Multiplexer.ClientName; if (!string.IsNullOrWhiteSpace(clientName)) { @@ -1017,15 +1017,7 @@ private async Task HandshakeAsync(PhysicalConnection connection, ILogger? log) // server version, so we will use this speculatively and hope for the best log?.LogInformation($"{Format.ToString(this)}: Setting client lib/ver"); - var libName = config.LibraryName; - if (string.IsNullOrWhiteSpace(libName)) - { - // defer to provider if missing (note re null vs blank; if caller wants to disable - // it, they should set SetClientLibrary to false, not set the name to empty string) - libName = config.Defaults.LibraryName; - } - - libName = ClientInfoSanitize(libName); + var libName = Multiplexer.GetFullLibraryName(); if (!string.IsNullOrWhiteSpace(libName)) { msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.CLIENT, diff --git a/tests/StackExchange.Redis.Tests/ConfigTests.cs b/tests/StackExchange.Redis.Tests/ConfigTests.cs index 97c48e356..a2329dc04 100644 --- a/tests/StackExchange.Redis.Tests/ConfigTests.cs +++ b/tests/StackExchange.Redis.Tests/ConfigTests.cs @@ -279,6 +279,41 @@ public void ClientName() Assert.Equal("TestRig", name); } + [Fact] + public async Task ClientLibraryName() + { + using var conn = Create(allowAdmin: true, shared: false); + var server = GetAnyPrimary(conn); + + await server.PingAsync(); + var possibleId = conn.GetConnectionId(server.EndPoint, ConnectionType.Interactive); + + if (possibleId is null) + { + Log("(client id not available)"); + return; + } + var id = possibleId.Value; + var libName = server.ClientList().Single(x => x.Id == id).LibraryName; + if (libName is not null) // server-version dependent + { + Log("library name: {0}", libName); + Assert.Equal("SE.Redis", libName); + + conn.AddLibraryNameSuffix("foo"); + conn.AddLibraryNameSuffix("bar"); + conn.AddLibraryNameSuffix("foo"); + + libName = (await server.ClientListAsync()).Single(x => x.Id == id).LibraryName; + Log("library name: {0}", libName); + Assert.Equal("SE.Redis-bar-foo", libName); + } + else + { + Log("(library name not available)"); + } + } + [Fact] public void DefaultClientName() { diff --git a/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs b/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs index c88c0ec4d..1d7396033 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs @@ -98,6 +98,8 @@ public bool IgnoreConnect public int GetSubscriptionsCount() => _inner.GetSubscriptionsCount(); public ConcurrentDictionary GetSubscriptions() => _inner.GetSubscriptions(); + public void AddLibraryNameSuffix(string suffix) => _inner.AddLibraryNameSuffix(suffix); + public string ClientName => _inner.ClientName; public string Configuration => _inner.Configuration;