Skip to content

File sqlite_record_logger.hpp

File List > src > sqlite_record_logger.hpp

Go to the documentation of this file

#pragma once

#ifndef __ANDROID__

#    include "illixr/concurrentqueue/blockingconcurrentqueue.hpp"
#    include "illixr/error_util.hpp"
#    include "illixr/global_module_defs.hpp"
#    include "illixr/record_logger.hpp"
#    include "sqlite3pp/sqlite3pp.hpp"

#    include 
#    include 
#    include 
#    include 
#    include 
#    include 

namespace ILLIXR {

class sqlite_thread {
public:
    sqlite3pp::database prep_db() {
        RAC_ERRNO_MSG("sqlite_record_logger at start of prep_db");

        if (!std::filesystem::exists(directory_)) {
            std::filesystem::create_directory(directory_);
        }

        const std::string path = directory_.string() + "/" + (table_name_ + std::string{".sqlite"});

        RAC_ERRNO_MSG("sqlite_record_logger before sqlite3pp::database");
        sqlite3pp::database database{path.c_str()};
        RAC_ERRNO_MSG("sqlite_record_logger after sqlite3pp::database");

        return database;
    }

    std::string prep_insert_str() {
        RAC_ERRNO_MSG("sqlite_record_logger at start of prep_insert_str");

        std::string drop_table_string = std::string{"DROP TABLE IF EXISTS "} + table_name_ + std::string{";"};
        database_.execute(drop_table_string.c_str());
        RAC_ERRNO_MSG("sqlite_record_logger after drop table execute.");

        std::string create_table_string = std::string{"CREATE TABLE "} + table_name_ + std::string{"("};
        for (unsigned int i = 0; i < record_header_.get_columns(); ++i) {
            create_table_string += record_header_.get_column_name(i) + std::string{" "};
            const std::type_info& rh_type = record_header_.get_column_type(i);
            if (rh_type == typeid(std::size_t) || rh_type == typeid(bool) || rh_type == typeid(std::chrono::nanoseconds) ||
                rh_type == typeid(std::chrono::high_resolution_clock::time_point) || rh_type == typeid(duration) ||
                rh_type == typeid(time_point)) {
                create_table_string += std::string{"INTEGER"};
            } else if (rh_type == typeid(double)) {
                create_table_string += std::string{"REAL"}; // For performance timing. Will be deleted when implementing #208.
            } else if (rh_type == typeid(std::string)) {
                create_table_string += std::string{"TEXT"};
            } else {
                throw std::runtime_error{std::string{"type "} + std::string{record_header_.get_column_type(i).name()} +
                                         std::string{" not found"}};
            }
            create_table_string += std::string{", "};
        }
        create_table_string.erase(create_table_string.size() - 2);
        create_table_string += std::string{");"};

        RAC_ERRNO_MSG("sqlite_record_logger before create table execute");
        database_.execute(create_table_string.c_str());
        RAC_ERRNO_MSG("sqlite_record_logger after create table execute");

        std::string insert_string = std::string{"INSERT INTO "} + table_name_ + std::string{" VALUES ("};
        for (unsigned int i = 0; i < record_header_.get_columns(); ++i) {
            insert_string += std::string{"?"} + std::to_string(i + 1) + std::string{", "};
        }
        insert_string.erase(insert_string.size() - 2);
        insert_string += std::string{");"};
        return insert_string;
    }

    explicit sqlite_thread(const record_header& rh_)
        : record_header_{rh_}
        , table_name_{record_header_.get_name()}
        , database_{prep_db()}
        , insert_str_{prep_insert_str()}
        , insert_cmd_{database_, insert_str_.c_str()}
        , thread_{[this] {
            pull_queue();
        }} { }

    void pull_queue() {
        const std::size_t   max_record_batch_size = 1024 * 256;
        std::vector<record> record_batch{max_record_batch_size};
        std::size_t         actual_batch_size;

        spdlog::get("illixr")->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%n] [%<%l%$] [sqlite_record_logger] thread_ %t %v");
        spdlog::get("illixr")->debug("{}", table_name_);
        spdlog::get("illixr")->set_pattern("%+");

        std::size_t processed = 0;
        while (!terminate_.load()) {
            std::this_thread::sleep_for(std::chrono::seconds{1});
            // Uncomment this block to log in "real time";
            // Otherwise, everything gets logged "post real time".
            /*
            const std::chrono::seconds max_record_match_wait_time {10};
            actual_batch_size = queue_.wait_dequeue_bulk_timed(record_batch.begin(), record_batch.size(),
            max_record_match_wait_time); process(record_batch, actual_batch_size); processed += actual_batch_size;
            */
        }

