Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support for CLIENT KILL MAXAGE #2727

Merged
merged 8 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions src/StackExchange.Redis/APITypes/ClientKillFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
using System.Net;

namespace StackExchange.Redis;

/// <summary>
/// Filter determining which Redis clients to kill.
/// </summary>
/// <seealso href="https://redis.io/docs/latest/commands/client-kill/"/>
public class ClientKillFilter
{
/// <summary>
/// Filter arguments builder for `CLIENT KILL`.
/// </summary>
public ClientKillFilter() { }

/// <summary>
/// The ID of the client to kill.
/// </summary>
public long? Id { get; private set; }

/// <summary>
/// The type of client.
/// </summary>
public ClientType? ClientType { get; private set; }

/// <summary>
/// The authenticated ACL username.
/// </summary>
public string? Username { get; private set; }

/// <summary>
/// The endpoint to kill.
/// </summary>
public EndPoint? Endpoint { get; private set; }

/// <summary>
/// The server endpoint to kill.
/// </summary>
public EndPoint? ServerEndpoint { get; private set; }

/// <summary>
/// Whether to skip the current connection.
/// </summary>
public bool? SkipMe { get; private set; }

/// <summary>
/// Age of connection in seconds.
/// </summary>
public long? MaxAgeInSeconds { get; private set; }

/// <summary>
/// Sets client id filter.
/// </summary>
/// <param name="id">Id of the client to kill.</param>
public ClientKillFilter WithId(long id)
{
Id = id;
return this;
}

/// <summary>
/// Sets client type filter.
/// </summary>
/// <param name="clientType">The type of the client.</param>
public ClientKillFilter WithClientType(ClientType clientType)
{
ClientType = clientType;
return this;
}

/// <summary>
/// Sets the username filter.
/// </summary>
/// <param name="username">Authenticated ACL username.</param>
public ClientKillFilter WithUsername(string username)
{
Username = username;
return this;
}

/// <summary>
/// Set the endpoint filter.
/// </summary>
/// <param name="endpoint">The endpoint to kill.</param>
public ClientKillFilter WithEndpoint(EndPoint endpoint)
{
Endpoint = endpoint;
return this;
}

/// <summary>
/// Set the server endpoint filter.
/// </summary>
/// <param name="serverEndpoint">The server endpoint to kill.</param>
public ClientKillFilter WithServerEndpoint(EndPoint serverEndpoint)
{
ServerEndpoint = serverEndpoint;
return this;
}

/// <summary>
/// Set the skipMe filter (whether to skip the current connection).
/// </summary>
/// <param name="skipMe">Whether to skip the current connection.</param>
public ClientKillFilter WithSkipMe(bool skipMe)
{
SkipMe = skipMe;
return this;
}

/// <summary>
/// Set the MaxAgeInSeconds filter.
/// </summary>
/// <param name="maxAgeInSeconds">Age of connection in seconds</param>
public ClientKillFilter WithMaxAgeInSeconds(long maxAgeInSeconds)
{
MaxAgeInSeconds = maxAgeInSeconds;
return this;
}
}
12 changes: 12 additions & 0 deletions src/StackExchange.Redis/Interfaces/IServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,18 @@ public partial interface IServer : IRedis
/// <inheritdoc cref="ClientKill(long?, ClientType?, EndPoint?, bool, CommandFlags)"/>
Task<long> ClientKillAsync(long? id = null, ClientType? clientType = null, EndPoint? endpoint = null, bool skipMe = true, CommandFlags flags = CommandFlags.None);

/// <summary>
/// The CLIENT KILL command closes multiple connections that match the specified filters.
/// </summary>
/// <param name="filter"></param>
/// <param name="flags"></param>
/// <returns></returns>
long ClientKill(ClientKillFilter filter, CommandFlags flags = CommandFlags.None);

