diff --git a/src/StackExchange.Redis/APITypes/ClientKillFilter.cs b/src/StackExchange.Redis/APITypes/ClientKillFilter.cs
new file mode 100644
index 000000000..b5c5e845c
--- /dev/null
+++ b/src/StackExchange.Redis/APITypes/ClientKillFilter.cs
@@ -0,0 +1,179 @@
+using System;
+using System.Collections.Generic;
+using System.Net;
+
+namespace StackExchange.Redis;
+
+///
+/// Filter determining which Redis clients to kill.
+///
+///
+public class ClientKillFilter
+{
+ ///
+ /// Filter arguments builder for `CLIENT KILL`.
+ ///
+ public ClientKillFilter() { }
+
+ ///
+ /// The ID of the client to kill.
+ ///
+ public long? Id { get; private set; }
+
+ ///
+ /// The type of client.
+ ///
+ public ClientType? ClientType { get; private set; }
+
+ ///
+ /// The authenticated ACL username.
+ ///
+ public string? Username { get; private set; }
+
+ ///
+ /// The endpoint to kill.
+ ///
+ public EndPoint? Endpoint { get; private set; }
+
+ ///
+ /// The server endpoint to kill.
+ ///
+ public EndPoint? ServerEndpoint { get; private set; }
+
+ ///
+ /// Whether to skip the current connection.
+ ///
+ public bool? SkipMe { get; private set; }
+
+ ///
+ /// Age of connection in seconds.
+ ///
+ public long? MaxAgeInSeconds { get; private set; }
+
+ ///
+ /// Sets client id filter.
+ ///
+ /// Id of the client to kill.
+ public ClientKillFilter WithId(long? id)
+ {
+ Id = id;
+ return this;
+ }
+
+ ///
+ /// Sets client type filter.
+ ///
+ /// The type of the client.
+ public ClientKillFilter WithClientType(ClientType? clientType)
+ {
+ ClientType = clientType;
+ return this;
+ }
+
+ ///
+ /// Sets the username filter.
+ ///
+ /// Authenticated ACL username.
+ public ClientKillFilter WithUsername(string? username)
+ {
+ Username = username;
+ return this;
+ }
+
+ ///
+ /// Set the endpoint filter.
+ ///
+ /// The endpoint to kill.
+ public ClientKillFilter WithEndpoint(EndPoint? endpoint)
+ {
+ Endpoint = endpoint;
+ return this;
+ }
+
+ ///
+ /// Set the server endpoint filter.
+ ///
+ /// The server endpoint to kill.
+ public ClientKillFilter WithServerEndpoint(EndPoint? serverEndpoint)
+ {
+ ServerEndpoint = serverEndpoint;
+ return this;
+ }
+
+ ///
+ /// Set the skipMe filter (whether to skip the current connection).
+ ///
+ /// Whether to skip the current connection.
+ public ClientKillFilter WithSkipMe(bool? skipMe)
+ {
+ SkipMe = skipMe;
+ return this;
+ }
+
+ ///
+ /// Set the MaxAgeInSeconds filter.
+ ///
+ /// Age of connection in seconds
+ public ClientKillFilter WithMaxAgeInSeconds(long? maxAgeInSeconds)
+ {
+ MaxAgeInSeconds = maxAgeInSeconds;
+ return this;
+ }
+
+ internal List ToList(bool withReplicaCommands)
+ {
+ var parts = new List(15)
+ {
+ RedisLiterals.KILL
+ };
+ if (Id != null)
+ {
+ parts.Add(RedisLiterals.ID);
+ parts.Add(Id.Value);
+ }
+ if (ClientType != null)
+ {
+ parts.Add(RedisLiterals.TYPE);
+ switch (ClientType.Value)
+ {
+ case Redis.ClientType.Normal:
+ parts.Add(RedisLiterals.normal);
+ break;
+ case Redis.ClientType.Replica:
+ parts.Add(withReplicaCommands ? RedisLiterals.replica : RedisLiterals.slave);
+ break;
+ case Redis.ClientType.PubSub:
+ parts.Add(RedisLiterals.pubsub);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(ClientType));
+ }
+ }
+ if (Username != null)
+ {
+ parts.Add(RedisLiterals.USERNAME);
+ parts.Add(Username);
+ }
+ if (Endpoint != null)
+ {
+ parts.Add(RedisLiterals.ADDR);
+ parts.Add((RedisValue)Format.ToString(Endpoint));
+ }
+ if (ServerEndpoint != null)
+ {
+ parts.Add(RedisLiterals.LADDR);
+ parts.Add((RedisValue)Format.ToString(ServerEndpoint));
+ }
+ if (SkipMe != null)
+ {
+ parts.Add(RedisLiterals.SKIPME);
+ parts.Add(SkipMe.Value ? RedisLiterals.yes : RedisLiterals.no);
+ }
+ if (MaxAgeInSeconds != null)
+ {
+ parts.Add(RedisLiterals.MAXAGE);
+ parts.Add(MaxAgeInSeconds);
+ }
+ return parts;
+ }
+}
diff --git a/src/StackExchange.Redis/Interfaces/IServer.cs b/src/StackExchange.Redis/Interfaces/IServer.cs
index 9f8f0b075..94baf5cd6 100644
--- a/src/StackExchange.Redis/Interfaces/IServer.cs
+++ b/src/StackExchange.Redis/Interfaces/IServer.cs
@@ -107,6 +107,18 @@ public partial interface IServer : IRedis
///
Task ClientKillAsync(long? id = null, ClientType? clientType = null, EndPoint? endpoint = null, bool skipMe = true, CommandFlags flags = CommandFlags.None);
+ ///
+ /// The CLIENT KILL command closes multiple connections that match the specified filters.
+ ///
+ ///
+ ///
+ ///
+ long ClientKill(ClientKillFilter filter, CommandFlags flags = CommandFlags.None);
+
+ ///
+ Task ClientKillAsync(ClientKillFilter filter, CommandFlags flags = CommandFlags.None);
+
+
///
/// The CLIENT LIST command returns information and statistics about the client connections server in a mostly human readable format.
///
diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt
index 4504423fe..8707cc1b4 100644
--- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt
+++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt
@@ -126,6 +126,22 @@ StackExchange.Redis.ClientInfo.ProtocolVersion.get -> string?
StackExchange.Redis.ClientInfo.Raw.get -> string?
StackExchange.Redis.ClientInfo.SubscriptionCount.get -> int
StackExchange.Redis.ClientInfo.TransactionCommandLength.get -> int
+StackExchange.Redis.ClientKillFilter
+StackExchange.Redis.ClientKillFilter.ClientKillFilter() -> void
+StackExchange.Redis.ClientKillFilter.ClientType.get -> StackExchange.Redis.ClientType?
+StackExchange.Redis.ClientKillFilter.Endpoint.get -> System.Net.EndPoint?
+StackExchange.Redis.ClientKillFilter.Id.get -> long?
+StackExchange.Redis.ClientKillFilter.MaxAgeInSeconds.get -> long?
+StackExchange.Redis.ClientKillFilter.ServerEndpoint.get -> System.Net.EndPoint?
+StackExchange.Redis.ClientKillFilter.SkipMe.get -> bool?
+StackExchange.Redis.ClientKillFilter.Username.get -> string?
+StackExchange.Redis.ClientKillFilter.WithClientType(StackExchange.Redis.ClientType? clientType) -> StackExchange.Redis.ClientKillFilter!
+StackExchange.Redis.ClientKillFilter.WithEndpoint(System.Net.EndPoint? endpoint) -> StackExchange.Redis.ClientKillFilter!
+StackExchange.Redis.ClientKillFilter.WithId(long? id) -> StackExchange.Redis.ClientKillFilter!
+StackExchange.Redis.ClientKillFilter.WithMaxAgeInSeconds(long? maxAgeInSeconds) -> StackExchange.Redis.ClientKillFilter!
+StackExchange.Redis.ClientKillFilter.WithServerEndpoint(System.Net.EndPoint? serverEndpoint) -> StackExchange.Redis.ClientKillFilter!
+StackExchange.Redis.ClientKillFilter.WithSkipMe(bool? skipMe) -> StackExchange.Redis.ClientKillFilter!
+StackExchange.Redis.ClientKillFilter.WithUsername(string? username) -> StackExchange.Redis.ClientKillFilter!
StackExchange.Redis.ClientType
StackExchange.Redis.ClientType.Normal = 0 -> StackExchange.Redis.ClientType
StackExchange.Redis.ClientType.PubSub = 2 -> StackExchange.Redis.ClientType
@@ -1006,8 +1022,10 @@ StackExchange.Redis.IServer.AllowSlaveWrites.get -> bool
StackExchange.Redis.IServer.AllowSlaveWrites.set -> void
StackExchange.Redis.IServer.ClientKill(long? id = null, StackExchange.Redis.ClientType? clientType = null, System.Net.EndPoint? endpoint = null, bool skipMe = true, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long
StackExchange.Redis.IServer.ClientKill(System.Net.EndPoint! endpoint, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void
+StackExchange.Redis.IServer.ClientKill(StackExchange.Redis.ClientKillFilter! filter, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long
StackExchange.Redis.IServer.ClientKillAsync(long? id = null, StackExchange.Redis.ClientType? clientType = null, System.Net.EndPoint? endpoint = null, bool skipMe = true, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
StackExchange.Redis.IServer.ClientKillAsync(System.Net.EndPoint! endpoint, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+StackExchange.Redis.IServer.ClientKillAsync(StackExchange.Redis.ClientKillFilter! filter, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
StackExchange.Redis.IServer.ClientList(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.ClientInfo![]!
StackExchange.Redis.IServer.ClientListAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
StackExchange.Redis.IServer.ClusterConfiguration.get -> StackExchange.Redis.ClusterConfiguration?
@@ -1851,4 +1869,5 @@ static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisValue[]!
virtual StackExchange.Redis.RedisResult.Length.get -> int
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
+StackExchange.Redis.IConnectionMultiplexer.AddLibraryNameSuffix(string! suffix) -> void
+
diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs
index bfd6b1a44..11d2ba016 100644
--- a/src/StackExchange.Redis/RedisLiterals.cs
+++ b/src/StackExchange.Redis/RedisLiterals.cs
@@ -89,6 +89,7 @@ public static readonly RedisValue
IDLETIME = "IDLETIME",
KEEPTTL = "KEEPTTL",
KILL = "KILL",
+ LADDR = "LADDR",
LATEST = "LATEST",
LEFT = "LEFT",
LEN = "LEN",
@@ -101,6 +102,7 @@ public static readonly RedisValue
MATCH = "MATCH",
MALLOC_STATS = "MALLOC-STATS",
MAX = "MAX",
+ MAXAGE = "MAXAGE",
MAXLEN = "MAXLEN",
MIN = "MIN",
MINMATCHLEN = "MINMATCHLEN",
@@ -137,6 +139,7 @@ public static readonly RedisValue
STATS = "STATS",
STORE = "STORE",
TYPE = "TYPE",
+ USERNAME = "USERNAME",
WEIGHTS = "WEIGHTS",
WITHMATCHLEN = "WITHMATCHLEN",
WITHSCORES = "WITHSCORES",
diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs
index 0fec00428..1f7791dd2 100644
--- a/src/StackExchange.Redis/RedisServer.cs
+++ b/src/StackExchange.Redis/RedisServer.cs
@@ -75,46 +75,22 @@ public Task ClientKillAsync(long? id = null, ClientType? clientType = null
return ExecuteAsync(msg, ResultProcessor.Int64);
}
- private Message GetClientKillMessage(EndPoint? endpoint, long? id, ClientType? clientType, bool skipMe, CommandFlags flags)
+ public long ClientKill(ClientKillFilter filter, CommandFlags flags = CommandFlags.None)
{
- var parts = new List(9)
- {
- RedisLiterals.KILL
- };
- if (id != null)
- {
- parts.Add(RedisLiterals.ID);
- parts.Add(id.Value);
- }
- if (clientType != null)
- {
- parts.Add(RedisLiterals.TYPE);
- switch (clientType.Value)
- {
- case ClientType.Normal:
- parts.Add(RedisLiterals.normal);
- break;
- case ClientType.Replica:
- parts.Add(Features.ReplicaCommands ? RedisLiterals.replica : RedisLiterals.slave);
- break;
- case ClientType.PubSub:
- parts.Add(RedisLiterals.pubsub);
- break;
- default:
- throw new ArgumentOutOfRangeException(nameof(clientType));
- }
- }
- if (endpoint != null)
- {
- parts.Add(RedisLiterals.ADDR);
- parts.Add((RedisValue)Format.ToString(endpoint));
- }
- if (!skipMe)
- {
- parts.Add(RedisLiterals.SKIPME);
- parts.Add(RedisLiterals.no);
- }
- return Message.Create(-1, flags, RedisCommand.CLIENT, parts);
+ var msg = Message.Create(-1, flags, RedisCommand.CLIENT, filter.ToList(Features.ReplicaCommands));
+ return ExecuteSync(msg, ResultProcessor.Int64);
+ }
+
+ public Task ClientKillAsync(ClientKillFilter filter, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(-1, flags, RedisCommand.CLIENT, filter.ToList(Features.ReplicaCommands));
+ return ExecuteAsync(msg, ResultProcessor.Int64);
+ }
+
+ private Message GetClientKillMessage(EndPoint? endpoint, long? id, ClientType? clientType, bool? skipMe, CommandFlags flags)
+ {
+ var args = new ClientKillFilter().WithId(id).WithClientType(clientType).WithEndpoint(endpoint).WithSkipMe(skipMe).ToList(Features.ReplicaCommands);
+ return Message.Create(-1, flags, RedisCommand.CLIENT, args);
}
public ClientInfo[] ClientList(CommandFlags flags = CommandFlags.None)
@@ -408,7 +384,7 @@ public Task LastSaveAsync(CommandFlags flags = CommandFlags.None)
}
public void MakeMaster(ReplicationChangeOptions options, TextWriter? log = null)
- {
+ {
// Do you believe in magic?
multiplexer.MakePrimaryAsync(server, options, log).Wait(60000);
}
diff --git a/tests/StackExchange.Redis.Tests/ClientKillTests.cs b/tests/StackExchange.Redis.Tests/ClientKillTests.cs
new file mode 100644
index 000000000..34f00c6bc
--- /dev/null
+++ b/tests/StackExchange.Redis.Tests/ClientKillTests.cs
@@ -0,0 +1,66 @@
+using System.Collections.Generic;
+using System.Net;
+using System.Threading;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace StackExchange.Redis.Tests;
+
+[RunPerProtocol]
+
+public class ClientKillTests : TestBase
+{
+ protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort;
+ public ClientKillTests(ITestOutputHelper output) : base(output) { }
+
+ [Fact]
+ public void ClientKill()
+ {
+ var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase();
+
+ SetExpectedAmbientFailureCount(-1);
+ using var otherConnection = Create(allowAdmin: true, shared: false, backlogPolicy: BacklogPolicy.FailFast);
+ var id = otherConnection.GetDatabase().Execute(RedisCommand.CLIENT.ToString(), RedisLiterals.ID);
+
+ using var conn = Create(allowAdmin: true, shared: false, backlogPolicy: BacklogPolicy.FailFast);
+ var server = conn.GetServer(conn.GetEndPoints()[0]);
+ long result = server.ClientKill(id.AsInt64(), ClientType.Normal, null, true);
+ Assert.Equal(1, result);
+ }
+
+ [Fact]
+ public void ClientKillWithMaxAge()
+ {
+ var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase();
+
+ SetExpectedAmbientFailureCount(-1);
+ using var otherConnection = Create(allowAdmin: true, shared: false, backlogPolicy: BacklogPolicy.FailFast);
+ var id = otherConnection.GetDatabase().Execute(RedisCommand.CLIENT.ToString(), RedisLiterals.ID);
+ Thread.Sleep(1000);
+
+ using var conn = Create(allowAdmin: true, shared: false, backlogPolicy: BacklogPolicy.FailFast);
+ var server = conn.GetServer(conn.GetEndPoints()[0]);
+ var filter = new ClientKillFilter().WithId(id.AsInt64()).WithMaxAgeInSeconds(1).WithSkipMe(true);
+ long result = server.ClientKill(filter, CommandFlags.DemandMaster);
+ Assert.Equal(1, result);
+ }
+
+ [Fact]
+ public void TestClientKillMessageWithAllArguments()
+ {
+ long id = 101;
+ ClientType type = ClientType.Normal;
+ string userName = "user1";
+ EndPoint endpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1234);
+ EndPoint serverEndpoint = new IPEndPoint(IPAddress.Parse("198.0.0.1"), 6379); ;
+ bool skipMe = true;
+ long maxAge = 102;
+
+ var filter = new ClientKillFilter().WithId(id).WithClientType(type).WithUsername(userName).WithEndpoint(endpoint).WithServerEndpoint(serverEndpoint).WithSkipMe(skipMe).WithMaxAgeInSeconds(maxAge);
+ List expected = new List()
+ {
+ "KILL", "ID", "101", "TYPE", "normal", "USERNAME", "user1", "ADDR", "127.0.0.1:1234", "LADDR", "198.0.0.1:6379", "SKIPME", "yes", "MAXAGE", "102"
+ };
+ Assert.Equal(expected, filter.ToList(true));
+ }
+}