diff --git a/docs/Configuration.md b/docs/Configuration.md index d085d967c..90df67b9f 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -96,7 +96,8 @@ The `ConfigurationOptions` object has a wide range of properties, all of which a | asyncTimeout={int} | `AsyncTimeout` | `SyncTimeout` | Time (ms) to allow for asynchronous operations | | tiebreaker={string} | `TieBreaker` | `__Booksleeve_TieBreak` | Key to use for selecting a server in an ambiguous primary scenario | | version={string} | `DefaultVersion` | (`4.0` in Azure, else `2.0`) | Redis version level (useful when the server does not make this available) | -| tunnel={string} | `Tunnel` | `null` | Tunnel for connections (use `http:{proxy url}` for "connect"-based proxy server) +| tunnel={string} | `Tunnel` | `null` | Tunnel for connections (use `http:{proxy url}` for "connect"-based proxy server) | +| setlib={bool} | `SetClientLibrary` | `true` | Whether to attempt to use `CLIENT SETINFO` to set the lib name/version on the connection | Additional code-only options: - ReconnectRetryPolicy (`IReconnectRetryPolicy`) - Default: `ReconnectRetryPolicy = ExponentialRetry(ConnectTimeout / 2);` diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 239b25aa8..d1cd9e7cb 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -9,6 +9,7 @@ Current package versions: ## Unreleased - Fix [#2400](https://github.com/StackExchange/StackExchange.Redis/issues/2400): Expose `ChannelMessageQueue` as `IAsyncEnumerable` ([#2402 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2402)) +- Add: support for `CLIENT SETINFO` (lib name/version) during handshake; opt-out is via `ConfigurationOptions`; also support read of `resp`, `lib-ver` and `lib-name` via `CLIENT LIST` ([#2414 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2414)) ## 2.6.96 diff --git a/src/StackExchange.Redis/ClientInfo.cs b/src/StackExchange.Redis/ClientInfo.cs index 272bd97da..4fa0aa378 100644 --- a/src/StackExchange.Redis/ClientInfo.cs +++ b/src/StackExchange.Redis/ClientInfo.cs @@ -180,6 +180,23 @@ public ClientType ClientType } } + /// + /// Client RESP protocol version. Added in Redis 7.0 + /// + public string? ProtocolVersion { get; private set; } + + /// + /// Client library name. Added in Redis 7.2 + /// + /// + public string? LibraryName { get; private set; } + + /// + /// Client library version. Added in Redis 7.2 + /// + /// + public string? LibraryVersion { get; private set; } + internal static bool TryParse(string? input, [NotNullWhen(true)] out ClientInfo[]? clientList) { if (input == null) @@ -241,6 +258,9 @@ internal static bool TryParse(string? input, [NotNullWhen(true)] out ClientInfo[ client.Flags = flags; break; case "id": client.Id = Format.ParseInt64(value); break; + case "resp": client.ProtocolVersion = value; break; + case "lib-name": client.LibraryName = value; break; + case "lib-ver": client.LibraryVersion = value; break; } } clients.Add(client); diff --git a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs index 72b1d3997..ad9a31031 100644 --- a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs +++ b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs @@ -197,6 +197,11 @@ protected virtual string GetDefaultClientName() => /// protected static string ComputerName => Environment.MachineName ?? Environment.GetEnvironmentVariable("ComputerName") ?? "Unknown"; + /// + /// Whether to identify the client by library name/version when possible + /// + public virtual bool SetClientLibrary => true; + /// /// Tries to get the RoleInstance Id if Microsoft.WindowsAzure.ServiceRuntime is loaded. /// In case of any failure, swallows the exception and returns null. diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index e773944d1..2d52640d8 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -97,7 +97,8 @@ internal const string Version = "version", WriteBuffer = "writeBuffer", CheckCertificateRevocation = "checkCertificateRevocation", - Tunnel = "tunnel"; + Tunnel = "tunnel", + SetClientLibrary = "setlib"; private static readonly Dictionary normalizedOptions = new[] { @@ -142,7 +143,7 @@ public static string TryNormalize(string value) private DefaultOptionsProvider? defaultOptions; private bool? allowAdmin, abortOnConnectFail, resolveDns, ssl, checkCertificateRevocation, - includeDetailInExceptions, includePerformanceCountersInExceptions; + includeDetailInExceptions, includePerformanceCountersInExceptions, setClientLibrary; private string? tieBreaker, sslHost, configChannel; @@ -231,6 +232,15 @@ public bool UseSsl set => Ssl = value; } + /// + /// Gets or sets whether the library should identify itself by library-name/version when possible + /// + public bool SetClientLibrary + { + get => setClientLibrary ?? Defaults.SetClientLibrary; + set => setClientLibrary = value; + } + /// /// Automatically encodes and decodes channels. /// @@ -652,6 +662,7 @@ public static ConfigurationOptions Parse(string configuration, bool ignoreUnknow SslClientAuthenticationOptions = SslClientAuthenticationOptions, #endif Tunnel = Tunnel, + setClientLibrary = setClientLibrary, }; /// @@ -731,6 +742,7 @@ public string ToString(bool includePassword) Append(sb, OptionKeys.ConfigCheckSeconds, configCheckSeconds); Append(sb, OptionKeys.ResponseTimeout, responseTimeout); Append(sb, OptionKeys.DefaultDatabase, DefaultDatabase); + Append(sb, OptionKeys.SetClientLibrary, setClientLibrary); if (Tunnel is { IsInbuilt: true } tunnel) { Append(sb, OptionKeys.Tunnel, tunnel.ToString()); @@ -768,7 +780,7 @@ private void Clear() { ClientName = ServiceName = User = Password = tieBreaker = sslHost = configChannel = null; keepAlive = syncTimeout = asyncTimeout = connectTimeout = connectRetry = configCheckSeconds = DefaultDatabase = null; - allowAdmin = abortOnConnectFail = resolveDns = ssl = null; + allowAdmin = abortOnConnectFail = resolveDns = ssl = setClientLibrary = null; SslProtocols = null; defaultVersion = null; EndPoints.Clear(); @@ -778,6 +790,7 @@ private void Clear() CertificateValidation = null; ChannelPrefix = default; SocketManager = null; + Tunnel = null; } object ICloneable.Clone() => Clone(); @@ -883,6 +896,9 @@ private ConfigurationOptions DoParse(string configuration, bool ignoreUnknown) case OptionKeys.SslProtocols: SslProtocols = OptionKeys.ParseSslProtocols(key, value); break; + case OptionKeys.SetClientLibrary: + SetClientLibrary = OptionKeys.ParseBoolean(key, value); + break; case OptionKeys.Tunnel: if (value.IsNullOrWhiteSpace()) { diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 986eab9c6..e0a17c546 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -118,9 +118,12 @@ StackExchange.Redis.ClientInfo.Host.get -> string? StackExchange.Redis.ClientInfo.Id.get -> long StackExchange.Redis.ClientInfo.IdleSeconds.get -> int StackExchange.Redis.ClientInfo.LastCommand.get -> string? +StackExchange.Redis.ClientInfo.LibraryName.get -> string? +StackExchange.Redis.ClientInfo.LibraryVersion.get -> string? StackExchange.Redis.ClientInfo.Name.get -> string? StackExchange.Redis.ClientInfo.PatternSubscriptionCount.get -> int StackExchange.Redis.ClientInfo.Port.get -> int +StackExchange.Redis.ClientInfo.ProtocolVersion.get -> string? StackExchange.Redis.ClientInfo.Raw.get -> string? StackExchange.Redis.ClientInfo.SubscriptionCount.get -> int StackExchange.Redis.ClientInfo.TransactionCommandLength.get -> int @@ -253,6 +256,8 @@ StackExchange.Redis.ConfigurationOptions.ResponseTimeout.get -> int StackExchange.Redis.ConfigurationOptions.ResponseTimeout.set -> void StackExchange.Redis.ConfigurationOptions.ServiceName.get -> string? StackExchange.Redis.ConfigurationOptions.ServiceName.set -> void +StackExchange.Redis.ConfigurationOptions.SetClientLibrary.get -> bool +StackExchange.Redis.ConfigurationOptions.SetClientLibrary.set -> void StackExchange.Redis.ConfigurationOptions.SetDefaultPorts() -> void StackExchange.Redis.ConfigurationOptions.SocketManager.get -> StackExchange.Redis.SocketManager? StackExchange.Redis.ConfigurationOptions.SocketManager.set -> void @@ -1785,5 +1790,6 @@ virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.KeepAliveInterv virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.Proxy.get -> StackExchange.Redis.Proxy virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.ReconnectRetryPolicy.get -> StackExchange.Redis.IReconnectRetryPolicy? virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.ResolveDns.get -> bool +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.SetClientLibrary.get -> bool virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.SyncTimeout.get -> System.TimeSpan virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.TieBreaker.get -> string! diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index 73363968c..e6d1e76c2 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -83,6 +83,8 @@ public static readonly RedisValue LATEST = "LATEST", LEFT = "LEFT", LEN = "LEN", + lib_name = "lib-name", + lib_ver = "lib-ver", LIMIT = "LIMIT", LIST = "LIST", LOAD = "LOAD", @@ -118,8 +120,10 @@ public static readonly RedisValue REWRITE = "REWRITE", RIGHT = "RIGHT", SAVE = "SAVE", + SE_Redis = "SE.Redis", SEGFAULT = "SEGFAULT", SET = "SET", + SETINFO = "SETINFO", SETNAME = "SETNAME", SKIPME = "SKIPME", STATS = "STATS", diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index 1e192d85e..b7d367cce 100644 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -929,6 +929,26 @@ private async Task HandshakeAsync(PhysicalConnection connection, LogProxy? log) await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.DemandOK).ForAwait(); } } + if (Multiplexer.RawConfig.SetClientLibrary) + { + // note that this is a relatively new feature, but usually we won't know the + // server version, so we will use this speculatively and hope for the best + log?.WriteLine($"{Format.ToString(this)}: Setting client lib/ver"); + + msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.CLIENT, + RedisLiterals.SETINFO, RedisLiterals.lib_name, RedisLiterals.SE_Redis); + msg.SetInternalCall(); + await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.DemandOK).ForAwait(); + + var version = Utils.GetLibVersion(); + if (!string.IsNullOrWhiteSpace(version)) + { + msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.CLIENT, + RedisLiterals.SETINFO, RedisLiterals.lib_ver, version); + msg.SetInternalCall(); + await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.DemandOK).ForAwait(); + } + } } var bridge = connection.BridgeCouldBeNull; diff --git a/tests/StackExchange.Redis.Tests/ConfigTests.cs b/tests/StackExchange.Redis.Tests/ConfigTests.cs index 668abe607..a90bd96df 100644 --- a/tests/StackExchange.Redis.Tests/ConfigTests.cs +++ b/tests/StackExchange.Redis.Tests/ConfigTests.cs @@ -648,4 +648,18 @@ public void CustomTunnelCanRoundtripMinusTunnel() options = ConfigurationOptions.Parse(cs); Assert.Null(options.Tunnel); } + + [Theory] + [InlineData("server:6379", true)] + [InlineData("server:6379,setlib=True", true)] + [InlineData("server:6379,setlib=False", false)] + public void DefaultConfigOptionsForSetLib(string configurationString, bool setlib) + { + var options = ConfigurationOptions.Parse(configurationString); + Assert.Equal(setlib, options.SetClientLibrary); + Assert.Equal(configurationString, options.ToString()); + options = options.Clone(); + Assert.Equal(setlib, options.SetClientLibrary); + Assert.Equal(configurationString, options.ToString()); + } }