/* * LMDB Abstraction Layer. * Copyright (C) 2023 Yury Gubich * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "base.h" #include "exceptions.h" #include "storage.h" #define UNUSED(x) (void)(x) /** * \class LMDBAL::Base * \brief Database abstraction * * This is a basic class that represents the database as a collection of storages. * Storages is something key-value database has instead of tables in classic SQL databases. */ /** * \brief Creates the database * * \param[in] name - name of the database, it is going to affect folder name that is created to store data * \param[in] mapSize - LMDB map size (MBi), multiplied by 1024^2 and passed to mdb_env_set_mapsize during the call of LMDBAL::Base::open() */ LMDBAL::Base::Base(const QString& p_name, uint16_t mapSize): name(p_name.toStdString()), opened(false), size(mapSize), environment(), storages(), transactions(new Transactions()) {} /** * \brief Destroys the database */ LMDBAL::Base::~Base() { close(); delete transactions; for (const std::pair& pair : storages) delete pair.second; } /** * \brief Closes the database * * Closes all lmdb handles, aborts all public transactions. * This function will do nothing on closed database * * \exception LMDBAL::Unknown - thrown if something went wrong aborting transactions */ void LMDBAL::Base::close() { if (opened) { for (LMDBAL::TransactionID id : *transactions) abortTransaction(id, emptyName); for (const std::pair& pair : storages) { iStorage* storage = pair.second; mdb_dbi_close(environment, storage->dbi); } mdb_env_close(environment); transactions->clear(); opened = false; } } /** * \brief Opens the database * * Almost every LMDBAL::Base require it to be opened, this function does it. * It laso creates the directory for the database if it was an initial launch. * This function will do nothing on opened database * * \exception LMDBAL::Unknown - thrown if something went wrong opening storages and caches */ void LMDBAL::Base::open() { if (!opened) { mdb_env_create(&environment); QString path = createDirectory(); mdb_env_set_maxdbs(environment, storages.size()); mdb_env_set_mapsize(environment, size * 1024UL * 1024UL); mdb_env_open(environment, path.toStdString().c_str(), 0, 0664); TransactionID txn = beginPrivateTransaction(emptyName); for (const std::pair& pair : storages) { iStorage* storage = pair.second; int rc = storage->createStorage(txn); if (rc) throw Unknown(name, mdb_strerror(rc)); } commitPrivateTransaction(txn, emptyName); opened = true; } } /** * \brief Removes database directory * * \returns true if removal was successfull of if no directory was created where it's expected to be, false otherwise * * \exception LMDBAL::Opened - thrown if this function was called on opened database */ bool LMDBAL::Base::removeDirectory() { if (opened) throw Opened(name, "remove database directory"); QString path(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); path += "/" + getName(); QDir cache(path); if (cache.exists()) return cache.removeRecursively(); else return true; } /** * \brief Creates database directory * * Creates or opens existing directory with the given name in the location acquired with * QStandardPaths::writableLocation(QStandardPaths::CacheLocation) * so, the file system destination of your data would depend on the * QCoreApplication configuration of your app. * This function does nothing if the directory was already created * * \returns the path of the created directory * * \exception LMDBAL::Opened - thrown if called on opened database * \exception LMDBAL::Directory - if the database couldn't create the folder */ QString LMDBAL::Base::createDirectory() { if (opened) throw Opened(name, "create database directory"); QString path(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); path += "/" + getName(); QDir cache(path); if (!cache.exists()) { bool res = cache.mkpath(path); if (!res) throw Directory(path.toStdString()); } return path; } /** * \brief Returns database name * * \returns database name */ QString LMDBAL::Base::getName() const { return QString::fromStdString(name);} /** * \brief Returns database state * * \returns true if the database is opened and ready for work, false otherwise */ bool LMDBAL::Base::ready() const { return opened;} /** * \brief Drops the database * * Clears all caches and storages of the database * * \exception LMDBAL::Closed - thrown if the database is closed * \exception LMDBAL::Unknown - thrown if something unexpected happend */ void LMDBAL::Base::drop() { if (!opened) throw Closed("drop", name); TransactionID txn = beginTransaction(); for (const std::pair& pair : storages) { int rc = pair.second->drop(txn); if (rc != MDB_SUCCESS) { abortTransaction(txn); throw Unknown(name, mdb_strerror(rc), pair.first); } } commitTransaction(txn); } /** * \brief Begins read-only transaction * * \returns read-only transaction ID * * \exception LMDBAL::Closed - thrown if the database is closed * \exception LMDBAL::Unknown - thrown if something unexpected happened */ LMDBAL::TransactionID LMDBAL::Base::beginReadOnlyTransaction() const { return beginReadOnlyTransaction(emptyName);} /** * \brief Begins writable transaction * * \returns writable transaction ID * * \exception LMDBAL::Closed - thrown if the database is closed * \exception LMDBAL::Unknown - thrown if something unexpected happened */ LMDBAL::TransactionID LMDBAL::Base::beginTransaction() const { return beginTransaction(emptyName);} /** * \brief Aborts transaction * * Terminates transaction cancelling changes. * This is an optimal way to terminate read-only transactions * * \param[in] id - transaction ID you want to abort * * \exception LMDBAL::Closed - thrown if the database is closed * \exception LMDBAL::Unknown - thrown if transaction with given ID was not found or if something unexpected happened */ void LMDBAL::Base::abortTransaction(LMDBAL::TransactionID id) const { return abortTransaction(id, emptyName);} /** * \brief Commits transaction * * Terminates transaction applying changes. * * \param[in] id - transaction ID you want to commit * * \exception LMDBAL::Closed - thrown if the database is closed * \exception LMDBAL::Unknown - thrown if transaction with given ID was not found or if something unexpected happened */ void LMDBAL::Base::commitTransaction(LMDBAL::TransactionID id) { return commitTransaction(id, emptyName);} /** * \brief Begins read-only transaction * * This function is intended to be called from subordinate storage or cache * * \param[in] storageName - name of the storage/cache that you begin transaction from, needed just to inform if something went wrong * * \returns read-only transaction ID * * \exception LMDBAL::Closed - thrown if the database is closed * \exception LMDBAL::Unknown - thrown if something unexpected happened */ LMDBAL::TransactionID LMDBAL::Base::beginReadOnlyTransaction(const std::string& storageName) const { if (!opened) throw Closed("beginReadOnlyTransaction", name, storageName); TransactionID txn = beginPrivateReadOnlyTransaction(storageName); transactions->emplace(txn); for (const std::pair& pair : storages) pair.second->transactionStarted(txn, true); return txn; } /** * \brief Begins writable transaction * * This function is intended to be called from subordinate storage or cache * * \param[in] storageName - name of the storage/cache that you begin transaction from, needed just to inform if something went wrong * * \returns writable transaction ID * * \exception LMDBAL::Closed - thrown if the database is closed * \exception LMDBAL::Unknown - thrown if something unexpected happened */ LMDBAL::TransactionID LMDBAL::Base::beginTransaction(const std::string& storageName) const { if (!opened) throw Closed("beginTransaction", name, storageName); TransactionID txn = beginPrivateTransaction(storageName); transactions->emplace(txn); for (const std::pair& pair : storages) pair.second->transactionStarted(txn, false); return txn; } /** * \brief Aborts transaction * * Terminates transaction cancelling changes. * This is an optimal way to terminate read-only transactions. * This function is intended to be called from subordinate storage or cache * * \param[in] id - transaction ID you want to abort * \param[in] storageName - name of the storage/cache that you begin transaction from, needed just to inform if something went wrong * * \exception LMDBAL::Closed - thrown if the database is closed * \exception LMDBAL::Unknown - thrown if transaction with given ID was not found or if something unexpected happened */ void LMDBAL::Base::abortTransaction(LMDBAL::TransactionID id, const std::string& storageName) const { if (!opened) throw Closed("abortTransaction", name, storageName); Transactions::iterator itr = transactions->find(id); if (itr == transactions->end()) //TODO may be it's a good idea to make an exception class for this throw Unknown(name, "unable to abort transaction: transaction was not found", storageName); abortPrivateTransaction(id, storageName); for (const std::pair& pair : storages) pair.second->transactionAborted(id); transactions->erase(itr); } /** * \brief Commits transaction * * Terminates transaction applying changes. * This function is intended to be called from subordinate storage or cache * * \param[in] id - transaction ID you want to commit * \param[in] storageName - name of the storage/cache that you begin transaction from, needed just to inform if something went wrong * * \exception LMDBAL::Closed - thrown if the database is closed * \exception LMDBAL::Unknown - thrown if transaction with given ID was not found or if something unexpected happened */ void LMDBAL::Base::commitTransaction(LMDBAL::TransactionID id, const std::string& storageName) { if (!opened) throw Closed("abortTransaction", name, storageName); Transactions::iterator itr = transactions->find(id); if (itr == transactions->end()) //TODO may be it's a good idea to make an exception class for this throw Unknown(name, "unable to commit transaction: transaction was not found", storageName); commitPrivateTransaction(id, storageName); for (const std::pair& pair : storages) pair.second->transactionCommited(id); transactions->erase(itr); } /** * \brief Begins read-only transaction * * This function is intended to be called from subordinate storage or cache, * it's not accounted in transaction collection, other storages and caches are not notified about this kind of transaction * * \param[in] storageName - name of the storage/cache that you begin transaction from, needed just to inform if something went wrong * * \returns read-only transaction ID * * \exception LMDBAL::Unknown - thrown if something unexpected happened */ LMDBAL::TransactionID LMDBAL::Base::beginPrivateReadOnlyTransaction(const std::string& storageName) const { MDB_txn* txn; int rc = mdb_txn_begin(environment, NULL, MDB_RDONLY, &txn); if (rc != MDB_SUCCESS) { mdb_txn_abort(txn); throw Unknown(name, mdb_strerror(rc), storageName); } return txn; } /** * \brief Begins writable transaction * * This function is intended to be called from subordinate storage or cache, * it's not accounted in transaction collection, other storages and caches are not notified about this kind of transaction * * \param[in] storageName - name of the storage/cache that you begin transaction from, needed just to inform if something went wrong * * \returns writable transaction ID * * \exception LMDBAL::Unknown - thrown if something unexpected happened */ LMDBAL::TransactionID LMDBAL::Base::beginPrivateTransaction(const std::string& storageName) const { MDB_txn* txn; int rc = mdb_txn_begin(environment, NULL, 0, &txn); if (rc != MDB_SUCCESS) { mdb_txn_abort(txn); throw Unknown(name, mdb_strerror(rc), storageName); } return txn; } /** * \brief Aborts transaction * * Terminates transaction cancelling changes. * This is an optimal way to terminate read-only transactions. * This function is intended to be called from subordinate storage or cache, * it's not accounted in transaction collection, other storages and caches are not notified about this kind of transaction * * \param[in] id - transaction ID you want to abort * \param[in] storageName - name of the storage/cache that you begin transaction from, unused here */ void LMDBAL::Base::abortPrivateTransaction(LMDBAL::TransactionID id, const std::string& storageName) const { UNUSED(storageName); mdb_txn_abort(id); } /** * \brief Commits transaction * * Terminates transaction applying changes. * This function is intended to be called from subordinate storage or cache * it's not accounted in transaction collection, other storages and caches are not notified about this kind of transaction * * \param[in] id - transaction ID you want to commit * \param[in] storageName - name of the storage/cache that you begin transaction from, needed just to inform if something went wrong * * \exception LMDBAL::Unknown - thrown if transaction with given ID was not found or if something unexpected happened */ void LMDBAL::Base::commitPrivateTransaction(LMDBAL::TransactionID id, const std::string& storageName) { int rc = mdb_txn_commit(id); if (rc != MDB_SUCCESS) throw Unknown(name, mdb_strerror(rc), storageName); }