Использование Wt::Dbo вызывает утечку подготовленного оператора MySQL

У меня есть проект, который использует Wt::Dbo в качестве управления объектно-реляционной базой данных с MySQL. Примерно через неделю я заметил утечку в подготовленном отчете базы данных. Ранее в этом проекте использовался SQLite.

Я пробовал разные flush() безуспешно и не могу точно понять, что вызывает эту утечку, но наверняка, когда подготовленные операторы растут в определенный момент, MySQL перестает отвечать.

Вот как я отслеживаю утечку подготовленного оператора:

$ mysql -uroot -p -e "SHOW SESSION STATUS LIKE '%prepare%';" | grep stmt_count
Enter password: 
Prepared_stmt_count 260

Утечка заявления очищается при перезапуске программы.

Все операции с базой данных централизованы внутри класса с именем DataBase, вот некоторые функции, которые известны утечкой:

DataBase::initialize()

void DataBase::initialize(void)
{
    m_oMutex.lock();

    if(!m_bInitialized)
    {
        m_bInitialized = true;

        try
        {
            m_oSessionConfigurations.setConnection(*new Wt::Dbo::backend::Sqlite3("databases/configuration.db"));
            m_oSessionConfigurations.mapClass<InformationSite>("informationsite");

            m_oSessionConfigurations.createTables();
        }
        catch(std::exception &e)
        {
            ajouterLog("error",std::string("DataBase::initialize() : ") + e.what());
        }

        try
        {
            #if defined(DATABASE_TYPE_SQLITE)
                Wt::Dbo::backend::Sqlite3 *pBackend = new Wt::Dbo::backend::Sqlite3("databases/dataBase.db");
            #elif defined(DATABASE_TYPE_MYSQL)
                Wt::Dbo::backend::MySQL *pBackend;

                try
                {
                    pBackend = new Wt::Dbo::backend::MySQL(DATABASE_NAME,DATABASE_USERNAME,DATABASE_PASSWORD,"localhost");
                }
                catch(Wt::Dbo::Exception &e)
                {
                    ajouterLog("error",std::string("DataBase::initialize() : ") + e.what());

                    // If MySQL is not available, this cause issue to program until restart.
                    exit(1);
                }
            #endif

            pBackend->setProperty("show-queries","true");

            m_oSession.setConnection(*pBackend);

            m_oSession.setFlushMode(Wt::Dbo::FlushMode::Auto);

            m_oSession.mapClass<RFNode>("rfnode");
            m_oSession.mapClass<NodeMeasure>("nodemeasure");

            // Override the default InnoDB from Wt, MyISAM is easier to repair in case of hardware failure with database corruption
            #if defined(DATABASE_TYPE_MYSQL)
                try
                {
                    Wt::Dbo::Transaction oTransaction(m_oSession);

                    m_oSession.execute("SET default_storage_engine=MYISAM;");

                    oTransaction.commit();
                }
                catch(Wt::Dbo::Exception &e)
                {
                    ajouterLog("error",std::string("DataBase::initialize() : ") + e.what());
                }
            #endif

            m_oSession.createTables();
        }
        catch(Wt::Dbo::Exception &e)
        {
            ajouterLog("error",std::string("DataBase::initialize() : ") + e.what());
        }
    }

    m_oMutex.unlock();
}

DataBase::addNodeMeasure()

void DataBase::addNodeMeasure(NodeMeasure *p_pItem)
{
    m_oMutex.lock();

    try
    {
        Wt::Dbo::Transaction oTransaction(m_oSession);

        Wt::Dbo::ptr<NodeMeasure> oItem = m_oSession.add(p_pItem);

        oItem.flush();

        oTransaction.commit();
    }
    catch(std::exception &e)
    {
        ajouterLog("error",std::string("Exception DataBase::addNodeMeasure() : ") + e.what());
    }

    m_oMutex.unlock();

    printPreparedStatementCount("DataBase::addNodeMeasure()");
}

База_данных::updateNode()

