C#. AspNetCore2.2. Exception при использовании библиотеки NCalc из разных потоков

100
04 февраля 2021, 01:30

на одном из Продакшен серверов возникло исключение.

2019-06-10 13:22:45.769 +10:00 [ERR] ОШИБКА ПОДГОТОВКИ ДАННЫХ К ОБМЕНУ. KeyExchange TcpIp=209 Plat=2 P=10/12 Stolb=_ Addr=9
NCalc.EvaluationException: Operations that change non-concurrent collections must have exclusive access. A concurrent update was performed on this collection and corrupted its state. The collection's state is no longer correct. ---> System.InvalidOperationException: Operations that change non-concurrent collections must have exclusive access. A concurrent update was performed on this collection and corrupted its state. The collection's state is no longer correct.
at System.Collections.Generic.Dictionary`2.FindEntry(TKey key)
at NCalc.Expression.Compile(String expression, Boolean nocache)
at NCalc.Expression.HasErrors()
--- End of inner exception stack trace ---
at NCalc.Expression.ToLambda[TResult]()
at Shared.Helpers.HelpersString.CalculateMathematicFormat(String str, Int32 row) in /src/Shared/Helpers/HelpersString.cs:line 145
at Shared.Helpers.HelpersString.<>c__DisplayClass0_0.<StringTemplateInsert>g__Evaluator|0(Match match) in /src/Shared/Helpers/HelpersString.cs:line 48
at System.Text.RegularExpressions.Regex.Replace(MatchEvaluator evaluator, Regex regex, String input, Int32 count, Int32 startat)
at System.Text.RegularExpressions.Regex.Replace(String input, MatchEvaluator evaluator, Int32 count, Int32 startat)
at System.Text.RegularExpressions.Regex.Replace(String input, MatchEvaluator evaluator)
at System.Text.RegularExpressions.Regex.Replace(String input, String pattern, MatchEvaluator evaluator)
at InputDataModel.Autodictor.DataProviders.ByRuleDataProviders.Rules.ViewRule.MakeBodySectionIndependentInserts(String body, AdInputType uit, Int32 currentRow) in /src/InputDataModel.Autodictor/DataProviders/ByRuleDataProviders/Rules/ViewRule.cs:line 268
at InputDataModel.Autodictor.DataProviders.ByRuleDataProviders.Rules.ViewRule.CreateStringRequest(IEnumerable`1 batch, Int32 startItemIndex) in /src/InputDataModel.Autodictor/DataProviders/ByRuleDataProviders/Rules/ViewRule.cs:line 209
at InputDataModel.Autodictor.DataProviders.ByRuleDataProviders.Rules.ViewRule.GetDataRequestString(List`1 items)+MoveNext() in /src/InputDataModel.Autodictor/DataProviders/ByRuleDataProviders/Rules/ViewRule.cs:line 122
at InputDataModel.Autodictor.DataProviders.ByRuleDataProviders.ByRulesDataProvider.ViewRuleSendData(Rule rule, List`1 takesItems) in /src/InputDataModel.Autodictor/DataProviders/ByRuleDataProviders/ByRulesDataProvider.cs:line 248
at InputDataModel.Autodictor.DataProviders.ByRuleDataProviders.ByRulesDataProvider.StartExchangePipeline(InDataWrapper`1 inData) in /src/InputDataModel.Autodictor/DataProviders/ByRuleDataProviders/ByRulesDataProvider.cs:line 193
at Exchange.Base.ExchangeUniversal`1.SendingPieceOfData(InDataWrapper`1 inData, CancellationToken ct) in /src/Exchange.Base/ExchangeUniversal.cs:line 453

Довольно большой стек вызовов, т.к. работа сервиса была оттестирована и перехват исключения произошел на самом "верху".

Как только возникло исключение, оно стало появляться постоянно, как будто "поломался" какой-то совместно используемый разными потоками ресурс.

Пакет NCalc вычисляет строковое представления математического выражения https://github.com/sklose/NCalc2

Я его использую для вычисления переменной номера строки и вставки его по формату. Например заданна формула для вычисления rowNumber "rowNumber+10-25" и результат преобразовать обратно в строку по формату "X2"

Много потоков, одновременно используют эти вычисления, через вызов HelperString.StringTemplateInsert(...); В методе CalculateMathematicFormat(...) Expression - это и есть экземпляр класса в библиотеке NCalc.

public static class HelperString
{
    /// Вставка переменных (по формату) в строку по шаблону. 
    /// </summary>
    /// <param name="template">базовая строка с местозаполнителем</param>
    /// <param name="dict">словарь переменных key= название переменной val= значение</param>
    /// <param name="pattern">Как выдедить переменную и ее формат, по умолчанию {val:format}</param>
    /// <returns></returns>
    public static string StringTemplateInsert(string template, Dictionary<string, object> dict, string pattern = @"\{(.*?)(:.+?)?\}")
    {
        string Evaluator(Match match)
        {
            string res;
            var key = match.Groups[1].Value;
            if (key.Contains("rowNumber"))
            {
                var replacement = dict["rowNumber"];
                var calcVal = CalculateMathematicFormat(key, (int)replacement);
                var formatValue = match.Groups[2].Value;
                var format = "{0" + formatValue + "}";
                res = string.Format(format, calcVal);
            }
            else
            {
                res = match.Value;
            }
            return res;
        }
        var result = Regex.Replace(template, pattern, Evaluator);
        return result;
    }

    private static int CalculateMathematicFormat(string str, int row)
    {
        var expr = new Expression(str)
        {
            Parameters = { ["rowNumber"] = row }
        };
        var func = expr.ToLambda<int>();
        var arithmeticResult = func();
        return arithmeticResult;
    }
}

Трассировка лога указывает что произошло внутренне исключение из-за одновременного доступа к Dictionary.

Внутри NCalc есть static метод Compile, который внутри себя, возможно, использует Dictionary Из разных потоков он будет вызываться и возможно работать с Dictionary что и приводит к исключению.

public static LogicalExpression Compile(string expression, bool nocache);

// Type: NCalc.Expression
// Assembly: NCalc, Version=3.5.0.2, Culture=neutral, PublicKeyToken=null
// MVID: 20EB71AB-FAFA-4DD3-BB8B-E5838F6706A1
// Assembly location: C:\Users\Admin\.nuget\packages\coreclr-ncalc\2.2.51\lib\netstandard2.0\NCalc.dll
using NCalc.Domain;
using System;
using System.Collections;
using System.Collections.Generic;
namespace NCalc
{
  public class Expression
  {
    protected string OriginalExpression;
    protected Dictionary<string, IEnumerator> ParameterEnumerators;
    protected Dictionary<string, object> ParametersBackup;
    public EvaluateOptions Options { get; set; }
    public Expression(string expression);
    public Expression(string expression, EvaluateOptions options);
    public Expression(LogicalExpression expression);
    public Expression(LogicalExpression expression, EvaluateOptions options);
    public static bool CacheEnabled { get; set; }
    public static LogicalExpression Compile(string expression, bool nocache);
    public bool HasErrors();
    public string Error { get; private set; }
    public Exception ErrorException { get; private set; }
    public LogicalExpression ParsedExpression { get; private set; }
    public System.Func<TResult> ToLambda<TResult>();
    public Func<TContext, TResult> ToLambda<TContext, TResult>() where TContext : class;
    public object Evaluate();
    public event EvaluateFunctionHandler EvaluateFunction;
    public event EvaluateParameterHandler EvaluateParameter;
    public Dictionary<string, object> Parameters { get; set; }
  }
}

Я попытался воспроизвести ошибку у себя, создав простое консольное приложение, но ошибку не воспроизвел, все отработало корректно. Так ли я понял причину Exception?

class Program
{
    static async Task Main(string[] args)
    {
        List<int> results = new List<int>();
        for (int i = 0; i < 10000; i++)
        {
            var t1 = Task<int>.Factory.StartNew(() =>
            {
                var res = CalculateMathematicFormat("rowNumber + 10", 15);
                return res;
            });
            var t2 = Task<int>.Factory.StartNew(() =>
            {
                var res = CalculateMathematicFormat("rowNumber + 10 * rowNumber % 2", 1285);
                return res;
            });
            var t3 = Task<int>.Factory.StartNew(() =>
            {
                var res = CalculateMathematicFormat("rowNumber + 10 * rowNumber % 2", 1285);
                return res;
            });
            var t4 = Task<int>.Factory.StartNew(() =>
            {
                var res = CalculateMathematicFormat("rowNumber + 10 * rowNumber % 2", 1263285);
                return res;
            });
            var t5 = Task<int>.Factory.StartNew(() =>
            {
                var res = CalculateMathematicFormat("rowNumber + 10 * rowNumber % 2", 453);
                return res;
            });
            var t6 = Task<int>.Factory.StartNew(() =>
            {
                var res = CalculateMathematicFormat("rowNumber + 10 * rowNumber % 2", 896);
                return res;
            });
            try
            {
              var array = await Task.WhenAll(t1, t2, t3, t4, t5, t6);
              results.AddRange(array);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
        }
    }
    private static int CalculateMathematicFormat(string str, int row)
    {
        var expr = new Expression(str)
        {
            Parameters = { ["rowNumber"] = row }
        };
        var func = expr.ToLambda<int>();
        var arithmeticResult = func();
        return arithmeticResult;
    }
}
READ ALSO
Как получить время выполнения теста?

Как получить время выполнения теста?

Всем привет, выполняю свои ui-тесты (C# + Selenium + NUnit)Необходимо получить значение - Время выполнения теста

121
Ожидание полной загрузки файла webclient C#

Ожидание полной загрузки файла webclient C#

Есть код, который скачивает список изображений по полученным ссылкам и конвертирует их для уменьшения размера:

113