Skip to content

Commit 469be6e

Browse files
committed
refactor: improve memory management and validation in scripting commands
- Fix memory leak in InvokeScriptInternalAsync by properly freeing all allocated memory - Extract cleanup logic into FreeScriptMemory helper method for better maintainability - Add null validation for Script and ScriptOptions parameters - Remove redundant string validation (handled by Script constructor) - Replace BitConverter.ToString() with Convert.ToHexStringLower() for better performance - Add thread-safety documentation to Script class Signed-off-by: Joseph Brinkman <[email protected]>
1 parent 2844a51 commit 469be6e

File tree

1 file changed

+90
-22
lines changed

1 file changed

+90
-22
lines changed

sources/Valkey.Glide/BaseClient.ScriptingCommands.cs

Lines changed: 90 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ public async Task<ValkeyResult> InvokeScriptAsync(
1919
CommandFlags flags = CommandFlags.None,
2020
CancellationToken cancellationToken = default)
2121
{
22+
if (script == null)
23+
{
24+
throw new ArgumentNullException(nameof(script));
25+
}
26+
2227
Utils.Requires<NotImplementedException>(flags == CommandFlags.None, "Command flags are not supported by GLIDE");
2328
return await InvokeScriptInternalAsync(script.Hash, null, null, null);
2429
}
@@ -30,6 +35,16 @@ public async Task<ValkeyResult> InvokeScriptAsync(
3035
CommandFlags flags = CommandFlags.None,
3136
CancellationToken cancellationToken = default)
3237
{
38+
if (script == null)
39+
{
40+
throw new ArgumentNullException(nameof(script));
41+
}
42+
43+
if (options == null)
44+
{
45+
throw new ArgumentNullException(nameof(options));
46+
}
47+
3348
Utils.Requires<NotImplementedException>(flags == CommandFlags.None, "Command flags are not supported by GLIDE");
3449
return await InvokeScriptInternalAsync(script.Hash, options.Keys, options.Args, null);
3550
}
@@ -43,17 +58,23 @@ private async Task<ValkeyResult> InvokeScriptInternalAsync(
4358
// Convert hash to C string
4459
IntPtr hashPtr = Marshal.StringToHGlobalAnsi(hash);
4560

61+
// Track allocated memory for cleanup
62+
IntPtr[]? keyPtrs = null;
63+
IntPtr keysPtr = IntPtr.Zero;
64+
IntPtr keysLenPtr = IntPtr.Zero;
65+
IntPtr[]? argPtrs = null;
66+
IntPtr argsPtr = IntPtr.Zero;
67+
IntPtr argsLenPtr = IntPtr.Zero;
68+
4669
try
4770
{
4871
// Prepare keys
49-
IntPtr keysPtr = IntPtr.Zero;
50-
IntPtr keysLenPtr = IntPtr.Zero;
5172
ulong keysCount = 0;
5273

5374
if (keys != null && keys.Length > 0)
5475
{
5576
keysCount = (ulong)keys.Length;
56-
IntPtr[] keyPtrs = new IntPtr[keys.Length];
77+
keyPtrs = new IntPtr[keys.Length];
5778
ulong[] keyLens = new ulong[keys.Length];
5879

5980
for (int i = 0; i < keys.Length; i++)
@@ -72,14 +93,12 @@ private async Task<ValkeyResult> InvokeScriptInternalAsync(
7293
}
7394

7495
// Prepare args
75-
IntPtr argsPtr = IntPtr.Zero;
76-
IntPtr argsLenPtr = IntPtr.Zero;
7796
ulong argsCount = 0;
7897

7998
if (args != null && args.Length > 0)
8099
{
81100
argsCount = (ulong)args.Length;
82-
IntPtr[] argPtrs = new IntPtr[args.Length];
101+
argPtrs = new IntPtr[args.Length];
83102
ulong[] argLens = new ulong[args.Length];
84103

85104
for (int i = 0; i < args.Length; i++)
@@ -129,12 +148,70 @@ private async Task<ValkeyResult> InvokeScriptInternalAsync(
129148
}
130149
finally
131150
{
132-
// Free allocated memory
133-
if (hashPtr != IntPtr.Zero)
151+
FreeScriptMemory(hashPtr, keyPtrs, keysPtr, keysLenPtr, argPtrs, argsPtr, argsLenPtr);
152+
}
153+
}
154+
155+
/// <summary>
156+
/// Frees all allocated memory for script invocation.
157+
/// </summary>
158+
private static void FreeScriptMemory(
159+
IntPtr hashPtr,
160+
IntPtr[]? keyPtrs,
161+
IntPtr keysPtr,
162+
IntPtr keysLenPtr,
163+
IntPtr[]? argPtrs,
164+
IntPtr argsPtr,
165+
IntPtr argsLenPtr)
166+
{
167+
// Free hash
168+
if (hashPtr != IntPtr.Zero)
169+
{
170+
Marshal.FreeHGlobal(hashPtr);
171+
}
172+
173+
// Free individual key strings
174+
if (keyPtrs != null)
175+
{
176+
foreach (IntPtr ptr in keyPtrs)
134177
{
135-
Marshal.FreeHGlobal(hashPtr);
178+
if (ptr != IntPtr.Zero)
179+
{
180+
Marshal.FreeHGlobal(ptr);
181+
}
136182
}
137-
// TODO: Free keys and args memory
183+
}
184+
185+
// Free keys array and lengths
186+
if (keysPtr != IntPtr.Zero)
187+
{
188+
Marshal.FreeHGlobal(keysPtr);
189+
}
190+
if (keysLenPtr != IntPtr.Zero)
191+
{
192+
Marshal.FreeHGlobal(keysLenPtr);
193+
}
194+
195+
// Free individual arg strings
196+
if (argPtrs != null)
197+
{
198+
foreach (IntPtr ptr in argPtrs)
199+
{
200+
if (ptr != IntPtr.Zero)
201+
{
202+
Marshal.FreeHGlobal(ptr);
203+
}
204+
}
205+
}
206+
207+
// Free args array and lengths
208+
if (argsPtr != IntPtr.Zero)
209+
{
210+
Marshal.FreeHGlobal(argsPtr);
211+
}
212+
if (argsLenPtr != IntPtr.Zero)
213+
{
214+
Marshal.FreeHGlobal(argsLenPtr);
138215
}
139216
}
140217

@@ -280,14 +357,10 @@ public async Task<string> FunctionFlushAsync(
280357
public async Task<ValkeyResult> ScriptEvaluateAsync(string script, ValkeyKey[]? keys = null, ValkeyValue[]? values = null,
281358
CommandFlags flags = CommandFlags.None)
282359
{
283-
if (string.IsNullOrEmpty(script))
284-
{
285-
throw new ArgumentException("Script cannot be null or empty", nameof(script));
286-
}
287-
288360
Utils.Requires<NotImplementedException>(flags == CommandFlags.None, "Command flags are not supported by GLIDE");
289361

290362
// Use the optimized InvokeScript path via Script object
363+
// Script constructor will validate the script parameter
291364
using Script scriptObj = new(script);
292365

293366
// Convert keys and values to string arrays
@@ -302,15 +375,10 @@ public async Task<ValkeyResult> ScriptEvaluateAsync(string script, ValkeyKey[]?
302375
public async Task<ValkeyResult> ScriptEvaluateAsync(byte[] hash, ValkeyKey[]? keys = null, ValkeyValue[]? values = null,
303376
CommandFlags flags = CommandFlags.None)
304377
{
305-
if (hash == null || hash.Length == 0)
306-
{
307-
throw new ArgumentException("Hash cannot be null or empty", nameof(hash));
308-
}
309-
310378
Utils.Requires<NotImplementedException>(flags == CommandFlags.None, "Command flags are not supported by GLIDE");
311379

312380
// Convert hash to hex string
313-
string hashString = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
381+
string hashString = Convert.ToHexStringLower(hash);
314382

315383
// Convert keys and values to string arrays
316384
string[]? keyStrings = keys?.Select(k => k.ToString()).ToArray();
@@ -369,7 +437,7 @@ public async Task<ValkeyResult> ScriptEvaluateAsync(LoadedLuaScript script, obje
369437

370438
// Convert the hash from byte[] to hex string
371439
// The hash in LoadedLuaScript is the hash of the script that was actually loaded on the server
372-
string hashString = BitConverter.ToString(script.Hash).Replace("-", "").ToLowerInvariant();
440+
string hashString = Convert.ToHexStringLower(script.Hash);
373441

374442
// Use InvokeScriptInternalAsync with the hash from LoadedLuaScript
375443
// The script was already loaded on the server, so EVALSHA will work

0 commit comments

Comments
 (0)