void DataBase::updateNode(RFNode *p_pItem)
{
    printPreparedStatementCount("DataBase::updateNode() Before");

    m_oMutex.lock();

    try
    {
        Wt::Dbo::Transaction oTransaction(m_oSession);

        Wt::Dbo::ptr<RFNode> oItem = m_oSession.find<RFNode>().where("mac = ?").bind(p_pItem->mac);

        oItem.modify()->zone                    = p_pItem->zone;
        oItem.modify()->subZone                 = p_pItem->subZone;
        oItem.modify()->unit                    = p_pItem->unit;
        oItem.modify()->pwm                     = p_pItem->pwm;
        oItem.modify()->led                     = p_pItem->led;
        oItem.modify()->network                 = p_pItem->network;
        oItem.modify()->lastContact             = p_pItem->lastContact;
        oItem.modify()->ioConfiguration         = p_pItem->ioConfiguration;
        oItem.modify()->networkAddress          = p_pItem->networkAddress;
        oItem.modify()->type                    = p_pItem->type;
        oItem.modify()->functionality           = p_pItem->functionality;
        oItem.modify()->transmitPowerLevel      = p_pItem->transmitPowerLevel;
        oItem.modify()->lastNetworkRoute        = p_pItem->lastNetworkRoute;
        oItem.modify()->lastNetworkJumpsCount   = p_pItem->lastNetworkJumpsCount;
        oItem.modify()->lastRequestDuration     = p_pItem->lastRequestDuration;
        oItem.modify()->hardwareVersion         = p_pItem->hardwareVersion;
        oItem.modify()->softwareVersion         = p_pItem->softwareVersion;

        oItem.flush();

        oTransaction.commit();
    }
    catch(std::exception &e)
    {
        ajouterLog("error",std::string("Exception DataBase::updateNode() : ") + e.what());
    }

    m_oMutex.unlock();

    printPreparedStatementCount("DataBase::updateNode() After");
}

DataBase::getNodeMeasures()

std::vector<NodeMeasure> DataBase::getNodeMeasures(std::string p_sMAC, int p_nType, Wt::WDateTime p_oStartDate, Wt::WDateTime p_oEndDate, std::string p_sOrder, int p_nLimit)
{
    std::vector<NodeMeasure> lNodeMeasures;

    m_oMutex.lock();

    try
    {
        Wt::Dbo::Transaction oTransaction(m_oSession);

        std::string sWhereClause = "", sOrderClause = "";

        if(!p_sMAC.empty())
        {
            if(!sWhereClause.empty())
            {
                sWhereClause += " AND ";
            }

            sWhereClause += "mac = '" + p_sMAC + "'";
        }

        if(p_nType != -1)
        {
            if(!sWhereClause.empty())
            {
                sWhereClause += " AND ";
            }

            sWhereClause += "type = " + std::to_string(p_nType);
        }

        if(p_oStartDate.isValid())
        {
            if(!sWhereClause.empty())
            {
                sWhereClause += " AND ";
            }

            // When not using type, we usually want nodes measures (not external temperature), so we want to find them using batchDate instead of date
            sWhereClause += (p_nType != -1 ? "date" : "batchDate");
            sWhereClause += " >= '";
            sWhereClause += p_oStartDate.toString("yyyy-MM-ddTHH:mm:ss").toUTF8();
            sWhereClause += "'";
        }

        if(p_oEndDate.isValid())
        {
            if(!sWhereClause.empty())
            {
                sWhereClause += " AND ";
            }

            // When not using type, we usually want nodes measures (not external temperature), so we want to find them using batchDate instead of date
            sWhereClause += (p_nType != -1 ? "date" : "batchDate");
            sWhereClause += " <= '";
            // Add one second because SQLite have microseconds, and we must include results no matter microseconds field
            sWhereClause += p_oEndDate.addSecs(1).toString("yyyy-MM-ddTHH:mm:ss").toUTF8();
            sWhereClause += "'";
        }

        if(!p_sOrder.empty())
        {
            sOrderClause = " ORDER BY " + p_sOrder;
        }

        std::string sQuery = "";

        if(!sWhereClause.empty())
        {
            sQuery += " WHERE ";
            sQuery += sWhereClause;
        }

        if(!sOrderClause.empty())
        {
            sQuery += sOrderClause;
        }

        //std::cout << "**************************************************************************" << std::endl;
        //std::cout << sQuery << std::endl;
        //Wt::WDateTime oStart = Wt::WDateTime::currentDateTime();

        if(Configuration::getParameter(Configuration::PARAMETER_DEBUG).getBooleanValue())
        {
            ajouterLog("debug","DataBase::getNodeMeasures() " + sQuery);
        }

        // TEST : find vs query
        Wt::Dbo::collection<Wt::Dbo::ptr<NodeMeasure>> lMeasures = m_oSession.find<NodeMeasure>(sQuery).limit(p_nLimit).resultList();

        // TODO : Get it cleaner... can't use Wt::Dbo::ptr outside transaction.
        for(Wt::Dbo::collection<Wt::Dbo::ptr<NodeMeasure>>::const_iterator pMeasure = lMeasures.begin();pMeasure != lMeasures.end();pMeasure++)
        {
            lNodeMeasures.push_back(
                    NodeMeasure(
                            (*pMeasure)->mac,
                            (*pMeasure)->type,
                            (*pMeasure)->date,
                            (*pMeasure)->batchDate,
                            (*pMeasure)->value
                    )
            );

            (*pMeasure).flush();
        }

        //lNodeMeasures = m_oSession.find<NodeMeasure>(sQuery).limit(p_nLimit).resultList();

        //std::cout << "Result : " << lNodeMeasures.size() << " in " << oStart.secsTo(Wt::WDateTime::currentDateTime()) << "s" << std::endl;
        //std::cout << "**************************************************************************" << std::endl;

        oTransaction.commit();
    }
    catch(std::exception &e)
    {
        ajouterLog("error",std::string("Exception DataBase::getNodeMeasures() : ") + e.what());
    }

    m_oMutex.unlock();

    printPreparedStatementCount("DataBase::getNodeMeasures()");

    return lNodeMeasures;
}

