From 32f562effd41e49086b2fb21545c9abdadc2fd1b Mon Sep 17 00:00:00 2001 From: Guriy Samarin <guriy@amazon.com> Date: Mon, 5 Feb 2024 17:52:57 +0000 Subject: [PATCH 01/10] up c# syntax, make cli defaults in one style --- benchmarks/csharp/Program.cs | 30 +++---- csharp/lib/AsyncClient.cs | 160 ++++++++++++++++----------------- csharp/lib/Logger.cs | 129 +++++++++++++------------- csharp/lib/Message.cs | 24 +---- csharp/lib/MessageContainer.cs | 82 ++++++++--------- 5 files changed, 196 insertions(+), 229 deletions(-) diff --git a/benchmarks/csharp/Program.cs b/benchmarks/csharp/Program.cs index 541e3100cd..f1aaabd34e 100644 --- a/benchmarks/csharp/Program.cs +++ b/benchmarks/csharp/Program.cs @@ -16,30 +16,30 @@ private enum ChosenAction { GET_NON_EXISTING, GET_EXISTING, SET }; public class CommandLineOptions { - [Option('r', "resultsFile", Required = false, HelpText = "Set the file to which the JSON results are written.")] - public string resultsFile { get; set; } = "../results/csharp-results.json"; + [Option('r', "resultsFile", Required = false, HelpText = "Set the file to which the JSON results are written.", Default = "../results/csharp-results.json")] + public string resultsFile { get; set; } - [Option('d', "dataSize", Required = false, HelpText = "The size of the sent data in bytes.")] - public int dataSize { get; set; } = 100; + [Option('d', "dataSize", Required = false, HelpText = "The size of the sent data in bytes.", Default = 100)] + public int dataSize { get; set; } [Option('c', "concurrentTasks", Required = false, HelpText = "The number of concurrent operations to perform.", Default = new[] { 1, 10, 100, 1000 })] public IEnumerable<int> concurrentTasks { get; set; } - [Option('l', "clients", Required = false, HelpText = "Which clients should run")] - public string clientsToRun { get; set; } = "all"; + [Option('l', "clients", Required = false, HelpText = "Which clients should run", Default = "all")] + public string clientsToRun { get; set; } - [Option('h', "host", Required = false, HelpText = "What host to target")] - public string host { get; set; } = "localhost"; + [Option('h', "host", Required = false, HelpText = "What host to target", Default = "localhost")] + public string host { get; set; } [Option('C', "clientCount", Required = false, HelpText = "Number of clients to run concurrently", Default = new[] { 1 })] public IEnumerable<int> clientCount { get; set; } - [Option('t', "tls", HelpText = "Should benchmark a TLS server")] - public bool tls { get; set; } = false; + [Option('t', "tls", HelpText = "Should benchmark a TLS server", Default = false)] + public bool tls { get; set; } - [Option('m', "minimal", HelpText = "Should use a minimal number of actions")] - public bool minimal { get; set; } = false; + [Option('m', "minimal", HelpText = "Should use a minimal number of actions", Default = false)] + public bool minimal { get; set; } } private const int PORT = 6379; @@ -118,10 +118,8 @@ private static double calculate_latency(IEnumerable<double> latency_list, double private static void print_results(string resultsFile) { - using (FileStream createStream = File.Create(resultsFile)) - { - JsonSerializer.Serialize(createStream, bench_json_results); - } + using FileStream createStream = File.Create(resultsFile); + JsonSerializer.Serialize(createStream, bench_json_results); } private static async Task redis_benchmark( diff --git a/csharp/lib/AsyncClient.cs b/csharp/lib/AsyncClient.cs index b50fe77bf4..459407704a 100644 --- a/csharp/lib/AsyncClient.cs +++ b/csharp/lib/AsyncClient.cs @@ -4,114 +4,110 @@ using System.Runtime.InteropServices; -namespace Glide +namespace Glide; + +public class AsyncClient : IDisposable { - public class AsyncClient : IDisposable + #region public methods + public AsyncClient(string host, UInt32 port, bool useTLS) { - #region public methods - public AsyncClient(string host, UInt32 port, bool useTLS) + successCallbackDelegate = SuccessCallback; + var successCallbackPointer = Marshal.GetFunctionPointerForDelegate(successCallbackDelegate); + failureCallbackDelegate = FailureCallback; + var failureCallbackPointer = Marshal.GetFunctionPointerForDelegate(failureCallbackDelegate); + clientPointer = CreateClientFfi(host, port, useTLS, successCallbackPointer, failureCallbackPointer); + if (clientPointer == IntPtr.Zero) { - successCallbackDelegate = SuccessCallback; - var successCallbackPointer = Marshal.GetFunctionPointerForDelegate(successCallbackDelegate); - failureCallbackDelegate = FailureCallback; - var failureCallbackPointer = Marshal.GetFunctionPointerForDelegate(failureCallbackDelegate); - clientPointer = CreateClientFfi(host, port, useTLS, successCallbackPointer, failureCallbackPointer); - if (clientPointer == IntPtr.Zero) - { - throw new Exception("Failed creating a client"); - } + throw new Exception("Failed creating a client"); } + } - public async Task SetAsync(string key, string value) - { - var message = messageContainer.GetMessageForCall(key, value); - SetFfi(clientPointer, (ulong)message.Index, message.KeyPtr, message.ValuePtr); - await message; - } + public async Task SetAsync(string key, string value) + { + var message = messageContainer.GetMessageForCall(key, value); + SetFfi(clientPointer, (ulong)message.Index, message.KeyPtr, message.ValuePtr); + await message; + } - public async Task<string?> GetAsync(string key) - { - var message = messageContainer.GetMessageForCall(key, null); - GetFfi(clientPointer, (ulong)message.Index, message.KeyPtr); - return await message; - } + public async Task<string?> GetAsync(string key) + { + var message = messageContainer.GetMessageForCall(key, null); + GetFfi(clientPointer, (ulong)message.Index, message.KeyPtr); + return await message; + } - public void Dispose() + public void Dispose() + { + if (clientPointer == IntPtr.Zero) { - if (clientPointer == IntPtr.Zero) - { - return; - } - messageContainer.DisposeWithError(null); - CloseClientFfi(clientPointer); - clientPointer = IntPtr.Zero; + return; } + messageContainer.DisposeWithError(null); + CloseClientFfi(clientPointer); + clientPointer = IntPtr.Zero; + } - #endregion public methods + #endregion public methods - #region private methods + #region private methods - private void SuccessCallback(ulong index, IntPtr str) + private void SuccessCallback(ulong index, IntPtr str) + { + var result = str == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(str); + // Work needs to be offloaded from the calling thread, because otherwise we might starve the client's thread pool. + Task.Run(() => { - var result = str == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(str); - // Work needs to be offloaded from the calling thread, because otherwise we might starve the client's thread pool. - Task.Run(() => - { - var message = messageContainer.GetMessage((int)index); - message.SetResult(result); - }); - } + var message = messageContainer.GetMessage((int)index); + message.SetResult(result); + }); + } - private void FailureCallback(ulong index) + private void FailureCallback(ulong index) + { + // Work needs to be offloaded from the calling thread, because otherwise we might starve the client's thread pool. + Task.Run(() => { - // Work needs to be offloaded from the calling thread, because otherwise we might starve the client's thread pool. - Task.Run(() => - { - var message = messageContainer.GetMessage((int)index); - message.SetException(new Exception("Operation failed")); - }); - } + var message = messageContainer.GetMessage((int)index); + message.SetException(new Exception("Operation failed")); + }); + } - ~AsyncClient() - { - Dispose(); - } - #endregion private methods + ~AsyncClient() => Dispose(); + #endregion private methods - #region private fields + #region private fields - /// Held as a measure to prevent the delegate being garbage collected. These are delegated once - /// and held in order to prevent the cost of marshalling on each function call. - private FailureAction failureCallbackDelegate; + /// Held as a measure to prevent the delegate being garbage collected. These are delegated once + /// and held in order to prevent the cost of marshalling on each function call. + private FailureAction failureCallbackDelegate; - /// Held as a measure to prevent the delegate being garbage collected. These are delegated once - /// and held in order to prevent the cost of marshalling on each function call. - private StringAction successCallbackDelegate; + /// Held as a measure to prevent the delegate being garbage collected. These are delegated once + /// and held in order to prevent the cost of marshalling on each function call. + private StringAction successCallbackDelegate; - /// Raw pointer to the underlying native client. - private IntPtr clientPointer; + /// Raw pointer to the underlying native client. + private IntPtr clientPointer; - private readonly MessageContainer<string> messageContainer = new(); + private readonly MessageContainer<string> messageContainer = new(); - #endregion private fields + #endregion private fields - #region FFI function declarations + #region FFI function declarations - private delegate void StringAction(ulong index, IntPtr str); - private delegate void FailureAction(ulong index); - [DllImport("libglide_rs", CallingConvention = CallingConvention.Cdecl, EntryPoint = "get")] - private static extern void GetFfi(IntPtr client, ulong index, IntPtr key); + private delegate void StringAction(ulong index, IntPtr str); + private delegate void FailureAction(ulong index); + [DllImport("libglide_rs", CallingConvention = CallingConvention.Cdecl, EntryPoint = "get")] + private static extern void GetFfi(IntPtr client, ulong index, IntPtr key); - [DllImport("libglide_rs", CallingConvention = CallingConvention.Cdecl, EntryPoint = "set")] - private static extern void SetFfi(IntPtr client, ulong index, IntPtr key, IntPtr value); + [DllImport("libglide_rs", CallingConvention = CallingConvention.Cdecl, EntryPoint = "set")] + private static extern void SetFfi(IntPtr client, ulong index, IntPtr key, IntPtr value); - private delegate void IntAction(IntPtr arg); - [DllImport("libglide_rs", CallingConvention = CallingConvention.Cdecl, EntryPoint = "create_client")] - private static extern IntPtr CreateClientFfi(String host, UInt32 port, bool useTLS, IntPtr successCallback, IntPtr failureCallback); + private delegate void IntAction(IntPtr arg); + [DllImport("libglide_rs", CallingConvention = CallingConvention.Cdecl, EntryPoint = "create_client")] + private static extern IntPtr CreateClientFfi(String host, UInt32 port, bool useTLS, IntPtr successCallback, IntPtr failureCallback); - [DllImport("libglide_rs", CallingConvention = CallingConvention.Cdecl, EntryPoint = "close_client")] - private static extern void CloseClientFfi(IntPtr client); + [DllImport("libglide_rs", CallingConvention = CallingConvention.Cdecl, EntryPoint = "close_client")] + private static extern void CloseClientFfi(IntPtr client); - #endregion - } + #endregion } diff --git a/csharp/lib/Logger.cs b/csharp/lib/Logger.cs index da9e172090..7edc16f16c 100644 --- a/csharp/lib/Logger.cs +++ b/csharp/lib/Logger.cs @@ -6,83 +6,80 @@ using System.Text; -namespace Glide -{ - // TODO - use a bindings generator to create this enum. - public enum Level - { - Error = 0, - Warn = 1, - Info = 2, - Debug = 3, - Trace = 4 - } +namespace Glide; - /* - A class that allows logging which is consistent with logs from the internal rust core. - Only one instance of this class can exist at any given time. The logger can be set up in 2 ways - - 1. By calling init, which creates and modifies a new logger only if one doesn't exist. - 2. By calling setConfig, which replaces the existing logger, and means that new logs will not be saved with the logs that were sent before the call. - If no call to any of these function is received, the first log attempt will initialize a new logger with default level decided by rust core (normally - console, error). - */ - public class Logger - { - #region private fields +// TODO - use a bindings generator to create this enum. +public enum Level +{ + Error = 0, + Warn = 1, + Info = 2, + Debug = 3, + Trace = 4 +} - private static Level? loggerLevel = null; - #endregion private fields +/* +A class that allows logging which is consistent with logs from the internal rust core. +Only one instance of this class can exist at any given time. The logger can be set up in 2 ways - + 1. By calling init, which creates and modifies a new logger only if one doesn't exist. + 2. By calling setConfig, which replaces the existing logger, and means that new logs will not be saved with the logs that were sent before the call. +If no call to any of these function is received, the first log attempt will initialize a new logger with default level decided by rust core (normally - console, error). +*/ +public class Logger +{ + #region private fields - #region internal methods - // Initialize a logger instance if none were initialized before - this method is meant to be used when there is no intention to replace an existing logger. - // The logger will filter all logs with a level lower than the given level, - // If given a fileName argument, will write the logs to files postfixed with fileName. If fileName isn't provided, the logs will be written to the console. - internal static void Init(Level? level, string? filename = null) - { - if (Logger.loggerLevel is null) - { - SetLoggerConfig(level, filename); - } - } + private static Level? loggerLevel = null; + #endregion private fields - // take the arguments from the user and provide to the core-logger (see ../logger-core) - // if the level is higher then the logger level (error is 0, warn 1, etc.) simply return without operation - // if a logger instance doesn't exist, create new one with default mode (decided by rust core, normally - level: error, target: console) - // logIdentifier arg is a string contain data that suppose to give the log a context and make it easier to find certain type of logs. - // when the log is connect to certain task the identifier should be the task id, when the log is not part of specific task the identifier should give a context to the log - for example, "create client". - internal static void Log(Level logLevel, string logIdentifier, string message) + #region internal methods + // Initialize a logger instance if none were initialized before - this method is meant to be used when there is no intention to replace an existing logger. + // The logger will filter all logs with a level lower than the given level, + // If given a fileName argument, will write the logs to files postfixed with fileName. If fileName isn't provided, the logs will be written to the console. + internal static void Init(Level? level, string? filename = null) + { + if (Logger.loggerLevel is null) { - if (Logger.loggerLevel is null) - { - SetLoggerConfig(logLevel); - } - if (!(logLevel <= Logger.loggerLevel)) return; - log(Convert.ToInt32(logLevel), Encoding.UTF8.GetBytes(logIdentifier), Encoding.UTF8.GetBytes(message)); + SetLoggerConfig(level, filename); } - #endregion internal methods + } - #region public methods - // config the logger instance - in fact - create new logger instance with the new args - // exist in addition to init for two main reason's: - // 1. if GLIDE dev want intentionally to change the logger instance configuration - // 2. external user want to set the logger and we don't want to return to him the logger itself, just config it - // the level argument is the level of the logs you want the system to provide (error logs, warn logs, etc.) - // the filename argument is optional - if provided the target of the logs will be the file mentioned, else will be the console - public static void SetLoggerConfig(Level? level, string? filename = null) + // take the arguments from the user and provide to the core-logger (see ../logger-core) + // if the level is higher then the logger level (error is 0, warn 1, etc.) simply return without operation + // if a logger instance doesn't exist, create new one with default mode (decided by rust core, normally - level: error, target: console) + // logIdentifier arg is a string contain data that suppose to give the log a context and make it easier to find certain type of logs. + // when the log is connect to certain task the identifier should be the task id, when the log is not part of specific task the identifier should give a context to the log - for example, "create client". + internal static void Log(Level logLevel, string logIdentifier, string message) + { + if (Logger.loggerLevel is null) { - var buffer = filename is null ? null : Encoding.UTF8.GetBytes(filename); - Logger.loggerLevel = InitInternalLogger(Convert.ToInt32(level), buffer); + SetLoggerConfig(logLevel); } - #endregion public methods - - #region FFI function declaration - [DllImport("libglide_rs", CallingConvention = CallingConvention.Cdecl, EntryPoint = "log")] - private static extern void log(Int32 logLevel, byte[] logIdentifier, byte[] message); - - [DllImport("libglide_rs", CallingConvention = CallingConvention.Cdecl, EntryPoint = "init")] - private static extern Level InitInternalLogger(Int32 level, byte[]? filename); + if (!(logLevel <= Logger.loggerLevel)) return; + log(Convert.ToInt32(logLevel), Encoding.UTF8.GetBytes(logIdentifier), Encoding.UTF8.GetBytes(message)); + } + #endregion internal methods - #endregion + #region public methods + // config the logger instance - in fact - create new logger instance with the new args + // exist in addition to init for two main reason's: + // 1. if GLIDE dev want intentionally to change the logger instance configuration + // 2. external user want to set the logger and we don't want to return to him the logger itself, just config it + // the level argument is the level of the logs you want the system to provide (error logs, warn logs, etc.) + // the filename argument is optional - if provided the target of the logs will be the file mentioned, else will be the console + public static void SetLoggerConfig(Level? level, string? filename = null) + { + var buffer = filename is null ? null : Encoding.UTF8.GetBytes(filename); + Logger.loggerLevel = InitInternalLogger(Convert.ToInt32(level), buffer); } + #endregion public methods + + #region FFI function declaration + [DllImport("libglide_rs", CallingConvention = CallingConvention.Cdecl, EntryPoint = "log")] + private static extern void log(Int32 logLevel, byte[] logIdentifier, byte[] message); + [DllImport("libglide_rs", CallingConvention = CallingConvention.Cdecl, EntryPoint = "init")] + private static extern Level InitInternalLogger(Int32 level, byte[]? filename); + #endregion } diff --git a/csharp/lib/Message.cs b/csharp/lib/Message.cs index 0263df7cd1..daa11602c6 100644 --- a/csharp/lib/Message.cs +++ b/csharp/lib/Message.cs @@ -38,7 +38,7 @@ public Message(int index, MessageContainer<T> container) private T? result; private Exception? exception; - /// Triggers a succesful completion of the task returned from the latest call + /// Triggers a succesful completion of the task returned from the latest call /// to CreateTask. public void SetResult(T? result) { @@ -78,10 +78,7 @@ private void CheckRaceAndCallContinuation() } } - public Message<T> GetAwaiter() - { - return this; - } + public Message<T> GetAwaiter() => this; /// This returns a task that will complete once SetException / SetResult are called, /// and ensures that the internal state of the message is set-up before the task is created, @@ -124,20 +121,7 @@ public void OnCompleted(Action continuation) CheckRaceAndCallContinuation(); } - public bool IsCompleted - { - get - { - return completionState == COMPLETION_STAGE_CONTINUATION_EXECUTED; - } - } + public bool IsCompleted => completionState == COMPLETION_STAGE_CONTINUATION_EXECUTED; - public T? GetResult() - { - if (this.exception != null) - { - throw this.exception; - } - return this.result; - } + public T? GetResult() => this.exception is null ? this.result : throw this.exception; } diff --git a/csharp/lib/MessageContainer.cs b/csharp/lib/MessageContainer.cs index ddc19e3323..5999702363 100644 --- a/csharp/lib/MessageContainer.cs +++ b/csharp/lib/MessageContainer.cs @@ -4,67 +4,59 @@ using System.Collections.Concurrent; -namespace Glide +namespace Glide; + + +internal class MessageContainer<T> { + internal Message<T> GetMessage(int index) => messages[index]; - internal class MessageContainer<T> + internal Message<T> GetMessageForCall(string? key, string? value) { - internal Message<T> GetMessage(int index) - { - return messages[index]; - } - - internal Message<T> GetMessageForCall(string? key, string? value) - { - var message = GetFreeMessage(); - message.StartTask(key, value, this); - return message; - } + var message = GetFreeMessage(); + message.StartTask(key, value, this); + return message; + } - private Message<T> GetFreeMessage() + private Message<T> GetFreeMessage() + { + if (!availableMessages.TryDequeue(out var message)) { - if (!availableMessages.TryDequeue(out var message)) + lock (messages) { - lock (messages) - { - var index = messages.Count; - message = new Message<T>(index, this); - messages.Add(message); - } + var index = messages.Count; + message = new Message<T>(index, this); + messages.Add(message); } - return message; } + return message; + } - public void ReturnFreeMessage(Message<T> message) - { - availableMessages.Enqueue(message); - } + public void ReturnFreeMessage(Message<T> message) => availableMessages.Enqueue(message); - internal void DisposeWithError(Exception? error) + internal void DisposeWithError(Exception? error) + { + lock (messages) { - lock (messages) + foreach (var message in messages.Where(message => !message.IsCompleted)) { - foreach (var message in messages.Where(message => !message.IsCompleted)) + try { - try - { - message.SetException(new TaskCanceledException("Client closed", error)); - } - catch (Exception) { } + message.SetException(new TaskCanceledException("Client closed", error)); } - messages.Clear(); + catch (Exception) { } } - availableMessages.Clear(); + messages.Clear(); } - - /// This list allows us random-access to the message in each index, - /// which means that once we receive a callback with an index, we can - /// find the message to resolve in constant time. - private List<Message<T>> messages = new(); - - /// This queue contains the messages that were created and are currently unused by any task, - /// so they can be reused y new tasks instead of allocating new messages. - private ConcurrentQueue<Message<T>> availableMessages = new(); + availableMessages.Clear(); } + /// This list allows us random-access to the message in each index, + /// which means that once we receive a callback with an index, we can + /// find the message to resolve in constant time. + private List<Message<T>> messages = new(); + + /// This queue contains the messages that were created and are currently unused by any task, + /// so they can be reused y new tasks instead of allocating new messages. + private ConcurrentQueue<Message<T>> availableMessages = new(); } From 9d1c26410374cb335227c4200a706c56d13a6507 Mon Sep 17 00:00:00 2001 From: Guriy Samarin <guriy@amazon.com> Date: Tue, 6 Feb 2024 00:46:27 +0000 Subject: [PATCH 02/10] add .editorconfig --- .editorconfig | 372 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 372 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..0002f363fa --- /dev/null +++ b/.editorconfig @@ -0,0 +1,372 @@ +root = true + +# All files +[*] +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space + +# Xml files +[*.xml] +indent_size = 2 + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +tab_width = 4 + +# New line preferences +end_of_line = crlf +insert_final_newline = false + +#### .NET Coding Conventions #### +[*.{cs,vb}] + +# Organize usings +dotnet_separate_import_directive_groups = true +dotnet_sort_system_directives_first = true +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false:silent +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_property = false:silent + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion + +# Expression-level preferences +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_object_initializer = true:suggestion +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion + +# Field preferences +dotnet_style_readonly_field = true:warning + +# Parameter preferences +dotnet_code_quality_unused_parameters = all:suggestion + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +#### C# Coding Conventions #### +[*.cs] + +# var preferences +csharp_style_var_elsewhere = false:suggestion +csharp_style_var_for_built_in_types = false:suggestion +csharp_style_var_when_type_is_apparent = false:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = true:suggestion +csharp_style_expression_bodied_lambdas = true:suggestion +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_expression_bodied_methods = true:suggestion +csharp_style_expression_bodied_operators = true:suggestion +csharp_style_expression_bodied_properties = true:suggestion + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_pattern_matching = true:silent +csharp_style_prefer_switch_expression = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_prefer_extended_property_pattern = true:suggestion +csharp_style_prefer_method_group_conversion = true:silent + +# Null-checking preferences +csharp_style_conditional_delegate_call = true:suggestion + +# Modifier preferences +csharp_prefer_static_local_function = true:warning +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion + +# Code-block preferences +csharp_prefer_braces = true:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_namespace_declarations = file_scoped:suggestion +dotnet_style_namespace_match_folder = true:suggestion +csharp_style_prefer_top_level_statements = true:silent + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:silent + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### +[*.{cs,vb}] + +# Naming rules + +dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces +dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion +dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces +dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase + +dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion +dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters +dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase + +dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods +dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties +dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.events_should_be_pascalcase.symbols = events +dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion +dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables +dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase + +dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion +dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants +dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase + +dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion +dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters +dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase + +dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields +dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion +dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields +dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase + +dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion +dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields +dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase + +dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields +dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields +dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields +dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields +dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums +dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions +dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase + +# Symbol specifications + +dotnet_naming_symbols.interfaces.applicable_kinds = interface +dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interfaces.required_modifiers = + +dotnet_naming_symbols.enums.applicable_kinds = enum +dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.enums.required_modifiers = + +dotnet_naming_symbols.events.applicable_kinds = event +dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.events.required_modifiers = + +dotnet_naming_symbols.methods.applicable_kinds = method +dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.methods.required_modifiers = + +dotnet_naming_symbols.properties.applicable_kinds = property +dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.properties.required_modifiers = + +dotnet_naming_symbols.public_fields.applicable_kinds = field +dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal +dotnet_naming_symbols.public_fields.required_modifiers = + +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_fields.required_modifiers = + +dotnet_naming_symbols.private_static_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_static_fields.required_modifiers = static + +dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum +dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types_and_namespaces.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +dotnet_naming_symbols.type_parameters.applicable_kinds = namespace +dotnet_naming_symbols.type_parameters.applicable_accessibilities = * +dotnet_naming_symbols.type_parameters.required_modifiers = + +dotnet_naming_symbols.private_constant_fields.applicable_kinds = field +dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_constant_fields.required_modifiers = const + +dotnet_naming_symbols.local_variables.applicable_kinds = local +dotnet_naming_symbols.local_variables.applicable_accessibilities = local +dotnet_naming_symbols.local_variables.required_modifiers = + +dotnet_naming_symbols.local_constants.applicable_kinds = local +dotnet_naming_symbols.local_constants.applicable_accessibilities = local +dotnet_naming_symbols.local_constants.required_modifiers = const + +dotnet_naming_symbols.parameters.applicable_kinds = parameter +dotnet_naming_symbols.parameters.applicable_accessibilities = * +dotnet_naming_symbols.parameters.required_modifiers = + +dotnet_naming_symbols.public_constant_fields.applicable_kinds = field +dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal +dotnet_naming_symbols.public_constant_fields.required_modifiers = const + +dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal +dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static + +dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static + +dotnet_naming_symbols.local_functions.applicable_kinds = local_function +dotnet_naming_symbols.local_functions.applicable_accessibilities = * +dotnet_naming_symbols.local_functions.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascalcase.required_prefix = +dotnet_naming_style.pascalcase.required_suffix = +dotnet_naming_style.pascalcase.word_separator = +dotnet_naming_style.pascalcase.capitalization = pascal_case + +dotnet_naming_style.ipascalcase.required_prefix = I +dotnet_naming_style.ipascalcase.required_suffix = +dotnet_naming_style.ipascalcase.word_separator = +dotnet_naming_style.ipascalcase.capitalization = pascal_case + +dotnet_naming_style.tpascalcase.required_prefix = T +dotnet_naming_style.tpascalcase.required_suffix = +dotnet_naming_style.tpascalcase.word_separator = +dotnet_naming_style.tpascalcase.capitalization = pascal_case + +dotnet_naming_style._camelcase.required_prefix = _ +dotnet_naming_style._camelcase.required_suffix = +dotnet_naming_style._camelcase.word_separator = +dotnet_naming_style._camelcase.capitalization = camel_case + +dotnet_naming_style.camelcase.required_prefix = +dotnet_naming_style.camelcase.required_suffix = +dotnet_naming_style.camelcase.word_separator = +dotnet_naming_style.camelcase.capitalization = camel_case + +dotnet_naming_style.s_camelcase.required_prefix = s_ +dotnet_naming_style.s_camelcase.required_suffix = +dotnet_naming_style.s_camelcase.word_separator = +dotnet_naming_style.s_camelcase.capitalization = camel_case From 6f4799c919a094e60862cbf411b43f2a3aeccdc2 Mon Sep 17 00:00:00 2001 From: Shachar Langbeheim <nihohit@gmail.com> Date: Tue, 6 Feb 2024 18:30:02 +0200 Subject: [PATCH 03/10] C#: Fail CI on warnings. (#904) * C#: Fail CI on warnings. * Fix found warnings. --- .github/workflows/csharp.yml | 2 +- benchmarks/csharp/Program.cs | 6 +++--- benchmarks/install_and_test.sh | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/csharp.yml b/.github/workflows/csharp.yml index f12d8eda2c..af017c542c 100644 --- a/.github/workflows/csharp.yml +++ b/.github/workflows/csharp.yml @@ -54,7 +54,7 @@ jobs: - name: Test working-directory: ./csharp - run: dotnet test --framework net6.0 + run: dotnet test --framework net6.0 /warnaserror - uses: ./.github/workflows/test-benchmark with: diff --git a/benchmarks/csharp/Program.cs b/benchmarks/csharp/Program.cs index f1aaabd34e..839d3099cb 100644 --- a/benchmarks/csharp/Program.cs +++ b/benchmarks/csharp/Program.cs @@ -23,7 +23,7 @@ public class CommandLineOptions public int dataSize { get; set; } [Option('c', "concurrentTasks", Required = false, HelpText = "The number of concurrent operations to perform.", Default = new[] { 1, 10, 100, 1000 })] - public IEnumerable<int> concurrentTasks { get; set; } + public IEnumerable<int> concurrentTasks { get; set; } = Enumerable.Empty<int>(); [Option('l', "clients", Required = false, HelpText = "Which clients should run", Default = "all")] public string clientsToRun { get; set; } @@ -32,7 +32,7 @@ public class CommandLineOptions public string host { get; set; } [Option('C', "clientCount", Required = false, HelpText = "Number of clients to run concurrently", Default = new[] { 1 })] - public IEnumerable<int> clientCount { get; set; } + public IEnumerable<int> clientCount { get; set; } = Enumerable.Empty<int>(); [Option('t', "tls", HelpText = "Should benchmark a TLS server", Default = false)] public bool tls { get; set; } @@ -335,7 +335,7 @@ private static int number_of_iterations(int num_of_concurrent_tasks) public static async Task Main(string[] args) { - CommandLineOptions options = new (); + CommandLineOptions options = new(); Parser.Default .ParseArguments<CommandLineOptions>(args).WithParsed<CommandLineOptions>(parsed => { options = parsed; }); diff --git a/benchmarks/install_and_test.sh b/benchmarks/install_and_test.sh index 7d3d8429bf..2e0a80957e 100755 --- a/benchmarks/install_and_test.sh +++ b/benchmarks/install_and_test.sh @@ -67,7 +67,7 @@ function runNodeBenchmark(){ function runCSharpBenchmark(){ cd ${BENCH_FOLDER}/csharp dotnet clean - dotnet build --configuration Release + dotnet build --configuration Release /warnaserror dotnet run --framework net6.0 --configuration Release --resultsFile=../$1 --dataSize $2 --concurrentTasks $concurrentTasks --clients $chosenClients --host $host --clientCount $clientCount $tlsFlag $portFlag $minimalFlag } From 69a310826f87d81b52bd0308b87245da07554316 Mon Sep 17 00:00:00 2001 From: Guriy Samarin <guriy@amazon.com> Date: Tue, 6 Feb 2024 23:09:12 +0000 Subject: [PATCH 04/10] enable cicd linter and fix formatting --- .github/workflows/csharp.yml | 3 + benchmarks/csharp/Program.cs | 10 +- csharp/lib/AsyncClient.cs | 6 +- csharp/lib/Logger.cs | 170 ++++++++--------- csharp/lib/Message.cs | 255 +++++++++++++------------- csharp/lib/MessageContainer.cs | 124 ++++++------- csharp/lib/Properties/AssemblyInfo.cs | 14 +- csharp/tests/AsyncClientTests.cs | 10 +- csharp/tests/Usings.cs | 10 +- 9 files changed, 305 insertions(+), 297 deletions(-) diff --git a/.github/workflows/csharp.yml b/.github/workflows/csharp.yml index af017c542c..62f4a34809 100644 --- a/.github/workflows/csharp.yml +++ b/.github/workflows/csharp.yml @@ -52,6 +52,9 @@ jobs: - name: Start redis server run: redis-server & + - name: Format + run: dotnet format --verify-no-changes --verbosity diagnostic + - name: Test working-directory: ./csharp run: dotnet test --framework net6.0 /warnaserror diff --git a/benchmarks/csharp/Program.cs b/benchmarks/csharp/Program.cs index 839d3099cb..eea19e2006 100644 --- a/benchmarks/csharp/Program.cs +++ b/benchmarks/csharp/Program.cs @@ -5,9 +5,13 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Text.Json; -using Glide; + using CommandLine; + +using Glide; + using LinqStatistics; + using StackExchange.Redis; public static class MainClass @@ -258,7 +262,7 @@ public void Dispose() internal Func<string, Task<string?>> get; internal Func<string, string, Task> set; - private Action disposalFunction; + private readonly Action disposalFunction; } private async static Task<ClientWrapper[]> createClients(int clientCount, @@ -350,4 +354,4 @@ public static async Task Main(string[] args) print_results(options.resultsFile); } -} +} \ No newline at end of file diff --git a/csharp/lib/AsyncClient.cs b/csharp/lib/AsyncClient.cs index 459407704a..9657907b64 100644 --- a/csharp/lib/AsyncClient.cs +++ b/csharp/lib/AsyncClient.cs @@ -79,11 +79,11 @@ private void FailureCallback(ulong index) /// Held as a measure to prevent the delegate being garbage collected. These are delegated once /// and held in order to prevent the cost of marshalling on each function call. - private FailureAction failureCallbackDelegate; + private readonly FailureAction failureCallbackDelegate; /// Held as a measure to prevent the delegate being garbage collected. These are delegated once /// and held in order to prevent the cost of marshalling on each function call. - private StringAction successCallbackDelegate; + private readonly StringAction successCallbackDelegate; /// Raw pointer to the underlying native client. private IntPtr clientPointer; @@ -110,4 +110,4 @@ private void FailureCallback(ulong index) private static extern void CloseClientFfi(IntPtr client); #endregion -} +} \ No newline at end of file diff --git a/csharp/lib/Logger.cs b/csharp/lib/Logger.cs index 7edc16f16c..03fb47707d 100644 --- a/csharp/lib/Logger.cs +++ b/csharp/lib/Logger.cs @@ -1,85 +1,85 @@ -/** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 - */ - -using System.Runtime.InteropServices; -using System.Text; - - -namespace Glide; - -// TODO - use a bindings generator to create this enum. -public enum Level -{ - Error = 0, - Warn = 1, - Info = 2, - Debug = 3, - Trace = 4 -} - -/* -A class that allows logging which is consistent with logs from the internal rust core. -Only one instance of this class can exist at any given time. The logger can be set up in 2 ways - - 1. By calling init, which creates and modifies a new logger only if one doesn't exist. - 2. By calling setConfig, which replaces the existing logger, and means that new logs will not be saved with the logs that were sent before the call. -If no call to any of these function is received, the first log attempt will initialize a new logger with default level decided by rust core (normally - console, error). -*/ -public class Logger -{ - #region private fields - - private static Level? loggerLevel = null; - #endregion private fields - - #region internal methods - // Initialize a logger instance if none were initialized before - this method is meant to be used when there is no intention to replace an existing logger. - // The logger will filter all logs with a level lower than the given level, - // If given a fileName argument, will write the logs to files postfixed with fileName. If fileName isn't provided, the logs will be written to the console. - internal static void Init(Level? level, string? filename = null) - { - if (Logger.loggerLevel is null) - { - SetLoggerConfig(level, filename); - } - } - - // take the arguments from the user and provide to the core-logger (see ../logger-core) - // if the level is higher then the logger level (error is 0, warn 1, etc.) simply return without operation - // if a logger instance doesn't exist, create new one with default mode (decided by rust core, normally - level: error, target: console) - // logIdentifier arg is a string contain data that suppose to give the log a context and make it easier to find certain type of logs. - // when the log is connect to certain task the identifier should be the task id, when the log is not part of specific task the identifier should give a context to the log - for example, "create client". - internal static void Log(Level logLevel, string logIdentifier, string message) - { - if (Logger.loggerLevel is null) - { - SetLoggerConfig(logLevel); - } - if (!(logLevel <= Logger.loggerLevel)) return; - log(Convert.ToInt32(logLevel), Encoding.UTF8.GetBytes(logIdentifier), Encoding.UTF8.GetBytes(message)); - } - #endregion internal methods - - #region public methods - // config the logger instance - in fact - create new logger instance with the new args - // exist in addition to init for two main reason's: - // 1. if GLIDE dev want intentionally to change the logger instance configuration - // 2. external user want to set the logger and we don't want to return to him the logger itself, just config it - // the level argument is the level of the logs you want the system to provide (error logs, warn logs, etc.) - // the filename argument is optional - if provided the target of the logs will be the file mentioned, else will be the console - public static void SetLoggerConfig(Level? level, string? filename = null) - { - var buffer = filename is null ? null : Encoding.UTF8.GetBytes(filename); - Logger.loggerLevel = InitInternalLogger(Convert.ToInt32(level), buffer); - } - #endregion public methods - - #region FFI function declaration - [DllImport("libglide_rs", CallingConvention = CallingConvention.Cdecl, EntryPoint = "log")] - private static extern void log(Int32 logLevel, byte[] logIdentifier, byte[] message); - - [DllImport("libglide_rs", CallingConvention = CallingConvention.Cdecl, EntryPoint = "init")] - private static extern Level InitInternalLogger(Int32 level, byte[]? filename); - - #endregion -} +/** + * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + */ + +using System.Runtime.InteropServices; +using System.Text; + + +namespace Glide; + +// TODO - use a bindings generator to create this enum. +public enum Level +{ + Error = 0, + Warn = 1, + Info = 2, + Debug = 3, + Trace = 4 +} + +/* +A class that allows logging which is consistent with logs from the internal rust core. +Only one instance of this class can exist at any given time. The logger can be set up in 2 ways - + 1. By calling init, which creates and modifies a new logger only if one doesn't exist. + 2. By calling setConfig, which replaces the existing logger, and means that new logs will not be saved with the logs that were sent before the call. +If no call to any of these function is received, the first log attempt will initialize a new logger with default level decided by rust core (normally - console, error). +*/ +public class Logger +{ + #region private fields + + private static Level? loggerLevel = null; + #endregion private fields + + #region internal methods + // Initialize a logger instance if none were initialized before - this method is meant to be used when there is no intention to replace an existing logger. + // The logger will filter all logs with a level lower than the given level, + // If given a fileName argument, will write the logs to files postfixed with fileName. If fileName isn't provided, the logs will be written to the console. + internal static void Init(Level? level, string? filename = null) + { + if (Logger.loggerLevel is null) + { + SetLoggerConfig(level, filename); + } + } + + // take the arguments from the user and provide to the core-logger (see ../logger-core) + // if the level is higher then the logger level (error is 0, warn 1, etc.) simply return without operation + // if a logger instance doesn't exist, create new one with default mode (decided by rust core, normally - level: error, target: console) + // logIdentifier arg is a string contain data that suppose to give the log a context and make it easier to find certain type of logs. + // when the log is connect to certain task the identifier should be the task id, when the log is not part of specific task the identifier should give a context to the log - for example, "create client". + internal static void Log(Level logLevel, string logIdentifier, string message) + { + if (Logger.loggerLevel is null) + { + SetLoggerConfig(logLevel); + } + if (!(logLevel <= Logger.loggerLevel)) return; + log(Convert.ToInt32(logLevel), Encoding.UTF8.GetBytes(logIdentifier), Encoding.UTF8.GetBytes(message)); + } + #endregion internal methods + + #region public methods + // config the logger instance - in fact - create new logger instance with the new args + // exist in addition to init for two main reason's: + // 1. if GLIDE dev want intentionally to change the logger instance configuration + // 2. external user want to set the logger and we don't want to return to him the logger itself, just config it + // the level argument is the level of the logs you want the system to provide (error logs, warn logs, etc.) + // the filename argument is optional - if provided the target of the logs will be the file mentioned, else will be the console + public static void SetLoggerConfig(Level? level, string? filename = null) + { + var buffer = filename is null ? null : Encoding.UTF8.GetBytes(filename); + Logger.loggerLevel = InitInternalLogger(Convert.ToInt32(level), buffer); + } + #endregion public methods + + #region FFI function declaration + [DllImport("libglide_rs", CallingConvention = CallingConvention.Cdecl, EntryPoint = "log")] + private static extern void log(Int32 logLevel, byte[] logIdentifier, byte[] message); + + [DllImport("libglide_rs", CallingConvention = CallingConvention.Cdecl, EntryPoint = "init")] + private static extern Level InitInternalLogger(Int32 level, byte[]? filename); + + #endregion +} \ No newline at end of file diff --git a/csharp/lib/Message.cs b/csharp/lib/Message.cs index daa11602c6..6c5f4203f3 100644 --- a/csharp/lib/Message.cs +++ b/csharp/lib/Message.cs @@ -1,127 +1,128 @@ -/** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 - */ - -using System.Diagnostics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using Glide; - -/// Reusable source of ValueTask. This object can be allocated once and then reused -/// to create multiple asynchronous operations, as long as each call to CreateTask -/// is awaited to completion before the next call begins. -internal class Message<T> : INotifyCompletion -{ - /// This is the index of the message in an external array, that allows the user to - /// know how to find the message and set its result. - public int Index { get; } - - /// The pointer to the unmanaged memory that contains the operation's key. - public IntPtr KeyPtr { get; private set; } - - /// The pointer to the unmanaged memory that contains the operation's key. - public IntPtr ValuePtr { get; private set; } - private MessageContainer<T> container; - - public Message(int index, MessageContainer<T> container) - { - Index = index; - continuation = () => { }; - this.container = container; - } - - private Action? continuation; - const int COMPLETION_STAGE_STARTED = 0; - const int COMPLETION_STAGE_NEXT_SHOULD_EXECUTE_CONTINUATION = 1; - const int COMPLETION_STAGE_CONTINUATION_EXECUTED = 2; - private int completionState; - private T? result; - private Exception? exception; - - /// Triggers a succesful completion of the task returned from the latest call - /// to CreateTask. - public void SetResult(T? result) - { - this.result = result; - FinishSet(); - } - - /// Triggers a failure completion of the task returned from the latest call to - /// CreateTask. - public void SetException(Exception exc) - { - this.exception = exc; - FinishSet(); - } - - private void FinishSet() - { - FreePointers(); - - CheckRaceAndCallContinuation(); - } - - private void CheckRaceAndCallContinuation() - { - if (Interlocked.CompareExchange(ref this.completionState, COMPLETION_STAGE_NEXT_SHOULD_EXECUTE_CONTINUATION, COMPLETION_STAGE_STARTED) == COMPLETION_STAGE_NEXT_SHOULD_EXECUTE_CONTINUATION) - { - Debug.Assert(this.continuation != null); - this.completionState = COMPLETION_STAGE_CONTINUATION_EXECUTED; - try - { - continuation(); - } - finally - { - this.container.ReturnFreeMessage(this); - } - } - } - - public Message<T> GetAwaiter() => this; - - /// This returns a task that will complete once SetException / SetResult are called, - /// and ensures that the internal state of the message is set-up before the task is created, - /// and cleaned once it is complete. - public void StartTask(string? key, string? value, object client) - { - continuation = null; - this.completionState = COMPLETION_STAGE_STARTED; - this.result = default(T); - this.exception = null; - this.client = client; - this.KeyPtr = key is null ? IntPtr.Zero : Marshal.StringToHGlobalAnsi(key); - this.ValuePtr = value is null ? IntPtr.Zero : Marshal.StringToHGlobalAnsi(value); - } - - // This function isn't thread-safe. Access to it should be from a single thread, and only once per operation. - // For the sake of performance, this responsibility is on the caller, and the function doesn't contain any safety measures. - private void FreePointers() - { - if (KeyPtr != IntPtr.Zero) - { - Marshal.FreeHGlobal(KeyPtr); - KeyPtr = IntPtr.Zero; - } - if (ValuePtr != IntPtr.Zero) - { - Marshal.FreeHGlobal(ValuePtr); - ValuePtr = IntPtr.Zero; - } - client = null; - } - - // Holding the client prevents it from being CG'd until all operations complete. - private object? client; - - - public void OnCompleted(Action continuation) - { - this.continuation = continuation; - CheckRaceAndCallContinuation(); - } - - public bool IsCompleted => completionState == COMPLETION_STAGE_CONTINUATION_EXECUTED; - - public T? GetResult() => this.exception is null ? this.result : throw this.exception; -} +/** + * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + */ + +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using Glide; + +/// Reusable source of ValueTask. This object can be allocated once and then reused +/// to create multiple asynchronous operations, as long as each call to CreateTask +/// is awaited to completion before the next call begins. +internal class Message<T> : INotifyCompletion +{ + /// This is the index of the message in an external array, that allows the user to + /// know how to find the message and set its result. + public int Index { get; } + + /// The pointer to the unmanaged memory that contains the operation's key. + public IntPtr KeyPtr { get; private set; } + + /// The pointer to the unmanaged memory that contains the operation's key. + public IntPtr ValuePtr { get; private set; } + private readonly MessageContainer<T> container; + + public Message(int index, MessageContainer<T> container) + { + Index = index; + continuation = () => { }; + this.container = container; + } + + private Action? continuation; + const int COMPLETION_STAGE_STARTED = 0; + const int COMPLETION_STAGE_NEXT_SHOULD_EXECUTE_CONTINUATION = 1; + const int COMPLETION_STAGE_CONTINUATION_EXECUTED = 2; + private int completionState; + private T? result; + private Exception? exception; + + /// Triggers a succesful completion of the task returned from the latest call + /// to CreateTask. + public void SetResult(T? result) + { + this.result = result; + FinishSet(); + } + + /// Triggers a failure completion of the task returned from the latest call to + /// CreateTask. + public void SetException(Exception exc) + { + this.exception = exc; + FinishSet(); + } + + private void FinishSet() + { + FreePointers(); + + CheckRaceAndCallContinuation(); + } + + private void CheckRaceAndCallContinuation() + { + if (Interlocked.CompareExchange(ref this.completionState, COMPLETION_STAGE_NEXT_SHOULD_EXECUTE_CONTINUATION, COMPLETION_STAGE_STARTED) == COMPLETION_STAGE_NEXT_SHOULD_EXECUTE_CONTINUATION) + { + Debug.Assert(this.continuation != null); + this.completionState = COMPLETION_STAGE_CONTINUATION_EXECUTED; + try + { + continuation(); + } + finally + { + this.container.ReturnFreeMessage(this); + } + } + } + + public Message<T> GetAwaiter() => this; + + /// This returns a task that will complete once SetException / SetResult are called, + /// and ensures that the internal state of the message is set-up before the task is created, + /// and cleaned once it is complete. + public void StartTask(string? key, string? value, object client) + { + continuation = null; + this.completionState = COMPLETION_STAGE_STARTED; + this.result = default(T); + this.exception = null; + this.client = client; + this.KeyPtr = key is null ? IntPtr.Zero : Marshal.StringToHGlobalAnsi(key); + this.ValuePtr = value is null ? IntPtr.Zero : Marshal.StringToHGlobalAnsi(value); + } + + // This function isn't thread-safe. Access to it should be from a single thread, and only once per operation. + // For the sake of performance, this responsibility is on the caller, and the function doesn't contain any safety measures. + private void FreePointers() + { + if (KeyPtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(KeyPtr); + KeyPtr = IntPtr.Zero; + } + if (ValuePtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(ValuePtr); + ValuePtr = IntPtr.Zero; + } + client = null; + } + + // Holding the client prevents it from being CG'd until all operations complete. + private object? client; + + + public void OnCompleted(Action continuation) + { + this.continuation = continuation; + CheckRaceAndCallContinuation(); + } + + public bool IsCompleted => completionState == COMPLETION_STAGE_CONTINUATION_EXECUTED; + + public T? GetResult() => this.exception is null ? this.result : throw this.exception; +} \ No newline at end of file diff --git a/csharp/lib/MessageContainer.cs b/csharp/lib/MessageContainer.cs index 5999702363..9c05ccb556 100644 --- a/csharp/lib/MessageContainer.cs +++ b/csharp/lib/MessageContainer.cs @@ -1,62 +1,62 @@ -/** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 - */ - -using System.Collections.Concurrent; - -namespace Glide; - - -internal class MessageContainer<T> -{ - internal Message<T> GetMessage(int index) => messages[index]; - - internal Message<T> GetMessageForCall(string? key, string? value) - { - var message = GetFreeMessage(); - message.StartTask(key, value, this); - return message; - } - - private Message<T> GetFreeMessage() - { - if (!availableMessages.TryDequeue(out var message)) - { - lock (messages) - { - var index = messages.Count; - message = new Message<T>(index, this); - messages.Add(message); - } - } - return message; - } - - public void ReturnFreeMessage(Message<T> message) => availableMessages.Enqueue(message); - - internal void DisposeWithError(Exception? error) - { - lock (messages) - { - foreach (var message in messages.Where(message => !message.IsCompleted)) - { - try - { - message.SetException(new TaskCanceledException("Client closed", error)); - } - catch (Exception) { } - } - messages.Clear(); - } - availableMessages.Clear(); - } - - /// This list allows us random-access to the message in each index, - /// which means that once we receive a callback with an index, we can - /// find the message to resolve in constant time. - private List<Message<T>> messages = new(); - - /// This queue contains the messages that were created and are currently unused by any task, - /// so they can be reused y new tasks instead of allocating new messages. - private ConcurrentQueue<Message<T>> availableMessages = new(); -} +/** + * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + */ + +using System.Collections.Concurrent; + +namespace Glide; + + +internal class MessageContainer<T> +{ + internal Message<T> GetMessage(int index) => messages[index]; + + internal Message<T> GetMessageForCall(string? key, string? value) + { + var message = GetFreeMessage(); + message.StartTask(key, value, this); + return message; + } + + private Message<T> GetFreeMessage() + { + if (!availableMessages.TryDequeue(out var message)) + { + lock (messages) + { + var index = messages.Count; + message = new Message<T>(index, this); + messages.Add(message); + } + } + return message; + } + + public void ReturnFreeMessage(Message<T> message) => availableMessages.Enqueue(message); + + internal void DisposeWithError(Exception? error) + { + lock (messages) + { + foreach (var message in messages.Where(message => !message.IsCompleted)) + { + try + { + message.SetException(new TaskCanceledException("Client closed", error)); + } + catch (Exception) { } + } + messages.Clear(); + } + availableMessages.Clear(); + } + + /// This list allows us random-access to the message in each index, + /// which means that once we receive a callback with an index, we can + /// find the message to resolve in constant time. + private readonly List<Message<T>> messages = new(); + + /// This queue contains the messages that were created and are currently unused by any task, + /// so they can be reused y new tasks instead of allocating new messages. + private readonly ConcurrentQueue<Message<T>> availableMessages = new(); +} diff --git a/csharp/lib/Properties/AssemblyInfo.cs b/csharp/lib/Properties/AssemblyInfo.cs index a6412c5b2e..876c013e0a 100644 --- a/csharp/lib/Properties/AssemblyInfo.cs +++ b/csharp/lib/Properties/AssemblyInfo.cs @@ -1,7 +1,7 @@ -/** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 - */ - -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("tests")] +/** + * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + */ + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("tests")] \ No newline at end of file diff --git a/csharp/tests/AsyncClientTests.cs b/csharp/tests/AsyncClientTests.cs index dad14c953f..a322e37514 100644 --- a/csharp/tests/AsyncClientTests.cs +++ b/csharp/tests/AsyncClientTests.cs @@ -1,7 +1,7 @@ -/** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 - */ - +/** + * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + */ + namespace tests; using Glide; @@ -119,4 +119,4 @@ public void ConcurrentOperationsWork() Task.WaitAll(operations.ToArray()); } } -} +} \ No newline at end of file diff --git a/csharp/tests/Usings.cs b/csharp/tests/Usings.cs index 6638c1219c..08ffe13f66 100644 --- a/csharp/tests/Usings.cs +++ b/csharp/tests/Usings.cs @@ -1,5 +1,5 @@ -/** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 - */ - -global using NUnit.Framework; +/** + * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + */ + +global using NUnit.Framework; \ No newline at end of file From 927f3519c4ee6702e063c94ad0093097e23bb99e Mon Sep 17 00:00:00 2001 From: Guriy Samarin <guriy@amazon.com> Date: Tue, 6 Feb 2024 23:12:52 +0000 Subject: [PATCH 05/10] fix working dir --- .github/workflows/csharp.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/csharp.yml b/.github/workflows/csharp.yml index 62f4a34809..3c838c38c5 100644 --- a/.github/workflows/csharp.yml +++ b/.github/workflows/csharp.yml @@ -53,6 +53,7 @@ jobs: run: redis-server & - name: Format + working-directory: ./csharp run: dotnet format --verify-no-changes --verbosity diagnostic - name: Test From 7f27c36c70501da4c2897cab2aee33969ddeba09 Mon Sep 17 00:00:00 2001 From: Guriy Samarin <guriy@amazon.com> Date: Tue, 6 Feb 2024 23:19:06 +0000 Subject: [PATCH 06/10] fix MessageContainer.cs --- csharp/lib/MessageContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csharp/lib/MessageContainer.cs b/csharp/lib/MessageContainer.cs index 9c05ccb556..7de0947650 100644 --- a/csharp/lib/MessageContainer.cs +++ b/csharp/lib/MessageContainer.cs @@ -59,4 +59,4 @@ internal void DisposeWithError(Exception? error) /// This queue contains the messages that were created and are currently unused by any task, /// so they can be reused y new tasks instead of allocating new messages. private readonly ConcurrentQueue<Message<T>> availableMessages = new(); -} +} \ No newline at end of file From 822b15f1acc7117f78387eb7bf98925242a02d59 Mon Sep 17 00:00:00 2001 From: Guriy Samarin <guriy@amazon.com> Date: Tue, 6 Feb 2024 23:27:50 +0000 Subject: [PATCH 07/10] Non-nullable property must contain a non-null value when exiting constructor --- benchmarks/csharp/Program.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/benchmarks/csharp/Program.cs b/benchmarks/csharp/Program.cs index eea19e2006..519e1ea887 100644 --- a/benchmarks/csharp/Program.cs +++ b/benchmarks/csharp/Program.cs @@ -21,7 +21,7 @@ private enum ChosenAction { GET_NON_EXISTING, GET_EXISTING, SET }; public class CommandLineOptions { [Option('r', "resultsFile", Required = false, HelpText = "Set the file to which the JSON results are written.", Default = "../results/csharp-results.json")] - public string resultsFile { get; set; } + public string resultsFile { get; set; } = string.Empty; [Option('d', "dataSize", Required = false, HelpText = "The size of the sent data in bytes.", Default = 100)] public int dataSize { get; set; } @@ -30,10 +30,10 @@ public class CommandLineOptions public IEnumerable<int> concurrentTasks { get; set; } = Enumerable.Empty<int>(); [Option('l', "clients", Required = false, HelpText = "Which clients should run", Default = "all")] - public string clientsToRun { get; set; } + public string clientsToRun { get; set; } = string.Empty; [Option('h', "host", Required = false, HelpText = "What host to target", Default = "localhost")] - public string host { get; set; } + public string host { get; set; } = string.Empty; [Option('C', "clientCount", Required = false, HelpText = "Number of clients to run concurrently", Default = new[] { 1 })] public IEnumerable<int> clientCount { get; set; } = Enumerable.Empty<int>(); @@ -354,4 +354,4 @@ public static async Task Main(string[] args) print_results(options.resultsFile); } -} \ No newline at end of file +} From bfd90c9e5a800e7d77a0d75ab0ca7a45b731f025 Mon Sep 17 00:00:00 2001 From: Guriy Samarin <guriy@amazon.com> Date: Tue, 6 Feb 2024 23:30:22 +0000 Subject: [PATCH 08/10] fix format --- benchmarks/csharp/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/csharp/Program.cs b/benchmarks/csharp/Program.cs index 519e1ea887..d753d09357 100644 --- a/benchmarks/csharp/Program.cs +++ b/benchmarks/csharp/Program.cs @@ -354,4 +354,4 @@ public static async Task Main(string[] args) print_results(options.resultsFile); } -} +} \ No newline at end of file From f082274b608629f934fa37543f66083b7ecebecf Mon Sep 17 00:00:00 2001 From: Guriy Samarin <guriy@amazon.com> Date: Tue, 6 Feb 2024 23:39:30 +0000 Subject: [PATCH 09/10] correct formatting --- .editorconfig | 4 +- benchmarks/csharp/Program.cs | 714 +++++++++++++------------- csharp/lib/AsyncClient.cs | 226 ++++---- csharp/lib/Logger.cs | 170 +++--- csharp/lib/Message.cs | 256 ++++----- csharp/lib/MessageContainer.cs | 124 ++--- csharp/lib/Properties/AssemblyInfo.cs | 14 +- csharp/tests/AsyncClientTests.cs | 244 ++++----- csharp/tests/Usings.cs | 10 +- 9 files changed, 881 insertions(+), 881 deletions(-) diff --git a/.editorconfig b/.editorconfig index 0002f363fa..824238f9c1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -20,8 +20,8 @@ indent_size = 4 tab_width = 4 # New line preferences -end_of_line = crlf -insert_final_newline = false +end_of_line = lf +insert_final_newline = true #### .NET Coding Conventions #### [*.{cs,vb}] diff --git a/benchmarks/csharp/Program.cs b/benchmarks/csharp/Program.cs index d753d09357..04d426a156 100644 --- a/benchmarks/csharp/Program.cs +++ b/benchmarks/csharp/Program.cs @@ -1,357 +1,357 @@ -/** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 - */ - -using System.Collections.Concurrent; -using System.Diagnostics; -using System.Text.Json; - -using CommandLine; - -using Glide; - -using LinqStatistics; - -using StackExchange.Redis; - -public static class MainClass -{ - private enum ChosenAction { GET_NON_EXISTING, GET_EXISTING, SET }; - - public class CommandLineOptions - { - [Option('r', "resultsFile", Required = false, HelpText = "Set the file to which the JSON results are written.", Default = "../results/csharp-results.json")] - public string resultsFile { get; set; } = string.Empty; - - [Option('d', "dataSize", Required = false, HelpText = "The size of the sent data in bytes.", Default = 100)] - public int dataSize { get; set; } - - [Option('c', "concurrentTasks", Required = false, HelpText = "The number of concurrent operations to perform.", Default = new[] { 1, 10, 100, 1000 })] - public IEnumerable<int> concurrentTasks { get; set; } = Enumerable.Empty<int>(); - - [Option('l', "clients", Required = false, HelpText = "Which clients should run", Default = "all")] - public string clientsToRun { get; set; } = string.Empty; - - [Option('h', "host", Required = false, HelpText = "What host to target", Default = "localhost")] - public string host { get; set; } = string.Empty; - - [Option('C', "clientCount", Required = false, HelpText = "Number of clients to run concurrently", Default = new[] { 1 })] - public IEnumerable<int> clientCount { get; set; } = Enumerable.Empty<int>(); - - [Option('t', "tls", HelpText = "Should benchmark a TLS server", Default = false)] - public bool tls { get; set; } - - - [Option('m', "minimal", HelpText = "Should use a minimal number of actions", Default = false)] - public bool minimal { get; set; } - } - - private const int PORT = 6379; - private static string getAddress(string host) - { - return $"{host}:{PORT}"; - } - - private static string getAddressForStackExchangeRedis(string host, bool useTLS) - { - return $"{getAddress(host)},ssl={useTLS}"; - } - - private static string getAddressWithRedisPrefix(string host, bool useTLS) - { - var protocol = useTLS ? "rediss" : "redis"; - return $"{protocol}://{getAddress(host)}"; - } - private const double PROB_GET = 0.8; - - private const double PROB_GET_EXISTING_KEY = 0.8; - private const int SIZE_GET_KEYSPACE = 3750000; // 3.75 million - private const int SIZE_SET_KEYSPACE = 3000000; // 3 million - - private static readonly Random randomizer = new(); - private static long started_tasks_counter = 0; - private static readonly List<Dictionary<string, object>> bench_json_results = new(); - - private static string generate_value(int size) - { - return new string('0', size); - } - - private static string generate_key_set() - { - return (randomizer.Next(SIZE_SET_KEYSPACE) + 1).ToString(); - } - private static string generate_key_get() - { - return (randomizer.Next(SIZE_SET_KEYSPACE, SIZE_GET_KEYSPACE) + 1).ToString(); - } - - private static ChosenAction choose_action() - { - if (randomizer.NextDouble() > PROB_GET) - { - return ChosenAction.SET; - } - if (randomizer.NextDouble() > PROB_GET_EXISTING_KEY) - { - return ChosenAction.GET_NON_EXISTING; - } - return ChosenAction.GET_EXISTING; - } - - /// copied from https://stackoverflow.com/questions/8137391/percentile-calculation - private static double Percentile(double[] sequence, double excelPercentile) - { - Array.Sort(sequence); - int N = sequence.Length; - double n = (N - 1) * excelPercentile + 1; - if (n == 1d) return sequence[0]; - else if (n == N) return sequence[N - 1]; - else - { - int k = (int)n; - double d = n - k; - return sequence[k - 1] + d * (sequence[k] - sequence[k - 1]); - } - } - - private static double calculate_latency(IEnumerable<double> latency_list, double percentile_point) - { - return Math.Round(Percentile(latency_list.ToArray(), percentile_point), 2); - } - - private static void print_results(string resultsFile) - { - using FileStream createStream = File.Create(resultsFile); - JsonSerializer.Serialize(createStream, bench_json_results); - } - - private static async Task redis_benchmark( - ClientWrapper[] clients, - long total_commands, - string data, - Dictionary<ChosenAction, ConcurrentBag<double>> action_latencies) - { - var stopwatch = new Stopwatch(); - do - { - Interlocked.Increment(ref started_tasks_counter); - var index = (int)(started_tasks_counter % clients.Length); - var client = clients[index]; - var action = choose_action(); - stopwatch.Start(); - switch (action) - { - case ChosenAction.GET_EXISTING: - await client.get(generate_key_set()); - break; - case ChosenAction.GET_NON_EXISTING: - await client.get(generate_key_get()); - break; - case ChosenAction.SET: - await client.set(generate_key_set(), data); - break; - } - stopwatch.Stop(); - var latency_list = action_latencies[action]; - latency_list.Add(((double)stopwatch.ElapsedMilliseconds) / 1000); - } while (started_tasks_counter < total_commands); - } - - private static async Task<long> create_bench_tasks( - ClientWrapper[] clients, - int total_commands, - string data, - int num_of_concurrent_tasks, - Dictionary<ChosenAction, ConcurrentBag<double>> action_latencies - ) - { - started_tasks_counter = 0; - var stopwatch = Stopwatch.StartNew(); - var running_tasks = new List<Task>(); - for (var i = 0; i < num_of_concurrent_tasks; i++) - { - running_tasks.Add( - redis_benchmark(clients, total_commands, data, action_latencies) - ); - } - await Task.WhenAll(running_tasks); - stopwatch.Stop(); - return stopwatch.ElapsedMilliseconds; - } - - private static Dictionary<string, object> latency_results( - string prefix, - ConcurrentBag<double> latencies - ) - { - return new Dictionary<string, object> - { - {prefix + "_p50_latency", calculate_latency(latencies, 0.5)}, - {prefix + "_p90_latency", calculate_latency(latencies, 0.9)}, - {prefix + "_p99_latency", calculate_latency(latencies, 0.99)}, - {prefix + "_average_latency", Math.Round(latencies.Average(), 3)}, - {prefix + "_std_dev", latencies.StandardDeviation()}, - }; - } - - private static async Task run_clients( - ClientWrapper[] clients, - string client_name, - int total_commands, - int data_size, - int num_of_concurrent_tasks - ) - { - Console.WriteLine($"Starting {client_name} data size: {data_size} concurrency: {num_of_concurrent_tasks} client count: {clients.Length} {DateTime.UtcNow.ToString("HH:mm:ss")}"); - var action_latencies = new Dictionary<ChosenAction, ConcurrentBag<double>>() { - {ChosenAction.GET_NON_EXISTING, new()}, - {ChosenAction.GET_EXISTING, new()}, - {ChosenAction.SET, new()}, - }; - var data = generate_value(data_size); - var elapsed_milliseconds = await create_bench_tasks( - clients, - total_commands, - data, - num_of_concurrent_tasks, - action_latencies - ); - var tps = Math.Round((double)started_tasks_counter / ((double)elapsed_milliseconds / 1000)); - - var get_non_existing_latencies = action_latencies[ChosenAction.GET_NON_EXISTING]; - var get_non_existing_latency_results = latency_results("get_non_existing", get_non_existing_latencies); - - var get_existing_latencies = action_latencies[ChosenAction.GET_EXISTING]; - var get_existing_latency_results = latency_results("get_existing", get_existing_latencies); - - var set_latencies = action_latencies[ChosenAction.SET]; - var set_latency_results = latency_results("set", set_latencies); - - var result = new Dictionary<string, object> - { - {"client", client_name}, - {"num_of_tasks", num_of_concurrent_tasks}, - {"data_size", data_size}, - {"tps", tps}, - {"client_count", clients.Length}, - {"is_cluster", "false"} - }; - result = result - .Concat(get_existing_latency_results) - .Concat(get_non_existing_latency_results) - .Concat(set_latency_results) - .ToDictionary(pair => pair.Key, pair => pair.Value); - bench_json_results.Add(result); - } - - private class ClientWrapper : IDisposable - { - internal ClientWrapper(Func<string, Task<string?>> get, Func<string, string, Task> set, Action disposalFunction) - { - this.get = get; - this.set = set; - this.disposalFunction = disposalFunction; - } - - public void Dispose() - { - this.disposalFunction(); - } - - internal Func<string, Task<string?>> get; - internal Func<string, string, Task> set; - - private readonly Action disposalFunction; - } - - private async static Task<ClientWrapper[]> createClients(int clientCount, - Func<Task<(Func<string, Task<string?>>, - Func<string, string, Task>, - Action)>> clientCreation) - { - var tasks = Enumerable.Range(0, clientCount).Select(async (_) => - { - var tuple = await clientCreation(); - return new ClientWrapper(tuple.Item1, tuple.Item2, tuple.Item3); - }); - return await Task.WhenAll(tasks); - } - - private static async Task run_with_parameters(int total_commands, - int data_size, - int num_of_concurrent_tasks, - string clientsToRun, - string host, - int clientCount, - bool useTLS) - { - if (clientsToRun == "all" || clientsToRun == "glide") - { - var clients = await createClients(clientCount, () => - { - var glide_client = new AsyncClient(host, PORT, useTLS); - return Task.FromResult<(Func<string, Task<string?>>, Func<string, string, Task>, Action)>( - (async (key) => await glide_client.GetAsync(key), - async (key, value) => await glide_client.SetAsync(key, value), - () => glide_client.Dispose())); - }); - - await run_clients( - clients, - "glide", - total_commands, - data_size, - num_of_concurrent_tasks - ); - } - - if (clientsToRun == "all") - { - var clients = await createClients(clientCount, () => - { - var connection = ConnectionMultiplexer.Connect(getAddressForStackExchangeRedis(host, useTLS)); - var db = connection.GetDatabase(); - return Task.FromResult<(Func<string, Task<string?>>, Func<string, string, Task>, Action)>( - (async (key) => await db.StringGetAsync(key), - async (key, value) => await db.StringSetAsync(key, value), - () => connection.Dispose())); - }); - await run_clients( - clients, - "StackExchange.Redis", - total_commands, - data_size, - num_of_concurrent_tasks - ); - - foreach (var client in clients) - { - client.Dispose(); - } - } - } - - private static int number_of_iterations(int num_of_concurrent_tasks) - { - return Math.Min(Math.Max(100000, num_of_concurrent_tasks * 10000), 10000000); - } - - public static async Task Main(string[] args) - { - CommandLineOptions options = new(); - Parser.Default - .ParseArguments<CommandLineOptions>(args).WithParsed<CommandLineOptions>(parsed => { options = parsed; }); - - Logger.SetLoggerConfig(Level.Info, Path.GetFileNameWithoutExtension(options.resultsFile)); - var product = options.concurrentTasks.SelectMany(concurrentTasks => - options.clientCount.Select(clientCount => (concurrentTasks: concurrentTasks, dataSize: options.dataSize, clientCount: clientCount))).Where(tuple => tuple.concurrentTasks >= tuple.clientCount); - foreach (var (concurrentTasks, dataSize, clientCount) in product) - { - var iterations = options.minimal ? 1000 : number_of_iterations(concurrentTasks); - await run_with_parameters(iterations, dataSize, concurrentTasks, options.clientsToRun, options.host, clientCount, options.tls); - } - - print_results(options.resultsFile); - } -} \ No newline at end of file +/** + * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + */ + +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Text.Json; + +using CommandLine; + +using Glide; + +using LinqStatistics; + +using StackExchange.Redis; + +public static class MainClass +{ + private enum ChosenAction { GET_NON_EXISTING, GET_EXISTING, SET }; + + public class CommandLineOptions + { + [Option('r', "resultsFile", Required = false, HelpText = "Set the file to which the JSON results are written.", Default = "../results/csharp-results.json")] + public string resultsFile { get; set; } = string.Empty; + + [Option('d', "dataSize", Required = false, HelpText = "The size of the sent data in bytes.", Default = 100)] + public int dataSize { get; set; } + + [Option('c', "concurrentTasks", Required = false, HelpText = "The number of concurrent operations to perform.", Default = new[] { 1, 10, 100, 1000 })] + public IEnumerable<int> concurrentTasks { get; set; } = Enumerable.Empty<int>(); + + [Option('l', "clients", Required = false, HelpText = "Which clients should run", Default = "all")] + public string clientsToRun { get; set; } = string.Empty; + + [Option('h', "host", Required = false, HelpText = "What host to target", Default = "localhost")] + public string host { get; set; } = string.Empty; + + [Option('C', "clientCount", Required = false, HelpText = "Number of clients to run concurrently", Default = new[] { 1 })] + public IEnumerable<int> clientCount { get; set; } = Enumerable.Empty<int>(); + + [Option('t', "tls", HelpText = "Should benchmark a TLS server", Default = false)] + public bool tls { get; set; } + + + [Option('m', "minimal", HelpText = "Should use a minimal number of actions", Default = false)] + public bool minimal { get; set; } + } + + private const int PORT = 6379; + private static string getAddress(string host) + { + return $"{host}:{PORT}"; + } + + private static string getAddressForStackExchangeRedis(string host, bool useTLS) + { + return $"{getAddress(host)},ssl={useTLS}"; + } + + private static string getAddressWithRedisPrefix(string host, bool useTLS) + { + var protocol = useTLS ? "rediss" : "redis"; + return $"{protocol}://{getAddress(host)}"; + } + private const double PROB_GET = 0.8; + + private const double PROB_GET_EXISTING_KEY = 0.8; + private const int SIZE_GET_KEYSPACE = 3750000; // 3.75 million + private const int SIZE_SET_KEYSPACE = 3000000; // 3 million + + private static readonly Random randomizer = new(); + private static long started_tasks_counter = 0; + private static readonly List<Dictionary<string, object>> bench_json_results = new(); + + private static string generate_value(int size) + { + return new string('0', size); + } + + private static string generate_key_set() + { + return (randomizer.Next(SIZE_SET_KEYSPACE) + 1).ToString(); + } + private static string generate_key_get() + { + return (randomizer.Next(SIZE_SET_KEYSPACE, SIZE_GET_KEYSPACE) + 1).ToString(); + } + + private static ChosenAction choose_action() + { + if (randomizer.NextDouble() > PROB_GET) + { + return ChosenAction.SET; + } + if (randomizer.NextDouble() > PROB_GET_EXISTING_KEY) + { + return ChosenAction.GET_NON_EXISTING; + } + return ChosenAction.GET_EXISTING; + } + + /// copied from https://stackoverflow.com/questions/8137391/percentile-calculation + private static double Percentile(double[] sequence, double excelPercentile) + { + Array.Sort(sequence); + int N = sequence.Length; + double n = (N - 1) * excelPercentile + 1; + if (n == 1d) return sequence[0]; + else if (n == N) return sequence[N - 1]; + else + { + int k = (int)n; + double d = n - k; + return sequence[k - 1] + d * (sequence[k] - sequence[k - 1]); + } + } + + private static double calculate_latency(IEnumerable<double> latency_list, double percentile_point) + { + return Math.Round(Percentile(latency_list.ToArray(), percentile_point), 2); + } + + private static void print_results(string resultsFile) + { + using FileStream createStream = File.Create(resultsFile); + JsonSerializer.Serialize(createStream, bench_json_results); + } + + private static async Task redis_benchmark( + ClientWrapper[] clients, + long total_commands, + string data, + Dictionary<ChosenAction, ConcurrentBag<double>> action_latencies) + { + var stopwatch = new Stopwatch(); + do + { + Interlocked.Increment(ref started_tasks_counter); + var index = (int)(started_tasks_counter % clients.Length); + var client = clients[index]; + var action = choose_action(); + stopwatch.Start(); + switch (action) + { + case ChosenAction.GET_EXISTING: + await client.get(generate_key_set()); + break; + case ChosenAction.GET_NON_EXISTING: + await client.get(generate_key_get()); + break; + case ChosenAction.SET: + await client.set(generate_key_set(), data); + break; + } + stopwatch.Stop(); + var latency_list = action_latencies[action]; + latency_list.Add(((double)stopwatch.ElapsedMilliseconds) / 1000); + } while (started_tasks_counter < total_commands); + } + + private static async Task<long> create_bench_tasks( + ClientWrapper[] clients, + int total_commands, + string data, + int num_of_concurrent_tasks, + Dictionary<ChosenAction, ConcurrentBag<double>> action_latencies + ) + { + started_tasks_counter = 0; + var stopwatch = Stopwatch.StartNew(); + var running_tasks = new List<Task>(); + for (var i = 0; i < num_of_concurrent_tasks; i++) + { + running_tasks.Add( + redis_benchmark(clients, total_commands, data, action_latencies) + ); + } + await Task.WhenAll(running_tasks); + stopwatch.Stop(); + return stopwatch.ElapsedMilliseconds; + } + + private static Dictionary<string, object> latency_results( + string prefix, + ConcurrentBag<double> latencies + ) + { + return new Dictionary<string, object> + { + {prefix + "_p50_latency", calculate_latency(latencies, 0.5)}, + {prefix + "_p90_latency", calculate_latency(latencies, 0.9)}, + {prefix + "_p99_latency", calculate_latency(latencies, 0.99)}, + {prefix + "_average_latency", Math.Round(latencies.Average(), 3)}, + {prefix + "_std_dev", latencies.StandardDeviation()}, + }; + } + + private static async Task run_clients( + ClientWrapper[] clients, + string client_name, + int total_commands, + int data_size, + int num_of_concurrent_tasks + ) + { + Console.WriteLine($"Starting {client_name} data size: {data_size} concurrency: {num_of_concurrent_tasks} client count: {clients.Length} {DateTime.UtcNow.ToString("HH:mm:ss")}"); + var action_latencies = new Dictionary<ChosenAction, ConcurrentBag<double>>() { + {ChosenAction.GET_NON_EXISTING, new()}, + {ChosenAction.GET_EXISTING, new()}, + {ChosenAction.SET, new()}, + }; + var data = generate_value(data_size); + var elapsed_milliseconds = await create_bench_tasks( + clients, + total_commands, + data, + num_of_concurrent_tasks, + action_latencies + ); + var tps = Math.Round((double)started_tasks_counter / ((double)elapsed_milliseconds / 1000)); + + var get_non_existing_latencies = action_latencies[ChosenAction.GET_NON_EXISTING]; + var get_non_existing_latency_results = latency_results("get_non_existing", get_non_existing_latencies); + + var get_existing_latencies = action_latencies[ChosenAction.GET_EXISTING]; + var get_existing_latency_results = latency_results("get_existing", get_existing_latencies); + + var set_latencies = action_latencies[ChosenAction.SET]; + var set_latency_results = latency_results("set", set_latencies); + + var result = new Dictionary<string, object> + { + {"client", client_name}, + {"num_of_tasks", num_of_concurrent_tasks}, + {"data_size", data_size}, + {"tps", tps}, + {"client_count", clients.Length}, + {"is_cluster", "false"} + }; + result = result + .Concat(get_existing_latency_results) + .Concat(get_non_existing_latency_results) + .Concat(set_latency_results) + .ToDictionary(pair => pair.Key, pair => pair.Value); + bench_json_results.Add(result); + } + + private class ClientWrapper : IDisposable + { + internal ClientWrapper(Func<string, Task<string?>> get, Func<string, string, Task> set, Action disposalFunction) + { + this.get = get; + this.set = set; + this.disposalFunction = disposalFunction; + } + + public void Dispose() + { + this.disposalFunction(); + } + + internal Func<string, Task<string?>> get; + internal Func<string, string, Task> set; + + private readonly Action disposalFunction; + } + + private async static Task<ClientWrapper[]> createClients(int clientCount, + Func<Task<(Func<string, Task<string?>>, + Func<string, string, Task>, + Action)>> clientCreation) + { + var tasks = Enumerable.Range(0, clientCount).Select(async (_) => + { + var tuple = await clientCreation(); + return new ClientWrapper(tuple.Item1, tuple.Item2, tuple.Item3); + }); + return await Task.WhenAll(tasks); + } + + private static async Task run_with_parameters(int total_commands, + int data_size, + int num_of_concurrent_tasks, + string clientsToRun, + string host, + int clientCount, + bool useTLS) + { + if (clientsToRun == "all" || clientsToRun == "glide") + { + var clients = await createClients(clientCount, () => + { + var glide_client = new AsyncClient(host, PORT, useTLS); + return Task.FromResult<(Func<string, Task<string?>>, Func<string, string, Task>, Action)>( + (async (key) => await glide_client.GetAsync(key), + async (key, value) => await glide_client.SetAsync(key, value), + () => glide_client.Dispose())); + }); + + await run_clients( + clients, + "glide", + total_commands, + data_size, + num_of_concurrent_tasks + ); + } + + if (clientsToRun == "all") + { + var clients = await createClients(clientCount, () => + { + var connection = ConnectionMultiplexer.Connect(getAddressForStackExchangeRedis(host, useTLS)); + var db = connection.GetDatabase(); + return Task.FromResult<(Func<string, Task<string?>>, Func<string, string, Task>, Action)>( + (async (key) => await db.StringGetAsync(key), + async (key, value) => await db.StringSetAsync(key, value), + () => connection.Dispose())); + }); + await run_clients( + clients, + "StackExchange.Redis", + total_commands, + data_size, + num_of_concurrent_tasks + ); + + foreach (var client in clients) + { + client.Dispose(); + } + } + } + + private static int number_of_iterations(int num_of_concurrent_tasks) + { + return Math.Min(Math.Max(100000, num_of_concurrent_tasks * 10000), 10000000); + } + + public static async Task Main(string[] args) + { + CommandLineOptions options = new(); + Parser.Default + .ParseArguments<CommandLineOptions>(args).WithParsed<CommandLineOptions>(parsed => { options = parsed; }); + + Logger.SetLoggerConfig(Level.Info, Path.GetFileNameWithoutExtension(options.resultsFile)); + var product = options.concurrentTasks.SelectMany(concurrentTasks => + options.clientCount.Select(clientCount => (concurrentTasks: concurrentTasks, dataSize: options.dataSize, clientCount: clientCount))).Where(tuple => tuple.concurrentTasks >= tuple.clientCount); + foreach (var (concurrentTasks, dataSize, clientCount) in product) + { + var iterations = options.minimal ? 1000 : number_of_iterations(concurrentTasks); + await run_with_parameters(iterations, dataSize, concurrentTasks, options.clientsToRun, options.host, clientCount, options.tls); + } + + print_results(options.resultsFile); + } +} diff --git a/csharp/lib/AsyncClient.cs b/csharp/lib/AsyncClient.cs index 9657907b64..83e3d4c39b 100644 --- a/csharp/lib/AsyncClient.cs +++ b/csharp/lib/AsyncClient.cs @@ -1,113 +1,113 @@ -/** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 - */ - -using System.Runtime.InteropServices; - -namespace Glide; - -public class AsyncClient : IDisposable -{ - #region public methods - public AsyncClient(string host, UInt32 port, bool useTLS) - { - successCallbackDelegate = SuccessCallback; - var successCallbackPointer = Marshal.GetFunctionPointerForDelegate(successCallbackDelegate); - failureCallbackDelegate = FailureCallback; - var failureCallbackPointer = Marshal.GetFunctionPointerForDelegate(failureCallbackDelegate); - clientPointer = CreateClientFfi(host, port, useTLS, successCallbackPointer, failureCallbackPointer); - if (clientPointer == IntPtr.Zero) - { - throw new Exception("Failed creating a client"); - } - } - - public async Task SetAsync(string key, string value) - { - var message = messageContainer.GetMessageForCall(key, value); - SetFfi(clientPointer, (ulong)message.Index, message.KeyPtr, message.ValuePtr); - await message; - } - - public async Task<string?> GetAsync(string key) - { - var message = messageContainer.GetMessageForCall(key, null); - GetFfi(clientPointer, (ulong)message.Index, message.KeyPtr); - return await message; - } - - public void Dispose() - { - if (clientPointer == IntPtr.Zero) - { - return; - } - messageContainer.DisposeWithError(null); - CloseClientFfi(clientPointer); - clientPointer = IntPtr.Zero; - } - - #endregion public methods - - #region private methods - - private void SuccessCallback(ulong index, IntPtr str) - { - var result = str == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(str); - // Work needs to be offloaded from the calling thread, because otherwise we might starve the client's thread pool. - Task.Run(() => - { - var message = messageContainer.GetMessage((int)index); - message.SetResult(result); - }); - } - - private void FailureCallback(ulong index) - { - // Work needs to be offloaded from the calling thread, because otherwise we might starve the client's thread pool. - Task.Run(() => - { - var message = messageContainer.GetMessage((int)index); - message.SetException(new Exception("Operation failed")); - }); - } - - ~AsyncClient() => Dispose(); - #endregion private methods - - #region private fields - - /// Held as a measure to prevent the delegate being garbage collected. These are delegated once - /// and held in order to prevent the cost of marshalling on each function call. - private readonly FailureAction failureCallbackDelegate; - - /// Held as a measure to prevent the delegate being garbage collected. These are delegated once - /// and held in order to prevent the cost of marshalling on each function call. - private readonly StringAction successCallbackDelegate; - - /// Raw pointer to the underlying native client. - private IntPtr clientPointer; - - private readonly MessageContainer<string> messageContainer = new(); - - #endregion private fields - - #region FFI function declarations - - private delegate void StringAction(ulong index, IntPtr str); - private delegate void FailureAction(ulong index); - [DllImport("libglide_rs", CallingConvention = CallingConvention.Cdecl, EntryPoint = "get")] - private static extern void GetFfi(IntPtr client, ulong index, IntPtr key); - - [DllImport("libglide_rs", CallingConvention = CallingConvention.Cdecl, EntryPoint = "set")] - private static extern void SetFfi(IntPtr client, ulong index, IntPtr key, IntPtr value); - - private delegate void IntAction(IntPtr arg); - [DllImport("libglide_rs", CallingConvention = CallingConvention.Cdecl, EntryPoint = "create_client")] - private static extern IntPtr CreateClientFfi(String host, UInt32 port, bool useTLS, IntPtr successCallback, IntPtr failureCallback); - - [DllImport("libglide_rs", CallingConvention = CallingConvention.Cdecl, EntryPoint = "close_client")] - private static extern void CloseClientFfi(IntPtr client); - - #endregion -} \ No newline at end of file +/** + * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + */ + +using System.Runtime.InteropServices; + +namespace Glide; + +public class AsyncClient : IDisposable +{ + #region public methods + public AsyncClient(string host, UInt32 port, bool useTLS) + { + successCallbackDelegate = SuccessCallback; + var successCallbackPointer = Marshal.GetFunctionPointerForDelegate(successCallbackDelegate); + failureCallbackDelegate = FailureCallback; + var failureCallbackPointer = Marshal.GetFunctionPointerForDelegate(failureCallbackDelegate); + clientPointer = CreateClientFfi(host, port, useTLS, successCallbackPointer, failureCallbackPointer); + if (clientPointer == IntPtr.Zero) + { + throw new Exception("Failed creating a client"); + } + } + + public async Task SetAsync(string key, string value) + { + var message = messageContainer.GetMessageForCall(key, value); + SetFfi(clientPointer, (ulong)message.Index, message.KeyPtr, message.ValuePtr); + await message; + } + + public async Task<string?> GetAsync(string key) + { + var message = messageContainer.GetMessageForCall(key, null); + GetFfi(clientPointer, (ulong)message.Index, message.KeyPtr); + return await message; + } + + public void Dispose() + { + if (clientPointer == IntPtr.Zero) + { + return; + } + messageContainer.DisposeWithError(null); + CloseClientFfi(clientPointer); + clientPointer = IntPtr.Zero; + } + + #endregion public methods + + #region private methods + + private void SuccessCallback(ulong index, IntPtr str) + { + var result = str == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(str); + // Work needs to be offloaded from the calling thread, because otherwise we might starve the client's thread pool. + Task.Run(() => + { + var message = messageContainer.GetMessage((int)index); + message.SetResult(result); + }); + } + + private void FailureCallback(ulong index) + { + // Work needs to be offloaded from the calling thread, because otherwise we might starve the client's thread pool. + Task.Run(() => + { + var message = messageContainer.GetMessage((int)index); + message.SetException(new Exception("Operation failed")); + }); + } + + ~AsyncClient() => Dispose(); + #endregion private methods + + #region private fields + + /// Held as a measure to prevent the delegate being garbage collected. These are delegated once + /// and held in order to prevent the cost of marshalling on each function call. + private readonly FailureAction failureCallbackDelegate; + + /// Held as a measure to prevent the delegate being garbage collected. These are delegated once + /// and held in order to prevent the cost of marshalling on each function call. + private readonly StringAction successCallbackDelegate; + + /// Raw pointer to the underlying native client. + private IntPtr clientPointer; + + private readonly MessageContainer<string> messageContainer = new(); + + #endregion private fields + + #region FFI function declarations + + private delegate void StringAction(ulong index, IntPtr str); + private delegate void FailureAction(ulong index); + [DllImport("libglide_rs", CallingConvention = CallingConvention.Cdecl, EntryPoint = "get")] + private static extern void GetFfi(IntPtr client, ulong index, IntPtr key); + + [DllImport("libglide_rs", CallingConvention = CallingConvention.Cdecl, EntryPoint = "set")] + private static extern void SetFfi(IntPtr client, ulong index, IntPtr key, IntPtr value); + + private delegate void IntAction(IntPtr arg); + [DllImport("libglide_rs", CallingConvention = CallingConvention.Cdecl, EntryPoint = "create_client")] + private static extern IntPtr CreateClientFfi(String host, UInt32 port, bool useTLS, IntPtr successCallback, IntPtr failureCallback); + + [DllImport("libglide_rs", CallingConvention = CallingConvention.Cdecl, EntryPoint = "close_client")] + private static extern void CloseClientFfi(IntPtr client); + + #endregion +} diff --git a/csharp/lib/Logger.cs b/csharp/lib/Logger.cs index 03fb47707d..7edc16f16c 100644 --- a/csharp/lib/Logger.cs +++ b/csharp/lib/Logger.cs @@ -1,85 +1,85 @@ -/** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 - */ - -using System.Runtime.InteropServices; -using System.Text; - - -namespace Glide; - -// TODO - use a bindings generator to create this enum. -public enum Level -{ - Error = 0, - Warn = 1, - Info = 2, - Debug = 3, - Trace = 4 -} - -/* -A class that allows logging which is consistent with logs from the internal rust core. -Only one instance of this class can exist at any given time. The logger can be set up in 2 ways - - 1. By calling init, which creates and modifies a new logger only if one doesn't exist. - 2. By calling setConfig, which replaces the existing logger, and means that new logs will not be saved with the logs that were sent before the call. -If no call to any of these function is received, the first log attempt will initialize a new logger with default level decided by rust core (normally - console, error). -*/ -public class Logger -{ - #region private fields - - private static Level? loggerLevel = null; - #endregion private fields - - #region internal methods - // Initialize a logger instance if none were initialized before - this method is meant to be used when there is no intention to replace an existing logger. - // The logger will filter all logs with a level lower than the given level, - // If given a fileName argument, will write the logs to files postfixed with fileName. If fileName isn't provided, the logs will be written to the console. - internal static void Init(Level? level, string? filename = null) - { - if (Logger.loggerLevel is null) - { - SetLoggerConfig(level, filename); - } - } - - // take the arguments from the user and provide to the core-logger (see ../logger-core) - // if the level is higher then the logger level (error is 0, warn 1, etc.) simply return without operation - // if a logger instance doesn't exist, create new one with default mode (decided by rust core, normally - level: error, target: console) - // logIdentifier arg is a string contain data that suppose to give the log a context and make it easier to find certain type of logs. - // when the log is connect to certain task the identifier should be the task id, when the log is not part of specific task the identifier should give a context to the log - for example, "create client". - internal static void Log(Level logLevel, string logIdentifier, string message) - { - if (Logger.loggerLevel is null) - { - SetLoggerConfig(logLevel); - } - if (!(logLevel <= Logger.loggerLevel)) return; - log(Convert.ToInt32(logLevel), Encoding.UTF8.GetBytes(logIdentifier), Encoding.UTF8.GetBytes(message)); - } - #endregion internal methods - - #region public methods - // config the logger instance - in fact - create new logger instance with the new args - // exist in addition to init for two main reason's: - // 1. if GLIDE dev want intentionally to change the logger instance configuration - // 2. external user want to set the logger and we don't want to return to him the logger itself, just config it - // the level argument is the level of the logs you want the system to provide (error logs, warn logs, etc.) - // the filename argument is optional - if provided the target of the logs will be the file mentioned, else will be the console - public static void SetLoggerConfig(Level? level, string? filename = null) - { - var buffer = filename is null ? null : Encoding.UTF8.GetBytes(filename); - Logger.loggerLevel = InitInternalLogger(Convert.ToInt32(level), buffer); - } - #endregion public methods - - #region FFI function declaration - [DllImport("libglide_rs", CallingConvention = CallingConvention.Cdecl, EntryPoint = "log")] - private static extern void log(Int32 logLevel, byte[] logIdentifier, byte[] message); - - [DllImport("libglide_rs", CallingConvention = CallingConvention.Cdecl, EntryPoint = "init")] - private static extern Level InitInternalLogger(Int32 level, byte[]? filename); - - #endregion -} \ No newline at end of file +/** + * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + */ + +using System.Runtime.InteropServices; +using System.Text; + + +namespace Glide; + +// TODO - use a bindings generator to create this enum. +public enum Level +{ + Error = 0, + Warn = 1, + Info = 2, + Debug = 3, + Trace = 4 +} + +/* +A class that allows logging which is consistent with logs from the internal rust core. +Only one instance of this class can exist at any given time. The logger can be set up in 2 ways - + 1. By calling init, which creates and modifies a new logger only if one doesn't exist. + 2. By calling setConfig, which replaces the existing logger, and means that new logs will not be saved with the logs that were sent before the call. +If no call to any of these function is received, the first log attempt will initialize a new logger with default level decided by rust core (normally - console, error). +*/ +public class Logger +{ + #region private fields + + private static Level? loggerLevel = null; + #endregion private fields + + #region internal methods + // Initialize a logger instance if none were initialized before - this method is meant to be used when there is no intention to replace an existing logger. + // The logger will filter all logs with a level lower than the given level, + // If given a fileName argument, will write the logs to files postfixed with fileName. If fileName isn't provided, the logs will be written to the console. + internal static void Init(Level? level, string? filename = null) + { + if (Logger.loggerLevel is null) + { + SetLoggerConfig(level, filename); + } + } + + // take the arguments from the user and provide to the core-logger (see ../logger-core) + // if the level is higher then the logger level (error is 0, warn 1, etc.) simply return without operation + // if a logger instance doesn't exist, create new one with default mode (decided by rust core, normally - level: error, target: console) + // logIdentifier arg is a string contain data that suppose to give the log a context and make it easier to find certain type of logs. + // when the log is connect to certain task the identifier should be the task id, when the log is not part of specific task the identifier should give a context to the log - for example, "create client". + internal static void Log(Level logLevel, string logIdentifier, string message) + { + if (Logger.loggerLevel is null) + { + SetLoggerConfig(logLevel); + } + if (!(logLevel <= Logger.loggerLevel)) return; + log(Convert.ToInt32(logLevel), Encoding.UTF8.GetBytes(logIdentifier), Encoding.UTF8.GetBytes(message)); + } + #endregion internal methods + + #region public methods + // config the logger instance - in fact - create new logger instance with the new args + // exist in addition to init for two main reason's: + // 1. if GLIDE dev want intentionally to change the logger instance configuration + // 2. external user want to set the logger and we don't want to return to him the logger itself, just config it + // the level argument is the level of the logs you want the system to provide (error logs, warn logs, etc.) + // the filename argument is optional - if provided the target of the logs will be the file mentioned, else will be the console + public static void SetLoggerConfig(Level? level, string? filename = null) + { + var buffer = filename is null ? null : Encoding.UTF8.GetBytes(filename); + Logger.loggerLevel = InitInternalLogger(Convert.ToInt32(level), buffer); + } + #endregion public methods + + #region FFI function declaration + [DllImport("libglide_rs", CallingConvention = CallingConvention.Cdecl, EntryPoint = "log")] + private static extern void log(Int32 logLevel, byte[] logIdentifier, byte[] message); + + [DllImport("libglide_rs", CallingConvention = CallingConvention.Cdecl, EntryPoint = "init")] + private static extern Level InitInternalLogger(Int32 level, byte[]? filename); + + #endregion +} diff --git a/csharp/lib/Message.cs b/csharp/lib/Message.cs index 6c5f4203f3..c0d4c7f07b 100644 --- a/csharp/lib/Message.cs +++ b/csharp/lib/Message.cs @@ -1,128 +1,128 @@ -/** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 - */ - -using System.Diagnostics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -using Glide; - -/// Reusable source of ValueTask. This object can be allocated once and then reused -/// to create multiple asynchronous operations, as long as each call to CreateTask -/// is awaited to completion before the next call begins. -internal class Message<T> : INotifyCompletion -{ - /// This is the index of the message in an external array, that allows the user to - /// know how to find the message and set its result. - public int Index { get; } - - /// The pointer to the unmanaged memory that contains the operation's key. - public IntPtr KeyPtr { get; private set; } - - /// The pointer to the unmanaged memory that contains the operation's key. - public IntPtr ValuePtr { get; private set; } - private readonly MessageContainer<T> container; - - public Message(int index, MessageContainer<T> container) - { - Index = index; - continuation = () => { }; - this.container = container; - } - - private Action? continuation; - const int COMPLETION_STAGE_STARTED = 0; - const int COMPLETION_STAGE_NEXT_SHOULD_EXECUTE_CONTINUATION = 1; - const int COMPLETION_STAGE_CONTINUATION_EXECUTED = 2; - private int completionState; - private T? result; - private Exception? exception; - - /// Triggers a succesful completion of the task returned from the latest call - /// to CreateTask. - public void SetResult(T? result) - { - this.result = result; - FinishSet(); - } - - /// Triggers a failure completion of the task returned from the latest call to - /// CreateTask. - public void SetException(Exception exc) - { - this.exception = exc; - FinishSet(); - } - - private void FinishSet() - { - FreePointers(); - - CheckRaceAndCallContinuation(); - } - - private void CheckRaceAndCallContinuation() - { - if (Interlocked.CompareExchange(ref this.completionState, COMPLETION_STAGE_NEXT_SHOULD_EXECUTE_CONTINUATION, COMPLETION_STAGE_STARTED) == COMPLETION_STAGE_NEXT_SHOULD_EXECUTE_CONTINUATION) - { - Debug.Assert(this.continuation != null); - this.completionState = COMPLETION_STAGE_CONTINUATION_EXECUTED; - try - { - continuation(); - } - finally - { - this.container.ReturnFreeMessage(this); - } - } - } - - public Message<T> GetAwaiter() => this; - - /// This returns a task that will complete once SetException / SetResult are called, - /// and ensures that the internal state of the message is set-up before the task is created, - /// and cleaned once it is complete. - public void StartTask(string? key, string? value, object client) - { - continuation = null; - this.completionState = COMPLETION_STAGE_STARTED; - this.result = default(T); - this.exception = null; - this.client = client; - this.KeyPtr = key is null ? IntPtr.Zero : Marshal.StringToHGlobalAnsi(key); - this.ValuePtr = value is null ? IntPtr.Zero : Marshal.StringToHGlobalAnsi(value); - } - - // This function isn't thread-safe. Access to it should be from a single thread, and only once per operation. - // For the sake of performance, this responsibility is on the caller, and the function doesn't contain any safety measures. - private void FreePointers() - { - if (KeyPtr != IntPtr.Zero) - { - Marshal.FreeHGlobal(KeyPtr); - KeyPtr = IntPtr.Zero; - } - if (ValuePtr != IntPtr.Zero) - { - Marshal.FreeHGlobal(ValuePtr); - ValuePtr = IntPtr.Zero; - } - client = null; - } - - // Holding the client prevents it from being CG'd until all operations complete. - private object? client; - - - public void OnCompleted(Action continuation) - { - this.continuation = continuation; - CheckRaceAndCallContinuation(); - } - - public bool IsCompleted => completionState == COMPLETION_STAGE_CONTINUATION_EXECUTED; - - public T? GetResult() => this.exception is null ? this.result : throw this.exception; -} \ No newline at end of file +/** + * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + */ + +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using Glide; + +/// Reusable source of ValueTask. This object can be allocated once and then reused +/// to create multiple asynchronous operations, as long as each call to CreateTask +/// is awaited to completion before the next call begins. +internal class Message<T> : INotifyCompletion +{ + /// This is the index of the message in an external array, that allows the user to + /// know how to find the message and set its result. + public int Index { get; } + + /// The pointer to the unmanaged memory that contains the operation's key. + public IntPtr KeyPtr { get; private set; } + + /// The pointer to the unmanaged memory that contains the operation's key. + public IntPtr ValuePtr { get; private set; } + private readonly MessageContainer<T> container; + + public Message(int index, MessageContainer<T> container) + { + Index = index; + continuation = () => { }; + this.container = container; + } + + private Action? continuation; + const int COMPLETION_STAGE_STARTED = 0; + const int COMPLETION_STAGE_NEXT_SHOULD_EXECUTE_CONTINUATION = 1; + const int COMPLETION_STAGE_CONTINUATION_EXECUTED = 2; + private int completionState; + private T? result; + private Exception? exception; + + /// Triggers a succesful completion of the task returned from the latest call + /// to CreateTask. + public void SetResult(T? result) + { + this.result = result; + FinishSet(); + } + + /// Triggers a failure completion of the task returned from the latest call to + /// CreateTask. + public void SetException(Exception exc) + { + this.exception = exc; + FinishSet(); + } + + private void FinishSet() + { + FreePointers(); + + CheckRaceAndCallContinuation(); + } + + private void CheckRaceAndCallContinuation() + { + if (Interlocked.CompareExchange(ref this.completionState, COMPLETION_STAGE_NEXT_SHOULD_EXECUTE_CONTINUATION, COMPLETION_STAGE_STARTED) == COMPLETION_STAGE_NEXT_SHOULD_EXECUTE_CONTINUATION) + { + Debug.Assert(this.continuation != null); + this.completionState = COMPLETION_STAGE_CONTINUATION_EXECUTED; + try + { + continuation(); + } + finally + { + this.container.ReturnFreeMessage(this); + } + } + } + + public Message<T> GetAwaiter() => this; + + /// This returns a task that will complete once SetException / SetResult are called, + /// and ensures that the internal state of the message is set-up before the task is created, + /// and cleaned once it is complete. + public void StartTask(string? key, string? value, object client) + { + continuation = null; + this.completionState = COMPLETION_STAGE_STARTED; + this.result = default(T); + this.exception = null; + this.client = client; + this.KeyPtr = key is null ? IntPtr.Zero : Marshal.StringToHGlobalAnsi(key); + this.ValuePtr = value is null ? IntPtr.Zero : Marshal.StringToHGlobalAnsi(value); + } + + // This function isn't thread-safe. Access to it should be from a single thread, and only once per operation. + // For the sake of performance, this responsibility is on the caller, and the function doesn't contain any safety measures. + private void FreePointers() + { + if (KeyPtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(KeyPtr); + KeyPtr = IntPtr.Zero; + } + if (ValuePtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(ValuePtr); + ValuePtr = IntPtr.Zero; + } + client = null; + } + + // Holding the client prevents it from being CG'd until all operations complete. + private object? client; + + + public void OnCompleted(Action continuation) + { + this.continuation = continuation; + CheckRaceAndCallContinuation(); + } + + public bool IsCompleted => completionState == COMPLETION_STAGE_CONTINUATION_EXECUTED; + + public T? GetResult() => this.exception is null ? this.result : throw this.exception; +} diff --git a/csharp/lib/MessageContainer.cs b/csharp/lib/MessageContainer.cs index 7de0947650..faa1b5a277 100644 --- a/csharp/lib/MessageContainer.cs +++ b/csharp/lib/MessageContainer.cs @@ -1,62 +1,62 @@ -/** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 - */ - -using System.Collections.Concurrent; - -namespace Glide; - - -internal class MessageContainer<T> -{ - internal Message<T> GetMessage(int index) => messages[index]; - - internal Message<T> GetMessageForCall(string? key, string? value) - { - var message = GetFreeMessage(); - message.StartTask(key, value, this); - return message; - } - - private Message<T> GetFreeMessage() - { - if (!availableMessages.TryDequeue(out var message)) - { - lock (messages) - { - var index = messages.Count; - message = new Message<T>(index, this); - messages.Add(message); - } - } - return message; - } - - public void ReturnFreeMessage(Message<T> message) => availableMessages.Enqueue(message); - - internal void DisposeWithError(Exception? error) - { - lock (messages) - { - foreach (var message in messages.Where(message => !message.IsCompleted)) - { - try - { - message.SetException(new TaskCanceledException("Client closed", error)); - } - catch (Exception) { } - } - messages.Clear(); - } - availableMessages.Clear(); - } - - /// This list allows us random-access to the message in each index, - /// which means that once we receive a callback with an index, we can - /// find the message to resolve in constant time. - private readonly List<Message<T>> messages = new(); - - /// This queue contains the messages that were created and are currently unused by any task, - /// so they can be reused y new tasks instead of allocating new messages. - private readonly ConcurrentQueue<Message<T>> availableMessages = new(); -} \ No newline at end of file +/** + * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + */ + +using System.Collections.Concurrent; + +namespace Glide; + + +internal class MessageContainer<T> +{ + internal Message<T> GetMessage(int index) => messages[index]; + + internal Message<T> GetMessageForCall(string? key, string? value) + { + var message = GetFreeMessage(); + message.StartTask(key, value, this); + return message; + } + + private Message<T> GetFreeMessage() + { + if (!availableMessages.TryDequeue(out var message)) + { + lock (messages) + { + var index = messages.Count; + message = new Message<T>(index, this); + messages.Add(message); + } + } + return message; + } + + public void ReturnFreeMessage(Message<T> message) => availableMessages.Enqueue(message); + + internal void DisposeWithError(Exception? error) + { + lock (messages) + { + foreach (var message in messages.Where(message => !message.IsCompleted)) + { + try + { + message.SetException(new TaskCanceledException("Client closed", error)); + } + catch (Exception) { } + } + messages.Clear(); + } + availableMessages.Clear(); + } + + /// This list allows us random-access to the message in each index, + /// which means that once we receive a callback with an index, we can + /// find the message to resolve in constant time. + private readonly List<Message<T>> messages = new(); + + /// This queue contains the messages that were created and are currently unused by any task, + /// so they can be reused y new tasks instead of allocating new messages. + private readonly ConcurrentQueue<Message<T>> availableMessages = new(); +} diff --git a/csharp/lib/Properties/AssemblyInfo.cs b/csharp/lib/Properties/AssemblyInfo.cs index 876c013e0a..a6412c5b2e 100644 --- a/csharp/lib/Properties/AssemblyInfo.cs +++ b/csharp/lib/Properties/AssemblyInfo.cs @@ -1,7 +1,7 @@ -/** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 - */ - -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("tests")] \ No newline at end of file +/** + * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + */ + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("tests")] diff --git a/csharp/tests/AsyncClientTests.cs b/csharp/tests/AsyncClientTests.cs index a322e37514..e9adfdf97b 100644 --- a/csharp/tests/AsyncClientTests.cs +++ b/csharp/tests/AsyncClientTests.cs @@ -1,122 +1,122 @@ -/** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 - */ - -namespace tests; - -using Glide; - -// TODO - need to start a new redis server for each test? -public class AsyncClientTests -{ - [OneTimeSetUp] - public void Setup() - { - Glide.Logger.SetLoggerConfig(Glide.Level.Info); - } - - private async Task GetAndSetRandomValues(AsyncClient client) - { - var key = Guid.NewGuid().ToString(); - var value = Guid.NewGuid().ToString(); - await client.SetAsync(key, value); - var result = await client.GetAsync(key); - Assert.That(result, Is.EqualTo(value)); - } - - [Test] - public async Task GetReturnsLastSet() - { - using (var client = new AsyncClient("localhost", 6379, false)) - { - await GetAndSetRandomValues(client); - } - } - - [Test] - public async Task GetAndSetCanHandleNonASCIIUnicode() - { - using (var client = new AsyncClient("localhost", 6379, false)) - { - var key = Guid.NewGuid().ToString(); - var value = "שלום hello 汉字"; - await client.SetAsync(key, value); - var result = await client.GetAsync(key); - Assert.That(result, Is.EqualTo(value)); - } - } - - [Test] - public async Task GetReturnsNull() - { - using (var client = new AsyncClient("localhost", 6379, false)) - { - var result = await client.GetAsync(Guid.NewGuid().ToString()); - Assert.That(result, Is.EqualTo(null)); - } - } - - [Test] - public async Task GetReturnsEmptyString() - { - using (var client = new AsyncClient("localhost", 6379, false)) - { - var key = Guid.NewGuid().ToString(); - var value = ""; - await client.SetAsync(key, value); - var result = await client.GetAsync(key); - Assert.That(result, Is.EqualTo(value)); - } - } - - [Test] - public async Task HandleVeryLargeInput() - { - using (var client = new AsyncClient("localhost", 6379, false)) - { - var key = Guid.NewGuid().ToString(); - var value = Guid.NewGuid().ToString(); - const int EXPECTED_SIZE = 2 << 23; - while (value.Length < EXPECTED_SIZE) - { - value += value; - } - await client.SetAsync(key, value); - var result = await client.GetAsync(key); - Assert.That(result, Is.EqualTo(value)); - } - } - - // This test is slow and hardly a unit test, but it caught timing and releasing issues in the past, - // so it's being kept. - [Test] - public void ConcurrentOperationsWork() - { - using (var client = new AsyncClient("localhost", 6379, false)) - { - var operations = new List<Task>(); - - for (int i = 0; i < 1000; ++i) - { - var index = i; - operations.Add(Task.Run(async () => - { - for (int i = 0; i < 1000; ++i) - { - if ((i + index) % 2 == 0) - { - await GetAndSetRandomValues(client); - } - else - { - var result = await client.GetAsync(Guid.NewGuid().ToString()); - Assert.That(result, Is.EqualTo(null)); - } - } - })); - } - - Task.WaitAll(operations.ToArray()); - } - } -} \ No newline at end of file +/** + * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + */ + +namespace tests; + +using Glide; + +// TODO - need to start a new redis server for each test? +public class AsyncClientTests +{ + [OneTimeSetUp] + public void Setup() + { + Glide.Logger.SetLoggerConfig(Glide.Level.Info); + } + + private async Task GetAndSetRandomValues(AsyncClient client) + { + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + await client.SetAsync(key, value); + var result = await client.GetAsync(key); + Assert.That(result, Is.EqualTo(value)); + } + + [Test] + public async Task GetReturnsLastSet() + { + using (var client = new AsyncClient("localhost", 6379, false)) + { + await GetAndSetRandomValues(client); + } + } + + [Test] + public async Task GetAndSetCanHandleNonASCIIUnicode() + { + using (var client = new AsyncClient("localhost", 6379, false)) + { + var key = Guid.NewGuid().ToString(); + var value = "שלום hello 汉字"; + await client.SetAsync(key, value); + var result = await client.GetAsync(key); + Assert.That(result, Is.EqualTo(value)); + } + } + + [Test] + public async Task GetReturnsNull() + { + using (var client = new AsyncClient("localhost", 6379, false)) + { + var result = await client.GetAsync(Guid.NewGuid().ToString()); + Assert.That(result, Is.EqualTo(null)); + } + } + + [Test] + public async Task GetReturnsEmptyString() + { + using (var client = new AsyncClient("localhost", 6379, false)) + { + var key = Guid.NewGuid().ToString(); + var value = ""; + await client.SetAsync(key, value); + var result = await client.GetAsync(key); + Assert.That(result, Is.EqualTo(value)); + } + } + + [Test] + public async Task HandleVeryLargeInput() + { + using (var client = new AsyncClient("localhost", 6379, false)) + { + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + const int EXPECTED_SIZE = 2 << 23; + while (value.Length < EXPECTED_SIZE) + { + value += value; + } + await client.SetAsync(key, value); + var result = await client.GetAsync(key); + Assert.That(result, Is.EqualTo(value)); + } + } + + // This test is slow and hardly a unit test, but it caught timing and releasing issues in the past, + // so it's being kept. + [Test] + public void ConcurrentOperationsWork() + { + using (var client = new AsyncClient("localhost", 6379, false)) + { + var operations = new List<Task>(); + + for (int i = 0; i < 1000; ++i) + { + var index = i; + operations.Add(Task.Run(async () => + { + for (int i = 0; i < 1000; ++i) + { + if ((i + index) % 2 == 0) + { + await GetAndSetRandomValues(client); + } + else + { + var result = await client.GetAsync(Guid.NewGuid().ToString()); + Assert.That(result, Is.EqualTo(null)); + } + } + })); + } + + Task.WaitAll(operations.ToArray()); + } + } +} diff --git a/csharp/tests/Usings.cs b/csharp/tests/Usings.cs index 08ffe13f66..6638c1219c 100644 --- a/csharp/tests/Usings.cs +++ b/csharp/tests/Usings.cs @@ -1,5 +1,5 @@ -/** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 - */ - -global using NUnit.Framework; \ No newline at end of file +/** + * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + */ + +global using NUnit.Framework; From 80bb57c2360969b7c1bdb099809188e6695c2a45 Mon Sep 17 00:00:00 2001 From: Guriy Samarin <guriy@amazon.com> Date: Tue, 6 Feb 2024 23:56:09 +0000 Subject: [PATCH 10/10] fix merge --- benchmarks/csharp/Program.cs | 357 +---------------------------------- 1 file changed, 1 insertion(+), 356 deletions(-) diff --git a/benchmarks/csharp/Program.cs b/benchmarks/csharp/Program.cs index 4a175b4b09..11df0e36be 100644 --- a/benchmarks/csharp/Program.cs +++ b/benchmarks/csharp/Program.cs @@ -14,360 +14,6 @@ using StackExchange.Redis; -public static class MainClass -{ - private enum ChosenAction { GET_NON_EXISTING, GET_EXISTING, SET }; - - public class CommandLineOptions - { - [Option('r', "resultsFile", Required = false, HelpText = "Set the file to which the JSON results are written.", Default = "../results/csharp-results.json")] - public string resultsFile { get; set; } = string.Empty; - - [Option('d', "dataSize", Required = false, HelpText = "The size of the sent data in bytes.", Default = 100)] - public int dataSize { get; set; } - - [Option('c', "concurrentTasks", Required = false, HelpText = "The number of concurrent operations to perform.", Default = new[] { 1, 10, 100, 1000 })] - public IEnumerable<int> concurrentTasks { get; set; } = Enumerable.Empty<int>(); - - [Option('l', "clients", Required = false, HelpText = "Which clients should run", Default = "all")] - public string clientsToRun { get; set; } = string.Empty; - - [Option('h', "host", Required = false, HelpText = "What host to target", Default = "localhost")] - public string host { get; set; } = string.Empty; - - [Option('C', "clientCount", Required = false, HelpText = "Number of clients to run concurrently", Default = new[] { 1 })] - public IEnumerable<int> clientCount { get; set; } = Enumerable.Empty<int>(); - - [Option('t', "tls", HelpText = "Should benchmark a TLS server", Default = false)] - public bool tls { get; set; } - - - [Option('m', "minimal", HelpText = "Should use a minimal number of actions", Default = false)] - public bool minimal { get; set; } - } - - private const int PORT = 6379; - private static string getAddress(string host) - { - return $"{host}:{PORT}"; - } - - private static string getAddressForStackExchangeRedis(string host, bool useTLS) - { - return $"{getAddress(host)},ssl={useTLS}"; - } - - private static string getAddressWithRedisPrefix(string host, bool useTLS) - { - var protocol = useTLS ? "rediss" : "redis"; - return $"{protocol}://{getAddress(host)}"; - } - private const double PROB_GET = 0.8; - - private const double PROB_GET_EXISTING_KEY = 0.8; - private const int SIZE_GET_KEYSPACE = 3750000; // 3.75 million - private const int SIZE_SET_KEYSPACE = 3000000; // 3 million - - private static readonly Random randomizer = new(); - private static long started_tasks_counter = 0; - private static readonly List<Dictionary<string, object>> bench_json_results = new(); - - private static string generate_value(int size) - { - return new string('0', size); - } - - private static string generate_key_set() - { - return (randomizer.Next(SIZE_SET_KEYSPACE) + 1).ToString(); - } - private static string generate_key_get() - { - return (randomizer.Next(SIZE_SET_KEYSPACE, SIZE_GET_KEYSPACE) + 1).ToString(); - } - - private static ChosenAction choose_action() - { - if (randomizer.NextDouble() > PROB_GET) - { - return ChosenAction.SET; - } - if (randomizer.NextDouble() > PROB_GET_EXISTING_KEY) - { - return ChosenAction.GET_NON_EXISTING; - } - return ChosenAction.GET_EXISTING; - } - - /// copied from https://stackoverflow.com/questions/8137391/percentile-calculation - private static double Percentile(double[] sequence, double excelPercentile) - { - Array.Sort(sequence); - int N = sequence.Length; - double n = (N - 1) * excelPercentile + 1; - if (n == 1d) return sequence[0]; - else if (n == N) return sequence[N - 1]; - else - { - int k = (int)n; - double d = n - k; - return sequence[k - 1] + d * (sequence[k] - sequence[k - 1]); - } - } - - private static double calculate_latency(IEnumerable<double> latency_list, double percentile_point) - { - return Math.Round(Percentile(latency_list.ToArray(), percentile_point), 2); - } - - private static void print_results(string resultsFile) - { - using FileStream createStream = File.Create(resultsFile); - JsonSerializer.Serialize(createStream, bench_json_results); - } - - private static async Task redis_benchmark( - ClientWrapper[] clients, - long total_commands, - string data, - Dictionary<ChosenAction, ConcurrentBag<double>> action_latencies) - { - var stopwatch = new Stopwatch(); - do - { - Interlocked.Increment(ref started_tasks_counter); - var index = (int)(started_tasks_counter % clients.Length); - var client = clients[index]; - var action = choose_action(); - stopwatch.Start(); - switch (action) - { - case ChosenAction.GET_EXISTING: - await client.get(generate_key_set()); - break; - case ChosenAction.GET_NON_EXISTING: - await client.get(generate_key_get()); - break; - case ChosenAction.SET: - await client.set(generate_key_set(), data); - break; - } - stopwatch.Stop(); - var latency_list = action_latencies[action]; - latency_list.Add(((double)stopwatch.ElapsedMilliseconds) / 1000); - } while (started_tasks_counter < total_commands); - } - - private static async Task<long> create_bench_tasks( - ClientWrapper[] clients, - int total_commands, - string data, - int num_of_concurrent_tasks, - Dictionary<ChosenAction, ConcurrentBag<double>> action_latencies - ) - { - started_tasks_counter = 0; - var stopwatch = Stopwatch.StartNew(); - var running_tasks = new List<Task>(); - for (var i = 0; i < num_of_concurrent_tasks; i++) - { - running_tasks.Add( - redis_benchmark(clients, total_commands, data, action_latencies) - ); - } - await Task.WhenAll(running_tasks); - stopwatch.Stop(); - return stopwatch.ElapsedMilliseconds; - } - - private static Dictionary<string, object> latency_results( - string prefix, - ConcurrentBag<double> latencies - ) - { - return new Dictionary<string, object> - { - {prefix + "_p50_latency", calculate_latency(latencies, 0.5)}, - {prefix + "_p90_latency", calculate_latency(latencies, 0.9)}, - {prefix + "_p99_latency", calculate_latency(latencies, 0.99)}, - {prefix + "_average_latency", Math.Round(latencies.Average(), 3)}, - {prefix + "_std_dev", latencies.StandardDeviation()}, - }; - } - - private static async Task run_clients( - ClientWrapper[] clients, - string client_name, - int total_commands, - int data_size, - int num_of_concurrent_tasks - ) - { - Console.WriteLine($"Starting {client_name} data size: {data_size} concurrency: {num_of_concurrent_tasks} client count: {clients.Length} {DateTime.UtcNow.ToString("HH:mm:ss")}"); - var action_latencies = new Dictionary<ChosenAction, ConcurrentBag<double>>() { - {ChosenAction.GET_NON_EXISTING, new()}, - {ChosenAction.GET_EXISTING, new()}, - {ChosenAction.SET, new()}, - }; - var data = generate_value(data_size); - var elapsed_milliseconds = await create_bench_tasks( - clients, - total_commands, - data, - num_of_concurrent_tasks, - action_latencies - ); - var tps = Math.Round((double)started_tasks_counter / ((double)elapsed_milliseconds / 1000)); - - var get_non_existing_latencies = action_latencies[ChosenAction.GET_NON_EXISTING]; - var get_non_existing_latency_results = latency_results("get_non_existing", get_non_existing_latencies); - - var get_existing_latencies = action_latencies[ChosenAction.GET_EXISTING]; - var get_existing_latency_results = latency_results("get_existing", get_existing_latencies); - - var set_latencies = action_latencies[ChosenAction.SET]; - var set_latency_results = latency_results("set", set_latencies); - - var result = new Dictionary<string, object> - { - {"client", client_name}, - {"num_of_tasks", num_of_concurrent_tasks}, - {"data_size", data_size}, - {"tps", tps}, - {"client_count", clients.Length}, - {"is_cluster", "false"} - }; - result = result - .Concat(get_existing_latency_results) - .Concat(get_non_existing_latency_results) - .Concat(set_latency_results) - .ToDictionary(pair => pair.Key, pair => pair.Value); - bench_json_results.Add(result); - } - - private class ClientWrapper : IDisposable - { - internal ClientWrapper(Func<string, Task<string?>> get, Func<string, string, Task> set, Action disposalFunction) - { - this.get = get; - this.set = set; - this.disposalFunction = disposalFunction; - } - - public void Dispose() - { - this.disposalFunction(); - } - - internal Func<string, Task<string?>> get; - internal Func<string, string, Task> set; - - private readonly Action disposalFunction; - } - - private async static Task<ClientWrapper[]> createClients(int clientCount, - Func<Task<(Func<string, Task<string?>>, - Func<string, string, Task>, - Action)>> clientCreation) - { - var tasks = Enumerable.Range(0, clientCount).Select(async (_) => - { - var tuple = await clientCreation(); - return new ClientWrapper(tuple.Item1, tuple.Item2, tuple.Item3); - }); - return await Task.WhenAll(tasks); - } - - private static async Task run_with_parameters(int total_commands, - int data_size, - int num_of_concurrent_tasks, - string clientsToRun, - string host, - int clientCount, - bool useTLS) - { - if (clientsToRun == "all" || clientsToRun == "glide") - { - var clients = await createClients(clientCount, () => - { - var glide_client = new AsyncClient(host, PORT, useTLS); - return Task.FromResult<(Func<string, Task<string?>>, Func<string, string, Task>, Action)>( - (async (key) => await glide_client.GetAsync(key), - async (key, value) => await glide_client.SetAsync(key, value), - () => glide_client.Dispose())); - }); - - await run_clients( - clients, - "glide", - total_commands, - data_size, - num_of_concurrent_tasks - ); - } - - if (clientsToRun == "all") - { - var clients = await createClients(clientCount, () => - { - var connection = ConnectionMultiplexer.Connect(getAddressForStackExchangeRedis(host, useTLS)); - var db = connection.GetDatabase(); - return Task.FromResult<(Func<string, Task<string?>>, Func<string, string, Task>, Action)>( - (async (key) => await db.StringGetAsync(key), - async (key, value) => await db.StringSetAsync(key, value), - () => connection.Dispose())); - }); - await run_clients( - clients, - "StackExchange.Redis", - total_commands, - data_size, - num_of_concurrent_tasks - ); - - foreach (var client in clients) - { - client.Dispose(); - } - } - } - - private static int number_of_iterations(int num_of_concurrent_tasks) - { - return Math.Min(Math.Max(100000, num_of_concurrent_tasks * 10000), 10000000); - } - - public static async Task Main(string[] args) - { - CommandLineOptions options = new(); - Parser.Default - .ParseArguments<CommandLineOptions>(args).WithParsed<CommandLineOptions>(parsed => { options = parsed; }); - - Logger.SetLoggerConfig(Level.Info, Path.GetFileNameWithoutExtension(options.resultsFile)); - var product = options.concurrentTasks.SelectMany(concurrentTasks => - options.clientCount.Select(clientCount => (concurrentTasks: concurrentTasks, dataSize: options.dataSize, clientCount: clientCount))).Where(tuple => tuple.concurrentTasks >= tuple.clientCount); - foreach (var (concurrentTasks, dataSize, clientCount) in product) - { - var iterations = options.minimal ? 1000 : number_of_iterations(concurrentTasks); - await run_with_parameters(iterations, dataSize, concurrentTasks, options.clientsToRun, options.host, clientCount, options.tls); - } - - print_results(options.resultsFile); - } -} -======= -/** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 - */ - -using System.Collections.Concurrent; -using System.Diagnostics; -using System.Text.Json; -using Glide; -using CommandLine; -using LinqStatistics; -using StackExchange.Redis; - public static class MainClass { private enum ChosenAction { GET_NON_EXISTING, GET_EXISTING, SET }; @@ -618,7 +264,7 @@ public void Dispose() internal Func<string, Task<string?>> get; internal Func<string, string, Task> set; - private Action disposalFunction; + private readonly Action disposalFunction; } private async static Task<ClientWrapper[]> createClients(int clientCount, @@ -711,4 +357,3 @@ public static async Task Main(string[] args) print_results(options.resultsFile); } } -