Project

General

Profile

[DBO] Keeping Wt::Dbo::ptr<C> instances synchronized between multiple clients

Added by Rhys Williams 10 months ago

Hi,

I have a web app where multiple users may alter information that is persisted in a Wt::Dbo:ptr object simultaneously, the web page refreshes every few seconds and any updated fields from the database pointer are updated on screen. In order to achieve this without needing to call reread() on every pointer each time the page has refreshed (Which can be in excess of 50 pointers) and needlessly hitting the database I've created a class which any pointers that need to be synchronized between users in real-ish time can extend from.

template <class T>
class SelfUpdating : public Wt::Dbo::Dbo<T> {

public:
    SelfUpdating() {
        std::lock_guard<std::mutex> lock(listeners_mutex);
        unknownListeners.push_back((T*) this); // DB objects are lazy-loaded therefore we will not know the ID when object instantiated.
        // We store each object in a separate list until the next time an object of type T calls notify and then we check each object
        // add add the <int, T> pair to the listeners map.
    }

    ~SelfUpdating() {
        std::lock_guard<std::mutex> lock(listeners_mutex);
        for (auto it = listeners.begin(); it != listeners.end();) {
            if (it->second == (T*)this) {
                it = listeners.erase(it);
            } else {
                ++it;
            }
        }
        unknownListeners.remove((T*)this);
    }

    void checkUnknownListeners() {
        typename std::list<T*>::iterator it = unknownListeners.begin();
        while (it != unknownListeners.end()) {
            T* cc = *it;
            if (cc->id() > 0) {
                listeners.insert(std::make_pair(cc->id(), cc));
                it = unknownListeners.erase(it);
            } else {
                it++;
            }
        }
    }

    void notify() {
        std::lock_guard<std::mutex> lock(listeners_mutex);
        checkUnknownListeners();
        auto pointers = listeners.equal_range(((T*)this)->id());
        for (auto i = pointers.first; i != pointers.second; ++i) {
            if (i->second != ((T*)this)) {
                i->second->setUpdateRequired();
            }
        }
    }

    void setUpdateRequired() {
        needsUpdate = true;
    }

    bool requiresUpdate() const {
        return needsUpdate;
    }

    void setUpdated() {
        needsUpdate = false;
    }

private:
    static std::multimap<int, T*> listeners;
    static std::list<T*> unknownListeners;
    static std::mutex listeners_mutex;

    bool needsUpdate = false;
};

template <class T>
std::multimap<int, T*> SelfUpdating<T>::listeners;

template <class T>
std::list<T*> SelfUpdating<T>::unknownListeners;

template <class T>
std::mutex SelfUpdating<T>::listeners_mutex;

Essentially when a dbo::ptr is created and the data retrieved from the database an entry is added to a map of <primary key index, T*>, when an user modifies the database pointer they can call notify() and the other instances with the same primary key will know to reread the database.

myDboPtr.modify()->name = value;
myDboPtr.modify()->notify();

And to check if the pointer needs to be updated.

if (myDboPtr.get()->requiresUpdate()) {
    myDboPtr.modify()->setUpdated();
    myDboPtr.reread();
}

This all works as expected without any issues, however it is very cumbersome and prone to mistakes that are difficult to catch until they throw a StaleObjectException. Firstly I just want to check that I haven't completely missed the mark and there isn't some other way to achieve my desired behaviour without doing any of this. Secondly, I would like to automatically handle the "myDboPtr.modify()->notify()" and "myDboPtr.modify()->setUpdated()" functionality by modifying the Wt::Dbo::ptr::modify() and Wt::Dbo::ptr::reread() functions.

I already gave it a shot but didn't have any success, not sure if it's possible or not and I'm not familiar enough with the library to really understand what's going on. Also if I'm going about this the wrong way please let me know.

Cheers.


Replies (7)

RE: [DBO] Keeping Wt::Dbo::ptr<C> instances synchronized between multiple clients - Added by lm at 10 months ago

I am under the impression this is how Wt::Dbo::Session works anyway. When you auto a{session->find(primary_key)}, then in another thread from another request, auto b{session->find(primary_key)}, a and b will refer to the same underlying object...I think. Anyway, test it and see. Get two sessions, have one make a change to the object and wait 15 seconds, then change it back. In the meantime, have another session read the value and observe the change? (Or any number of different schemes to accomplish the same thing.)

If the Wt::Dbo::Session returns the same object every time, this does raise threading concerns if sessions are serviced on multiple threads. Then again, the Wt::WServer's message pump is serviced by only one thread? Good luck, my friend!

RE: [DBO] Keeping Wt::Dbo::ptr<C> instances synchronized between multiple clients - Added by Rhys Williams 10 months ago

Unfortunately this doesn't seem to be the case. It seems that each session will always return the same Wt::Dbo::ptr<C> instance for the same database entry, however each session returns a different instance.

RE: [DBO] Keeping Wt::Dbo::ptr<C> instances synchronized between multiple clients - Added by Mark Petryk 10 months ago

Hi Rhys,

This is an issue I also have been working to resolve. I have handled it differently using the Dbo::version() capability.

https://www.webtoolkit.eu/wt/doc/reference/html/classWt_1_1Dbo_1_1ptr.html#a3144ce3a7c4caddb4746a097d82aa841

You are maintaining a static map of pointers to all of your 'monitored' db-objects, and then sending 'updates' to any other open session that has its own db-objects.

I have a couple comments for you, if it helps...

