Skip to content

Commit 4117a80

Browse files
C#: Add response checks for batch for every command (valkey-io#4236)
* Add type check for batch Signed-off-by: Yury-Fridlyand <yury.fridlyand@improving.com> Co-authored-by: Edward Liang <76571219+edlng@users.noreply.github.com>
1 parent 75179b8 commit 4117a80

File tree

12 files changed

+296
-126
lines changed

12 files changed

+296
-126
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
2+
3+
using Valkey.Glide.Commands;
4+
using Valkey.Glide.Internals;
5+
6+
namespace Valkey.Glide;
7+
8+
public abstract partial class BaseClient : IStringBaseCommands
9+
{
10+
public async Task<string> Set(GlideString key, GlideString value)
11+
=> await Command(Request.Set(key, value));
12+
13+
public async Task<GlideString?> Get(GlideString key)
14+
=> await Command(Request.Get(key));
15+
}

csharp/sources/Valkey.Glide/BaseClient.cs

Lines changed: 24 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,21 @@
22

33
using System.Runtime.InteropServices;
44

5-
using Valkey.Glide.Commands;
65
using Valkey.Glide.Internals;
76
using Valkey.Glide.Pipeline;
87

98
using static Valkey.Glide.ConnectionConfiguration;
109
using static Valkey.Glide.Errors;
1110
using static Valkey.Glide.Internals.FFI;
11+
using static Valkey.Glide.Internals.ResponseConverters;
1212
using static Valkey.Glide.Internals.ResponseHandler;
1313
using static Valkey.Glide.Pipeline.Options;
14-
using static Valkey.Glide.Route;
1514

1615
namespace Valkey.Glide;
1716

