Skip to content

Commit 347b71b

Browse files
committed
Refactor InfoDetail parsing
1 parent d643c53 commit 347b71b

7 files changed

Lines changed: 279 additions & 98 deletions

File tree

src/core/StackExchange.Redis.Extensions.Core/Abstractions/IRedisDatabase.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,12 +407,24 @@ public Task<bool> SetContainsAsync<T>(string key, T item, CommandFlags flag = Co
407407
/// </summary>
408408
public Task<Dictionary<string, string>> GetInfoAsync();
409409

410+
/// <summary>
411+
/// Gets the information about redis.
412+
/// More info see http://redis.io/commands/INFO
413+
/// </summary>
414+
public Task<Dictionary<string, string>> GetInfoAsync(string section);
415+
410416
/// <summary>
411417
/// Gets the information about redis with category.
412418
/// More info see http://redis.io/commands/INFO
413419
/// </summary>
414420
public Task<InfoDetail[]> GetInfoCategorizedAsync();
415421

422+
/// <summary>
423+
/// Gets the information about redis with category.
424+
/// More info see http://redis.io/commands/INFO
425+
/// </summary>
426+
public Task<InfoDetail[]> GetInfoCategorizedAsync(string section);
427+
416428
/// <summary>
417429
/// Updates the expiry time of a redis cache object
418430
/// </summary>

src/core/StackExchange.Redis.Extensions.Core/Extensions/SpanExtensions.cs

Lines changed: 0 additions & 50 deletions
This file was deleted.

src/core/StackExchange.Redis.Extensions.Core/Helpers/ExceptionThrowHelper.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
// Copyright (c) Ugo Lattanzi. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.
2+
13
using System;
24
using System.Diagnostics.CodeAnalysis;
35

46
namespace StackExchange.Redis.Extensions.Core.Helpers;
7+
58
internal static class ExceptionThrowHelper
69
{
710
public static void ThrowIfExistsNullElement<T>(ReadOnlySpan<T> argument, string paramName)
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
// Copyright (c) Ugo Lattanzi. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.
2+
3+
using System;
4+
using System.Collections.Generic;
5+
6+
namespace StackExchange.Redis.Extensions.Core.Helpers;
7+
8+
/// <summary>
9+
/// A helper class that provides methods to parse Redis info strings into structured data.
10+
/// </summary>
11+
/// <remarks>
12+
/// See <see href="https://redis.io/docs/latest/commands/info/#return-information"/> for details about the format of the info command output.
13+
/// </remarks>
14+
internal static class InfoDetailsParser
15+
{
16+
private const string LineSeparator = "\r\n";
17+
18+
/// <summary>
19+
/// Parses the given info string into a list of sections, where each section contains a list of key-value pairs.
20+
/// </summary>
21+
/// <param name="info"></param>
22+
/// <returns></returns>
23+
public static List<List<KeyValuePair<string, string>>> ParseAsTreeRows(ReadOnlySpan<char> info)
24+
{
25+
List<List<KeyValuePair<string, string>>> sections = [];
26+
#if NET9_0_OR_GREATER
27+
foreach (var r in info.Split(LineSeparator))
28+
{
29+
var line = info[r].Trim();
30+
if (line.IsEmpty)
31+
continue;
32+
33+
if (TryParseSection(line, out var section))
34+
sections.Add([]);
35+
else if (TryParseDetail(line, out var detail))
36+
sections[^1].Add(detail);
37+
}
38+
#else
39+
var start = 0;
40+
while (start < info.Length)
41+
{
42+
var end = info[start..].IndexOf(LineSeparator);
43+
ReadOnlySpan<char> line;
44+
if (end < 0)
45+
{
46+
line = info[start..].Trim();
47+
start = info.Length;
48+
}
49+
else
50+
{
51+
line = info[start..(start + end)].Trim();
52+
start += end + LineSeparator.Length;
53+
}
54+
55+
if (line.IsEmpty)
56+
continue;
57+
58+
if (TryParseSection(line, out var section))
59+
sections.Add([]);
60+
else if (TryParseDetail(line, out var detail))
61+
sections[^1].Add(detail);
62+
}
63+
64+
#endif
65+
return sections;
66+
}
67+
68+
/// <summary>
69+
/// Parses the given info string into a flat list of tuples, where each tuple contains the section name, key, and value.
70+
/// </summary>
71+
/// <param name="info"></param>
72+
/// <returns></returns>
73+
public static List<(string Section, string Key, string Value)> ParseAsFlatRows(ReadOnlySpan<char> info)
74+
{
75+
List<(string Section, string Key, string Value)> rows = [];
76+
77+
var currentSection = string.Empty;
78+
#if NET9_0_OR_GREATER
79+
foreach (var r in info.Split(LineSeparator))
80+
{
81+
var line = info[r].Trim();
82+
if (line.IsEmpty)
83+
continue;
84+
85+
if (TryParseSection(line, out var section))
86+
currentSection = section;
87+
else if (TryParseDetail(line, out var detail))
88+
rows.Add((currentSection, detail.Key, detail.Value));
89+
}
90+
#else
91+
var start = 0;
92+
while (start < info.Length)
93+
{
94+
var end = info[start..].IndexOf(LineSeparator);
95+
ReadOnlySpan<char> line;
96+
if (end < 0)
97+
{
98+
line = info[start..].Trim();
99+
start = info.Length;
100+
}
101+
else
102+
{
103+
line = info[start..(start + end)].Trim();
104+
start += end + LineSeparator.Length;
105+
}
106+
107+
if (line.IsEmpty)
108+
continue;
109+
110+
if (TryParseSection(line, out var section))
111+
currentSection = section;
112+
else if (TryParseDetail(line, out var detail))
113+
rows.Add((currentSection, detail.Key, detail.Value));
114+
}
115+
116+
#endif
117+
return rows;
118+
}
119+
120+
/// <summary>
121+
/// Parses the given info string into a dictionary of key-value pairs. Ingores section.
122+
/// </summary>
123+
/// <param name="info"></param>
124+
/// <returns></returns>
125+
public static Dictionary<string, string> ParseAsDictionary(ReadOnlySpan<char> info)
126+
{
127+
var dict = new Dictionary<string, string>(StringComparer.Ordinal);
128+
129+
// No need to trim the line, as TryParseDetail will handle it
130+
#if NET9_0_OR_GREATER
131+
foreach (var r in info.Split(LineSeparator))
132+
{
133+
var line = info[r];
134+
if (TryParseDetail(line, out var detail))
135+
dict.Add(detail.Key, detail.Value);
136+
}
137+
#else
138+
var start = 0;
139+
while (start < info.Length)
140+
{
141+
var end = info[start..].IndexOf(LineSeparator);
142+
ReadOnlySpan<char> line;
143+
if (end < 0)
144+
{
145+
line = info[start..];
146+
start = info.Length;
147+
}
148+
else
149+
{
150+
line = info[start..(start + end)];
151+
start += end + LineSeparator.Length;
152+
}
153+
154+
if (TryParseDetail(line, out var detail))
155+
dict.Add(detail.Key, detail.Value);
156+
}
157+
158+
#endif
159+
return dict;
160+
}
161+
162+
private static bool TryParseSection(ReadOnlySpan<char> line, out string section)
163+
{
164+
section = string.Empty;
165+
166+
if (line[0] != '#')
167+
return false;
168+
169+
section = new(line[1..].Trim());
170+
return true;
171+
}
172+
173+
private static bool TryParseDetail(ReadOnlySpan<char> line, out KeyValuePair<string, string> detail)
174+
{
175+
var idx = line.IndexOf(':');
176+
if (idx <= 0)
177+
{
178+
detail = default;
179+
return false;
180+
}
181+
182+
var key = new string(line[..idx].Trim());
183+
var value = new string(line[(idx + 1)..].Trim());
184+
detail = new(key, value);
185+
return true;
186+
}
187+
}

src/core/StackExchange.Redis.Extensions.Core/Implementations/RedisDatabase.cs

Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -561,19 +561,37 @@ public async Task<Dictionary<string, string>> GetInfoAsync()
561561
{
562562
var info = (await Database.ScriptEvaluateAsync("return redis.call('INFO')").ConfigureAwait(false)).ToString();
563563

564-
return string.IsNullOrEmpty(info)
565-
? new Dictionary<string, string>()
566-
: ParseInfo(info);
564+
return InfoDetailsParser.ParseAsDictionary(info);
565+
}
566+
567+
/// <inheritdoc/>
568+
public async Task<Dictionary<string, string>> GetInfoAsync(string section)
569+
{
570+
var script = string.IsNullOrWhiteSpace(section)
571+
? "return redis.call('INFO')"
572+
: $"return redis.call('INFO','{section}')";
573+
var info = (await Database.ScriptEvaluateAsync(script).ConfigureAwait(false)).ToString();
574+
575+
return InfoDetailsParser.ParseAsDictionary(info);
567576
}
568577

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

574-
return string.IsNullOrEmpty(info)
575-
? []
576-
: ParseCategorizedInfo(info);
583+
return InfoDetail.ParseFrom(info);
584+
}
585+
586+
/// <inheritdoc/>
587+
public async Task<InfoDetail[]> GetInfoCategorizedAsync(string section)
588+
{
589+
var script = string.IsNullOrWhiteSpace(section)
590+
? "return redis.call('INFO')"
591+
: $"return redis.call('INFO','{section}')";
592+
var info = (await Database.ScriptEvaluateAsync(script).ConfigureAwait(false)).ToString();
593+
594+
return InfoDetail.ParseFrom(info);
577595
}
578596

579597
/// <inheritdoc/>
@@ -614,29 +632,4 @@ public Task<RedisType> KeyTypeAsync(string key, CommandFlags flag = CommandFlags
614632
/// <inheritdoc/>
615633
public Task KeyRestoreAsync(string key, byte[] value, TimeSpan? expiry = null, CommandFlags flag = CommandFlags.None)
616634
=> Database.KeyRestoreAsync(key, value, expiry, flag);
617-
618-
private static Dictionary<string, string> ParseInfo(string info)
619-
{
620-
// Call Parse Categorized Info to cut back on duplicated code.
621-
var data = ParseCategorizedInfo(info);
622-
623-
// Return a dictionary of the Info Key and Info value
624-
625-
var result = new Dictionary<string, string>(data.Length);
626-
627-
foreach (var detail in data)
628-
result.TryAdd(detail.Key, detail.InfoValue);
629-
630-
return result;
631-
}
632-
633-
private static InfoDetail[] ParseCategorizedInfo(string info)
634-
{
635-
var data = new List<InfoDetail>();
636-
var category = string.Empty;
637-
638-
info.AsSpan().EnumerateLines(ref data, ref category);
639-
640-
return [.. data];
641-
}
642635
}

0 commit comments

Comments
 (0)