Skip to content

File record_logger.hpp

File List > illixr > record_logger.hpp

Go to the documentation of this file

#pragma once

#include "phonebook.hpp"

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

#ifndef NDEBUG
#    include 
#    include 
#endif

namespace ILLIXR {

class record_header {
public:
    record_header(const std::string& name, std::vector<std::pair<std::string, const std::type_info&>> columns_)
        : id_{std::hash<std::string>{}(name)}
        , name_{name}
        , columns_{std::move(columns_)} { }

    bool operator==(const record_header& other) const {
        // Check pointer first
        if (this == &other) {
            return true;
        }

        if (name_ != other.name_ || columns_.size() != other.columns_.size() || id_ != other.id_) {
            return false;
        }
        for (std::size_t i = 0; i < columns_.size(); ++i) {
            if (columns_[i] != other.columns_[i]) {
                return false;
            }
        }
        return true;
    }

    bool operator!=(const record_header& other) const {
        return !(*this == other);
    }

    [[nodiscard]] std::size_t get_id() const {
        return id_;
    }

    [[nodiscard]] const std::string& get_name() const {
        return name_;
    }

    [[nodiscard]] const std::string& get_column_name(unsigned column) const {
        return columns_[column].first;
    }

    [[nodiscard]] const std::type_info& get_column_type(unsigned column) const {
        return columns_[column].second;
    }

    [[nodiscard]] unsigned get_columns() const {
        return columns_.size();
    }

    [[nodiscard]] std::string to_string() const {
        std::string ret = std::string{"record_header "} + name_ + std::string{" { "};
        for (const auto& pair : columns_) {
            ret += std::string{pair.second.name()} + std::string{" "} + pair.first + std::string{"; "};
        }
        ret.erase(ret.size() - 2);
        ret += std::string{" }"};
        return ret;
    }

private:
    std::size_t                                                      id_;
    std::string                                                      name_;
    const std::vector<std::pair<std::string, const std::type_info&>> columns_;
};

class data_use_indicator {
public:
    data_use_indicator()
        : used_{false} { }

    data_use_indicator(const data_use_indicator& other)
        : used_{false} {
        other.used_ = true;
    }

    data_use_indicator& operator=(const data_use_indicator& other) {
        if (&other != this) {
            other.used_ = true;
            used_       = false;
        }
        return *this;
    }

    /*
      copy constructors are just as efficient as move constructors would be,
      so I won't define move constructors. C++ will invoke copy instead (for no loss).
    */
    bool is_used() const {
        return used_;
    }

    void mark_used() const {
        used_ = true;
    }

    [[maybe_unused]] void mark_unused() const {
        used_ = false;
    }

private:
    mutable bool used_;
};

class record {
public:
    record(const record_header& rh, std::vector<std::any> values)
        : record_header_{rh}
        , values_(std::move(values)) {
#ifndef NDEBUG
        assert(record_header_);
        if (values_.size() != record_header_->get().get_columns()) {
            spdlog::get("illixr")->error("[record_logger] {} elements passed, but rh for {} only specifies {}.", values_.size(),
                                         record_header_->get().get_name(), record_header_->get().get_columns());
            abort();
        }
        for (auto column = 0; column < values_.size(); ++column) {
            if (values_[column].type() != record_header_->get().get_column_type(column)) {
                spdlog::get("illixr")->error("[record_logger] Caller got wrong type for column {} of {}.", column,
                                             record_header_->get().get_name());
                spdlog::get("illixr")->error("[record_logger] Caller passed: {}; record_header specifies: {}",
                                             values_[column].type().name(),
                                             record_header_->get().get_column_type(column).name());
                abort();
            }
        }
#endif
    }

    record() = default;

    ~record() {
#ifndef NDEBUG
        if (record_header_ && !data_use_indicator_.is_used()) {
            spdlog::get("illixr")->error("[record_logger] Record was deleted without being logged.");
            abort();
        }
#endif
    }

    template<typename T>
    T get_value(unsigned column) const {
#ifndef NDEBUG
        assert(record_header_);
        data_use_indicator_.mark_used();
        if (record_header_->get().get_column_type(column) != typeid(T)) {
            std::ostringstream ss;
            ss << "Caller column type for " << column << " of " << record_header_->get().get_name() << ". "
               << "Caller passed: " << typeid(T).name() << "; "
               << "record_header specifies: " << record_header_->get().get_column_type(column).name() << ". ";
            throw std::runtime_error{ss.str()};
        }
#endif
        return std::any_cast<T>(values_[column]);
    }

    const record_header& get_record_header() const {
        assert(record_header_);
        return record_header_->get();
    }

    [[maybe_unused]] void mark_used() const {
#ifndef NDEBUG
        assert(record_header_);
        data_use_indicator_.mark_used();
#endif
    }

private:
    // Holding a pointer to a record_header is more efficient than
    // requiring each record to hold a list of its column names
    // and table name_. This is just one pointer.
    std::optional<std::reference_wrapper<const record_header>> record_header_;
    std::vector<std::any>                                      values_;
#ifndef NDEBUG
    data_use_indicator data_use_indicator_;
#endif
};

class record_logger : public phonebook::service {
public:
    ~record_logger() override = default;

    virtual void log(const record& r) = 0;

    virtual void log(const std::vector<record>& rs) {
        for (const record& r : rs) {
            log(r);
        }
    }
};

static std::chrono::milliseconds LOG_BUFFER_DELAY{1000};

class record_coalescer {
public:
    explicit record_coalescer(std::shared_ptr<record_logger> logger_)
        : logger_{std::move(logger_)}
        , last_log_{std::chrono::high_resolution_clock::now()} { }

    ~record_coalescer() {
        flush();
    }

    void log(const record& r) {
        if (logger_) {
            buffer_.push_back(r);
            // Log coalescer should only be used with
            // In the common case, they will be the same pointer, quickly check the pointers.
            // In the less common case, we check for object-structural equality.
#ifndef NDEBUG
            if (&r.get_record_header() != &buffer_[0].get_record_header() &&
                r.get_record_header() == buffer_[0].get_record_header()) {
                spdlog::get("illixr")->error("[record_logger] Tried to push a record of type {} to a record logger for type {}",
                                             r.get_record_header().to_string(), buffer_[0].get_record_header().to_string());
                abort();
            }
#endif
            maybe_flush();
        }
    }

    void maybe_flush() {
        if (std::chrono::high_resolution_clock::now() > last_log_ + LOG_BUFFER_DELAY) {
            flush();
        }
    }

    void flush() {
        if (logger_) {
            std::vector<record> buffer2;
            buffer_.swap(buffer2);
            logger_->log(buffer2);
            last_log_ = std::chrono::high_resolution_clock::now();
        }
    }

    explicit operator bool() const {
        return bool(logger_);
    }

private:
    std::shared_ptr<record_logger>                              logger_;
    std::chrono::time_point<std::chrono::high_resolution_clock> last_log_;
    std::vector<record>                                         buffer_;
};
} // namespace ILLIXR