Что не так с этой оптимистичной реализацией параллелизма?

Я попытался реализовать оптимистичный параллелизм «работника».

Цель состоит в том, чтобы прочитать пакет данных из одной и той же таблицы базы данных (одна таблица без отношений) с несколькими параллельными «рабочими». Это, похоже, работало до сих пор. Я получаю оптимистичные исключения параллелизма здесь и там, ловлю их и повторяю попытку.

Пока все хорошо, и функция получения данных стабильно работает на моей локальной установке. Однако при перемещении приложения в тестовую среду я получаю странное исключение тайм-аута, которое, даже если оно будет поймано, завершит асинхронную функцию (разорвет цикл while). Кто-то видит недостаток в реализации? Что может вызвать тайм-аут? Что может привести к завершению асинхронной функции?

public async IAsyncEnumerable<List<WorkItem>> LoadBatchedWorkload([EnumeratorCancellation] CancellationToken token, int batchSize, int runID)
{
    DataContext context = null;
    try
    {
        context = GetNewContext(); // create a new dbContext
        List<WorkItem> workItems;
        bool loadSuccessInner;
        while (true)
        {
            if (token.IsCancellationRequested) break;

            loadSuccessInner = false;

            context.Dispose();
            context = GetNewContext(); // create a new dbContext

            RunState currentRunState = context.Runs.Where(a => a.Id == runID).First().Status;

            try
            {
                // Error happens on the following line: Microsoft.Data.SqlClient.SqlException: Timeout
                workItems = context.WorkItems.Where(a => a.State == ProcessState.ToProcess).Take(batchSize).ToList();
                loadSuccessInner = true;
            }
            catch (Exception ex)
            {
                workItems = new List<WorkItem>();
            }

            if (workItems.Count == 0 && loadSuccessInner)
            {
                break;
            }

            //... update to a different RunState
            //... if set successful yield the result
            //... else cleanup and retry
        }
    }
    finally
    {
        if (context != null) context.Dispose();
    }
}
  • Я проверил, что EntityFramework (здесь с адаптером MS SQL Server) выполняет полный запрос на стороне сервера, который преобразуется в простой запрос, подобный этому: SELECT TOP 10 field_1, field_2 FROM WorkItems WHERE field_2 = 0

  • Запрос обычно занимает ‹1 мс, а время ожидания по умолчанию составляет 30 с.

  • Я убедился, что запросов на отмену не было.

  • Это также происходит, когда есть только один рабочий процесс, и никто другой не обращается к базе данных. Я знаю, что тайм-аут может произойти, когда ресурс занят или заблокирован. Но до сих пор я никогда не видел тайм-аут ни в одном другом запросе.


