Commit 8672ca23 authored by Marc Gravell's avatar Marc Gravell Committed by Nick Craver

Add support for the LATENCY and MEMORY commands (#1204)

Adds LATENCY and MEMORY command support and testing.
parent b0a07a76
...@@ -76,6 +76,7 @@ internal enum RedisCommand ...@@ -76,6 +76,7 @@ internal enum RedisCommand
KEYS, KEYS,
LASTSAVE, LASTSAVE,
LATENCY,
LINDEX, LINDEX,
LINSERT, LINSERT,
LLEN, LLEN,
...@@ -87,6 +88,7 @@ internal enum RedisCommand ...@@ -87,6 +88,7 @@ internal enum RedisCommand
LSET, LSET,
LTRIM, LTRIM,
MEMORY,
MGET, MGET,
MIGRATE, MIGRATE,
MONITOR, MONITOR,
......
...@@ -622,6 +622,98 @@ public partial interface IServer : IRedis ...@@ -622,6 +622,98 @@ public partial interface IServer : IRedis
/// <remarks>https://redis.io/commands/time</remarks> /// <remarks>https://redis.io/commands/time</remarks>
Task<DateTime> TimeAsync(CommandFlags flags = CommandFlags.None); Task<DateTime> TimeAsync(CommandFlags flags = CommandFlags.None);
/// <summary>
/// Gets a text-based latency diagnostic
/// </summary>
/// <remarks>https://redis.io/topics/latency-monitor</remarks>
Task<string> LatencyDoctorAsync(CommandFlags flags = CommandFlags.None);
/// <summary>
/// Gets a text-based latency diagnostic
/// </summary>
/// <remarks>https://redis.io/topics/latency-monitor</remarks>
string LatencyDoctor(CommandFlags flags = CommandFlags.None);
/// <summary>
/// Resets the given events (or all if none are specified), discarding the currently logged latency spike events, and resetting the maximum event time register.
/// </summary>
/// <remarks>https://redis.io/topics/latency-monitor</remarks>
Task<long> LatencyResetAsync(string[] eventNames = null, CommandFlags flags = CommandFlags.None);
/// <summary>
/// Resets the given events (or all if none are specified), discarding the currently logged latency spike events, and resetting the maximum event time register.
/// </summary>
/// <remarks>https://redis.io/topics/latency-monitor</remarks>
long LatencyReset(string[] eventNames = null, CommandFlags flags = CommandFlags.None);
/// <summary>
/// Fetch raw latency data from the event time series, as timestamp-latency pairs
/// </summary>
/// <remarks>https://redis.io/topics/latency-monitor</remarks>
Task<LatencyHistoryEntry[]> LatencyHistoryAsync(string eventName, CommandFlags flags = CommandFlags.None);
/// <summary>
/// Fetch raw latency data from the event time series, as timestamp-latency pairs
/// </summary>
/// <remarks>https://redis.io/topics/latency-monitor</remarks>
LatencyHistoryEntry[] LatencyHistory(string eventName, CommandFlags flags = CommandFlags.None);
/// <summary>
/// Fetch raw latency data from the event time series, as timestamp-latency pairs
/// </summary>
/// <remarks>https://redis.io/topics/latency-monitor</remarks>
Task<LatencyLatestEntry[]> LatencyLatestAsync(CommandFlags flags = CommandFlags.None);
/// <summary>
/// Fetch raw latency data from the event time series, as timestamp-latency pairs
/// </summary>
/// <remarks>https://redis.io/topics/latency-monitor</remarks>
LatencyLatestEntry[] LatencyLatest(CommandFlags flags = CommandFlags.None);
/// <summary>
/// Reports about different memory-related issues that the Redis server experiences, and advises about possible remedies.
/// </summary>
/// <remarks>https://redis.io/commands/memory-doctor</remarks>
Task<string> MemoryDoctorAsync(CommandFlags flags = CommandFlags.None);
/// <summary>
/// Reports about different memory-related issues that the Redis server experiences, and advises about possible remedies.
/// </summary>
/// <remarks>https://redis.io/commands/memory-doctor</remarks>
string MemoryDoctor(CommandFlags flags = CommandFlags.None);
/// <summary>
/// Attempts to purge dirty pages so these can be reclaimed by the allocator.
/// </summary>
/// <remarks>https://redis.io/commands/memory-purge</remarks>
Task MemoryPurgeAsync(CommandFlags flags = CommandFlags.None);
/// <summary>
/// Attempts to purge dirty pages so these can be reclaimed by the allocator.
/// </summary>
/// <remarks>https://redis.io/commands/memory-purge</remarks>
void MemoryPurge(CommandFlags flags = CommandFlags.None);
/// <summary>
/// Returns an array reply about the memory usage of the server.
/// </summary>
/// <remarks>https://redis.io/commands/memory-stats</remarks>
Task<RedisResult> MemoryStatsAsync(CommandFlags flags = CommandFlags.None);
/// <summary>
/// Returns an array reply about the memory usage of the server.
/// </summary>
/// <remarks>https://redis.io/commands/memory-stats</remarks>
RedisResult MemoryStats(CommandFlags flags = CommandFlags.None);
/// <summary>
/// Provides an internal statistics report from the memory allocator.
/// </summary>
/// <remarks>https://redis.io/commands/memory-malloc-stats</remarks>
Task<string> MemoryAllocatorStatsAsync(CommandFlags flags = CommandFlags.None);
/// <summary>
/// Provides an internal statistics report from the memory allocator.
/// </summary>
/// <remarks>https://redis.io/commands/memory-malloc-stats</remarks>
string MemoryAllocatorStats(CommandFlags flags = CommandFlags.None);
#region Sentinel #region Sentinel
/// <summary> /// <summary>
...@@ -734,6 +826,107 @@ public partial interface IServer : IRedis ...@@ -734,6 +826,107 @@ public partial interface IServer : IRedis
#endregion #endregion
} }
/// <summary>
/// A latency entry as reported by the built-in LATENCY HISTORY command
/// </summary>
public readonly struct LatencyHistoryEntry
{
internal static readonly ResultProcessor<LatencyHistoryEntry[]> ToArray = new Processor();
private sealed class Processor : ArrayResultProcessor<LatencyHistoryEntry>
{
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;
}
}
/// <summary>
/// The time at which this entry was recorded
/// </summary>
public DateTime Timestamp { get; }
/// <summary>
/// The latency recorded for this event
/// </summary>
public int DurationMilliseconds { get; }
internal LatencyHistoryEntry(long timestamp, long duration)
{
Timestamp = RedisBase.UnixEpoch.AddSeconds(timestamp);
DurationMilliseconds = checked((int)duration);
}
}
/// <summary>
/// A latency entry as reported by the built-in LATENCY LATEST command
/// </summary>
public readonly struct LatencyLatestEntry
{
internal static readonly ResultProcessor<LatencyLatestEntry[]> ToArray = new Processor();
private sealed class Processor : ArrayResultProcessor<LatencyLatestEntry>
{
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;
}
}
/// <summary>
/// The name of this event
/// </summary>
public string EventName { get; }
/// <summary>
/// The time at which this entry was recorded
/// </summary>
public DateTime Timestamp { get; }
/// <summary>
/// The latency recorded for this event
/// </summary>
public int DurationMilliseconds { get; }
/// <summary>
/// The max latency recorded for all events
/// </summary>
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 internal static class IServerExtensions
{ {
/// <summary> /// <summary>
......
...@@ -549,6 +549,8 @@ internal static bool RequiresDatabase(RedisCommand command) ...@@ -549,6 +549,8 @@ internal static bool RequiresDatabase(RedisCommand command)
case RedisCommand.FLUSHALL: case RedisCommand.FLUSHALL:
case RedisCommand.INFO: case RedisCommand.INFO:
case RedisCommand.LASTSAVE: case RedisCommand.LASTSAVE:
case RedisCommand.LATENCY:
case RedisCommand.MEMORY:
case RedisCommand.MONITOR: case RedisCommand.MONITOR:
case RedisCommand.MULTI: case RedisCommand.MULTI:
case RedisCommand.PING: case RedisCommand.PING:
......
...@@ -90,6 +90,11 @@ public RedisFeatures(Version version) ...@@ -90,6 +90,11 @@ public RedisFeatures(Version version)
/// </summary> /// </summary>
public bool ListInsert => Version >= v2_1_1; public bool ListInsert => Version >= v2_1_1;
/// <summary>
/// Is MEMORY available?
/// </summary>
public bool Memory => Version >= v4_0_0;
/// <summary> /// <summary>
/// Indicates whether PEXPIRE and PTTL are supported /// Indicates whether PEXPIRE and PTTL are supported
/// </summary> /// </summary>
......
...@@ -51,18 +51,22 @@ public static readonly RedisValue ...@@ -51,18 +51,22 @@ public static readonly RedisValue
COPY = "COPY", COPY = "COPY",
COUNT = "COUNT", COUNT = "COUNT",
DESC = "DESC", DESC = "DESC",
DOCTOR = "DOCTOR",
EX = "EX", EX = "EX",
EXISTS = "EXISTS", EXISTS = "EXISTS",
FLUSH = "FLUSH", FLUSH = "FLUSH",
GET = "GET", GET = "GET",
GETNAME = "GETNAME", GETNAME = "GETNAME",
HISTORY = "HISTORY",
ID = "ID", ID = "ID",
IDLETIME = "IDLETIME", IDLETIME = "IDLETIME",
KILL = "KILL", KILL = "KILL",
LATEST = "LATEST",
LIMIT = "LIMIT", LIMIT = "LIMIT",
LIST = "LIST", LIST = "LIST",
LOAD = "LOAD", LOAD = "LOAD",
MATCH = "MATCH", MATCH = "MATCH",
MALLOC_STATS = "MALLOC-STATS",
MAX = "MAX", MAX = "MAX",
MIN = "MIN", MIN = "MIN",
NODES = "NODES", NODES = "NODES",
...@@ -75,6 +79,7 @@ public static readonly RedisValue ...@@ -75,6 +79,7 @@ public static readonly RedisValue
OR = "OR", OR = "OR",
PAUSE = "PAUSE", PAUSE = "PAUSE",
PING = "PING", PING = "PING",
PURGE = "PURGE",
PX = "PX", PX = "PX",
REPLACE = "REPLACE", REPLACE = "REPLACE",
RESET = "RESET", RESET = "RESET",
...@@ -85,6 +90,7 @@ public static readonly RedisValue ...@@ -85,6 +90,7 @@ public static readonly RedisValue
SET = "SET", SET = "SET",
SETNAME = "SETNAME", SETNAME = "SETNAME",
SKIPME = "SKIPME", SKIPME = "SKIPME",
STATS = "STATS",
STORE = "STORE", STORE = "STORE",
TYPE = "TYPE", TYPE = "TYPE",
WEIGHTS = "WEIGHTS", WEIGHTS = "WEIGHTS",
......
using System; using System;
using System.Collections.Generic;
namespace StackExchange.Redis namespace StackExchange.Redis
{ {
...@@ -61,7 +62,7 @@ internal static RedisResult TryCreate(PhysicalConnection connection, in RawResul ...@@ -61,7 +62,7 @@ internal static RedisResult TryCreate(PhysicalConnection connection, in RawResul
if (items.Length == 0) return EmptyArray; if (items.Length == 0) return EmptyArray;
var arr = new RedisResult[items.Length]; var arr = new RedisResult[items.Length];
int i = 0; int i = 0;
foreach(ref RawResult item in items) foreach (ref RawResult item in items)
{ {
var next = TryCreate(connection, in item); var next = TryCreate(connection, in item);
if (next == null) return null; // means we didn't understand if (next == null) return null; // means we didn't understand
...@@ -100,7 +101,7 @@ internal static RedisResult TryCreate(PhysicalConnection connection, in RawResul ...@@ -100,7 +101,7 @@ internal static RedisResult TryCreate(PhysicalConnection connection, in RawResul
/// Interprets the result as a <see cref="T:byte[]"/>. /// Interprets the result as a <see cref="T:byte[]"/>.
/// </summary> /// </summary>
/// <param name="result">The result to convert to a <see cref="T:byte[]"/>.</param> /// <param name="result">The result to convert to a <see cref="T:byte[]"/>.</param>
public static explicit operator byte[] (RedisResult result) => result.AsByteArray(); public static explicit operator byte[](RedisResult result) => result.AsByteArray();
/// <summary> /// <summary>
/// Interprets the result as a <see cref="double"/>. /// Interprets the result as a <see cref="double"/>.
/// </summary> /// </summary>
...@@ -141,79 +142,94 @@ internal static RedisResult TryCreate(PhysicalConnection connection, in RawResul ...@@ -141,79 +142,94 @@ internal static RedisResult TryCreate(PhysicalConnection connection, in RawResul
/// Interprets the result as a <see cref="T:Nullable{double}"/>. /// Interprets the result as a <see cref="T:Nullable{double}"/>.
/// </summary> /// </summary>
/// <param name="result">The result to convert to a <see cref="T:Nullable{double}"/>.</param> /// <param name="result">The result to convert to a <see cref="T:Nullable{double}"/>.</param>
public static explicit operator double? (RedisResult result) => result.AsNullableDouble(); public static explicit operator double?(RedisResult result) => result.AsNullableDouble();
/// <summary> /// <summary>
/// Interprets the result as a <see cref="T:Nullable{long}"/>. /// Interprets the result as a <see cref="T:Nullable{long}"/>.
/// </summary> /// </summary>
/// <param name="result">The result to convert to a <see cref="T:Nullable{long}"/>.</param> /// <param name="result">The result to convert to a <see cref="T:Nullable{long}"/>.</param>
public static explicit operator long? (RedisResult result) => result.AsNullableInt64(); public static explicit operator long?(RedisResult result) => result.AsNullableInt64();
/// <summary> /// <summary>
/// Interprets the result as a <see cref="T:Nullable{ulong}"/>. /// Interprets the result as a <see cref="T:Nullable{ulong}"/>.
/// </summary> /// </summary>
/// <param name="result">The result to convert to a <see cref="T:Nullable{ulong}"/>.</param> /// <param name="result">The result to convert to a <see cref="T:Nullable{ulong}"/>.</param>
[CLSCompliant(false)] [CLSCompliant(false)]
public static explicit operator ulong? (RedisResult result) => result.AsNullableUInt64(); public static explicit operator ulong?(RedisResult result) => result.AsNullableUInt64();
/// <summary> /// <summary>
/// Interprets the result as a <see cref="T:Nullable{int}"/>. /// Interprets the result as a <see cref="T:Nullable{int}"/>.
/// </summary> /// </summary>
/// <param name="result">The result to convert to a <see cref="T:Nullable{int}"/>.</param> /// <param name="result">The result to convert to a <see cref="T:Nullable{int}"/>.</param>
public static explicit operator int? (RedisResult result) => result.AsNullableInt32(); public static explicit operator int?(RedisResult result) => result.AsNullableInt32();
/// <summary> /// <summary>
/// Interprets the result as a <see cref="T:Nullable{bool}"/>. /// Interprets the result as a <see cref="T:Nullable{bool}"/>.
/// </summary> /// </summary>
/// <param name="result">The result to convert to a <see cref="T:Nullable{bool}"/>.</param> /// <param name="result">The result to convert to a <see cref="T:Nullable{bool}"/>.</param>
public static explicit operator bool? (RedisResult result) => result.AsNullableBoolean(); public static explicit operator bool?(RedisResult result) => result.AsNullableBoolean();
/// <summary> /// <summary>
/// Interprets the result as a <see cref="T:string[]"/>. /// Interprets the result as a <see cref="T:string[]"/>.
/// </summary> /// </summary>
/// <param name="result">The result to convert to a <see cref="T:string[]"/>.</param> /// <param name="result">The result to convert to a <see cref="T:string[]"/>.</param>
public static explicit operator string[] (RedisResult result) => result.AsStringArray(); public static explicit operator string[](RedisResult result) => result.AsStringArray();
/// <summary> /// <summary>
/// Interprets the result as a <see cref="T:byte[][]"/>. /// Interprets the result as a <see cref="T:byte[][]"/>.
/// </summary> /// </summary>
/// <param name="result">The result to convert to a <see cref="T:byte[][]"/>.</param> /// <param name="result">The result to convert to a <see cref="T:byte[][]"/>.</param>
public static explicit operator byte[][] (RedisResult result) => result.AsByteArrayArray(); public static explicit operator byte[][](RedisResult result) => result.AsByteArrayArray();
/// <summary> /// <summary>
/// Interprets the result as a <see cref="T:double[]"/>. /// Interprets the result as a <see cref="T:double[]"/>.
/// </summary> /// </summary>
/// <param name="result">The result to convert to a <see cref="T:double[]"/>.</param> /// <param name="result">The result to convert to a <see cref="T:double[]"/>.</param>
public static explicit operator double[] (RedisResult result) => result.AsDoubleArray(); public static explicit operator double[](RedisResult result) => result.AsDoubleArray();
/// <summary> /// <summary>
/// Interprets the result as a <see cref="T:long[]"/>. /// Interprets the result as a <see cref="T:long[]"/>.
/// </summary> /// </summary>
/// <param name="result">The result to convert to a <see cref="T:long[]"/>.</param> /// <param name="result">The result to convert to a <see cref="T:long[]"/>.</param>
public static explicit operator long[] (RedisResult result) => result.AsInt64Array(); public static explicit operator long[](RedisResult result) => result.AsInt64Array();
/// <summary> /// <summary>
/// Interprets the result as a <see cref="T:ulong[]"/>. /// Interprets the result as a <see cref="T:ulong[]"/>.
/// </summary> /// </summary>
/// <param name="result">The result to convert to a <see cref="T:ulong[]"/>.</param> /// <param name="result">The result to convert to a <see cref="T:ulong[]"/>.</param>
[CLSCompliant(false)] [CLSCompliant(false)]
public static explicit operator ulong[] (RedisResult result) => result.AsUInt64Array(); public static explicit operator ulong[](RedisResult result) => result.AsUInt64Array();
/// <summary> /// <summary>
/// Interprets the result as a <see cref="T:int[]"/>. /// Interprets the result as a <see cref="T:int[]"/>.
/// </summary> /// </summary>
/// <param name="result">The result to convert to a <see cref="T:int[]"/>.</param> /// <param name="result">The result to convert to a <see cref="T:int[]"/>.</param>
public static explicit operator int[] (RedisResult result) => result.AsInt32Array(); public static explicit operator int[](RedisResult result) => result.AsInt32Array();
/// <summary> /// <summary>
/// Interprets the result as a <see cref="T:bool[]"/>. /// Interprets the result as a <see cref="T:bool[]"/>.
/// </summary> /// </summary>
/// <param name="result">The result to convert to a <see cref="T:bool[]"/>.</param> /// <param name="result">The result to convert to a <see cref="T:bool[]"/>.</param>
public static explicit operator bool[] (RedisResult result) => result.AsBooleanArray(); public static explicit operator bool[](RedisResult result) => result.AsBooleanArray();
/// <summary> /// <summary>
/// Interprets the result as a <see cref="T:RedisValue[]"/>. /// Interprets the result as a <see cref="T:RedisValue[]"/>.
/// </summary> /// </summary>
/// <param name="result">The result to convert to a <see cref="T:RedisValue[]"/>.</param> /// <param name="result">The result to convert to a <see cref="T:RedisValue[]"/>.</param>
public static explicit operator RedisValue[] (RedisResult result) => result.AsRedisValueArray(); public static explicit operator RedisValue[](RedisResult result) => result.AsRedisValueArray();
/// <summary> /// <summary>
/// Interprets the result as a <see cref="T:RedisKey[]"/>. /// Interprets the result as a <see cref="T:RedisKey[]"/>.
/// </summary> /// </summary>
/// <param name="result">The result to convert to a <see cref="T:RedisKey[]"/>.</param> /// <param name="result">The result to convert to a <see cref="T:RedisKey[]"/>.</param>
public static explicit operator RedisKey[] (RedisResult result) => result.AsRedisKeyArray(); public static explicit operator RedisKey[](RedisResult result) => result.AsRedisKeyArray();
/// <summary> /// <summary>
/// Interprets the result as a <see cref="T:RedisResult[]"/>. /// Interprets the result as a <see cref="T:RedisResult[]"/>.
/// </summary> /// </summary>
/// <param name="result">The result to convert to a <see cref="T:RedisResult[]"/>.</param> /// <param name="result">The result to convert to a <see cref="T:RedisResult[]"/>.</param>
public static explicit operator RedisResult[] (RedisResult result) => result.AsRedisResultArray(); public static explicit operator RedisResult[](RedisResult result) => result.AsRedisResultArray();
/// <summary>
/// Interprets a multi-bulk result with successive key/name values as a dictionary keyed by name
/// </summary>
public Dictionary<string, RedisResult> ToDictionary(IEqualityComparer<string> comparer = null)
{
var arr = AsRedisResultArray();
int len = arr.Length / 2;
var result = new Dictionary<string, RedisResult>(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 AsBoolean();
internal abstract bool[] AsBooleanArray(); internal abstract bool[] AsBooleanArray();
......
...@@ -869,5 +869,119 @@ public Task<RedisResult> ExecuteAsync(string command, ICollection<object> args, ...@@ -869,5 +869,119 @@ public Task<RedisResult> ExecuteAsync(string command, ICollection<object> args,
/// For testing only /// For testing only
/// </summary> /// </summary>
internal void SimulateConnectionFailure() => server.SimulateConnectionFailure(); internal void SimulateConnectionFailure() => server.SimulateConnectionFailure();
public Task<string> 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<string>();
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<long> 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<LatencyHistoryEntry[]> 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<LatencyLatestEntry[]> 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<string> 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<string> 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<RedisResult> 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);
}
} }
} }
using System; using Pipelines.Sockets.Unofficial.Arenas;
using System;
using System.Buffers; using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Pipelines.Sockets.Unofficial.Arenas;
namespace StackExchange.Redis namespace StackExchange.Redis
{ {
...@@ -2019,4 +2018,37 @@ protected void SetResult(Message message, T value) ...@@ -2019,4 +2018,37 @@ protected void SetResult(Message message, T value)
box?.SetResult(value); box?.SetResult(value);
} }
} }
internal abstract class ArrayResultProcessor<T> : ResultProcessor<T[]>
{
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<T>();
}
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);
}
} }
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));
}
}
}
}
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);
}
}
}
}
...@@ -32,5 +32,6 @@ ...@@ -32,5 +32,6 @@
<ItemGroup Condition=" '$(TargetFramework)' == 'net462' "> <ItemGroup Condition=" '$(TargetFramework)' == 'net462' ">
<Reference Include="Microsoft.CSharp" /> <Reference Include="Microsoft.CSharp" />
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.6.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment