Приложение winforms c# 4.8, подключенное к Firebird 2.5 зависает, а сервер передает ошибку 10054

Недавно мы обновили версию приложения с Net 4.0 => 4.8, провайдер Firebird 2.1.0.0 => 2.7.0.0, заменили передачу типа транзакции FbTransactionOptions.ReadCommitted => System.Data.IsolationLevel.ReadCommited, а также вынесли выполнение крупных и продолжительных(5+ сек) select запросов в фоновые задачи. Теперь у некоторых пользователей стало навечно зависать приложение, оно поедает ресурсы и нет свойственного эффекта от зависания (Не отвечает), как будто происходит взаимоблокировка потоков, о чем также свидетельствуют дампы памяти зависших приложений. Причем зависание происходит не в момент непосредственной работы с приложением, а после непродолжительного простоя(от пары минут и более), когда им даже не пользовались. У кого-то при выходе из спящего режима, а у кото-то и без него, но уже после того, как все фоновые загрузки завершились. Таймеров и прочего, что могло запуститься в работу спустя время на формах нет. Сервер в логах в момент зависания регистрирует ошибку 10054, хотя он и до обновления регистрировал эти ошибки(неполадок ранее это не вызывало). Ошибка скорее всего связана с нестабильным сетевым подключением по неким ИЗВЕСТНЫМ причинам, но данная ошибка вызывает зависание лишь у некоторых пользователей(причем у них зависание происходит каждый раз при возникновении этой ошибки(на Windows 10, 11 и по-моему 7)).

Основное подключение к базе идет со включенным пулом потоков, оно открывается с момента запуска приложения и закрывается при выходе. Фоновые подключения идут с выключенным пулом потоков.

Вот пример фоновых загрузок:

CancellationTokenSource token = null;
public bool IsBusy => token != null;

private void SomeFunc()
{
    if (IsBusy)
        return;
    token = new CancellationTokenSource();
    Task.Run(() =>
    {
        FbConnection con = null;

        try
        {
            Action action = () =>
            {
                // Обязательные действия до выполнения полезной нагрузки
            };
            if (InvokeRequired)
                Invoke(action);
            else
                action();

            con = GetTempCon(); // Тут просто копируется текст подключения основного подключения, добавляется приставка Pooling=false и открывается соединение

            // Полезная нагрузка

            token.Token.ThrowIfCancellationRequested();

            if (this.IsHandleCreated)
            {
                action = () =>
                {
                    // Действия при успешном выполнении полезной нагрузки
                };
                if (InvokeRequired)
                    Invoke(action);
                else
                    action();
            }
        }
        catch (OperationCanceledException)
        {
            // Если операция отменена
            return;
        }
        catch (Exception exc)
        {
            // Обработка ошибок
        }
        finally
        {
            if (con != null)
            {
                if (con.State != ConnectionState.Closed)
                    con.Close();
                con.Dispose();
                con = null;
            }
            if (token != null)
            {
                token.Dispose();
                token = null;
            }
            if (this.IsHandleCreated)
            {
                Action action = () =>
                {
                    // Обязательные действия после выполнения полезной нагрузки
                };
                if (InvokeRequired)
                    Invoke(action);
                else
                    action();
            }
        }
    });
}

Мы пробовали откатить версию провайдера, тип транзакции и Net с заменой вызовов Task.Run на Task.Factory.StartNew, но ничего не помогло. В будущем мы планируем переход на новую версию Firebird, но решение нужно сейчас.


Ответы (1 шт):

Автор решения: aepot

Налицо плавающий дедлок, но глядя на обрезанный код без даже кода вызывающего метода, здесь мало что можно сказать. Для нормального анализа нужно всё: код вызова начиная с обработчика события или команды, заканчивая всеми дочерними функциями. В противном случае это угадайка.

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

private CancellationTokenSource cts = null;
public bool IsBusy => cts != null;

private async Task SomeFuncAsync()
{
    using (cts = new CancellationTokenSource())
    {
        // Обязательные действия с UI до выполнения полезной нагрузки
        try
        {
            await Task.Run(() =>
            {
                using (FbConnection con = GetTempCon())
                {
                    // Полезная нагрузка
                    cts.Token.ThrowIfCancellationRequested();
                    // Ещё полезная нагрузка
                }

                cts.Token.ThrowIfCancellationRequested();
            });

            // Действия с UI при успешном выполнении полезной нагрузки
        }
        catch (OperationCanceledException) { }
        catch (Exception exc)
        {
            // Обработка ошибок
        }
        // Обязательные действия с UI после выполнения полезной нагрузки
    }
    cts = null;
}

Вообще без инвоков.

Вызывающий код, позволяющий организовать асинхронный вызов, будет выглядеть так:

private async void Button_Click(object sender, EventArgs e)
{
    if (IsBusy)
        return;
    Button btn = (Button)sender;
    btn.Enabled = false;
    await SomeFuncAsync();
    btn.Enabled = true;
}

Такое исполнение обработчика события допустимо только если есть гарантия, что SomeFuncAsync не выбросит исключение ни при каких обстоятельствах. В противном случае, его требуется обернуть в try-catch с перехватом Exception.

Обратите внимание, что метод SomeFuncAsync предназначен только для вызова в контексте UI потока, то есть вот так await Task.Run(SomeFuncAsync) делать нельзя.

→ Ссылка