1. you are correct in discovering that every 'session' maintains its own set of db-objects (aka; Dbo). So, no, you cannot update a db-pointer in one session and have it affect another db-pointer in another session unless you connect the two like you have done with globals.

2. Wt has two different modes of run: a:shared, and b:dedicated.

In 'shared mode', which is the mode your method can work on, there is only 1 (one) running "exe" (binary) server program, listening on one port, serving up the "site" to any connected browser. When multiple browsers hit this server, it spawns another "session" within that same "program". This allows each "session" to get a hold of the static resources because the static resources are held in the single running "server program".

In 'dedicated mode', the Wt "server program" acts as a process-dispatcher to "launch" or "spawn" or "kick-off" (pick your term) a new running completely separate "exe" (binary) server program. That newly running program then opens the "session" which ultimately contains all your db-object pointers. In this case, your "static global" map of db-object pointers lives independently in each spawned "server program", and you will loose the communication conduit between each session that you are relying on.

So, in order to accomplish what you want to accomplish in a way that does not break when the server is deployed in 'dedicated' mode, you have to be able to establish a 'conduit' between all your independently running sessions.

I'll paste the relevant section from the wt_config.xml;

    27          <!-- Session management. -->
    28      <session-management>
    29              <!-- Every session runs within a dedicated process.
    30  
    31             This mode guarantees kernel-level session privacy, but as every
    32             session requires a seperate process, it is also an easy target
    33             for DoS attacks if not shielded by access control.
    34  
    35             Note: currently only supported by the wtfcgi and wthttp
    36             connectors.
    37                -->
    38  
    39          <!--
    40             <dedicated-process>
    41           <max-num-sessions>100</max-num-sessions>
    42             </dedicated-process>
    43            -->
    44  
    45          <!-- Multiple sessions within one process.
    46  
    47             This mode spawns a number of processes, and sessions are
    48             allocated randomly to one of these processes (you should not
    49             use this for dynamic FCGI servers, but only in conjunction
    50             with a fixed number of static FCGI servers.
    51  
    52             This requires careful programming, as memory corruption in one
    53             session will kill all of the sessions in the same process. You
    54             should debug extensively using valgrind. Also, it is your
    55             responsibility to keep session state not interfering and
    56             seperated.
    57  
    58             On the other hand, sessions are inexpensive, and this mode
    59             suffers far less from DoS attacks than dedicated-process mode.
    60             Use it for non-critical and well-debugged web applications.
    61  
    62             Note: the wthttp connector will ignore the num-processes
    63             setting and use only process.
    64                -->
    65          <shared-process>
    66              <num-processes>5</num-processes>
    67          </shared-process>
    68  

RE: [DBO] Keeping Wt::Dbo::ptr<C> instances synchronized between multiple clients - Added by lm at 10 months ago

Thanks, Mark.

Original Poster, it sounds like you're working with an antipattern. If you want one session to be notified of a change in another session, set up a notification mechanism. Use server push to handle server-side information changes being reflected in the client without polling. Set up static (across all sessions) event handlers that can respond properly to changes in state.

I wrote https://lmat.gun.vn/speedlines/, for instance, which is an online multi-player game. Each session needs to be notified of lots of things from other sessions (for instance, moves that other players make, or deaths of other players, etc.). It seems like a problem similar to yours.

RE: [DBO] Keeping Wt::Dbo::ptr<C> instances synchronized between multiple clients - Added by Mark Petryk 10 months ago

Hi lm,

Have you ever done any work with postgres notify?

https://www.postgresql.org/docs/9.0/sql-notify.html

What I'm wondering is how do you signal another session that's running in a different server instance?

RE: [DBO] Keeping Wt::Dbo::ptr<C> instances synchronized between multiple clients - Added by lm at 10 months ago

I was not considering a database-level event, but a Wt::Signal. Postgres notify sounds pretty cool! I was thinking that before inserting or updating the database, you would invoke your own Wt::Signal to let subscribers know what you're about to do. If you're running all in the same process, this is pretty straightforward. If you're running in different processes (as you noted above), this will require inter-process communication and is a little more complicated.

RE: [DBO] Keeping Wt::Dbo::ptr<C> instances synchronized between multiple clients - Added by Mark Petryk 10 months ago

Hi lm,

Yea, that's why I was asking. I have exactly the same 'issue' the poster noted - notifying "other" sessions that one of it's DBO has updated, but I've not yet implemented a cool solution yet. At best what I have right now is a simple timer in each session that "requeries" the DBO to see if the version() has changed;

3352   Wt::Dbo::Transaction t( *Rtm::session() );·
3353   auto diskVersion =·
3354     Rtm::session()->·
3355       query<int>( "SELECT version FROM \"rtmJob\" WHERE id = ?" )·
3356       .bind( jobItem().id() )·
3357       .resultValue()·
3358       ;·
3359 ·
3360   /*·
3361   ** If the disk-version is greater than what·
3362   **  we have in memory, then re-read from·
3363   **  disk and cause everything related to this job·
3364   **  to be re-read as well.·
3365   **·
3366   */·
3367   if( jobItem().version() < diskVersion )·

This is really really sloppy, but it is also effective. I would like a better signalling system rather than having to requery a DBO to find out if something in it has changed. It's poll vs push, and I would prefer push without a doubt!

Koen pointed out to me the postgres::notify, but I've not attempted any kind of interface to it yet.

I would like to have a "Signal<> DBO::change" where the change signal gets fired any time the back-end db makes an update to this DBO. It could then signal that specific DBO that something in it has changed and it itself can decide how to handle it. Still working on that :)

~mark

    (1-7/7)