Баги штатной (amxx) mysql - mysqlx (multi-statement -> drop connections)

Refresh

Скриптер
Участник
Сообщения
60
Реакции
14
Тестовая запись, чтобы не забыть.. а то пока я закончу править исходники, забуду с чего начиналось.. хотя я и так забыл, зачем я полез на сервер СУБД...

Вспомнил. Начала падать MariaDB, после случайного обновления до 10.11.6... Практически с любыми настройками в конфиге вы гарантированно получаете OOM Kill из-за SEGV в ha_maria::drop_table. Обновление MariaDB до 10.11.18 решило проблему... Однако черт меня надоумил выполнить команду:

C-подобный:
journalctl -u mariadb --since "2 hour ago" | grep  "62.122.213.230"
C-подобный:
Sep 25 12:35:36 *.academy-cs.ru mariadbd[4755]: 2026-09-25 12:35:36 167662 [Warning] Aborted connection 167662 to db: 'db_msk_stats' user: '***SECRET***' host: '62.122.213.230' (Unknown error)
Sep 25 12:35:36 *.academy-cs.ru mariadbd[4755]: 2026-09-25 12:35:36 167664 [Warning] Aborted connection 167664 to db: 'db_msk_stats' user: '***SECRET***' host: '62.122.213.230' (Got an error writing communication packets)
Sep 25 12:36:23 *.academy-cs.ru mariadbd[4755]: 2026-09-25 12:36:23 168037 [Warning] Aborted connection 168037 to db: 'db_msk_stats' user: '***SECRET***' host: '62.122.213.230' (Unknown error)
Sep 25 12:37:14 *.academy-cs.ru mariadbd[4755]: 2026-09-25 12:37:14 168576 [Warning] Aborted connection 168576 to db: 'db_msk_stats' user: '***SECRET***' host: '62.122.213.230' (Unknown error)

Сейчас тут остались только следы от builk-insert-ов статистики, поскольку в своем модуле я уже все пофиксил, но первоначально я тут увидел и следы своих "служебных" таблиц, что и заставило меня дальше разбираться с этой проблемой.

ССУТЬ ПРОБЛЕМЫ:
Изначально мой модуль refsapi и штатный mysqlx некорректно закрывали соединения после потоковых запросов, типа: insert ..; insert ...; insert ...; или update ...; update ...; update ...; либо ЛЮБОЙ другой набор multi-statement запросов (insert ...; select ...; delete ...; update ...; - и все в одном запросе).

1790336832972.png

Так вот эти запросы формируют в памяти драйвера СУБД наборы result-set, которые "по спецификации" - вы ОБЯЗАНЫ ВСЕ ПРОЧИТАТЬ до закрытия соединения. Поскольку я спецификации читаю редко, только когда что-то сломалось или не получается сделать "по наитию", то я этого не знал, как и, очевидно, разработчики mysqlx. Никогда в жизни я такой фигней не занимался, и всегда делал просто - mysql_close(). И все, дальше не мои проблемы.

Более того, у меня еще есть режим асинхронной работы с базой (это чтобы супер-шустро было) - и там я ОБЯЗАН, не только прочитать все result-set-ы, но еще и прочитать в них все строки 🙄 Ну ладно, у себя я поправил.. Теперь будем править mysqlx.. последней версии с github.

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

В общем он даже сделал попытку корректного чтения всех наборов result-set в деструкторе:
C-подобный:
MysqlResultSet::~MysqlResultSet()
{
    if (m_pRes == NULL)
    {
        return;
    }

    mysql_free_result(m_pRes);
    while (mysql_next_result(m_pMySQL) == 0)
    {
        m_pRes = mysql_store_result(m_pMySQL);
        if (m_pRes != NULL)
        {
            mysql_free_result(m_pRes);
        }
    }
}

Но это не помогло, потому что стата открывает "1 подключение" на несколько секунд и юзает его "в хвост и в гриву", выполняя multi-statmen-запросы чанками/пачками. И к моменту прихода в деструктор m_pMySQL может содержать m_pRes от совсем другого запроса.

В общем смысл в том, что поскольку в коде mysqlx тоже через жопу реализована работа с multi-statmen-запросами, считаем, что все они это bulk-(update или insert), и нам не важны наборы данных (кроме первого), присылаемые драйвером СУБД, нам главное корректно все это отработать "по спецификации" и корректно закрыть соединения, чтобы вот в этой команде:
C-подобный:
SET GLOBAL general_log = 1;
на стороне MariaDB, мы увидили в логах правильную цепочку для всех запросов: connect + запрос + quit.

