Project

General

Profile

Wt unexpected logout when clicking too fast

Added by Fil liF about 4 years ago

Hi,

I'm new to Wt and I'm experimenting with a small site inspired by the Hangman example.

I have two pages shown in the Wt::WStackedWidget.

One of them shows items of a database table but they are divided in pages in order to load only some items at a time.

So there is the Wt::WTableView plus two Wt::WPushButton to navigate to next/previous page.

When I click very fast and many times on theese buttons the page logout and I have to login again.

It may sound like excessive server load that rejects the client by logging it out but if I do the same on other unrelated buttons it doesn't happen.

So I think the problem is in the slot connected with clicked() signal of theese 2 buttons.

Here is the code:

Model header:

#ifndef SEARCHITEMMODEL_H
#define SEARCHITEMMODEL_H

#include <Wt/WAbstractTableModel.h>

#include <Wt/Dbo/Types.h>
#include <Wt/WGlobal.h>

namespace dbo = Wt::Dbo;

class Session;
class ExchangeableItem;

namespace Wt {
class WTimer;
}

class CustomModel : public Wt::WAbstractTableModel
{
public:
    enum Columns
    {
        Tipologia = 0,
        Colore,
        Taglia,
        Utente,
        NCols
    };

    static constexpr int ItemsForPage = 10;

    CustomModel(Session *s);
    ~CustomModel();

    virtual int rowCount(const Wt::WModelIndex& parent = Wt::WModelIndex()) const override;

    virtual int columnCount(const Wt::WModelIndex& parent = Wt::WModelIndex()) const override;

    virtual Wt::cpp17::any data(const Wt::WModelIndex& index, Wt::ItemDataRole role = Wt::ItemDataRole::Display) const override;

    virtual Wt::cpp17::any headerData(int section,
                                      Wt::Orientation orientation = Wt::Orientation::Horizontal,
                                      Wt::ItemDataRole role = Wt::ItemDataRole::Display) const override;

    void recalcSize();
    void loadPage(int page);

    inline Wt::Signal<>& pageChanged() { return pageChanged_; }

    inline int currentPage() const { return curPage; }
    inline int pageCount() const { return pageCount_; }

private:
    Session *session;
    Wt::WTimer *clearIterTimer;

    typedef dbo::collection<dbo::ptr<ExchangeableItem>> Items;
    Items items;
    Items::iterator cached_iter;
    int64_t cached_row;

    void clearCachedIter();

    std::chrono::steady_clock::time_point lastIterAccessed;

    int64_t totalItemsCount;
    int pageCount_;
    int curPage;

    Wt::Signal<> pageChanged_;
};
#endif // SEARCHITEMMODEL_H

Model implementation:

#include "searchitemmodel.h"

#include "session.h"

#include "exchangableitem.h"

#include <Wt/WTimer.h>

#include <cmath>

CustomModel::CustomModel(Session *s) :
    WAbstractTableModel(),
    session(s),
    clearIterTimer(nullptr),
    items(),
    cached_iter(items.end()),
    cached_row(-1),
    totalItemsCount(-1),
    pageCount_(-1),
    curPage(-1)
{
}

CustomModel::~CustomModel()
{
    if(clearIterTimer)
    {
        delete clearIterTimer;
        clearIterTimer = nullptr;
    }
}

int CustomModel::rowCount(const Wt::WModelIndex &parent) const
{
    return parent.isValid() ? 0 : items.size();
}

int CustomModel::columnCount(const Wt::WModelIndex &parent) const
{
    return parent.isValid() ? 0 : NCols;
}

Wt::cpp17::any CustomModel::data(const Wt::WModelIndex &index, Wt::ItemDataRole role) const
{
    Wt::cpp17::any ret;

    if (role != Wt::ItemDataRole::Display || index.row() >= int64_t(items.size()))
        return ret;

    int row = index.row();

    dbo::Transaction transaction(*session);
    dbo::ptr<ExchangeableItem> ptr;

    //NOTE: dbo::collection<> allows only forward iterators
    //This means that every time we should start from begin() (row 0)
    //and advance in a loop until we arrive to requested row (index.row())
    //Instead to provide better performance we cache the last iterator:
    //If quering the same row but different role/column we already have the iterator
    //If quering next row we just have to advance by 1
    //If quering for the first time or a row before cached then slow path of begin() and std::advance()

    CustomModel *that = const_cast<CustomModel *>(this);
    if(cached_row != -1 && cached_row == row - 1)
    {
        that->cached_iter++;
        that->cached_row++;
    }
    else
    {
        if(cached_row == -1 || cached_row > row)
        {
            that->cached_iter = that->items.begin();
            that->cached_row = 0;
        }

        if(row > cached_row)
        {
            std::advance(that->cached_iter, row - cached_row);
            that->cached_row = row;
        }
    }

    ptr = *that->cached_iter;

    if(ptr)
    {
        switch (index.column())
        {
        case Tipologia:
            ret = ptr->tipologia;
            break;
        case Colore:
            ret = ptr->colore;
            break;
        case Taglia:
            ret = ptr->taglia;
            break;
        case Utente:
            if(ptr->user)
                ret = ptr->user->getUserName();
            break;
        }
    }

    transaction.commit();

    if(!clearIterTimer)
    {
        that->clearIterTimer = new Wt::WTimer();
        that->clearIterTimer->setInterval(std::chrono::milliseconds(500));
        that->clearIterTimer->timeout().connect(that, &CustomModel::clearCachedIter);
        that->clearIterTimer->start();
    }

    that->lastIterAccessed = std::chrono::steady_clock::now();

    return ret;
}

Wt::cpp17::any CustomModel::headerData(int section, Wt::Orientation orientation, Wt::ItemDataRole role) const
{
    if (role == Wt::ItemDataRole::Display)
    {
        if (orientation == Wt::Orientation::Horizontal)
        {
            switch (section)
            {
            case Tipologia:
                return Wt::WString("Tipologia");
            case Colore:
                return Wt::WString("Colore");
            case Taglia:
                return Wt::WString("Taglia");
            case Utente:
                return Wt::WString("Utente");
            }
        }
        else
        {
            return curPage * ItemsForPage + section;
        }
    }

    return Wt::cpp17::any();
}

void CustomModel::recalcSize()
{
    dbo::Transaction transaction(*session);
    totalItemsCount = session->query<int64_t, Wt::Dbo::DirectBinding>("SELECT COUNT() FROM items");
    transaction.commit();

    pageCount_ = std::ceil(double(totalItemsCount) / ItemsForPage);
    pageChanged_.emit();
}

void CustomModel::loadPage(int page)
{
    if(page == curPage || page >= pageCount_ || page < 0)
        return; //Already loaded or invalid

    int offset = page * ItemsForPage;

    layoutAboutToBeChanged().emit();

    dbo::Transaction transaction(*session);
    items = session->find<ExchangeableItem>().orderBy("type").offset(offset).limit(ItemsForPage);
    int size = items.size(); //Trigger size calculation
    transaction.commit();

    std::cerr << std::endl << std::endl
              << "LOADING PAGE " << page << " OF " << pageCount_ << " ITEMS " << size << " OF " << totalItemsCount
              << std::endl << std::endl;

    cached_row = -1;
    cached_iter = items.end();

    curPage = page;

    layoutChanged().emit();
    pageChanged_.emit();
}

void CustomModel::clearCachedIter()
{
    if(std::chrono::steady_clock::now() - lastIterAccessed < std::chrono::milliseconds(500))
        return; //If last requset (e.g call to data() ) was too recent, wait next timeout

    cached_row = -1;
    cached_iter = items.end();

    delete clearIterTimer;
    clearIterTimer = nullptr;
}

Widget header:

#ifndef SEARCHITEMWIDGET_H
#define SEARCHITEMWIDGET_H

#include <Wt/WContainerWidget.h>

class Session;
class CustomModel;

namespace Wt {
class WPushButton;
class WLineEdit;
class WText;
class WSpinBox;
}

class SearchItemWidget : public Wt::WContainerWidget
{
public:
    SearchItemWidget(Session *s);

    void prevPage();
    void nextPage();

private:
    Session *session;

    Wt::WText *infoLabel;
    Wt::WLineEdit *searchEdit;

