Back to Documentations

Signature Description Public Member Functions
// The template arguments are similar to DataFrame itself
//
template<typename I, typename H = HeteroVector<0>>
class   StreamAppender;
This is not a DataFrame memeber function. StreamAppender is the live feed counterpart to ChunkedReader and StreamWriter. Those two both assume a bounded source or destination -- something with a definite end (a file, or however many chunks an ETL job produces). StreamAppender is for the opposite case: a source that never ends (a market-data handler, a socket, a queue) that you want to keep feeding into a DataFrame while other code (on the same or another thread) queries it.

It's a thin, locking wrapper around a DataFrame you already own -- it does not read from or write to any external stream itself. Once you start feeding a DataFrame through a StreamAppender, every subsequent access to that DataFrame (pushing new rows, and reading/visiting it) should go through this StreamAppender (push_row() / push_chunk() / with_lock()) rather than touching the underlying DataFrame directly.

StreamAppender also accounts for multithreading and uses the spin-lock in your target DataFrame.
StreamAppender Header File

S: A stream type
DF: A DataFrame type
static void test_StreamAppender()  {

    std::cout << "\nTesting StreamAppender{ } ..." << std::endl;

    using MyDataFrame = StdDataFrame<unsigned long>;

    // push_row(): basic correctness
    //
    {
        MyDataFrame                     df;
        StreamAppender<unsigned long>   appender { df };

        for (unsigned long i { 0 }; i < 500; ++i)  {
            appender.push_row(i,
                              std::make_pair("price", double(i) * 1.5),
                              std::make_pair("size", int(i)));
        }

        assert(appender.size() == 500);
        assert(df.get_index().size() == 500);

        const auto  &price { df.get_column<double>("price") };
        const auto  &size { df.get_column<int>("size") };

        for (unsigned long i { 0 }; i < 500; ++i)  {
            assert(price[i] == double(i) * 1.5);
            assert(size[i] == int(i));
        }
    }

    // push_chunk(): basic correctness
    //
    {
        MyDataFrame                     df;
        StreamAppender<unsigned long>   appender { df };

        for (unsigned long batch { 0 }; batch < 10; ++batch)  {
            std::vector<unsigned long>  idx;
            std::vector<double>         price;

            for (unsigned long i { 0 }; i < 50; ++i)  {
                const unsigned long gi { batch * 50 + i };

                idx.push_back(gi);
                price.push_back(double(gi) * 2.0);
            }

            MyDataFrame chunk;

            chunk.load_data(std::move(idx), std::make_pair("price", price));
            appender.push_chunk<MyDataFrame, double>(chunk);
        }

        assert(appender.size() == 500);

        const auto  &price { df.get_column<double>("price") };

        for (unsigned long i { 0 }; i < 500; ++i)
            assert(price[i] == double(i) * 2.0);
    }

    // Sliding window: eviction keeps roughly the last window_size
    //
    {
        MyDataFrame                     df;
        const long                      window { 100 };
        StreamAppender<unsigned long>   appender { df, window };

        for (unsigned long i { 0 }; i < 1000; ++i)
            appender.push_row(i, std::make_pair("v", double(i)));

        const auto  final_size { appender.size() };

        // With eviction_margin defaulting to 0, size should settle at
        // exactly window_size after the final push.
        //
        assert(final_size == window);

        const auto  &idx { df.get_index() };
        const auto  &v_col { df.get_column<double>("v") };

        // The surviving rows should be the most recent ones, in order.
        //
        assert(idx.front() == 1000 - window);
        assert(idx.back() == 999);
        for (std::size_t i { 0 }; i < idx.size(); ++i)
            assert(v_col[i] == double(idx[i]));
    }

    // Sliding window with an eviction margin: bounded but not exact
    //
    {
        MyDataFrame                     df;
        const long                      window { 100 };
        const long                      margin { 20 };
        StreamAppender<unsigned long>   appender { df, window, margin };

        for (unsigned long i { 0 }; i < 1000; ++i)
            appender.push_row(i, std::make_pair("v", double(i)));

        const auto  final_size { appender.size() };

        // Should never exceed window + margin, and should be at least
        // window (since we only trim back down to window, not below).
        //
        assert(long(final_size) >= window);
        assert(long(final_size) <= window + margin);
    }

    // with_lock(): read access, including a visitor
    //
    {
        MyDataFrame                     df;
        StreamAppender<unsigned long>   appender { df };

        for (unsigned long i { 1 }; i <= 10; ++i)
            appender.push_row(i, std::make_pair("v", double(i)));

        const double    sum {
            appender.with_lock([](MyDataFrame &d) -> double  {
                double      total { 0.0 };
                const auto  &v_col { d.get_column<double>("v")};

                for (const auto &x : v_col)  total += x;
                return (total);
            })
        };

        assert(sum == 55.0);  // 1 + 2 + ... + 10
    }

    // Real concurrency: one thread pushes while another reads
    //
    {
        MyDataFrame                     df;
        StreamAppender<unsigned long>   appender { df, 5000L };
        std::atomic<bool>               stop { false };
        std::atomic<std::size_t>        reads_done { 0 };
        std::atomic<bool>               saw_inconsistency { false };

        // That is how you set the lock
        //
        SpinLock    lock;

        MyDataFrame::set_lock(&lock);

        std::thread writer([&]()  {
            for (unsigned long i { 0 }; i < 20000; ++i)
                appender.push_row(i, std::make_pair("v", double(i)));
            stop = true;
        });
        std::thread reader([&]()  {
            while (! stop.load())  {
                appender.with_lock([&](MyDataFrame &d) -> void  {
                    const auto  &idx { d.get_index() };
                    const auto  &v_col { d.get_column<double>("v") };

                    // Whatever we see, it must be internally consistent:
                    // same length, and v_col[i] == double(idx[i]) for every
                    // row actually present right now.
                    //
                    if (idx.size() != v_col.size())
                        saw_inconsistency = true;
                    for (std::size_t i { 0 }; i < idx.size(); ++i)
                        if (v_col[i] != double(idx[i]))
                            saw_inconsistency = true;
                });
                reads_done.fetch_add(1);
            }
        });

        writer.join();
        reader.join();

        assert(! saw_inconsistency.load());
        assert(appender.size() > 0);
    }
}

C++ DataFrame