Поэтому мы лезем в код, быстренько разбираемся, что нам нужна MysqlQuery::ExecuteR(), после чего лезем туда и на манер моей (::get_result()) вносим аналогичные изменения, поскольку закрывать multi-statmen наборы данных -> нужно именно здесь, а не в деструкторе. Почему? Потому!

Было:
C-подобный:
bool MysqlQuery::ExecuteR(QueryInfo *info, char *error, size_t maxlength)
{
    int err;

    if ( (err=mysql_real_query(m_pDatabase->m_pMysql, m_QueryString, (unsigned long)m_QueryLen)) )
    {
        info->errorcode = mysql_errno(m_pDatabase->m_pMysql);
        info->success = false;
        info->affected_rows = 0;
        info->rs = NULL;
        if (error && maxlength)
        {
            ke::SafeSprintf(error, maxlength, "%s", mysql_error(m_pDatabase->m_pMysql));
        }
    }
    else
    {
        MYSQL_RES *res = mysql_store_result(m_pDatabase->m_pMysql);
        if (!res)
        {
            if (mysql_field_count(m_pDatabase->m_pMysql) > 0)
            {
                //error !111!!11
                info->errorcode = mysql_errno(m_pDatabase->m_pMysql);
                info->success = false;
                info->affected_rows = 0;
                info->rs = NULL;
            } else {
                info->errorcode = 0;
                info->success = true;
                info->affected_rows = mysql_affected_rows(m_pDatabase->m_pMysql);
                info->rs = NULL;
            }
        } else {
            info->errorcode = 0;
            info->success = true;
            info->affected_rows = mysql_affected_rows(m_pDatabase->m_pMysql);
            MysqlResultSet *rs = new MysqlResultSet(res, m_pDatabase->m_pMysql);
            info->rs = rs;
        }
    }

    return info->success;
}

Правим еще 2 косячка попутно.. Получаем:

MysqlQuery.cpp
C-подобный:
bool MysqlQuery::ExecuteR(QueryInfo *info, char *error, size_t maxlength)
{
    MYSQL *mysql = m_pDatabase->m_pMysql;

    // При срабатывании MYSQL_OPT_RECONNECT кодировка соединения сбрасывается.
    // Проверяем, не было ли тихого реконнекта, и при необходимости восстанавливаем её.
    m_pDatabase->EnsureConnection();

    int err;
    if ((err = mysql_real_query(mysql, m_QueryString, (unsigned long)m_QueryLen)))
    {
        info->errorcode = mysql_errno(mysql);
        info->success = false;
        info->affected_rows = 0;
        info->rs = NULL;
        if (error && maxlength)
        {
            ke::SafeSprintf(error, maxlength, "%s", mysql_error(mysql));
        }
        return false;
    }

    info->errorcode = 0;
    info->success = true;
    info->affected_rows = 0;
    info->rs = NULL;

    int status;

    // Обрабатываем результат первого стейтмента, затем ОБЯЗАТЕЛЬНО дренажируем
    // все остальные. Без этого при CLIENT_MULTI_STATEMENTS непрочитанные
    // ответы остаются в буфере, и следующий запрос получает
    // 2014 "Commands out of sync".
    do
    {
        MYSQL_RES *res = mysql_store_result(mysql);
        if (res)
        {
            // Первый встретившийся результирующий набор отдаём вызывающему,
            // остальные — освобождаем, чтобы не оставлять их в соединении.
            if (info->rs == NULL)
            {
                info->rs = new MysqlResultSet(res, mysql);
            }
            else
            {
                mysql_free_result(res);
            }
            info->affected_rows += mysql_affected_rows(mysql);
        }
        else if (mysql_field_count(mysql) == 0)
        {
            // Стейтмент без результирующего набора (INSERT / UPDATE / DELETE и т.п.).
            // Это штатная ситуация: накапливаем затронутые строки.
            info->affected_rows += mysql_affected_rows(mysql);
        }
        else
        {
            // store_result вернул NULL при ненулевом числе полей — ошибка
            // получения результата (например, обрыв соединения или нехватка памяти).
            info->errorcode = mysql_errno(mysql);
            info->success = false;
            if (error && maxlength)
            {
                ke::SafeSprintf(error, maxlength, "%s", mysql_error(mysql));
            }
        }

        // Переходим к следующему результату.
        // 0 = есть ещё результат, -1 = результатов больше нет, >0 = ошибка.
        status = mysql_next_result(mysql);
        if (status > 0)
        {
            info->errorcode = mysql_errno(mysql);
            info->success = false;
            if (error && maxlength)
            {
                ke::SafeSprintf(error, maxlength, "%s", mysql_error(mysql));
            }
        }
    } while (status == 0);

    return info->success;
}

