Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -407,12 +407,24 @@ public Task<bool> SetContainsAsync<T>(string key, T item, CommandFlags flag = Co
/// </summary>
public Task<Dictionary<string, string>> GetInfoAsync();

/// <summary>
/// Gets the information about redis.
/// More info see http://redis.io/commands/INFO
/// </summary>
public Task<Dictionary<string, string>> GetInfoAsync(string section);

/// <summary>
/// Gets the information about redis with category.
/// More info see http://redis.io/commands/INFO
/// </summary>
public Task<InfoDetail[]> GetInfoCategorizedAsync();

/// <summary>
/// Gets the information about redis with category.
/// More info see http://redis.io/commands/INFO
/// </summary>
public Task<InfoDetail[]> GetInfoCategorizedAsync(string section);

/// <summary>
/// Updates the expiry time of a redis cache object
/// </summary>
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
// Copyright (c) Ugo Lattanzi. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using System;
using System.Diagnostics.CodeAnalysis;

namespace StackExchange.Redis.Extensions.Core.Helpers;

internal static class ExceptionThrowHelper
{
public static void ThrowIfExistsNullElement<T>(ReadOnlySpan<T> argument, string paramName)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Copyright (c) Ugo Lattanzi. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;

namespace StackExchange.Redis.Extensions.Core.Helpers;

/// <summary>
/// A helper class that provides methods to parse Redis info strings into structured data.
/// </summary>
/// <remarks>
/// See <see href="https://redis.io/docs/latest/commands/info/#return-information"/> for details about the format of the info command output.
/// </remarks>
internal static class InfoDetailsParser
{
private const string LineSeparator = "\r\n";

/// <summary>
/// Parses the given info string into a flat list of tuples, where each tuple contains the section name, key, and value.
/// </summary>
/// <param name="info"></param>
/// <returns></returns>
public static List<(string Section, string Key, string Value)> ParseAsFlatRows(ReadOnlySpan<char> info)
{
List<(string Section, string Key, string Value)> rows = [];

var currentSection = string.Empty;
#if NET9_0_OR_GREATER
foreach (var r in info.Split(LineSeparator))
{
var line = info[r].Trim();
if (line.IsEmpty)
continue;

if (TryParseSection(line, out var section))
currentSection = section;
else if (TryParseDetail(line, out var detail))
rows.Add((currentSection, detail.Key, detail.Value));
}
#else
var start = 0;
while (start < info.Length)
{
var end = info[start..].IndexOf(LineSeparator);
ReadOnlySpan<char> line;
if (end < 0)
{
line = info[start..].Trim();
start = info.Length;
}
else
{
line = info[start..(start + end)].Trim();
start += end + LineSeparator.Length;
}

if (line.IsEmpty)
continue;

if (TryParseSection(line, out var section))
currentSection = section;
else if (TryParseDetail(line, out var detail))
rows.Add((currentSection, detail.Key, detail.Value));
}

#endif
return rows;
}

/// <summary>
/// Parses the given info string into a dictionary of key-value pairs. Ingore section.
/// </summary>
/// <param name="info"></param>
/// <returns></returns>
public static Dictionary<string, string> ParseAsDictionary(ReadOnlySpan<char> info)
{
var dict = new Dictionary<string, string>(StringComparer.Ordinal);

// No need to trim the line, as TryParseDetail will handle it
#if NET9_0_OR_GREATER
foreach (var r in info.Split(LineSeparator))
{
var line = info[r];
if (TryParseDetail(line, out var detail))
dict.TryAdd(detail.Key, detail.Value); // It's possible that `INFO` emits duplicate keys
}
#else
var start = 0;
while (start < info.Length)
{
var end = info[start..].IndexOf(LineSeparator);
ReadOnlySpan<char> line;
if (end < 0)
{
line = info[start..];
start = info.Length;
}
else
{
line = info[start..(start + end)];
start += end + LineSeparator.Length;
}

if (TryParseDetail(line, out var detail))
dict.TryAdd(detail.Key, detail.Value); // It's possible that `INFO` emits duplicate keys
}

#endif
return dict;
}

private static bool TryParseSection(ReadOnlySpan<char> line, out string section)
{
section = string.Empty;

if (line[0] != '#')
return false;

section = new(line[1..].Trim());
return true;
}

private static bool TryParseDetail(ReadOnlySpan<char> line, out KeyValuePair<string, string> detail)
{
var idx = line.IndexOf(':');
if (idx <= 0)
{
detail = default;
return false;
}

var key = new string(line[..idx].Trim());
var value = new string(line[(idx + 1)..].Trim());
detail = new(key, value);
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ namespace StackExchange.Redis.Extensions.Core.Implementations;
/// <inheritdoc/>
public partial class RedisDatabase : IRedisDatabase
{
private const string InfoScript = "return redis.call('INFO')";
private const string InfoSectionScript = "return redis.call('INFO', ARGV[1])";

private readonly IRedisConnectionPoolManager connectionPoolManager;
private readonly ServerEnumerationStrategy serverEnumerationStrategy;
private readonly string keyPrefix;
Expand Down Expand Up @@ -559,21 +562,38 @@ public Task SaveAsync(SaveType saveType, CommandFlags flag = CommandFlags.None)
/// <inheritdoc/>
public async Task<Dictionary<string, string>> GetInfoAsync()
{
var info = (await Database.ScriptEvaluateAsync("return redis.call('INFO')").ConfigureAwait(false)).ToString();
var info = (await Database.ScriptEvaluateAsync(InfoScript).ConfigureAwait(false)).ToString();

return string.IsNullOrEmpty(info)
? new Dictionary<string, string>()
: ParseInfo(info);
return InfoDetailsParser.ParseAsDictionary(info);
}

/// <inheritdoc/>
public async Task<Dictionary<string, string>> GetInfoAsync(string section)
{
if (string.IsNullOrWhiteSpace(section))
return await GetInfoAsync().ConfigureAwait(false);

var info = (await Database.ScriptEvaluateAsync(InfoSectionScript, values: [section]).ConfigureAwait(false)).ToString();
return InfoDetailsParser.ParseAsDictionary(info);
}

/// <inheritdoc/>
public async Task<InfoDetail[]> GetInfoCategorizedAsync()
{
var info = (await Database.ScriptEvaluateAsync("return redis.call('INFO')").ConfigureAwait(false)).ToString();
var info = (await Database.ScriptEvaluateAsync(InfoScript).ConfigureAwait(false)).ToString();

return string.IsNullOrEmpty(info)
? []
: ParseCategorizedInfo(info);
return InfoDetail.ParseFrom(info);
}

/// <inheritdoc/>
public async Task<InfoDetail[]> GetInfoCategorizedAsync(string section)
{
if (string.IsNullOrWhiteSpace(section))
return await GetInfoCategorizedAsync().ConfigureAwait(false);

var info = (await Database.ScriptEvaluateAsync(InfoSectionScript, values: [section]).ConfigureAwait(false)).ToString();

return InfoDetail.ParseFrom(info);
}

/// <inheritdoc/>
Expand Down Expand Up @@ -614,29 +634,4 @@ public Task<RedisType> KeyTypeAsync(string key, CommandFlags flag = CommandFlags
/// <inheritdoc/>
public Task KeyRestoreAsync(string key, byte[] value, TimeSpan? expiry = null, CommandFlags flag = CommandFlags.None)
=> Database.KeyRestoreAsync(key, value, expiry, flag);

private static Dictionary<string, string> ParseInfo(string info)
{
// Call Parse Categorized Info to cut back on duplicated code.
var data = ParseCategorizedInfo(info);

// Return a dictionary of the Info Key and Info value

var result = new Dictionary<string, string>(data.Length);

foreach (var detail in data)
result.TryAdd(detail.Key, detail.InfoValue);

return result;
}

private static InfoDetail[] ParseCategorizedInfo(string info)
{
var data = new List<InfoDetail>();
var category = string.Empty;

info.AsSpan().EnumerateLines(ref data, ref category);

return [.. data];
}
}
42 changes: 25 additions & 17 deletions src/core/StackExchange.Redis.Extensions.Core/Models/InfoDetail.cs
Original file line number Diff line number Diff line change
@@ -1,37 +1,45 @@
// Copyright (c) Ugo Lattanzi. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using StackExchange.Redis.Extensions.Core.Helpers;

namespace StackExchange.Redis.Extensions.Core.Models;

/// <summary>
/// A class that contains redis info.
/// </summary>
public class InfoDetail
/// <param name="category">The category name.</param>
/// <param name="key">The redis key.</param>
/// <param name="infoValue">The information</param>
public class InfoDetail(string category, string key, string infoValue)
{
/// <summary>
/// Initializes a new instance of the <see cref="InfoDetail"/> class.
/// </summary>
/// <param name="category">The category name.</param>
/// <param name="key">The redis key.</param>
/// <param name="infoValue">The information</param>
public InfoDetail(string category, string key, string infoValue)
{
Category = category;
Key = key;
InfoValue = infoValue;
}

/// <summary>
/// Gets or sets the category name
/// </summary>
public string Category { get; }
public string Category { get; } = category;

/// <summary>
/// Gets or sets the redis key.
/// </summary>
public string Key { get; }
public string Key { get; } = key;

/// <summary>
/// Gets or sets the informations.
/// </summary>
public string InfoValue { get; }
public string InfoValue { get; } = infoValue;

internal static InfoDetail[] ParseFrom(string? info)
{
var rows = InfoDetailsParser.ParseAsFlatRows(info);
if (rows.Count < 1)
return [];

var details = new InfoDetail[rows.Count];
for (var i = 0; i < rows.Count; i++)
{
var (category, key, val) = rows[i];
details[i] = new(category, key, val);
}

return details;
}
}
Loading
Loading