diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index 41656cbb4..7d199e564 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -76,6 +76,7 @@ internal enum RedisCommand KEYS, LASTSAVE, + LATENCY, LINDEX, LINSERT, LLEN, @@ -87,6 +88,7 @@ internal enum RedisCommand LSET, LTRIM, + MEMORY, MGET, MIGRATE, MONITOR, diff --git a/src/StackExchange.Redis/Interfaces/IServer.cs b/src/StackExchange.Redis/Interfaces/IServer.cs index 48e61d4f9..1eec8bdb5 100644 --- a/src/StackExchange.Redis/Interfaces/IServer.cs +++ b/src/StackExchange.Redis/Interfaces/IServer.cs @@ -622,6 +622,98 @@ public partial interface IServer : IRedis /// https://redis.io/commands/time Task TimeAsync(CommandFlags flags = CommandFlags.None); + /// + /// Gets a text-based latency diagnostic + /// + /// https://redis.io/topics/latency-monitor + Task LatencyDoctorAsync(CommandFlags flags = CommandFlags.None); + /// + /// Gets a text-based latency diagnostic + /// + /// https://redis.io/topics/latency-monitor + string LatencyDoctor(CommandFlags flags = CommandFlags.None); + + /// + /// Resets the given events (or all if none are specified), discarding the currently logged latency spike events, and resetting the maximum event time register. + /// + /// https://redis.io/topics/latency-monitor + Task LatencyResetAsync(string[] eventNames = null, CommandFlags flags = CommandFlags.None); + /// + /// Resets the given events (or all if none are specified), discarding the currently logged latency spike events, and resetting the maximum event time register. + /// + /// https://redis.io/topics/latency-monitor + long LatencyReset(string[] eventNames = null, CommandFlags flags = CommandFlags.None); + + /// + /// Fetch raw latency data from the event time series, as timestamp-latency pairs + /// + /// https://redis.io/topics/latency-monitor + Task LatencyHistoryAsync(string eventName, CommandFlags flags = CommandFlags.None); + /// + /// Fetch raw latency data from the event time series, as timestamp-latency pairs + /// + /// https://redis.io/topics/latency-monitor + LatencyHistoryEntry[] LatencyHistory(string eventName, CommandFlags flags = CommandFlags.None); + + /// + /// Fetch raw latency data from the event time series, as timestamp-latency pairs + /// + /// https://redis.io/topics/latency-monitor + Task LatencyLatestAsync(CommandFlags flags = CommandFlags.None); + /// + /// Fetch raw latency data from the event time series, as timestamp-latency pairs + /// + /// https://redis.io/topics/latency-monitor + LatencyLatestEntry[] LatencyLatest(CommandFlags flags = CommandFlags.None); + + /// + /// Reports about different memory-related issues that the Redis server experiences, and advises about possible remedies. + /// + /// https://redis.io/commands/memory-doctor + Task MemoryDoctorAsync(CommandFlags flags = CommandFlags.None); + + /// + /// Reports about different memory-related issues that the Redis server experiences, and advises about possible remedies. + /// + /// https://redis.io/commands/memory-doctor + string MemoryDoctor(CommandFlags flags = CommandFlags.None); + + /// + /// Attempts to purge dirty pages so these can be reclaimed by the allocator. + /// + /// https://redis.io/commands/memory-purge + Task MemoryPurgeAsync(CommandFlags flags = CommandFlags.None); + + /// + /// Attempts to purge dirty pages so these can be reclaimed by the allocator. + /// + /// https://redis.io/commands/memory-purge + void MemoryPurge(CommandFlags flags = CommandFlags.None); + + /// + /// Returns an array reply about the memory usage of the server. + /// + /// https://redis.io/commands/memory-stats + Task MemoryStatsAsync(CommandFlags flags = CommandFlags.None); + + /// + /// Returns an array reply about the memory usage of the server. + /// + /// https://redis.io/commands/memory-stats + RedisResult MemoryStats(CommandFlags flags = CommandFlags.None); + + /// + /// Provides an internal statistics report from the memory allocator. + /// + /// https://redis.io/commands/memory-malloc-stats + Task MemoryAllocatorStatsAsync(CommandFlags flags = CommandFlags.None); + + /// + /// Provides an internal statistics report from the memory allocator. + /// + /// https://redis.io/commands/memory-malloc-stats + string MemoryAllocatorStats(CommandFlags flags = CommandFlags.None); + #region Sentinel /// @@ -734,6 +826,107 @@ public partial interface IServer : IRedis #endregion } + /// + /// A latency entry as reported by the built-in LATENCY HISTORY command + /// + public readonly struct LatencyHistoryEntry + { + internal static readonly ResultProcessor ToArray = new Processor(); + + private sealed class Processor : ArrayResultProcessor + { + protected override bool TryParse(in RawResult raw, out LatencyHistoryEntry parsed) + { + if (raw.Type == ResultType.MultiBulk) + { + var items = raw.GetItems(); + if (items.Length >= 2 + && items[0].TryGetInt64(out var timestamp) + && items[1].TryGetInt64(out var duration)) + { + parsed = new LatencyHistoryEntry(timestamp, duration); + return true; + } + } + parsed = default; + return false; + } + } + + /// + /// The time at which this entry was recorded + /// + public DateTime Timestamp { get; } + + /// + /// The latency recorded for this event + /// + public int DurationMilliseconds { get; } + + internal LatencyHistoryEntry(long timestamp, long duration) + { + Timestamp = RedisBase.UnixEpoch.AddSeconds(timestamp); + DurationMilliseconds = checked((int)duration); + } + } + + /// + /// A latency entry as reported by the built-in LATENCY LATEST command + /// + public readonly struct LatencyLatestEntry + { + internal static readonly ResultProcessor ToArray = new Processor(); + + private sealed class Processor : ArrayResultProcessor + { + protected override bool TryParse(in RawResult raw, out LatencyLatestEntry parsed) + { + if (raw.Type == ResultType.MultiBulk) + { + var items = raw.GetItems(); + if (items.Length >= 4 + && items[1].TryGetInt64(out var timestamp) + && items[2].TryGetInt64(out var duration) + && items[3].TryGetInt64(out var maxDuration)) + { + parsed = new LatencyLatestEntry(items[0].GetString(), timestamp, duration, maxDuration); + return true; + } + } + parsed = default; + return false; + } + } + + /// + /// The name of this event + /// + public string EventName { get; } + + /// + /// The time at which this entry was recorded + /// + public DateTime Timestamp { get; } + + /// + /// The latency recorded for this event + /// + public int DurationMilliseconds { get; } + + /// + /// The max latency recorded for all events + /// + public int MaxDurationMilliseconds { get; } + + internal LatencyLatestEntry(string eventName, long timestamp, long duration, long maxDuration) + { + EventName = eventName; + Timestamp = RedisBase.UnixEpoch.AddSeconds(timestamp); + DurationMilliseconds = checked((int)duration); + MaxDurationMilliseconds = checked((int)maxDuration); + } + } + internal static class IServerExtensions { /// diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index b5f36dd45..2113ee5c6 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -549,6 +549,8 @@ internal static bool RequiresDatabase(RedisCommand command) case RedisCommand.FLUSHALL: case RedisCommand.INFO: case RedisCommand.LASTSAVE: + case RedisCommand.LATENCY: + case RedisCommand.MEMORY: case RedisCommand.MONITOR: case RedisCommand.MULTI: case RedisCommand.PING: diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs index 8d03b03f9..9d5223a5e 100644 --- a/src/StackExchange.Redis/RedisFeatures.cs +++ b/src/StackExchange.Redis/RedisFeatures.cs @@ -90,6 +90,11 @@ public RedisFeatures(Version version) /// public bool ListInsert => Version >= v2_1_1; + /// + /// Is MEMORY available? + /// + public bool Memory => Version >= v4_0_0; + /// /// Indicates whether PEXPIRE and PTTL are supported /// diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index 609800668..1dda150cc 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -51,18 +51,22 @@ public static readonly RedisValue COPY = "COPY", COUNT = "COUNT", DESC = "DESC", + DOCTOR = "DOCTOR", EX = "EX", EXISTS = "EXISTS", FLUSH = "FLUSH", GET = "GET", GETNAME = "GETNAME", + HISTORY = "HISTORY", ID = "ID", IDLETIME = "IDLETIME", KILL = "KILL", + LATEST = "LATEST", LIMIT = "LIMIT", LIST = "LIST", LOAD = "LOAD", MATCH = "MATCH", + MALLOC_STATS = "MALLOC-STATS", MAX = "MAX", MIN = "MIN", NODES = "NODES", @@ -75,6 +79,7 @@ public static readonly RedisValue OR = "OR", PAUSE = "PAUSE", PING = "PING", + PURGE = "PURGE", PX = "PX", REPLACE = "REPLACE", RESET = "RESET", @@ -85,6 +90,7 @@ public static readonly RedisValue SET = "SET", SETNAME = "SETNAME", SKIPME = "SKIPME", + STATS = "STATS", STORE = "STORE", TYPE = "TYPE", WEIGHTS = "WEIGHTS", diff --git a/src/StackExchange.Redis/RedisResult.cs b/src/StackExchange.Redis/RedisResult.cs index d88868448..b1c3d51b1 100644 --- a/src/StackExchange.Redis/RedisResult.cs +++ b/src/StackExchange.Redis/RedisResult.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace StackExchange.Redis { @@ -61,7 +62,7 @@ internal static RedisResult TryCreate(PhysicalConnection connection, in RawResul if (items.Length == 0) return EmptyArray; var arr = new RedisResult[items.Length]; int i = 0; - foreach(ref RawResult item in items) + foreach (ref RawResult item in items) { var next = TryCreate(connection, in item); if (next == null) return null; // means we didn't understand @@ -100,7 +101,7 @@ internal static RedisResult TryCreate(PhysicalConnection connection, in RawResul /// Interprets the result as a . /// /// The result to convert to a . - public static explicit operator byte[] (RedisResult result) => result.AsByteArray(); + public static explicit operator byte[](RedisResult result) => result.AsByteArray(); /// /// Interprets the result as a . /// @@ -141,79 +142,94 @@ internal static RedisResult TryCreate(PhysicalConnection connection, in RawResul /// Interprets the result as a . /// /// The result to convert to a . - public static explicit operator double? (RedisResult result) => result.AsNullableDouble(); + public static explicit operator double?(RedisResult result) => result.AsNullableDouble(); /// /// Interprets the result as a . /// /// The result to convert to a . - public static explicit operator long? (RedisResult result) => result.AsNullableInt64(); + public static explicit operator long?(RedisResult result) => result.AsNullableInt64(); /// /// Interprets the result as a . /// /// The result to convert to a . [CLSCompliant(false)] - public static explicit operator ulong? (RedisResult result) => result.AsNullableUInt64(); + public static explicit operator ulong?(RedisResult result) => result.AsNullableUInt64(); /// /// Interprets the result as a . /// /// The result to convert to a . - public static explicit operator int? (RedisResult result) => result.AsNullableInt32(); + public static explicit operator int?(RedisResult result) => result.AsNullableInt32(); /// /// Interprets the result as a . /// /// The result to convert to a . - public static explicit operator bool? (RedisResult result) => result.AsNullableBoolean(); + public static explicit operator bool?(RedisResult result) => result.AsNullableBoolean(); /// /// Interprets the result as a . /// /// The result to convert to a . - public static explicit operator string[] (RedisResult result) => result.AsStringArray(); + public static explicit operator string[](RedisResult result) => result.AsStringArray(); /// /// Interprets the result as a . /// /// The result to convert to a . - public static explicit operator byte[][] (RedisResult result) => result.AsByteArrayArray(); + public static explicit operator byte[][](RedisResult result) => result.AsByteArrayArray(); /// /// Interprets the result as a . /// /// The result to convert to a . - public static explicit operator double[] (RedisResult result) => result.AsDoubleArray(); + public static explicit operator double[](RedisResult result) => result.AsDoubleArray(); /// /// Interprets the result as a . /// /// The result to convert to a . - public static explicit operator long[] (RedisResult result) => result.AsInt64Array(); + public static explicit operator long[](RedisResult result) => result.AsInt64Array(); /// /// Interprets the result as a . /// /// The result to convert to a . [CLSCompliant(false)] - public static explicit operator ulong[] (RedisResult result) => result.AsUInt64Array(); + public static explicit operator ulong[](RedisResult result) => result.AsUInt64Array(); /// /// Interprets the result as a . /// /// The result to convert to a . - public static explicit operator int[] (RedisResult result) => result.AsInt32Array(); + public static explicit operator int[](RedisResult result) => result.AsInt32Array(); /// /// Interprets the result as a . /// /// The result to convert to a . - public static explicit operator bool[] (RedisResult result) => result.AsBooleanArray(); + public static explicit operator bool[](RedisResult result) => result.AsBooleanArray(); /// /// Interprets the result as a . /// /// The result to convert to a . - public static explicit operator RedisValue[] (RedisResult result) => result.AsRedisValueArray(); + public static explicit operator RedisValue[](RedisResult result) => result.AsRedisValueArray(); /// /// Interprets the result as a . /// /// The result to convert to a . - public static explicit operator RedisKey[] (RedisResult result) => result.AsRedisKeyArray(); + public static explicit operator RedisKey[](RedisResult result) => result.AsRedisKeyArray(); /// /// Interprets the result as a . /// /// The result to convert to a . - public static explicit operator RedisResult[] (RedisResult result) => result.AsRedisResultArray(); + public static explicit operator RedisResult[](RedisResult result) => result.AsRedisResultArray(); + + /// + /// Interprets a multi-bulk result with successive key/name values as a dictionary keyed by name + /// + public Dictionary ToDictionary(IEqualityComparer comparer = null) + { + var arr = AsRedisResultArray(); + int len = arr.Length / 2; + var result = new Dictionary(len, comparer ?? StringComparer.InvariantCultureIgnoreCase); + for (int i = 0; i < len;) + { + result.Add(arr[i++].AsString(), arr[i++]); + } + return result; + } internal abstract bool AsBoolean(); internal abstract bool[] AsBooleanArray(); diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs index e30870dae..c2d69e945 100644 --- a/src/StackExchange.Redis/RedisServer.cs +++ b/src/StackExchange.Redis/RedisServer.cs @@ -869,5 +869,119 @@ public Task ExecuteAsync(string command, ICollection args, /// For testing only /// internal void SimulateConnectionFailure() => server.SimulateConnectionFailure(); + + public Task LatencyDoctorAsync(CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(-1, flags, RedisCommand.LATENCY, RedisLiterals.DOCTOR); + return ExecuteAsync(msg, ResultProcessor.String); + } + + public string LatencyDoctor(CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(-1, flags, RedisCommand.LATENCY, RedisLiterals.DOCTOR); + return ExecuteSync(msg, ResultProcessor.String); + } + + private static Message LatencyResetCommand(string[] eventNames, CommandFlags flags) + { + if (eventNames == null) eventNames = Array.Empty(); + switch (eventNames.Length) + { + case 0: + return Message.Create(-1, flags, RedisCommand.LATENCY, RedisLiterals.RESET); + case 1: + return Message.Create(-1, flags, RedisCommand.LATENCY, RedisLiterals.RESET, (RedisValue)eventNames[0]); + default: + var arr = new RedisValue[eventNames.Length + 1]; + arr[0] = RedisLiterals.RESET; + for (int i = 0; i < eventNames.Length; i++) + arr[i + 1] = eventNames[i]; + return Message.Create(-1, flags, RedisCommand.LATENCY, arr); + + } + } + public Task LatencyResetAsync(string[] eventNames = null, CommandFlags flags = CommandFlags.None) + { + var msg = LatencyResetCommand(eventNames, flags); + return ExecuteAsync(msg, ResultProcessor.Int64); + } + + public long LatencyReset(string[] eventNames = null, CommandFlags flags = CommandFlags.None) + { + var msg = LatencyResetCommand(eventNames, flags); + return ExecuteSync(msg, ResultProcessor.Int64); + } + + public Task LatencyHistoryAsync(string eventName, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(-1, flags, RedisCommand.LATENCY, RedisLiterals.HISTORY, (RedisValue)eventName); + return ExecuteAsync(msg, LatencyHistoryEntry.ToArray); + } + + public LatencyHistoryEntry[] LatencyHistory(string eventName, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(-1, flags, RedisCommand.LATENCY, RedisLiterals.HISTORY, (RedisValue)eventName); + return ExecuteSync(msg, LatencyHistoryEntry.ToArray); + } + + public Task LatencyLatestAsync(CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(-1, flags, RedisCommand.LATENCY, RedisLiterals.LATEST); + return ExecuteAsync(msg, LatencyLatestEntry.ToArray); + } + + public LatencyLatestEntry[] LatencyLatest(CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(-1, flags, RedisCommand.LATENCY, RedisLiterals.LATEST); + return ExecuteSync(msg, LatencyLatestEntry.ToArray); + } + + public Task MemoryDoctorAsync(CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(-1, flags, RedisCommand.MEMORY, RedisLiterals.DOCTOR); + return ExecuteAsync(msg, ResultProcessor.String); + } + + public string MemoryDoctor(CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(-1, flags, RedisCommand.MEMORY, RedisLiterals.DOCTOR); + return ExecuteSync(msg, ResultProcessor.String); + } + + public Task MemoryPurgeAsync(CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(-1, flags, RedisCommand.MEMORY, RedisLiterals.PURGE); + return ExecuteAsync(msg, ResultProcessor.DemandOK); + } + + public void MemoryPurge(CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(-1, flags, RedisCommand.MEMORY, RedisLiterals.PURGE); + ExecuteSync(msg, ResultProcessor.DemandOK); + } + + public Task MemoryAllocatorStatsAsync(CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(-1, flags, RedisCommand.MEMORY, RedisLiterals.MALLOC_STATS); + return ExecuteAsync(msg, ResultProcessor.String); + } + + public string MemoryAllocatorStats(CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(-1, flags, RedisCommand.MEMORY, RedisLiterals.MALLOC_STATS); + return ExecuteSync(msg, ResultProcessor.String); + } + + public Task MemoryStatsAsync(CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(-1, flags, RedisCommand.MEMORY, RedisLiterals.STATS); + return ExecuteAsync(msg, ResultProcessor.ScriptResult); + } + + public RedisResult MemoryStats(CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(-1, flags, RedisCommand.MEMORY, RedisLiterals.STATS); + return ExecuteSync(msg, ResultProcessor.ScriptResult); + } } } diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 9708c3528..1bcc5bdb8 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -1,13 +1,12 @@ -using System; +using Pipelines.Sockets.Unofficial.Arenas; +using System; using System.Buffers; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Net; -using System.Runtime.CompilerServices; using System.Text.RegularExpressions; -using Pipelines.Sockets.Unofficial.Arenas; namespace StackExchange.Redis { @@ -2019,4 +2018,37 @@ protected void SetResult(Message message, T value) box?.SetResult(value); } } + + internal abstract class ArrayResultProcessor : ResultProcessor + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + switch(result.Type) + { + case ResultType.MultiBulk: + var items = result.GetItems(); + T[] arr; + if (items.IsEmpty) + { + arr = Array.Empty(); + } + else + { + arr = new T[checked((int)items.Length)]; + int index = 0; + foreach (ref RawResult inner in items) + { + if (!TryParse(inner, out arr[index++])) + return false; + } + } + SetResult(message, arr); + return true; + default: + return false; + } + } + + protected abstract bool TryParse(in RawResult raw, out T parsed); + } } diff --git a/tests/StackExchange.Redis.Tests/Latency.cs b/tests/StackExchange.Redis.Tests/Latency.cs new file mode 100644 index 000000000..325b47bb0 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/Latency.cs @@ -0,0 +1,88 @@ +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace StackExchange.Redis.Tests +{ + [Collection(SharedConnectionFixture.Key)] + public class Latency : TestBase + { + + public Latency(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + + [Fact] + public async Task CanCallDoctor() + { + using (var conn = Create()) + { + var server = conn.GetServer(conn.GetEndPoints()[0]); + string doctor = server.LatencyDoctor(); + Assert.NotNull(doctor); + Assert.NotEqual("", doctor); + + doctor = await server.LatencyDoctorAsync(); + Assert.NotNull(doctor); + Assert.NotEqual("", doctor); + } + } + + [Fact] + public async Task CanReset() + { + using (var conn = Create()) + { + var server = conn.GetServer(conn.GetEndPoints()[0]); + _ = server.LatencyReset(); + var count = await server.LatencyResetAsync(new string[] { "command" }); + Assert.Equal(0, count); + + count = await server.LatencyResetAsync(new string[] { "command", "fast-command" }); + Assert.Equal(0, count); + } + } + + [Fact] + public async Task GetLatest() + { + using (var conn = Create(allowAdmin: true)) + { + var server = conn.GetServer(conn.GetEndPoints()[0]); + server.ConfigSet("latency-monitor-threshold", 100); + server.LatencyReset(); + var arr = server.LatencyLatest(); + Assert.Empty(arr); + + var now = await server.TimeAsync(); + server.Execute("debug", "sleep", "0.5"); // cause something to be slow + + arr = await server.LatencyLatestAsync(); + var item = Assert.Single(arr); + Assert.Equal("command", item.EventName); + Assert.True(item.DurationMilliseconds >= 400 && item.DurationMilliseconds <= 600); + Assert.Equal(item.DurationMilliseconds, item.MaxDurationMilliseconds); + Assert.True(item.Timestamp >= now.AddSeconds(-2) && item.Timestamp <= now.AddSeconds(2)); + } + } + + [Fact] + public async Task GetHistory() + { + using (var conn = Create(allowAdmin: true)) + { + var server = conn.GetServer(conn.GetEndPoints()[0]); + server.ConfigSet("latency-monitor-threshold", 100); + server.LatencyReset(); + var arr = server.LatencyHistory("command"); + Assert.Empty(arr); + + var now = await server.TimeAsync(); + server.Execute("debug", "sleep", "0.5"); // cause something to be slow + + arr = await server.LatencyHistoryAsync("command"); + var item = Assert.Single(arr); + Assert.True(item.DurationMilliseconds >= 400 && item.DurationMilliseconds <= 600); + Assert.True(item.Timestamp >= now.AddSeconds(-2) && item.Timestamp <= now.AddSeconds(2)); + } + } + } +} diff --git a/tests/StackExchange.Redis.Tests/Memory.cs b/tests/StackExchange.Redis.Tests/Memory.cs new file mode 100644 index 000000000..ba0d37674 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/Memory.cs @@ -0,0 +1,84 @@ +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace StackExchange.Redis.Tests +{ + [Collection(SharedConnectionFixture.Key)] + public class Memory : TestBase + { + public Memory(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + + [Fact] + public async Task CanCallDoctor() + { + using (var conn = Create()) + { + Skip.IfMissingFeature(conn, nameof(RedisFeatures.Memory), r => r.Streams); + var server = conn.GetServer(conn.GetEndPoints()[0]); + string doctor = server.MemoryDoctor(); + Assert.NotNull(doctor); + Assert.NotEqual("", doctor); + + doctor = await server.MemoryDoctorAsync(); + Assert.NotNull(doctor); + Assert.NotEqual("", doctor); + } + } + + [Fact] + public async Task CanPurge() + { + using (var conn = Create()) + { + Skip.IfMissingFeature(conn, nameof(RedisFeatures.Memory), r => r.Streams); + var server = conn.GetServer(conn.GetEndPoints()[0]); + server.MemoryPurge(); + await server.MemoryPurgeAsync(); + + await server.MemoryPurgeAsync(); + } + } + + [Fact] + public async Task GetAllocatorStats() + { + using (var conn = Create()) + { + Skip.IfMissingFeature(conn, nameof(RedisFeatures.Memory), r => r.Streams); + var server = conn.GetServer(conn.GetEndPoints()[0]); + + var stats = server.MemoryAllocatorStats(); + Assert.False(string.IsNullOrWhiteSpace(stats)); + + stats = await server.MemoryAllocatorStatsAsync(); + Assert.False(string.IsNullOrWhiteSpace(stats)); + } + } + + [Fact] + public async Task GetStats() + { + using (var conn = Create()) + { + Skip.IfMissingFeature(conn, nameof(RedisFeatures.Memory), r => r.Streams); + var server = conn.GetServer(conn.GetEndPoints()[0]); + var stats = server.MemoryStats(); + Assert.Equal(ResultType.MultiBulk, stats.Type); + + var parsed = stats.ToDictionary(); + + var alloc = parsed["total.allocated"]; + Assert.Equal(ResultType.Integer, alloc.Type); + Assert.True(alloc.AsInt64() > 0); + + stats = await server.MemoryStatsAsync(); + Assert.Equal(ResultType.MultiBulk, stats.Type); + + alloc = parsed["total.allocated"]; + Assert.Equal(ResultType.Integer, alloc.Type); + Assert.True(alloc.AsInt64() > 0); + } + } + } +} diff --git a/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj b/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj index 48eba5061..0d909b8d8 100644 --- a/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj +++ b/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj @@ -32,5 +32,6 @@ +