person Alexandre Lavoie    schedule 29.03.2017    source источник


Ответы (1)


Размер операторов выходит из-под контроля? Wt::Dbo кэширует все операторы в объекте MySQL SqlConnection и очищает их при уничтожении. Поскольку приложения часто используют пул соединений, например. Размер 10, количество подготовленных операторов часто кратно количеству операторов, которые вы используете.

Поскольку вы, кажется, составляете свои операторы как текст ascii, а не используете привязку параметров, вы действительно можете получить много операторов в своем кеше.

Dbo поддерживает такой стиль кода:

    auto measuresQuery = m_oSession.find<NodeMeasure>(sQuery).limit(p_nLimit);
    if(!p_sMAC.empty())
    {
        measureQuery.where("mac = ?").bind(p_sMAC);
    }

    if(p_nType != -1)
    {
        measureQuery.where("type = ?").bind(std::to_string(p_nType));
    }
    if(p_oStartDate.isValid())
    {
        // When not using type, we usually want nodes measures (not external temperature), so we want to find them using batchDate instead of date
        measureQuery.where(p_nType != -1 ? "date >= ?" : "batchDate >= ?").bind(p_oStartData);
    }
    if(!p_sOrder.empty())
    {
        measureQuery.orderBy(p_sOrder);
    }

    Wt::Dbo::collection<Wt::Dbo::ptr<NodeMeasure>> lMeasures = measureQuery.resultList();

Это даст меньшее количество подготовленных операторов и, на мой взгляд, более короткий, ясный и безопасный код.

person user52875    schedule 06.04.2017
comment
На самом деле я использую только одно соединение, оно живет с начала работы приложения до краха или перезагрузки, 24/7. Операторы могут расти до тех пор, пока MySQL не перестанет отвечать должным образом (около 5000-10000). Есть ли способ отключить этот кеш? Я никогда не видел, чтобы какой-либо другой фреймворк делал это! - person Alexandre Lavoie; 10.04.2017
comment
Что касается where и bind, я использовал это давным-давно, но не сработало, как ожидалось, я попробую еще раз. - person Alexandre Lavoie; 10.04.2017
comment
Кэш нельзя отключить. Существует защищенный метод очистки кеша (clearStatementCahce()) в MySqlConnection, но я не думаю, что это правильный ответ на эту проблему. Я могу только порекомендовать действительно использовать подготовленные операторы и параметры привязки. Ваш код будет более производительным (сервер sql оптимизирует каждый повторно используемый запрос только один раз) и, что более важно, будет защищен от SQL-инъекций, что является одним из самых серьезных недостатков безопасности в веб-приложениях. - person user52875; 11.04.2017
comment
Я согласен с безопасностью, но поскольку никакие параметры не поступают из форм или URL-адресов или чего-либо еще (в основном автоматизированная обработка), это не было проблемой. Я изменил большинство жестко закодированных запросов, и на данный момент утечек стало меньше. Проверка на другой системе! - person Alexandre Lavoie; 11.04.2017