18-
public abstract class BaseClient : IDisposable, IStringBaseCommands
17+
public abstract partial class BaseClient : IDisposable
1918
{
2019
#region public methods
21-
public async Task<string> Set(GlideString key, GlideString value)
22-
=> await Command(RequestType.Set, [key, value], HandleOk);
23-
24-
public async Task<GlideString?> Get(GlideString key)
25-
=> await Command(RequestType.Get, [key], response => HandleServerResponse<GlideString>(response, true));
26-
2720
public void Dispose()
2821
{
2922
GC.SuppressFinalize(this);
@@ -71,10 +64,17 @@ protected BaseClient()
7164

7265
protected internal delegate T ResponseHandler<T>(IntPtr response);
7366

74-
internal async Task<T> Command<T>(RequestType requestType, GlideString[] arguments, ResponseHandler<T> responseHandler, Route? route = null) where T : class?
67+
/// <summary>
68+
/// </summary>
69+
/// <typeparam name="R">Type received from server.</typeparam>
70+
/// <typeparam name="T">Type we return to the user.</typeparam>
71+
/// <param name="command"></param>
72+
/// <param name="route"></param>
73+
/// <returns></returns>
74+
internal async Task<T> Command<R, T>(Request.Cmd<R, T> command, Route? route = null)
7575
{
7676
// 1. Create Cmd which wraps CmdInfo and manages all memory allocations
77-
using Cmd cmd = new(requestType, arguments);
77+
using Cmd cmd = command.ToFfi();
7878

7979
// 2. Allocate memory for route
8080
using FFI.Route? ffiRoute = route?.ToFfi();
@@ -84,7 +84,15 @@ internal async Task<T> Command<T>(RequestType requestType, GlideString[] argumen
8484
CommandFfi(_clientPointer, (ulong)message.Index, cmd.ToPtr(), ffiRoute?.ToPtr() ?? IntPtr.Zero);
8585

8686
// 4. Get a response and Handle it
87-
return responseHandler(await message);
87+
IntPtr response = await message;
88+
try
89+
{
90+
return HandleServerValue(HandleResponse(response), command.IsNullable, command.Converter);
91+
}
92+
finally
93+
{
94+
FreeResponse(response);
95+
}
8896

8997
// All memory allocated is auto-freed by `using` operator
9098
}
@@ -102,79 +110,17 @@ internal async Task<T> Command<T>(RequestType requestType, GlideString[] argumen
102110
BatchFfi(_clientPointer, (ulong)message.Index, ffiBatch.ToPtr(), raiseOnError, ffiOptions?.ToPtr() ?? IntPtr.Zero);
103111

104112
// 4. Get a response and Handle it
105-
return HandleServerResponse<object?[]?>(await message, true);
106-
107-
// All memory allocated is auto-freed by `using` operator
108-
}
109-
110-
protected internal static string HandleOk(IntPtr response)
111-
=> HandleServerResponse<string>(response, false);
112-
113-
protected internal static T HandleServerResponse<T>(IntPtr response, bool isNullable) where T : class?
114-
=> HandleServerResponse<T, T>(response, isNullable, o => o);
115-
116-
protected static ClusterValue<object?> HandleCustomCommandClusterResponse(IntPtr response, Route? route = null)
117-
=> HandleServerResponse<object, ClusterValue<object?>>(response, true, data
118-
=> (data is string str && str == "OK") || route is SingleNodeRoute || data is not Dictionary<GlideString, object?>
119-
? ClusterValue<object?>.OfSingleValue(data)
120-
: ClusterValue<object?>.OfMultiValue((Dictionary<GlideString, object?>)data));
121-
122-
/// <summary>
123-
/// Process and convert a server response that may be a multi-node response.
124-
/// </summary>
125-
/// <typeparam name="R">GLIDE's return type per node.</typeparam>
126-
/// <typeparam name="T">Command's return type.</typeparam>
127-
/// <param name="response"></param>
128-
/// <param name="isNullable"></param>
129-
/// <param name="converter">Function to convert <typeparamref name="R"/> to <typeparamref name="T"/>.</param>
130-
protected static ClusterValue<T> HandleClusterValueResponse<R, T>(IntPtr response, bool isNullable, Route route, Func<R, T> converter) where T : class?
131-
=> HandleServerResponse<object, ClusterValue<T>>(response, isNullable, data => route is SingleNodeRoute
132-
? ClusterValue<T>.OfSingleValue(converter((R)data))
133-
: ClusterValue<T>.OfMultiValue(((Dictionary<GlideString, object>)data).ConvertValues(converter)));
134-
135-
/// <summary>
136-
/// Process and convert a cluster multi-node response.
137-
/// </summary>
138-
/// <typeparam name="R">GLIDE's return type per node.</typeparam>
139-
/// <typeparam name="T">Command's return type.</typeparam>
140-
/// <param name="response"></param>
141-
/// <param name="converter">Function to convert <typeparamref name="R"/> to <typeparamref name="T"/>.</param>
142-
protected static Dictionary<string, T> HandleMultiNodeResponse<R, T>(IntPtr response, Func<R, T> converter) where T : class?
143-
=> HandleServerResponse<Dictionary<GlideString, object>, Dictionary<string, T>>(response, false, dict => dict.DownCastKeys().ConvertValues(converter));
144-
145-
/// <summary>
146-
/// Process and convert a server response.
147-
/// </summary>
148-
/// <typeparam name="R">GLIDE's return type.</typeparam>
149-
/// <typeparam name="T">Command's return type.</typeparam>
150-
/// <param name="response"></param>
151-
/// <param name="isNullable"></param>
152-
/// <param name="converter">Optional function to convert <typeparamref name="R" /> to <typeparamref name="T" />.</param>
153-
/// <returns></returns>
154-
/// <exception cref="Exception"></exception>
155-
protected internal static T HandleServerResponse<R, T>(IntPtr response, bool isNullable, Func<R, T> converter) where T : class? where R : class?
156-
{
113+
IntPtr response = await message;
157114
try
158115
{
159-
object? value = HandleResponse(response);
160-
if (value is null)
161-
{
162-
if (isNullable)
163-
{
164-
#pragma warning disable CS8603 // Possible null reference return.
165-
return null;
166-
#pragma warning restore CS8603 // Possible null reference return.
167-
}
168-
throw new Exception($"Unexpected return type from Glide: got null expected {typeof(T).GetRealTypeName()}");
169-
}
170-
return value is R
171-
? converter((value as R)!)
172-
: throw new RequestException($"Unexpected return type from Glide: got {value?.GetType().GetRealTypeName()} expected {typeof(T).GetRealTypeName()}");
116+
return batch.ConvertResponse(HandleServerValue(HandleResponse(response), true, (object?[]? o) => o));
173117
}
174118
finally
175119
{
176120
FreeResponse(response);
177121
}
122+
123+
// All memory allocated is auto-freed by `using` operator
178124
}
179125
#endregion protected methods
180126

csharp/sources/Valkey.Glide/GlideClient.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@
22

33
using Valkey.Glide.Commands;
44
using Valkey.Glide.Commands.Options;
5+
using Valkey.Glide.Internals;
56
using Valkey.Glide.Pipeline;
67

78
using static Valkey.Glide.ConnectionConfiguration;
89
using static Valkey.Glide.Pipeline.Options;
910

10-
using RequestType = Valkey.Glide.Internals.FFI.RequestType;
11-
1211
namespace Valkey.Glide;
1312

1413
// TODO add wiki link
@@ -67,12 +66,10 @@ public static async Task<GlideClient> CreateClient(StandaloneClientConfiguration
6766
=> await Batch(batch, raiseOnError, options);
6867

6968
public async Task<object?> CustomCommand(GlideString[] args)
70-
=> await Command(RequestType.CustomCommand, args, resp
71-
=> HandleServerResponse<object?>(resp, true));
69+
=> await Command(Request.CustomCommand(args));
7270

7371
public async Task<string> Info() => await Info([]);
7472

7573
public async Task<string> Info(InfoOptions.Section[] sections)
76-
=> await Command(RequestType.Info, sections.ToGlideStrings(), resp
77-
=> HandleServerResponse<GlideString, string>(resp, false, gs => gs.ToString()));
74+
=> await Command(Request.Info(sections));
7875
}

csharp/sources/Valkey.Glide/GlideClusterClient.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
using Valkey.Glide.Commands;
44
using Valkey.Glide.Commands.Options;
5+
using Valkey.Glide.Internals;
56
using Valkey.Glide.Pipeline;
67

78
using static Valkey.Glide.ConnectionConfiguration;
89
using static Valkey.Glide.Errors;
9-
using static Valkey.Glide.Internals.FFI;
1010
using static Valkey.Glide.Pipeline.Options;
11+
using static Valkey.Glide.Route;
1112

1213
namespace Valkey.Glide;
1314

@@ -66,20 +67,18 @@ public static async Task<GlideClusterClient> CreateClient(ClusterClientConfigura
6667
: await Batch(batch, raiseOnError, options);
6768

6869
public async Task<ClusterValue<object?>> CustomCommand(GlideString[] args)
69-
=> await Command(RequestType.CustomCommand, args, resp => HandleCustomCommandClusterResponse(resp));
70+
=> await Command(Request.CustomCommand(args, resp => ResponseConverters.HandleCustomCommandClusterValue(resp)));
7071

7172
public async Task<ClusterValue<object?>> CustomCommand(GlideString[] args, Route route)
72-
=> await Command(RequestType.CustomCommand, args, resp => HandleCustomCommandClusterResponse(resp, route), route);
73+
=> await Command(Request.CustomCommand(args, resp => ResponseConverters.HandleCustomCommandClusterValue(resp, route)), route);
7374

7475
public async Task<Dictionary<string, string>> Info() => await Info([]);
7576

7677
public async Task<Dictionary<string, string>> Info(InfoOptions.Section[] sections)
77-
=> await Command(RequestType.Info, sections.ToGlideStrings(), resp
78-
=> HandleMultiNodeResponse<GlideString, string>(resp, gs => gs.ToString()));
78+
=> await Command(Request.Info(sections).ToMultiNodeValue());
7979

8080
public async Task<ClusterValue<string>> Info(Route route) => await Info([], route);
8181

8282
public async Task<ClusterValue<string>> Info(InfoOptions.Section[] sections, Route route)
83-
=> await Command(RequestType.Info, sections.ToGlideStrings(), resp
84-
=> HandleClusterValueResponse<GlideString, string>(resp, false, route, gs => gs.ToString()), route);
83+
=> await Command(Request.Info(sections).ToClusterValue(route is SingleNodeRoute), route);
8584
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
2+
3+
using System.Diagnostics;
4+
5+
using Valkey.Glide.Commands.Options;
6+
7+
using static Valkey.Glide.Errors;
8+
using static Valkey.Glide.Internals.FFI;
9+
10+
namespace Valkey.Glide.Internals;
11+
12+
internal class Request // TODO naming
13+
{
14+
internal interface ICmd
15+
{
16+
/// <summary>
17+
/// Convert to an FFI-ready struct.
18+
/// </summary>
19+
Cmd ToFfi();
20+
/// <summary>
21+
/// Get untyped converted (used for batch).
22+
/// </summary>
23+
Func<object?, object?> GetConverter();
24+
}
25+
26+
internal class Cmd<R, T> : ICmd
27+
{
28+
public readonly bool IsNullable;
29+
public readonly Func<R, T> Converter;
30+
public readonly RequestType Request;
31+
public readonly ArgsArray ArgsArray;
32+
33+
#pragma warning disable IDE0046 // Convert to conditional expression
34+
public Func<object?, object?> GetConverter() => value =>
35+
{
36+
if (value is null)
37+
{
38+
if (IsNullable)
39+
{
40+
return null;
41+
}
42+
throw new RequestException($"Unexpected return type from Glide: got null expected {typeof(T).GetRealTypeName()}");
43+
}
44+
if (value is RequestException)
45+
{
46+
return value;
47+
}
48+
Debug.Assert(value!.GetType() == typeof(R) || typeof(R).IsAssignableFrom(value!.GetType()),
49+
$"Unexpected return type from Glide: got {value?.GetType().GetRealTypeName()} expected {typeof(R).GetRealTypeName()}");
50+
51+
return Converter((R)value!);
52+
};
53+
#pragma warning restore IDE0046 // Convert to conditional expression
54+
55+
public Cmd ToFfi() => new(Request, ArgsArray.Args);
56+
57+
public new string ToString() => $"{Request} [{string.Join(' ', ArgsArray.Args.ToStrings())}]";
58+
59+
public Cmd(RequestType request, GlideString[] args, bool isNullable, Func<R, T> converter)
60+
{
61+
Request = request;
62+
ArgsArray = new() { Args = args };
63+
IsNullable = isNullable;
64+
Converter = converter;
65+
}
66+
67+
/// <summary>
68+
/// Convert a command to one which handles a multi-node cluster value.
69+
/// </summary>
70+
public Cmd<Dictionary<GlideString, object>, Dictionary<string, T>> ToMultiNodeValue()
71+
=> new(Request, ArgsArray.Args, IsNullable, map => ResponseConverters.HandleMultiNodeValue(map, Converter));
72+
73+
/// <summary>
74+
/// Convert a command to one which handles a <see cref="ClusterValue{T}" />.
75+
/// </summary>
76+
/// <param name="isSingleValue">Whether current command call returns a single value.</param>
77+
public Cmd<object, ClusterValue<T>> ToClusterValue(bool isSingleValue)
78+
=> new(Request, ArgsArray.Args, IsNullable, ResponseConverters.MakeClusterValueHandler(Converter, isSingleValue));
79+
}
80+
81+
internal record ArgsArray
82+
{
83+
public GlideString[] Args = [];
84+
}
85+
86+
public static Cmd<object?, object?> CustomCommand(GlideString[] args)
87+
=> new(RequestType.CustomCommand, args, true, o => o);
88+
89+
public static Cmd<object?, T> CustomCommand<T>(GlideString[] args, Func<object?, T> converter) where T : class?
90+
=> new(RequestType.CustomCommand, args, true, converter);
91+
92+
public static Cmd<GlideString, string> Info(InfoOptions.Section[] sections)
93+
=> new(RequestType.Info, sections.ToGlideStrings(), false, gs => gs.ToString());
94+
95+
public static Cmd<GlideString, GlideString> Get(GlideString key)
96+
=> Simple<GlideString>(RequestType.Get, [key], true);
97+
98+
public static Cmd<string, string> Set(GlideString key, GlideString value)
99+
=> OK(RequestType.Set, [key, value]);
100+
101+
/// <summary>
102+
/// Create a Cmd which returns OK
103+
/// </summary>
104+
private static Cmd<string, string> OK(RequestType request, GlideString[] args)
105+
=> Simple<string>(request, args);
106+
107+
/// <summary>
108+
/// Create a Cmd which does not need type conversion
109+
/// </summary>
110+
private static Cmd<T, T> Simple<T>(RequestType request, GlideString[] args, bool isNullable = false)
111+
=> new(request, args, isNullable, o => o);
112+
}

csharp/sources/Valkey.Glide/Internals/Helpers.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,15 @@ public static Dictionary<string, T> DownCastKeys<T>(this Dictionary<GlideString,
1515
public static Dictionary<T, string> DownCastVals<T>(this Dictionary<T, GlideString> dict) where T : class
1616
=> dict.Select(p => (p.Key, Value: p.Value.ToString())).ToDictionary(p => p.Key, p => p.Value);
1717

18-
// Convert values in a dictionary from T to V where input dict has type `Dictionary<K, object>`
19-
public static Dictionary<K, V> ConvertValues<K, V, T>(this Dictionary<K, object> dict, Func<T, V> converter) where K : class
20-
=> dict.Select(p => (p.Key, Value: converter((T)p.Value))).ToDictionary(p => p.Key, p => p.Value);
18+
// Convert values in a dictionary from V to T where input dict has type `Dictionary<K, V>` and returns `Dictionary<K, T>`
19+
public static Dictionary<K, T> ConvertValues<K, V, T>(this Dictionary<K, V> dict, Func<V, T> converter) where K : notnull
20+
=> dict.Select(p => (p.Key, Value: converter(p.Value))).ToDictionary(p => p.Key, p => p.Value);
21+
22+
// Convert values in a dictionary from (object)V to T where input dict has type `Dictionary<K, V>` and returns `Dictionary<K, T>`
23+
// We receive maps as `Dictionary<GlideString, object>` or `Dictionary<String, object>` from `ResponseHandler`. This converter
24+
// assumes that all values in a map have the same type V.
25+
public static Dictionary<K, T> ConvertValues<K, V, T>(this Dictionary<K, object> dict, Func<V, T> converter) where K : notnull
26+
=> dict.Select(p => (p.Key, Value: converter((V)p.Value))).ToDictionary(p => p.Key, p => p.Value);
2127

2228
// Get type name in format like "Dictionary<GlideString, GlideString>" (not "Dictionary`2")
2329
public static string GetRealTypeName(this Type t)

0 commit comments

Comments
 (0)