    Wt::WPushButton *prevPageBut;
    Wt::WPushButton *nextPageBut;
    Wt::WPushButton *goToPageBut;
    Wt::WSpinBox *goToPageSpin;

    std::shared_ptr<CustomModel> model;
    void handlePageChanged();
    void goToPage();
};

#endif // SEARCHITEMWIDGET_H

Widget implementation:

#include "searchitemwidget.h"
#include "session.h"

#include <Wt/WText.h>
#include <Wt/WLineEdit.h>
#include <Wt/WSpinBox.h>
#include <Wt/WPushButton.h>
#include <Wt/WTableView.h>

#include <Wt/WGridLayout.h>

#include "searchitemmodel.h"

SearchItemWidget::SearchItemWidget(Session *s) :
    session(s)
{
    Wt::WGridLayout *lay = setLayout(std::make_unique<Wt::WGridLayout>());

    lay->addWidget(std::make_unique<Wt::WText>("Work in progress... Items"), 0, 0);

    searchEdit = lay->addWidget(std::make_unique<Wt::WLineEdit>(), 1, 0, 1, 5);
    searchEdit->setPlaceholderText("Search for items here...");

    infoLabel = lay->addWidget(std::make_unique<Wt::WText>(), 2, 0);
    prevPageBut = lay->addWidget(std::make_unique<Wt::WPushButton>("Prev Page"), 2, 1);
    nextPageBut = lay->addWidget(std::make_unique<Wt::WPushButton>("Next Page"), 2, 2);
    goToPageSpin = lay->addWidget(std::make_unique<Wt::WSpinBox>(), 2, 3);
    goToPageBut = lay->addWidget(std::make_unique<Wt::WPushButton>("Go"), 2, 4);

    goToPageSpin->setMinimum(1);

    Wt::WTableView *view = lay->addWidget(std::make_unique<Wt::WTableView>(), 3, 0, 1, 5);
    view->setOverflow(Wt::Overflow::Scroll);

    model = std::make_shared<CustomModel>(session);
    view->setModel(model);

    model->pageChanged().connect(this, &SearchItemWidget::handlePageChanged);

    prevPageBut->clicked().connect(this, &SearchItemWidget::prevPage);
    nextPageBut->clicked().connect(this, &SearchItemWidget::nextPage);
    goToPageBut->clicked().connect(this, &SearchItemWidget::goToPage);

    model->recalcSize();
    model->loadPage(0);
}

void SearchItemWidget::handlePageChanged()
{
    int curPage = model->currentPage();
    int pageCount = model->pageCount();

    goToPageSpin->setMaximum(pageCount);
    goToPageSpin->setValue(curPage + 1);

    prevPageBut->setEnabled(curPage > 0);
    nextPageBut->setEnabled(curPage < pageCount - 1);

    infoLabel->setText(Wt::WString("Page {1} / {2}")
                       .arg(curPage + 1)
                       .arg(pageCount));
}

void SearchItemWidget::prevPage()
{
    model->loadPage(model->currentPage() - 1);
}

void SearchItemWidget::nextPage()
{
    model->loadPage(model->currentPage() + 1);
}

void SearchItemWidget::goToPage()
{
    int page = goToPageSpin->value() - 1;
    model->loadPage(page);
}

Thank you in advance!


Replies (3)

RE: Wt unexpected logout when clicking too fast - Added by Roel Standaert about 4 years ago

What do you see in Wt's logs? Maybe there's an exception or some other error being logged?

RE: Wt unexpected logout when clicking too fast - Added by Fil liF about 4 years ago

There is an SQLite exception but that's normal because it tries to create again tables (database already exist) and it's catch'ed like in hangman example.

I noticed though that it happens twice: when I open a new Firefox tab and navigate to server address a new Session is created in AuthApplication constructor and ties to create tables leading to exception.

But when I login it happens again: it seems the AuthApplication is constructed again leading to the second exception.

Theese exception shouldn't be related to the problem, the database works fine and I can see the data correctly in the Firefox page.

There cant't find other exceptions in the log but there are often messages about expired timeouts, maybe there is a timeout in session login? If I don't click frenetically on those buttons I can stay logged in for quite a long time.

    (1-3/3)