/// <inheritdoc cref="ClientKill(ClientKillFilter, CommandFlags)"/>
Task<long> ClientKillAsync(ClientKillFilter filter, CommandFlags flags = CommandFlags.None);


/// <summary>
/// The <c>CLIENT LIST</c> command returns information and statistics about the client connections server in a mostly human readable format.
/// </summary>
Expand Down
21 changes: 20 additions & 1 deletion src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<long>!
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<long>!
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.ClientInfo![]!>!
StackExchange.Redis.IServer.ClusterConfiguration.get -> StackExchange.Redis.ClusterConfiguration?
Expand Down Expand Up @@ -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
StackExchange.Redis.IConnectionMultiplexer.AddLibraryNameSuffix(string! suffix) -> void

1 change: 1 addition & 0 deletions src/StackExchange.Redis/RedisLiterals.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ public static readonly RedisValue
MATCH = "MATCH",
MALLOC_STATS = "MALLOC-STATS",
MAX = "MAX",
MAXAGE = "MAXAGE",
MAXLEN = "MAXLEN",
MIN = "MIN",
MINMATCHLEN = "MINMATCHLEN",
Expand Down
29 changes: 23 additions & 6 deletions src/StackExchange.Redis/RedisServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,29 @@ public Task ClientKillAsync(EndPoint endpoint, CommandFlags flags = CommandFlags

public long ClientKill(long? id = null, ClientType? clientType = null, EndPoint? endpoint = null, bool skipMe = true, CommandFlags flags = CommandFlags.None)
{
var msg = GetClientKillMessage(endpoint, id, clientType, skipMe, flags);
var msg = GetClientKillMessage(endpoint, id, clientType, skipMe, null, flags);
return ExecuteSync(msg, ResultProcessor.Int64);
}

public Task<long> ClientKillAsync(long? id = null, ClientType? clientType = null, EndPoint? endpoint = null, bool skipMe = true, CommandFlags flags = CommandFlags.None)
{
var msg = GetClientKillMessage(endpoint, id, clientType, skipMe, flags);
var msg = GetClientKillMessage(endpoint, id, clientType, skipMe, null, flags);
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 msg = GetClientKillMessage(filter.Endpoint, filter.Id, filter.ClientType, filter.SkipMe, filter.MaxAgeInSeconds, flags);
return ExecuteSync(msg, ResultProcessor.Int64);
}

public Task<long> ClientKillAsync(ClientKillFilter filter, CommandFlags flags = CommandFlags.None)
{
var msg = GetClientKillMessage(filter.Endpoint, filter.Id, filter.ClientType, filter.SkipMe, filter.MaxAgeInSeconds, flags);
return ExecuteAsync(msg, ResultProcessor.Int64);
}

private Message GetClientKillMessage(EndPoint? endpoint, long? id, ClientType? clientType, bool? skipMe, long? maxAgeInSeconds, CommandFlags flags)
{
var parts = new List<RedisValue>(9)
{
Expand Down Expand Up @@ -109,10 +121,15 @@ private Message GetClientKillMessage(EndPoint? endpoint, long? id, ClientType? c
parts.Add(RedisLiterals.ADDR);
parts.Add((RedisValue)Format.ToString(endpoint));
}
if (!skipMe)
if (skipMe != null)
{
parts.Add(RedisLiterals.SKIPME);
parts.Add(RedisLiterals.no);
parts.Add(skipMe.Value ? RedisLiterals.yes : RedisLiterals.no);
}
if (maxAgeInSeconds != null)
{
parts.Add(RedisLiterals.MAXAGE);
parts.Add(maxAgeInSeconds);
}
return Message.Create(-1, flags, RedisCommand.CLIENT, parts);
}
Expand Down Expand Up @@ -408,7 +425,7 @@ public Task<DateTime> 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);
}
Expand Down
47 changes: 47 additions & 0 deletions tests/StackExchange.Redis.Tests/ClientKillTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
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);
}
}
Loading