From c531f86ca7e457744b098fb147ab4ef5d7d8fed1 Mon Sep 17 00:00:00 2001 From: Guriy Samarin Date: Wed, 7 Feb 2024 17:54:13 +0000 Subject: [PATCH] up c# syntax, make cli defaults in one style (#903) * up c# syntax, make cli defaults in one style * add .editorconfig --------- Co-authored-by: Guriy Samarin Co-authored-by: Shachar Langbeheim --- .editorconfig | 372 ++++++++++++++++ .github/workflows/csharp.yml | 4 + benchmarks/csharp/Program.cs | 714 ++++++++++++++++--------------- csharp/lib/AsyncClient.cs | 230 +++++----- csharp/lib/Logger.cs | 129 +++--- csharp/lib/Message.cs | 27 +- csharp/lib/MessageContainer.cs | 82 ++-- csharp/tests/AsyncClientTests.cs | 236 +++++----- 8 files changed, 1072 insertions(+), 722 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..824238f9c1 --- /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 = lf +insert_final_newline = true + +#### .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 diff --git a/.github/workflows/csharp.yml b/.github/workflows/csharp.yml index af017c542c..3c838c38c5 100644 --- a/.github/workflows/csharp.yml +++ b/.github/workflows/csharp.yml @@ -52,6 +52,10 @@ jobs: - name: Start redis server run: redis-server & + - name: Format + working-directory: ./csharp + 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 5cbdba6aa5..11df0e36be 100644 --- a/benchmarks/csharp/Program.cs +++ b/benchmarks/csharp/Program.cs @@ -1,355 +1,359 @@ -/** - * 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 }; - - 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('d', "dataSize", Required = false, HelpText = "The size of the sent data in bytes.")] - public int dataSize { get; set; } = 100; - - [Option('c', "concurrentTasks", Required = false, HelpText = "The number of concurrent operations to perform.", Default = new[] { 1, 10, 100, 1000 })] - public IEnumerable concurrentTasks { get; set; } = Enumerable.Empty(); - - [Option('l', "clients", Required = false, HelpText = "Which clients should run")] - public string clientsToRun { get; set; } = "all"; - - [Option('h', "host", Required = false, HelpText = "What host to target")] - public string host { get; set; } = "localhost"; - - [Option('C', "clientCount", Required = false, HelpText = "Number of clients to run concurrently", Default = new[] { 1 })] - public IEnumerable clientCount { get; set; } = Enumerable.Empty(); - - [Option('t', "tls", HelpText = "Should benchmark a TLS server")] - public bool tls { get; set; } = false; - - - [Option('m', "minimal", HelpText = "Should use a minimal number of actions")] - public bool minimal { get; set; } = false; - } - - 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> 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 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> 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 create_bench_tasks( - ClientWrapper[] clients, - int total_commands, - string data, - int num_of_concurrent_tasks, - Dictionary> action_latencies - ) - { - started_tasks_counter = 0; - var stopwatch = Stopwatch.StartNew(); - var running_tasks = new List(); - 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 latency_results( - string prefix, - ConcurrentBag latencies - ) - { - return new Dictionary - { - {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.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 - { - {"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> get, Func set, Action disposalFunction) - { - this.get = get; - this.set = set; - this.disposalFunction = disposalFunction; - } - - public void Dispose() - { - this.disposalFunction(); - } - - internal Func> get; - internal Func set; - - private Action disposalFunction; - } - - private async static Task createClients(int clientCount, - Func>, - Func, - 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>, Func, 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>, Func, 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(args).WithParsed(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 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.")] + public string resultsFile { get; set; } = "../results/csharp-results.json"; + + [Option('d', "dataSize", Required = false, HelpText = "The size of the sent data in bytes.")] + public int dataSize { get; set; } = 100; + + [Option('c', "concurrentTasks", Required = false, HelpText = "The number of concurrent operations to perform.", Default = new[] { 1, 10, 100, 1000 })] + public IEnumerable concurrentTasks { get; set; } = Enumerable.Empty(); + + [Option('l', "clients", Required = false, HelpText = "Which clients should run")] + public string clientsToRun { get; set; } = "all"; + + [Option('h', "host", Required = false, HelpText = "What host to target")] + public string host { get; set; } = "localhost"; + + [Option('C', "clientCount", Required = false, HelpText = "Number of clients to run concurrently", Default = new[] { 1 })] + public IEnumerable clientCount { get; set; } = Enumerable.Empty(); + + [Option('t', "tls", HelpText = "Should benchmark a TLS server")] + public bool tls { get; set; } = false; + + + [Option('m', "minimal", HelpText = "Should use a minimal number of actions")] + public bool minimal { get; set; } = false; + } + + 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> 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 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> 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 create_bench_tasks( + ClientWrapper[] clients, + int total_commands, + string data, + int num_of_concurrent_tasks, + Dictionary> action_latencies + ) + { + started_tasks_counter = 0; + var stopwatch = Stopwatch.StartNew(); + var running_tasks = new List(); + 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 latency_results( + string prefix, + ConcurrentBag latencies + ) + { + return new Dictionary + { + {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.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 + { + {"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> get, Func set, Action disposalFunction) + { + this.get = get; + this.set = set; + this.disposalFunction = disposalFunction; + } + + public void Dispose() + { + this.disposalFunction(); + } + + internal Func> get; + internal Func set; + + private readonly Action disposalFunction; + } + + private async static Task createClients(int clientCount, + Func>, + Func, + 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>, Func, 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>, Func, 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(args).WithParsed(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 b50fe77bf4..83e3d4c39b 100644 --- a/csharp/lib/AsyncClient.cs +++ b/csharp/lib/AsyncClient.cs @@ -1,117 +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 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 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; - - /// Raw pointer to the underlying native client. - private IntPtr clientPointer; - - private readonly MessageContainer 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 - } -} +/** + * 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 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 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 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..c0d4c7f07b 100644 --- a/csharp/lib/Message.cs +++ b/csharp/lib/Message.cs @@ -5,6 +5,7 @@ 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 @@ -21,7 +22,7 @@ internal class Message : INotifyCompletion /// The pointer to the unmanaged memory that contains the operation's key. public IntPtr ValuePtr { get; private set; } - private MessageContainer container; + private readonly MessageContainer container; public Message(int index, MessageContainer container) { @@ -38,7 +39,7 @@ public Message(int index, MessageContainer 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 +79,7 @@ private void CheckRaceAndCallContinuation() } } - public Message GetAwaiter() - { - return this; - } + public Message 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 +122,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..faa1b5a277 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 { + internal Message GetMessage(int index) => messages[index]; - internal class MessageContainer + internal Message GetMessageForCall(string? key, string? value) { - internal Message GetMessage(int index) - { - return messages[index]; - } - - internal Message 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 GetFreeMessage() + private Message GetFreeMessage() + { + if (!availableMessages.TryDequeue(out var message)) { - if (!availableMessages.TryDequeue(out var message)) + lock (messages) { - lock (messages) - { - var index = messages.Count; - message = new Message(index, this); - messages.Add(message); - } + var index = messages.Count; + message = new Message(index, this); + messages.Add(message); } - return message; } + return message; + } - public void ReturnFreeMessage(Message message) - { - availableMessages.Enqueue(message); - } + public void ReturnFreeMessage(Message 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> 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> 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 readonly List> 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> availableMessages = new(); } diff --git a/csharp/tests/AsyncClientTests.cs b/csharp/tests/AsyncClientTests.cs index dad14c953f..e9adfdf97b 100644 --- a/csharp/tests/AsyncClientTests.cs +++ b/csharp/tests/AsyncClientTests.cs @@ -2,121 +2,121 @@ * 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(); - - 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()); - } - } -} +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(); + + 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()); + } + } +}