diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md
index 3a50f1ef3..ce759ab42 100644
--- a/docs/ReleaseNotes.md
+++ b/docs/ReleaseNotes.md
@@ -13,6 +13,7 @@ Current package versions:
- Add `Condition.SortedSet[Not]ContainsStarting` condition for transactions ([#2638 by ArnoKoll](https://github.com/StackExchange/StackExchange.Redis/pull/2638))
- Add support for XPENDING Idle time filter ([#2822 by david-brink-talogy](https://github.com/StackExchange/StackExchange.Redis/pull/2822))
- Improve `double` formatting performance on net8+ ([#2928 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2928))
+- Add `GetServer(RedisKey, ...)` API ([#2936 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2936))
## 2.8.58
diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs
index bf1b75e25..bf6b66674 100644
--- a/src/StackExchange.Redis/ConnectionMultiplexer.cs
+++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs
@@ -210,7 +210,7 @@ internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOpt
{
throw ExceptionFactory.AdminModeNotEnabled(RawConfig.IncludeDetailInExceptions, cmd, null, server);
}
- var srv = new RedisServer(this, server, null);
+ var srv = server.GetRedisServer(null);
if (!srv.IsConnected)
{
throw ExceptionFactory.NoConnectionAvailable(this, null, server, GetServerSnapshot(), command: cmd);
@@ -1229,7 +1229,21 @@ public IServer GetServer(EndPoint? endpoint, object? asyncState = null)
throw new NotSupportedException($"The server API is not available via {RawConfig.Proxy}");
}
var server = servers[endpoint] as ServerEndPoint ?? throw new ArgumentException("The specified endpoint is not defined", nameof(endpoint));
- return new RedisServer(this, server, asyncState);
+ return server.GetRedisServer(asyncState);
+ }
+
+ ///
+#pragma warning disable RS0026
+ public IServer GetServer(RedisKey key, object? asyncState = null, CommandFlags flags = CommandFlags.None)
+#pragma warning restore RS0026
+ {
+ // We'll spoof the GET command for this; we're not supporting ad-hoc access to the pub/sub channel, because: bad things.
+ // Any read-only-replica vs writable-primary concerns should be managed by the caller via "flags"; the default is PreferPrimary.
+ // Note that ServerSelectionStrategy treats "null" (default) keys as NoSlot, aka Any.
+ return (SelectServer(RedisCommand.GET, flags, key) ?? Throw()).GetRedisServer(asyncState);
+
+ [DoesNotReturn]
+ static ServerEndPoint Throw() => throw new InvalidOperationException("It was not possible to resolve a connection to the server owning the specified key");
}
///
@@ -1241,7 +1255,7 @@ public IServer[] GetServers()
var result = new IServer[snapshot.Length];
for (var i = 0; i < snapshot.Length; i++)
{
- result[i] = new RedisServer(this, snapshot[i], null);
+ result[i] = snapshot[i].GetRedisServer(null);
}
return result;
}
diff --git a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs
index b4bdb0950..96b4ce8f6 100644
--- a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs
+++ b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs
@@ -8,299 +8,311 @@
using StackExchange.Redis.Profiling;
using static StackExchange.Redis.ConnectionMultiplexer;
-namespace StackExchange.Redis
+namespace StackExchange.Redis;
+
+internal interface IInternalConnectionMultiplexer : IConnectionMultiplexer
{
- internal interface IInternalConnectionMultiplexer : IConnectionMultiplexer
- {
- bool AllowConnect { get; set; }
-
- bool IgnoreConnect { get; set; }
-
- ReadOnlySpan GetServerSnapshot();
- ServerEndPoint GetServerEndPoint(EndPoint endpoint);
-
- ConfigurationOptions RawConfig { get; }
-
- long? GetConnectionId(EndPoint endPoint, ConnectionType type);
-
- ServerSelectionStrategy ServerSelectionStrategy { get; }
-
- int GetSubscriptionsCount();
- ConcurrentDictionary GetSubscriptions();
-
- ConnectionMultiplexer UnderlyingMultiplexer { get; }
- }
-
- ///
- /// Represents the abstract multiplexer API.
- ///
- public interface IConnectionMultiplexer : IDisposable, IAsyncDisposable
- {
- ///
- /// Gets the client-name that will be used on all new connections.
- ///
- string ClientName { get; }
-
- ///
- /// Gets the configuration of the connection.
- ///
- string Configuration { get; }
-
- ///
- /// Gets the timeout associated with the connections.
- ///
- int TimeoutMilliseconds { get; }
-
- ///
- /// The number of operations that have been performed on all connections.
- ///
- long OperationCount { get; }
-
- ///
- /// Gets or sets whether asynchronous operations should be invoked in a way that guarantees their original delivery order.
- ///
- [Obsolete("Not supported; if you require ordered pub/sub, please see " + nameof(ChannelMessageQueue), false)]
- [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
- bool PreserveAsyncOrder { get; set; }
-
- ///
- /// Indicates whether any servers are connected.
- ///
- bool IsConnected { get; }
-
- ///
- /// Indicates whether any servers are connecting.
- ///
- bool IsConnecting { get; }
-
- ///
- /// Should exceptions include identifiable details? (key names, additional annotations).
- ///
- [Obsolete($"Please use {nameof(ConfigurationOptions)}.{nameof(ConfigurationOptions.IncludeDetailInExceptions)} instead - this will be removed in 3.0.")]
- [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
- bool IncludeDetailInExceptions { get; set; }
-
- ///
- /// Limit at which to start recording unusual busy patterns (only one log will be retained at a time.
- /// Set to a negative value to disable this feature).
- ///
- int StormLogThreshold { get; set; }
-
- ///
- /// Register a callback to provide an on-demand ambient session provider based on the calling context.
- /// The implementing code is responsible for reliably resolving the same provider
- /// based on ambient context, or returning null to not profile.
- ///
- /// The profiling session provider.
- void RegisterProfiler(Func profilingSessionProvider);
-
- ///
- /// Get summary statistics associates with this server.
- ///
- ServerCounters GetCounters();
-
- ///
- /// A server replied with an error message.
- ///
- event EventHandler ErrorMessage;
-
- ///
- /// Raised whenever a physical connection fails.
- ///
- event EventHandler ConnectionFailed;
-
- ///
- /// Raised whenever an internal error occurs (this is primarily for debugging).
- ///
- event EventHandler InternalError;
-
- ///
- /// Raised whenever a physical connection is established.
- ///
- event EventHandler ConnectionRestored;
-
- ///
- /// Raised when configuration changes are detected.
- ///
- event EventHandler ConfigurationChanged;
-
- ///
- /// Raised when nodes are explicitly requested to reconfigure via broadcast.
- /// This usually means primary/replica changes.
- ///
- event EventHandler ConfigurationChangedBroadcast;
-
- ///
- /// Raised when server indicates a maintenance event is going to happen.
- ///
- event EventHandler ServerMaintenanceEvent;
-
- ///
- /// Gets all endpoints defined on the multiplexer.
- ///
- /// Whether to return only the explicitly configured endpoints.
- EndPoint[] GetEndPoints(bool configuredOnly = false);
-
- ///
- /// Wait for a given asynchronous operation to complete (or timeout).
- ///
- /// The task to wait on.
- void Wait(Task task);
-
- ///
- /// Wait for a given asynchronous operation to complete (or timeout).
- ///
- /// The type in .
- /// The task to wait on.
- T Wait(Task task);
-
- ///
- /// Wait for the given asynchronous operations to complete (or timeout).
- ///
- /// The tasks to wait on.
- void WaitAll(params Task[] tasks);
-
- ///
- /// Raised when a hash-slot has been relocated.
- ///
- event EventHandler HashSlotMoved;
-
- ///
- /// Compute the hash-slot of a specified key.
- ///
- /// The key to get a slot ID for.
- int HashSlot(RedisKey key);
-
- ///
- /// Obtain a pub/sub subscriber connection to the specified server.
- ///
- /// The async state to pass to the created .
- ISubscriber GetSubscriber(object? asyncState = null);
-
- ///
- /// Obtain an interactive connection to a database inside redis.
- ///
- /// The database ID to get.
- /// The async state to pass to the created .
- IDatabase GetDatabase(int db = -1, object? asyncState = null);
-
- ///
- /// Obtain a configuration API for an individual server.
- ///
- /// The host to get a server for.
- /// The specific port for to get a server for.
- /// The async state to pass to the created .
- IServer GetServer(string host, int port, object? asyncState = null);
-
- ///
- /// Obtain a configuration API for an individual server.
- ///
- /// The "host:port" string to get a server for.
- /// The async state to pass to the created .
- IServer GetServer(string hostAndPort, object? asyncState = null);
-
- ///
- /// Obtain a configuration API for an individual server.
- ///
- /// The host to get a server for.
- /// The specific port for to get a server for.
- IServer GetServer(IPAddress host, int port);
-
- ///
- /// Obtain a configuration API for an individual server.
- ///
- /// The endpoint to get a server for.
- /// The async state to pass to the created .
- IServer GetServer(EndPoint endpoint, object? asyncState = null);
-
- ///
- /// Obtain configuration APIs for all servers in this multiplexer.
- ///
- IServer[] GetServers();
-
- ///
- /// Reconfigure the current connections based on the existing configuration.
- ///
- /// The log to write output to.
- Task ConfigureAsync(TextWriter? log = null);
-
- ///
- /// Reconfigure the current connections based on the existing configuration.
- ///
- /// The log to write output to.
- bool Configure(TextWriter? log = null);
-
- ///
- /// Provides a text overview of the status of all connections.
- ///
- string GetStatus();
-
- ///
- /// Provides a text overview of the status of all connections.
- ///
- /// The log to write output to.
- void GetStatus(TextWriter log);
-
- ///
- /// See .
- ///
- string ToString();
-
- ///
- /// Close all connections and release all resources associated with this object.
- ///
- /// Whether to allow in-queue commands to complete first.
- void Close(bool allowCommandsToComplete = true);
-
- ///
- /// Close all connections and release all resources associated with this object.
- ///
- /// Whether to allow in-queue commands to complete first.
- Task CloseAsync(bool allowCommandsToComplete = true);
-
- ///
- /// Obtains the log of unusual busy patterns.
- ///
- string? GetStormLog();
-
- ///
- /// Resets the log of unusual busy patterns.
- ///
- void ResetStormLog();
-
- ///
- /// Request all compatible clients to reconfigure or reconnect.
- ///
- /// The command flags to use.
- /// The number of instances known to have received the message (however, the actual number can be higher; returns -1 if the operation is pending).
- long PublishReconfigure(CommandFlags flags = CommandFlags.None);
-
- ///
- /// Request all compatible clients to reconfigure or reconnect.
- ///
- /// The command flags to use.
- /// The number of instances known to have received the message (however, the actual number can be higher).
- Task PublishReconfigureAsync(CommandFlags flags = CommandFlags.None);
-
- ///
- /// Get the hash-slot associated with a given key, if applicable; this can be useful for grouping operations.
- ///
- /// The key to get a the slot for.
- int GetHashSlot(RedisKey key);
-
- ///
- /// Write the configuration of all servers to an output stream.
- ///
- /// 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);
- }
+ bool AllowConnect { get; set; }
+
+ bool IgnoreConnect { get; set; }
+
+ ReadOnlySpan GetServerSnapshot();
+ ServerEndPoint GetServerEndPoint(EndPoint endpoint);
+
+ ConfigurationOptions RawConfig { get; }
+
+ long? GetConnectionId(EndPoint endPoint, ConnectionType type);
+
+ ServerSelectionStrategy ServerSelectionStrategy { get; }
+
+ int GetSubscriptionsCount();
+ ConcurrentDictionary GetSubscriptions();
+
+ ConnectionMultiplexer UnderlyingMultiplexer { get; }
+}
+
+///
+/// Represents the abstract multiplexer API.
+///
+public interface IConnectionMultiplexer : IDisposable, IAsyncDisposable
+{
+ ///
+ /// Gets the client-name that will be used on all new connections.
+ ///
+ string ClientName { get; }
+
+ ///
+ /// Gets the configuration of the connection.
+ ///
+ string Configuration { get; }
+
+ ///
+ /// Gets the timeout associated with the connections.
+ ///
+ int TimeoutMilliseconds { get; }
+
+ ///
+ /// The number of operations that have been performed on all connections.
+ ///
+ long OperationCount { get; }
+
+ ///
+ /// Gets or sets whether asynchronous operations should be invoked in a way that guarantees their original delivery order.
+ ///
+ [Obsolete("Not supported; if you require ordered pub/sub, please see " + nameof(ChannelMessageQueue), false)]
+ [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
+ bool PreserveAsyncOrder { get; set; }
+
+ ///
+ /// Indicates whether any servers are connected.
+ ///
+ bool IsConnected { get; }
+
+ ///
+ /// Indicates whether any servers are connecting.
+ ///
+ bool IsConnecting { get; }
+
+ ///
+ /// Should exceptions include identifiable details? (key names, additional annotations).
+ ///
+ [Obsolete($"Please use {nameof(ConfigurationOptions)}.{nameof(ConfigurationOptions.IncludeDetailInExceptions)} instead - this will be removed in 3.0.")]
+ [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
+ bool IncludeDetailInExceptions { get; set; }
+
+ ///
+ /// Limit at which to start recording unusual busy patterns (only one log will be retained at a time.
+ /// Set to a negative value to disable this feature).
+ ///
+ int StormLogThreshold { get; set; }
+
+ ///
+ /// Register a callback to provide an on-demand ambient session provider based on the calling context.
+ /// The implementing code is responsible for reliably resolving the same provider
+ /// based on ambient context, or returning null to not profile.
+ ///
+ /// The profiling session provider.
+ void RegisterProfiler(Func profilingSessionProvider);
+
+ ///
+ /// Get summary statistics associates with this server.
+ ///
+ ServerCounters GetCounters();
+
+ ///
+ /// A server replied with an error message.
+ ///
+ event EventHandler ErrorMessage;
+
+ ///
+ /// Raised whenever a physical connection fails.
+ ///
+ event EventHandler ConnectionFailed;
+
+ ///
+ /// Raised whenever an internal error occurs (this is primarily for debugging).
+ ///
+ event EventHandler InternalError;
+
+ ///
+ /// Raised whenever a physical connection is established.
+ ///
+ event EventHandler ConnectionRestored;
+
+ ///
+ /// Raised when configuration changes are detected.
+ ///
+ event EventHandler ConfigurationChanged;
+
+ ///
+ /// Raised when nodes are explicitly requested to reconfigure via broadcast.
+ /// This usually means primary/replica changes.
+ ///
+ event EventHandler ConfigurationChangedBroadcast;
+
+ ///
+ /// Raised when server indicates a maintenance event is going to happen.
+ ///
+ event EventHandler ServerMaintenanceEvent;
+
+ ///
+ /// Gets all endpoints defined on the multiplexer.
+ ///
+ /// Whether to return only the explicitly configured endpoints.
+ EndPoint[] GetEndPoints(bool configuredOnly = false);
+
+ ///
+ /// Wait for a given asynchronous operation to complete (or timeout).
+ ///
+ /// The task to wait on.
+ void Wait(Task task);
+
+ ///
+ /// Wait for a given asynchronous operation to complete (or timeout).
+ ///
+ /// The type in .
+ /// The task to wait on.
+ T Wait(Task task);
+
+ ///
+ /// Wait for the given asynchronous operations to complete (or timeout).
+ ///
+ /// The tasks to wait on.
+ void WaitAll(params Task[] tasks);
+
+ ///
+ /// Raised when a hash-slot has been relocated.
+ ///
+ event EventHandler HashSlotMoved;
+
+ ///
+ /// Compute the hash-slot of a specified key.
+ ///
+ /// The key to get a slot ID for.
+ int HashSlot(RedisKey key);
+
+ ///
+ /// Obtain a pub/sub subscriber connection to the specified server.
+ ///
+ /// The async state to pass to the created .
+ ISubscriber GetSubscriber(object? asyncState = null);
+
+ ///
+ /// Obtain an interactive connection to a database inside redis.
+ ///
+ /// The database ID to get.
+ /// The async state to pass to the created .
+ IDatabase GetDatabase(int db = -1, object? asyncState = null);
+
+ ///
+ /// Obtain a configuration API for an individual server.
+ ///
+ /// The host to get a server for.
+ /// The specific port for to get a server for.
+ /// The async state to pass to the created .
+ IServer GetServer(string host, int port, object? asyncState = null);
+
+ ///
+ /// Obtain a configuration API for an individual server.
+ ///
+ /// The "host:port" string to get a server for.
+ /// The async state to pass to the created .
+ IServer GetServer(string hostAndPort, object? asyncState = null);
+
+ ///
+ /// Obtain a configuration API for an individual server.
+ ///
+ /// The host to get a server for.
+ /// The specific port for to get a server for.
+ IServer GetServer(IPAddress host, int port);
+
+ ///
+ /// Obtain a configuration API for an individual server.
+ ///
+ /// The endpoint to get a server for.
+ /// The async state to pass to the created .
+ IServer GetServer(EndPoint endpoint, object? asyncState = null);
+
+ ///
+ /// Gets a server that would be used for a given key and flags.
+ ///
+ /// The endpoint to get a server for. In a non-cluster environment, this parameter is ignored. A key may be specified
+ /// on cluster, which will return a connection to an arbitrary server matching the specified flags.
+ /// The async state to pass to the created .
+ /// The command flags to use.
+ /// This method is particularly useful when communicating with a cluster environment, to obtain a connection to the server that owns the specified key
+ /// and ad-hoc commands with unusual routing requirements. Note that provides a connection that automatically routes commands by
+ /// looking for parameters, so this method is only necessary when used with commands that do not take a parameter,
+ /// but require consistent routing using key-like semantics.
+ IServer GetServer(RedisKey key, object? asyncState = null, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Obtain configuration APIs for all servers in this multiplexer.
+ ///
+ IServer[] GetServers();
+
+ ///
+ /// Reconfigure the current connections based on the existing configuration.
+ ///
+ /// The log to write output to.
+ Task ConfigureAsync(TextWriter? log = null);
+
+ ///
+ /// Reconfigure the current connections based on the existing configuration.
+ ///
+ /// The log to write output to.
+ bool Configure(TextWriter? log = null);
+
+ ///
+ /// Provides a text overview of the status of all connections.
+ ///
+ string GetStatus();
+
+ ///
+ /// Provides a text overview of the status of all connections.
+ ///
+ /// The log to write output to.
+ void GetStatus(TextWriter log);
+
+ ///
+ /// See .
+ ///
+ string ToString();
+
+ ///
+ /// Close all connections and release all resources associated with this object.
+ ///
+ /// Whether to allow in-queue commands to complete first.
+ void Close(bool allowCommandsToComplete = true);
+
+ ///
+ /// Close all connections and release all resources associated with this object.
+ ///
+ /// Whether to allow in-queue commands to complete first.
+ Task CloseAsync(bool allowCommandsToComplete = true);
+
+ ///
+ /// Obtains the log of unusual busy patterns.
+ ///
+ string? GetStormLog();
+
+ ///
+ /// Resets the log of unusual busy patterns.
+ ///
+ void ResetStormLog();
+
+ ///
+ /// Request all compatible clients to reconfigure or reconnect.
+ ///
+ /// The command flags to use.
+ /// The number of instances known to have received the message (however, the actual number can be higher; returns -1 if the operation is pending).
+ long PublishReconfigure(CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Request all compatible clients to reconfigure or reconnect.
+ ///
+ /// The command flags to use.
+ /// The number of instances known to have received the message (however, the actual number can be higher).
+ Task PublishReconfigureAsync(CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Get the hash-slot associated with a given key, if applicable; this can be useful for grouping operations.
+ ///
+ /// The key to get a the slot for.
+ int GetHashSlot(RedisKey key);
+
+ ///
+ /// Write the configuration of all servers to an output stream.
+ ///
+ /// 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/Interfaces/IServer.cs b/src/StackExchange.Redis/Interfaces/IServer.cs
index 4971c7f18..fd090514d 100644
--- a/src/StackExchange.Redis/Interfaces/IServer.cs
+++ b/src/StackExchange.Redis/Interfaces/IServer.cs
@@ -266,6 +266,7 @@ public partial interface IServer : IRedis
///
Task ExecuteAsync(string command, params object[] args);
+#pragma warning disable RS0026, RS0027 // multiple overloads
///
/// Execute an arbitrary command against the server; this is primarily intended for
/// executing modules, but may also be used to provide access to new features that lack
@@ -280,6 +281,23 @@ public partial interface IServer : IRedis
///
Task ExecuteAsync(string command, ICollection