        // We got the terminate_ command,
        // So drain whatever is left in the queue_.
        // But don't wait around once it is empty.
        std::size_t post_processed = 0;
        while ((actual_batch_size = queue_.try_dequeue_bulk(record_batch.begin(), record_batch.size()))) {
            process(record_batch, actual_batch_size);
            post_processed += actual_batch_size;
        }
        spdlog::get("illixr")->debug("[sqlite_record_logger] Drained {} (sqlite); {}/{} done post real time", table_name_,
                                     post_processed, (processed + post_processed));
    }

    void process(const std::vector<record>& record_batch, std::size_t batch_size) {
        sqlite3pp::transaction xct{database_};
        for (std::size_t i = 0; i < batch_size; ++i) {
            // TODO(performance): reuse the sqlite3pp statement
            // This currently has to be 'reinterpreted' for every iteration.
            sqlite3pp::command cmd{database_, insert_str_.c_str()};
            const record&      r = record_batch[i];
            for (int j = 0; j < static_cast<int>(record_header_.get_columns()); ++j) {
                /*
                  If you get a `std::bad_any_cast` here, make sure the user didn't lie about record.get_record_header().
                  The types there should be the same as those in record.get_values().
                */
                const std::type_info& rh_type = record_header_.get_column_type(j);
                if (rh_type == typeid(std::size_t)) {
                    cmd.bind(j + 1, static_cast<long long>(r.get_value<std::size_t>(j)));
                } else if (rh_type == typeid(bool)) {
                    cmd.bind(j + 1, static_cast<long long>(r.get_value<bool>(j)));
                } else if (rh_type == typeid(double)) {
                    cmd.bind(j + 1, r.get_value<double>(j));
                } else if (rh_type == typeid(std::chrono::nanoseconds)) {
                    auto val = r.get_value<duration>(j);
                    cmd.bind(j + 1, static_cast<long long>(std::chrono::nanoseconds{val}.count()));
                } else if (rh_type == typeid(std::chrono::high_resolution_clock::time_point)) {
                    auto val = r.get_value<std::chrono::high_resolution_clock::time_point>(j).time_since_epoch();
                    cmd.bind(j + 1, static_cast<long long>(std::chrono::nanoseconds{val}.count()));
                } else if (rh_type == typeid(duration)) {
                    auto val = r.get_value<duration>(j);
                    cmd.bind(j + 1, static_cast<long long>(std::chrono::nanoseconds{val}.count()));
                } else if (rh_type == typeid(time_point)) {
                    auto val = r.get_value<time_point>(j).time_since_epoch();
                    cmd.bind(j + 1, static_cast<long long>(std::chrono::nanoseconds{val}.count()));
                } else if (rh_type == typeid(std::string)) {
                    // r.get_value(j) returns a std::string temporary
                    // c_str() returns a pointer into that std::string temporary
                    // Therefore, need to copy.
                    cmd.bind(j + 1, r.get_value<std::string>(j).c_str(), sqlite3pp::copy);
                } else {
                    throw std::runtime_error{std::string{"type "} + std::string{record_header_.get_column_type(j).name()} +
                                             std::string{" not implemented"}};
                }
            }
            RAC_ERRNO_MSG("sqlite_record_logger set errno before process cmd execute");

            cmd.execute();
            RAC_ERRNO_MSG("sqlite_record_logger after process cmd execute");
        }
        xct.commit();
    }

    void put_queue(const std::vector<record>& buffer_in) {
        queue_.enqueue_bulk(buffer_in.begin(), buffer_in.size());
    }

    void put_queue(const record& record_in) {
        queue_.enqueue(record_in);
    }

    ~sqlite_thread() {
        terminate_.store(true);
        thread_.join();
    }

private:
    static const std::filesystem::path          directory_;
    const record_header&                        record_header_;
    std::string                                 table_name_;
    sqlite3pp::database                         database_;
    std::string                                 insert_str_;
    sqlite3pp::command                          insert_cmd_;
    moodycamel::BlockingConcurrentQueue<record> queue_;
    std::atomic<bool>                           terminate_{false};
    std::thread                                 thread_;
};

const std::filesystem::path sqlite_thread::directory_{"metrics"};

class sqlite_record_logger : public record_logger {
protected:
    void log(const std::vector<record>& r) override {
        if (!r.empty()) {
            get_sqlite_thread(r[0]).put_queue(r);
        }
    }

    void log(const record& r) override {
        get_sqlite_thread(r).put_queue(r);
    }

private:
    sqlite_thread& get_sqlite_thread(const record& r) {
        const record_header& record_header_ = r.get_record_header();
        {
            const std::shared_lock<std::shared_mutex> lock{registry_lock_};
            auto                                      result = registered_tables_.find(record_header_.get_id());
            if (result != registered_tables_.cend()) {
                return result->second;
            }
        }
        const std::unique_lock<std::shared_mutex> lock{registry_lock_};
        auto pair = registered_tables_.try_emplace(record_header_.get_id(), record_header_);
        return pair.first->second;
    }

    std::unordered_map<std::size_t, sqlite_thread> registered_tables_;
    std::shared_mutex                              registry_lock_;
};

} // namespace ILLIXR

#endif