person Peter Pakre    schedule 22.07.2020    source источник
comment
Я бы поставил Task.Delay где-нибудь в цикле, чтобы не заливать сервер   -  person Bizhan    schedule 22.07.2020
comment
Да, было сделано, позже, когда данные не возвращаются, возникает двухсекундная задержка. Но спасибо (:   -  person Peter Pakre    schedule 22.07.2020


Ответы (1)


(Я буду обновлять этот ответ всякий раз, когда будет предоставляться дополнительная информация.)

Кто-то видит недостаток в реализации?

В целом, ваш код выглядит нормально.

Что может привести к завершению асинхронной функции?

Ничто в показанном вами коде не должно обычно вызывать проблемы. Начните с помещения в цикл еще одного блока try-catch, чтобы гарантировать, что никакие другие исключения не будут выброшены где-либо еще (особенно позже в не показанном коде):

public async IAsyncEnumerable<List<WorkItem>> LoadBatchedWorkload([EnumeratorCancellation] CancellationToken token, int batchSize, int runID)
{
    DataContext context = null;
    try
    {
        context = GetNewContext();
        List<WorkItem> workItems;
        bool loadSuccessInner;
        while (true)
        {
            try
            {
                // ... (the inner loop code)
            }
            catch (Exception e)
            {
                // TODO: Log the exception here using your favorite method.
                throw;
            }
        }
    }
    finally
    {
        if (context != null) context.Dispose();
    }
}

Взгляните на свой журнал и убедитесь, что в журнале не отображаются какие-либо исключения. Затем дополнительно запишите все возможные условия выхода (break и return) из цикла, чтобы выяснить, как и почему код выходит из цикла.

Если в вашем коде нет других операторов break или return, то код может выйти из цикла только в том случае, если из базы данных будет успешно возвращено ноль workItems.

Что может вызвать тайм-аут?

Убедитесь, что любые Task возвращающие/async методы, которые вы вызываете, вызываются с использованием await.


Чтобы отследить, откуда на самом деле берутся исключения, вы должны развернуть выпуск Debug с pdb файлами, чтобы получить полную трассировку стека со ссылками на строки исходного кода.

Вы также можете самостоятельно реализовать команды DbCommandInterceptor и трассировки ошибок:

public class TracingCommandInterceptor : DbCommandInterceptor
{
    public override void CommandFailed(DbCommand command, CommandErrorEventData eventData)
    {
        LogException(eventData);
    }

    public override Task CommandFailedAsync(DbCommand command, CommandErrorEventData eventData, CancellationToken cancellationToken = new CancellationToken())
    {
        LogException(eventData);
        return Task.CompletedTask;
    }

    private static void LogException(CommandErrorEventData eventData)
    {
        if (eventData.Exception is SqlException sqlException)
        {
            // -2 = Timeout error
            // See https://docs.microsoft.com/en-us/previous-versions/sql/sql-server-2008-r2/cc645611(v=sql.105)?redirectedfrom=MSDN
            if (sqlException.Number == -2)
            {
                var stackTrace = new StackTrace();
                var stackTraceText = stackTrace.ToString();

                // TODO: Do some logging here and output the stackTraceText
                //       and other helpful information like the command text etc.
                // -->
            }
        }
    }
}

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.UseLoggerFactory(LoggingFactory);
    optionsBuilder.UseSqlServer(connectionString);
    optionsBuilder.EnableSensitiveDataLogging();
    optionsBuilder.EnableDetailedErrors();

    // Add the command interceptor.
    optionsBuilder.AddInterceptors(new TracingCommandInterceptor());

    base.OnConfiguring(optionsBuilder);
}

Кроме того, хорошей идеей является регистрация текста неудачной команды в перехватчике.

person lauxjpn    schedule 22.07.2020
comment
Спасибо! Обернул весь внутренний цикл while и проверил. Результат тот же. Функция заканчивается. В исходном коде есть регистратор в части исключения. Я получаю два журнала, один из регистратора базы данных и один из пойманного исключения. Первый [2020-07-22 12:14:40Z - ERRR]: Microsoft.EntityFrameworkCore.Database.Command и второй [2020-07-22 12:14:40Z - ERRR]: Microsoft.EntityFrameworkCore.Query Я рад поделиться полным кодом - person Peter Pakre; 22.07.2020
comment
Конечно, если вы можете поделиться кодом, просто загрузите его на github или куда-нибудь еще. Я посмотрю на это. - person lauxjpn; 22.07.2020
comment
Сорри! Мне потребовалось довольно много времени, чтобы очистить проект. Скоро будет добавлен / улучшенный файл readme и небольшие изменения форматирования. Это в основном весь соответствующий код, работает локально, не может сказать, создает ли он ту же ошибку на сервере. - person Peter Pakre; 26.07.2020
comment
Я посмотрю на это. Каково полное сообщение и трассировка стека перехваченного исключения после добавления блока try-catch? - person lauxjpn; 26.07.2020
comment
Спасибо, очень ценю это! Добавлен небольшой журнал, который существует, и полный журнал сеанса. Пожалуйста, имейте в виду, что это не 100% оригинальный код. Я постарался максимально приблизить его к оригиналу. Меня действительно беспокоит вопрос, почему исключение всегда возникает в этом простом операторе выбора и всегда после обработки первого пакета. Самое близкое, что я мог воссоздать, - это отключить сервер sql. Или, может быть, это просто ошибка дампа /: - person Peter Pakre; 26.07.2020
comment
Возможно, еще одно важное обновление. Напечатанные исключения не поступают из блока try and catch! Это сообщение ERRR, сообщение об исключении было бы напечатано как VERB. - person Peter Pakre; 26.07.2020
comment
Я добавил еще один раздел к ответу. - person lauxjpn; 26.07.2020
comment
Тай, ... поскольку почти невозможно ответить на глупые вопросы, я приму ваш ответ, поскольку он уже имеет некоторую ценность (: я мог бы добавить обновление в будущем. - person Peter Pakre; 27.07.2020