MysqlDatabase.h
C-подобный:
class MysqlDatabase : public IDatabase
{
    friend class MysqlQuery;
public:
    MysqlDatabase(MYSQL *mysql, MysqlDriver *drvr);
    ~MysqlDatabase();
public:
    void FreeHandle();
    ISQLDriver *Driver();
public:
    IQuery *PrepareQueryFmt(const char *fmt, ...);
    IQuery *PrepareQueryFmt(const char *fmt, va_list ap);
    IQuery *PrepareQuery(const char *query);
    int QuoteString(const char *str, char buffer[], size_t maxlen, size_t *newsize);
    bool SetCharacterSet(const char *characterset);

    // НОВОЕ: проверка на тихий авто-реконнект и восстановление кодировки.
    void EnsureConnection();
private:
    void Disconnect();
private:
    MYSQL *m_pMysql;
    MysqlDriver *m_pParent;

    // НОВОЕ: желаемая кодировка и thread_id на момент её установки.
    char m_charset[64];
    unsigned long m_threadId;
};

MysqlDatabase.cpp
C-подобный:
MysqlDatabase::MysqlDatabase(MYSQL *mysql, MysqlDriver *drvr)
    : m_pMysql(mysql), m_pParent(drvr)
{
    m_charset[0] = '\0';
    m_threadId = mysql_thread_id(m_pMysql);
}

bool MysqlDatabase::SetCharacterSet(const char *characterset)
{
    if (mysql_set_character_set(m_pMysql, characterset) == 0)
    {
        // Запоминаем кодировку и текущий thread_id, чтобы после
        // возможного авто-реконнекта уметь восстановить её.
        ke::SafeStrcpy(m_charset, sizeof(m_charset), characterset);
        m_threadId = mysql_thread_id(m_pMysql);
        return true;
    }
    return false;
}

void MysqlDatabase::EnsureConnection()
{
    unsigned long currentThreadId = mysql_thread_id(m_pMysql);
    if (currentThreadId != m_threadId)
    {
        // Сменился thread_id => библиотека тихо переподключилась.
        // При авто-реконнекте теряются: кодировка, сессионные переменные,
        // транзакции, prepared statements. Восстанавливаем хотя бы кодировку.
        m_threadId = currentThreadId;
        if (m_charset[0] != '\0')
        {
            mysql_set_character_set(m_pMysql, m_charset);
        }
    }
}

int MysqlDatabase::QuoteString(const char *str, char buffer[], size_t maxlen, size_t *newsize)
{
    unsigned long size = static_cast<unsigned long>(strlen(str));
    // Худший случай: каждый символ экранируется (2 байта) + нуль-терминатор.
    size_t needed = size * 2 + 1;
    if (maxlen < needed)
    {
        return static_cast<int>(needed);
    }

    // Используем хендл соединения: экранирование учитывает реальную
    // кодировку соединения (в отличие от mysql_escape_string).
    unsigned long result = mysql_real_escape_string(m_pMysql, buffer, str, size);
    if (result == static_cast<unsigned long>(-1))
    {
        return -1; // ошибка (например, некорректная кодировка)
    }
    if (newsize)
    {
        *newsize = static_cast<size_t>(result);
    }
    return 0;
}

void MysqlDatabase::Disconnect()
{
    if (m_pMysql)
    {
        mysql_close(m_pMysql);
        m_pMysql = NULL;
    }
}

Добавляем в начало MysqlDatabase.cpp
C-подобный:
#include <amtl/am-string.h>

Меняем тег в public/amxmodx_version.h
C-подобный:
#define AMXX_BUILD_TAG        "refs"

Компилируем "по спецификации". Бац, получили набор сошек и персональную редакцию:
C-подобный:
[ 8] AMX Mod X           RUN   -    amxmodx_mm_i386.so             v1.10.0-refs       ini  Start ANY

Во вложении *.so. По факту нужна только mysql_amxx_i386.so. Но я замутил свой релиз :cool:
Исходники тут: https://disk.yandex.ru/d/j0NCn9eXDHH2Zg

PS: Результат пока проверил только на тестовом.. Ошибки ушли. Завтра Оки "сломает" сервер МСК и будет понятно, работает или нет...🤣
 

Вложения

Последнее редактирование:

Кто просматривает тему

Назад
Верх