sqlite/Transaction.hpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // <Transaction.hpp> -*- C++ -*- | ||
| 2 | |||
| 3 | #pragma once | ||
| 4 | |||
| 5 | #include "simdb/Assert.hpp" | ||
| 6 | #include "simdb/Exceptions.hpp" | ||
| 7 | |||
| 8 | #include <chrono> | ||
| 9 | #include <functional> | ||
| 10 | #include <memory> | ||
| 11 | #include <mutex> | ||
| 12 | #include <sqlite3.h> | ||
| 13 | #include <thread> | ||
| 14 | |||
| 15 | namespace simdb { | ||
| 16 | |||
| 17 | /// To support SimDB self-profiling, return TRUE only if the transaction | ||
| 18 | /// involved touching the database (setProperty*(), INSERT, SELECT, etc.) | ||
| 19 | using TransactionFunc = std::function<void()>; | ||
| 20 | |||
| 21 | /*! | ||
| 22 | * \class SQLiteReturnCode | ||
| 23 | * \brief This class wraps a return code and throws a | ||
| 24 | * SafeTransactionSilentException when it encounters a "SQL locked" return code. | ||
| 25 | */ | ||
| 26 | class SQLiteReturnCode | ||
| 27 | { | ||
| 28 | public: | ||
| 29 | 1400364 | explicit SQLiteReturnCode(const int rc) : | |
| 30 | 1400364 | rc_(rc) | |
| 31 | { | ||
| 32 | 1400364 | if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED || rc == SQLITE_READONLY) | |
| 33 | { | ||
| 34 | 1 | throw SafeTransactionSilentException(rc); | |
| 35 | } | ||
| 36 | 1400363 | } | |
| 37 | |||
| 38 | 8 | operator int() const { return rc_; } | |
| 39 | |||
| 40 | 655322 | operator bool() const { return rc_ != SQLITE_OK; } | |
| 41 | |||
| 42 | 859886 | bool operator==(const int rc) { return rc_ == rc; } | |
| 43 | |||
| 44 | bool operator!=(const int rc) { return rc_ != rc; } | ||
| 45 | |||
| 46 | private: | ||
| 47 | const int rc_; | ||
| 48 | }; | ||
| 49 | |||
| 50 | 8 | inline bool operator==(const int rc, const SQLiteReturnCode& obj) | |
| 51 | { | ||
| 52 | 8 | return static_cast<int>(obj) == rc; | |
| 53 | } | ||
| 54 | |||
| 55 | ✗ | inline std::ostream& operator<<(std::ostream& os, const SQLiteReturnCode& rc) | |
| 56 | { | ||
| 57 | ✗ | os << (int)rc; | |
| 58 | ✗ | return os; | |
| 59 | } | ||
| 60 | |||
| 61 | /*! | ||
| 62 | * \class SQLitePreparedStatement | ||
| 63 | * \brief This class wraps a sqlite3_stmt* and uses RAII to ensure that | ||
| 64 | * sqlite3_finalize() is called so we don't leak resources. | ||
| 65 | */ | ||
| 66 | class SQLitePreparedStatement | ||
| 67 | { | ||
| 68 | public: | ||
| 69 | 230186 | SQLitePreparedStatement(sqlite3* db_conn, const std::string_view cmd) | |
| 70 | 230186 | { | |
| 71 | 230186 | sqlite3_stmt* stmt = nullptr; | |
| 72 | 230186 | auto rc = sqlite3_prepare_v2(db_conn, cmd.data(), -1, &stmt, 0); | |
| 73 | 230186 | if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED || rc == SQLITE_READONLY) | |
| 74 | { | ||
| 75 | ✗ | sqlite3_finalize(stmt); | |
| 76 | ✗ | throw SafeTransactionSilentException(rc); | |
| 77 | } else | ||
| 78 | { | ||
| 79 | 230192 | simdb_assert(stmt, "Invalid prepared statement for cmd: " << cmd); | |
| 80 | } | ||
| 81 | |||
| 82 | 230183 | stmt_ = stmt; | |
| 83 | 230183 | } | |
| 84 | |||
| 85 | SQLitePreparedStatement(sqlite3_stmt* stmt) : | ||
| 86 | stmt_(stmt) | ||
| 87 | { | ||
| 88 | } | ||
| 89 | |||
| 90 | 218 | SQLitePreparedStatement(SQLitePreparedStatement&& other) : | |
| 91 | 218 | stmt_(other.stmt_) | |
| 92 | { | ||
| 93 | 218 | other.stmt_ = nullptr; | |
| 94 | 218 | } | |
| 95 | |||
| 96 | SQLitePreparedStatement(const SQLitePreparedStatement& other) = delete; | ||
| 97 | |||
| 98 | 230401 | ~SQLitePreparedStatement() | |
| 99 | { | ||
| 100 | 230401 | if (stmt_) | |
| 101 | { | ||
| 102 | 210697 | sqlite3_finalize(stmt_); | |
| 103 | } | ||
| 104 | 230401 | } | |
| 105 | |||
| 106 | 421157 | operator sqlite3_stmt*() const { return stmt_; } | |
| 107 | |||
| 108 | 19486 | sqlite3_stmt* release() | |
| 109 | { | ||
| 110 | 19486 | auto stmt = stmt_; | |
| 111 | 19486 | stmt_ = nullptr; | |
| 112 | 19486 | return stmt; | |
| 113 | } | ||
| 114 | |||
| 115 | private: | ||
| 116 | sqlite3_stmt* stmt_ = nullptr; | ||
| 117 | }; | ||
| 118 | |||
| 119 | /*! | ||
| 120 | * \class Transaction | ||
| 121 | * | ||
| 122 | * \brief Base class for Connection. Made into a base class | ||
| 123 | * to make it easier for SimDB to be a header-only library | ||
| 124 | * that avoids cyclic header includes. | ||
| 125 | */ | ||
| 126 | class Transaction | ||
| 127 | { | ||
| 128 | public: | ||
| 129 | /// Destructor | ||
| 130 | 28 | virtual ~Transaction() = default; | |
| 131 | |||
| 132 | /// Execute the functor inside BEGIN/COMMIT TRANSACTION. | ||
| 133 | 221804 | void safeTransaction(const TransactionFunc& transaction, | |
| 134 | uint64_t num_retries = std::numeric_limits<uint64_t>::max()) | ||
| 135 | { | ||
| 136 | 221804 | uint64_t num_tries = 0; | |
| 137 | |||
| 138 | while (true) | ||
| 139 | { | ||
| 140 | 221804 | std::unique_lock<std::recursive_mutex> lock(mutex_); | |
| 141 | |||
| 142 | // Check to see if we are already in a transaction, in which | ||
| 143 | // case we simply call the transaction function. We cannot | ||
| 144 | // call "BEGIN TRANSACTION" recursively. | ||
| 145 | // | ||
| 146 | // Note that any exception thrown here (including a | ||
| 147 | // SafeTransactionSilentException) must propagate up to the | ||
| 148 | // outermost safeTransaction() call below -- that call is the | ||
| 149 | // one that issued "BEGIN TRANSACTION" and is the only one | ||
| 150 | // that should roll back and/or retry. Retrying or rolling | ||
| 151 | // back here would act on only part of the outer transaction. | ||
| 152 | 221804 | if (in_transaction_flag_) | |
| 153 | { | ||
| 154 | 203178 | transaction(); | |
| 155 | 203178 | return; | |
| 156 | } | ||
| 157 | |||
| 158 | // We are the outermost safeTransaction() call. Set the flag | ||
| 159 | // for the duration of BEGIN/COMMIT/ROLLBACK so that any nested | ||
| 160 | // safeTransaction() calls (including ones made indirectly by | ||
| 161 | // the exception paths below) know not to issue their own | ||
| 162 | // BEGIN TRANSACTION. The flag is unconditionally cleared | ||
| 163 | // before this call returns or throws, on every code path. | ||
| 164 | 18626 | in_transaction_flag_ = true; | |
| 165 | try | ||
| 166 | { | ||
| 167 | 18626 | executeCommand_("BEGIN TRANSACTION"); | |
| 168 | 18626 | transaction(); | |
| 169 | 18622 | executeCommand_("COMMIT TRANSACTION"); | |
| 170 | 18622 | in_transaction_flag_ = false; | |
| 171 | 18622 | return; | |
| 172 | 4 | } catch (const SafeTransactionSilentException&) | |
| 173 | { | ||
| 174 | ✗ | in_transaction_flag_ = false; | |
| 175 | ✗ | rollbackSilently_(); | |
| 176 | ✗ | lock.unlock(); | |
| 177 | ✗ | std::this_thread::sleep_for(std::chrono::milliseconds(25)); | |
| 178 | |||
| 179 | // Loop around and retry the whole transaction from scratch. | ||
| 180 | ✗ | if (++num_tries > num_retries) | |
| 181 | { | ||
| 182 | ✗ | throw DBException("Max number of safeTransaction retries exceeded"); | |
| 183 | } | ||
| 184 | 4 | } catch (...) | |
| 185 | { | ||
| 186 | 4 | in_transaction_flag_ = false; | |
| 187 | 4 | rollbackSilently_(); | |
| 188 | 4 | throw; | |
| 189 | 4 | } | |
| 190 | 221804 | } | |
| 191 | } | ||
| 192 | |||
| 193 | /// For debug purposes only. | ||
| 194 | bool isInTransaction() const { return in_transaction_flag_; } | ||
| 195 | |||
| 196 | protected: | ||
| 197 | /// Underlying database connection | ||
| 198 | sqlite3* db_conn_ = nullptr; | ||
| 199 | |||
| 200 | private: | ||
| 201 | /// \brief Flag used in RAII safeTransaction() calls. This is | ||
| 202 | /// needed to we know whether to tell SQL to "BEGIN | ||
| 203 | /// TRANSACTION" or not (i.e. if we're already in the | ||
| 204 | /// middle of another safeTransaction). | ||
| 205 | /// | ||
| 206 | /// This allows users to freely do something like this: | ||
| 207 | /// | ||
| 208 | /// \code | ||
| 209 | /// db_mgr_->safeTransaction([&]() { | ||
| 210 | /// doFoo(); | ||
| 211 | /// doBar(); | ||
| 212 | /// }); | ||
| 213 | /// \endcode | ||
| 214 | /// | ||
| 215 | /// Even if doFoo() and doBar() do the same thing: | ||
| 216 | /// | ||
| 217 | /// \code | ||
| 218 | /// void MyClass::doFoo() { | ||
| 219 | /// db_mgr_->safeTransaction([&](){ | ||
| 220 | /// ... | ||
| 221 | /// }); | ||
| 222 | /// } | ||
| 223 | /// | ||
| 224 | /// void MyClass::doBar() { | ||
| 225 | /// db_mgr_->safeTransaction([&](){ | ||
| 226 | /// ... | ||
| 227 | /// }); | ||
| 228 | /// } | ||
| 229 | /// \endcode | ||
| 230 | bool in_transaction_flag_ = false; | ||
| 231 | |||
| 232 | /// Mutex for thread-safe reentrant safeTransaction's. | ||
| 233 | std::recursive_mutex mutex_; | ||
| 234 | |||
| 235 | /// Execute the provided statement against the database connection. | ||
| 236 | /// This will validate the command, and throw if this command is | ||
| 237 | /// disallowed. Used for "BEGIN TRANSACTION" and "COMMIT TRANSACTION" | ||
| 238 | /// -- both of these are run as regular (non-destructor) code so that | ||
| 239 | /// any exception they throw, including SafeTransactionSilentException, | ||
| 240 | /// is always caught by safeTransaction()'s try/catch instead of | ||
| 241 | /// escaping from a noexcept destructor and calling std::terminate(). | ||
| 242 | 37248 | void executeCommand_(const char* cmd) | |
| 243 | { | ||
| 244 | 37248 | auto rc = SQLiteReturnCode(sqlite3_exec(db_conn_, cmd, nullptr, nullptr, nullptr)); | |
| 245 | 37248 | if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED || rc == SQLITE_READONLY) | |
| 246 | { | ||
| 247 | ✗ | throw SafeTransactionSilentException(rc); | |
| 248 | } else | ||
| 249 | { | ||
| 250 | 37248 | simdb_assert(!rc, sqlite3_errmsg(db_conn_)); | |
| 251 | } | ||
| 252 | 37248 | } | |
| 253 | |||
| 254 | /// Best-effort "ROLLBACK TRANSACTION", used to undo a transaction that | ||
| 255 | /// failed partway through (whether from a SafeTransactionSilentException | ||
| 256 | /// that we are about to retry, or any other exception propagating out of | ||
| 257 | /// safeTransaction()). We are already unwinding/handling another error | ||
| 258 | /// here, so failures from the rollback itself are intentionally ignored | ||
| 259 | /// rather than thrown -- throwing here could mask the original exception | ||
| 260 | /// or (if BEGIN TRANSACTION never actually succeeded) fail spuriously. | ||
| 261 | 4 | void rollbackSilently_() noexcept { sqlite3_exec(db_conn_, "ROLLBACK TRANSACTION", nullptr, nullptr, nullptr); } | |
| 262 | }; | ||
| 263 | |||
| 264 | } // namespace simdb | ||
| 265 |