Skip to content
36 changes: 20 additions & 16 deletions include/simdb/Exceptions.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,34 @@

namespace simdb {

/// Used to construct and throw a standard C++ exception
/*!
* \class DBException
*
* \brief SimDB exception type; stream additional context with operator<< before
* throwing. what() returns the full message built from the initial
* reason and any appended values.
*/
class DBException : public std::exception
{
public:
DBException() = default;

/// Construct a DBException object
DBException(const std::string& reason) { reason_ << reason; }
/// \brief Construct with an initial reason string.
explicit DBException(const std::string& reason) { reason_ << reason; }

/// Copy construct a DBException object
/// \brief Copy constructor; copies the accumulated message.
DBException(const DBException& rhs) { reason_ << rhs.reason_.str(); }

/// Destroy!
virtual ~DBException() noexcept override {}

/**
* \brief Overload from std::exception
* \return Const char * of the exception reason
*/
/// \brief Override from std::exception; returns the full exception message.
virtual const char* what() const noexcept override
{
reason_str_ = reason_.str();
return reason_str_.c_str();
}

/**
* \brief Append additional information to the message.
*/
/// \brief Append a value to the exception message (stream-style).
template <typename T> DBException& operator<<(const T& msg)
{
reason_ << msg;
Expand All @@ -52,13 +52,17 @@ class DBException : public std::exception
mutable std::string reason_str_;
};

/// Used in order to signal to safeTransaction() that the transaction
/// must be retried. Since SimDB is multi-threaded, we expect the database
/// to encounter locked tables etc. which should not be thrown out of
/// calls to safeTransaction().
/*!
* \class SafeTransactionSilentException
*
* \brief Internal exception signaling that safeTransaction() should retry (e.g.
* SQLITE_BUSY, SQLITE_LOCKED). Not propagated out of safeTransaction();
* the transaction is retried instead.
*/
class SafeTransactionSilentException : public std::exception
{
public:
/// \brief Construct with the SQLite return code (used in the message).
explicit SafeTransactionSilentException(int rc) :
msg_("The database is locked (return code " + std::to_string(rc) + ")")
{
Expand Down
51 changes: 47 additions & 4 deletions include/simdb/apps/App.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,23 +50,45 @@ class PipelineManager;
class AppManager;
class ThreadSafeLogger;

/// Base class for SimDB applications. Note that app subclasses are given
/// the DatabaseManager instance as a constructor argument, so they can
/// access the database and perform operations like appending schemas,
/// inserting records, etc.)
/*!
* \class App
*
* \brief Base class for SimDB applications. Subclasses receive a
* DatabaseManager in the constructor and can append schemas, insert
* records, and create pipelines. Lifecycle hooks: postInit(),
* createPipeline(), preTeardown(), postTeardown(). Use AppManager
* to register, enable, and instantiate apps.
*/
class App
{
public:
virtual ~App() = default;

/// \brief Set the instance number (1-based; 0 = single-instance). Called by AppManager.
void setInstance(size_t instance) { instance_ = instance; }

/// \brief Get the instance number (0 if single-instance).
size_t getInstance() const { return instance_; }

/// \brief Hook called after command-line parsing, before simulation starts.
virtual void postInit([[maybe_unused]] int argc, [[maybe_unused]] char** argv) {}

/// \brief Hook to create and configure this app's pipeline(s) on the given manager.
virtual void createPipeline(pipeline::PipelineManager*) {}

/// \brief Hook called before simulation teardown.
virtual void preTeardown() {}

/// \brief Hook called after simulation teardown (resource cleanup).
virtual void postTeardown() {}

/// \brief Return the stdout logger (set by AppManager); may be null.
ThreadSafeLogger* getStdoutLogger() const { return stdout_logger_; }

/// \brief Return the stderr logger (set by AppManager); may be null.
ThreadSafeLogger* getStderrLogger() const { return stderr_logger_; }

/// \brief Return the file logger (set by AppManager); may be null.
ThreadSafeLogger* getFileLogger() const { return file_logger_; }

protected:
Expand All @@ -86,14 +108,35 @@ class App
friend class AppManager;
};

/*!
* \class AppFactoryBase
*
* \brief Abstract factory for creating App instances and defining their schema.
* Each app type exposes a nested AppFactory that implements this interface;
* AppManager uses it to instantiate apps and register their tables.
*
* \note It is optional to define a custom AppFactory nested in your app class.
* If you don't, the default AppFactory will be used, which creates the app
* with the default constructor (DatabaseManager*).
*/
class AppFactoryBase
{
public:
virtual ~AppFactoryBase() = default;

/// \brief Create an App instance for the given DatabaseManager.
virtual App* createApp(DatabaseManager*) = 0;

/// \brief Define this app's schema (tables, columns) on the given Schema.
virtual void defineSchema(Schema& schema) const = 0;
};

/*!
* \class AppFactory
*
* \brief Default factory that creates AppT and delegates defineSchema to AppT::defineSchema.
* \tparam AppT App subclass (must have defineSchema(Schema&) and a constructor taking DatabaseManager*).
*/
template <typename AppT> class AppFactory : public AppFactoryBase
{
public:
Expand Down
55 changes: 42 additions & 13 deletions include/simdb/apps/AppManager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,22 @@ template <typename T> struct has_nested_factory<T, std::void_t<typename T::AppFa
class AppManager;
class AppManagers;

/*!
* \class AppRegistrationBase
*
* \brief Abstract registration that adds an app type to an AppManager when
* registerApp() is called. Used by AppManagers to register app types
* before createAppManager().
*/
class AppRegistrationBase
{
public:
virtual ~AppRegistrationBase() = default;
/// \brief Register this app type with the given AppManager (e.g. enable it for later creation).
virtual void registerApp(AppManager* app_manager) const = 0;
};

/// \brief Type-specific registration for AppT; created by AppManagers::registerApp<AppT>().
template <typename AppT> class AppRegistration : public AppRegistrationBase
{
private:
Expand All @@ -44,16 +53,20 @@ template <typename AppT> class AppRegistration : public AppRegistrationBase
friend class AppManagers;
};

/// This class is responsible for registering, enabling, instantiating,
/// and managing the lifecycle of all SimDB applications running in a
/// simulation.
/*!
* \class AppManager
*
* \brief Registers, enables, instantiates, and manages the lifecycle of SimDB
* apps for one database. Obtainfrom AppManagers::createAppManager() or
* AppManagers::getAppManager().
*/
class AppManager
{
public:
/// Get our associated DatabaseManager.
/// \brief Return the associated DatabaseManager.
DatabaseManager* getDatabaseManager() const { return db_mgr_; }

/// Parameterize an app factory. Call this before createEnabledApps().
/// \brief Parameterize an app factory. Must be called before createEnabledApps().
/// Your app subclass must have a public nested class called "AppFactory",
/// inheriting publicly from simdb::AppFactoryBase. Your nested AppFactory
/// can have any signature it needs to accept all required app constructor
Expand Down Expand Up @@ -715,15 +728,24 @@ template <typename AppT> void AppRegistration<AppT>::registerApp(AppManager* app
app_manager->registerApp_<AppT>();
}

/// This class holds onto all DatabaseManagers and their AppManagers.
/*!
* \class AppManagers
*
* \brief Singleton-like holder for all DatabaseManagers and AppManagers. Call
* registerApp<AppT>() for each app type before createAppManager(); then
* createAppManager(db_file), createEnabledApps(), createSchemas(),
* postInit(), initializePipelines(), openPipelines(). Use getAppManager()
* or getDatabaseManager() to access managers. Obtain from
* AppManagers::getInstance() or AppRegistrations::getInstance().
*/
class AppManagers
{
public:
/// Register an app as early as possible (doesn't create it yet).
/// You need to call this method for all apps you might end up
/// creating **before** calling createAppManager(). As soon as
/// createAppManager() is called the first time, you can no
/// longer call registerApp().
/// \brief Register an app type; must be called before createAppManager()
/// \note You need to call this method for all apps you might end up
/// creating **before** calling createAppManager(). As soon as
/// createAppManager() is called the first time, you can no
/// longer call registerApp().
template <typename AppT> void registerApp()
{
if (app_registration_locked_)
Expand Down Expand Up @@ -980,15 +1002,22 @@ class AppManagers
bool accepting_logger_requests_ = true;
};

/// Helper class to only expose AppManagers::registerApp() api.
/*!
* \class AppRegistrations
*
* \brief Thin wrapper that exposes only registerApp<AppT>() on an AppManagers
* instance. Use when you want to restrict API surface to registration only.
*/
class AppRegistrations
{
public:
AppRegistrations(AppManagers* app_managers) :
/// \brief Construct with the AppManagers instance to wrap.
explicit AppRegistrations(AppManagers* app_managers) :
app_managers_(app_managers)
{
}

/// \brief Register an app type (forwards to AppManagers::registerApp<AppT>()).
template <typename AppT> void registerApp() { app_managers_->registerApp<AppT>(); }

private:
Expand Down
53 changes: 31 additions & 22 deletions include/simdb/pipeline/AsyncDatabaseAccessor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,21 @@ class DatabaseManager;

namespace simdb::pipeline {

/// Function that is queued by any pipeline thread (or the main
/// thread) and invoked on the dedicated database thread.
/// \brief Callable queued by any thread and invoked on the dedicated database thread.
using AsyncDbAccessFunc = std::function<void(DatabaseManager*)>;

/// This struct is used to wrap a DB access function and
/// an exception that is suitable for std::future/promise.
/*!
* \struct AsyncDatabaseTask
*
* \brief Wraps an AsyncDbAccessFunc and a std::promise for the exception message;
* used to run the function on the DB thread and propagate exceptions to the caller.
*/
struct AsyncDatabaseTask
{
AsyncDbAccessFunc func;
std::promise<std::string> exception_reason;

AsyncDatabaseTask(AsyncDbAccessFunc func) :
explicit AsyncDatabaseTask(AsyncDbAccessFunc func) :
func(func)
{
}
Expand All @@ -33,34 +36,40 @@ struct AsyncDatabaseTask

using AsyncDatabaseTaskPtr = std::shared_ptr<AsyncDatabaseTask>;

/// Base class to be implemented by the DatabaseThread.
/*!
* \class AsyncDatabaseAccessHandler
*
* \brief Interface implemented by DatabaseThread. Receives tasks and runs them
* on the DB thread inside safeTransaction(); callers block until the
* task completes (or timeout).
*/
class AsyncDatabaseAccessHandler
{
public:
virtual ~AsyncDatabaseAccessHandler() = default;

/// Put a task on the DB thread for evaluation, and BLOCK
/// until it is called. The DB thread will complete its
/// current transaction (INSERTs) immediately and evaluate
/// this task inside a separate safeTransaction().
///
/// If a nonzero timeout is given, throws a DBException
/// if the task is not completed within the timeout.
/// \brief Run the task on the DB thread; block until done (or timeout).
/// \param task Task to run inside a safeTransaction().
/// \param timeout_seconds If > 0, throw if not completed within this many seconds.
/// \throws DBException on timeout or if the task throws.
virtual void eval(AsyncDatabaseTaskPtr&& task, double timeout_seconds = 0) = 0;
};

/// This class is used by SimDB apps and pipeline elements to
/// asynchronously access the database. It supports async data
/// writes and enqueuing general-purpose std::functions.
/*!
* \class AsyncDatabaseAccessor
*
* \brief Handle for pipeline stages and apps to run code on the dedicated
* database thread. eval(func) blocks the caller until func(DatabaseManager*)
* has run. Obtained from PipelineManager::getAsyncDatabaseAccessor() or
* DatabaseThread::getAsyncDatabaseAccessor().
*/
class AsyncDatabaseAccessor
{
public:
/// Invoke a std::function on the DB thread. This BLOCKS the
/// calling thread until the function is processed.
/// (Uses std::future/promise).
///
/// If a nonzero timeout is given, throws a DBException
/// if the task is not completed within the timeout.
/// \brief Run \p func on the DB thread; block until it completes (or timeout).
/// \param func Callable that receives the DatabaseManager*.
/// \param timeout_seconds If > 0, throw if not completed within this many seconds.
/// \throws DBException on timeout or if \p func throws.
void eval(const AsyncDbAccessFunc& func, double timeout_seconds = 0)
{
auto task = std::make_shared<AsyncDatabaseTask>(func);
Expand Down
17 changes: 15 additions & 2 deletions include/simdb/pipeline/DatabaseAccessor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,31 @@

namespace simdb::pipeline {

/// This class provides access to the DatabaseManager as well as prepared
/// INSERT objects that are specific to a particular App's schema.
/*!
* \class DatabaseAccessor
*
* \brief Provides DatabaseManager access and per-App prepared INSERT objects
* for DatabaseStage. Used by DatabaseStage<AppT> to get the manager
* and getTableInserter<AppT>(table_name) for high-volume inserts.
*/
class DatabaseAccessor
{
public:
/// \brief Construct with the DatabaseManager used for all operations.
/// \param db_mgr Non-null DatabaseManager.
DatabaseAccessor(DatabaseManager* db_mgr) :
db_mgr_(db_mgr)
{
}

/// \brief Return the DatabaseManager.
DatabaseManager* getDatabaseManager() const { return db_mgr_; }

/// \brief Return a prepared INSERT for the given table; lazily builds inserters from App::defineSchema().
/// \tparam App App type (must have NAME and defineSchema()).
/// \param tbl_name Table name in the App's schema.
/// \return Raw pointer to the PreparedINSERT (owned by this accessor).
/// \throws DBException if the table is not in the App's schema.
template <typename App> PreparedINSERT* getTableInserter(const std::string& tbl_name)
{
auto& inserters = tbl_inserters_by_app_[App::NAME];
Expand Down
Loading