arximboldi / lager

C++ library for value-oriented design using the unidirectional data-flow architecture — Redux for C++
https://sinusoid.es/lager/
MIT License
708 stars 66 forks source link

Batch updates and readers #91

Open tusooa opened 3 years ago

tusooa commented 3 years ago

Consider the following program:


#include <lager/store.hpp>
#include <lager/reader.hpp>
#include <lager/event_loop/boost_asio.hpp>

#include <thread>
#include <boost/asio.hpp>

#include <iostream>

using Model = int;
using Action = int;
Model update(Model orig, Action add)
{
    return orig + add;
}

int main()
{
    boost::asio::io_context io;
    auto store = lager::make_store<Action>(
        Model{},
        &update,
        lager::with_boost_asio_event_loop{io.get_executor()});

    auto reader = lager::reader<Model>(store);

    lager::watch(reader,
                 [](Model m) {
                     std::cerr << "model is now: " << m << std::endl;
                 });

    auto guard = boost::asio::executor_work_guard(io.get_executor());

    std::thread([&] { io.run(); }).detach();

    for (int i = 0; i < 100; ++i) {
        store.dispatch(1);
    }

    std::this_thread::sleep_for(std::chrono::milliseconds(100));
}

One may expect that upon running this one will get 100 lines of output from 1 to 100. But this is actually not the case. It actually only outputs starting from ~50 and will sometimes skip values.

Is this expected? From my understanding, the dispatch() is just a call to event loop's post(), which should run the whole push_down() process, including those notify()s, which basically call the connected callback function, in io_context's thread (which we detach()ed earlier). If this is correct, there should not be any data races and no output should be skipped... but it is not the case.

The thing behind this is that I am trying to use store as an event listener (https://gitlab.com/kazv/libkazv/-/blob/servant/src/eventemitter/lagerstoreeventemitter.hpp). Or does this idea just not work?

arximboldi commented 3 years ago

It is true that current implementation of observing via cursors may not expose every value when two actions happen very close in time. This is not really a data race. This works for UI since you are only interested in the "current" value and not every single intermediate value.

I think this could be changed if you have a good use-case for observing every single intermediate value. I can guide you through the code on why this is happening and we can discuss ways to change it.

arximboldi commented 3 years ago

Btw, as a workaround, to use the store as an event listener, you can call your callbacks in an effect returned by the update function. The effect would be guaranteed to be called every time.

tusooa commented 3 years ago

Btw, as a workaround, to use the store as an event listener, you can call your callbacks in an effect returned by the update function. The effect would be guaranteed to be called every time.

Thank you, calling it in effects does help (although leads to more code lol).

arximboldi commented 3 years ago

I'd be happy to have a tag for the store<> type, like now we have automatic_tag and transactional_tag, maybe we could have something like discrete_tag (maybe there is a better name?) for the behavior of ensuring that every update() causes a notification.