mozStorageHelper.h (11288B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 #ifndef MOZSTORAGEHELPER_H 7 #define MOZSTORAGEHELPER_H 8 9 #include "nsCOMPtr.h" 10 #include "nsString.h" 11 #include "mozilla/DebugOnly.h" 12 #include "mozilla/ScopeExit.h" 13 14 #include "mozilla/storage/SQLiteMutex.h" 15 #include "mozIStorageConnection.h" 16 #include "mozIStorageStatement.h" 17 #include "mozIStoragePendingStatement.h" 18 #include "mozilla/DebugOnly.h" 19 #include "nsCOMPtr.h" 20 #include "nsError.h" 21 22 /** 23 * This class wraps a transaction inside a given C++ scope, guaranteeing that 24 * the transaction will be completed even if you have an exception or 25 * return early. 26 * 27 * A common use is to create an instance with aCommitOnComplete = false 28 * (rollback), then call Commit() on this object manually when your function 29 * completes successfully. 30 * 31 * @note nested transactions are not supported by Sqlite, only nested 32 * savepoints, so if a transaction is already in progress, this object creates 33 * a nested savepoint to the existing transaction which is considered as 34 * anonymous savepoint itself. However, aType and aAsyncCommit are ignored 35 * in the case of nested savepoints. 36 * 37 * @param aConnection 38 * The connection to create the transaction on. 39 * @param aCommitOnComplete 40 * Controls whether the transaction is committed or rolled back when 41 * this object goes out of scope. 42 * @param aType [optional] 43 * The transaction type, as defined in mozIStorageConnection. Uses the 44 * default transaction behavior for the connection if unspecified. 45 * @param aAsyncCommit [optional] 46 * Whether commit should be executed asynchronously on the helper thread. 47 * This is a special option introduced as an interim solution to reduce 48 * main-thread fsyncs in Places. Can only be used on main-thread. 49 * 50 * WARNING: YOU SHOULD _NOT_ WRITE NEW MAIN-THREAD CODE USING THIS! 51 * 52 * Notice that async commit might cause synchronous statements to fail 53 * with SQLITE_BUSY. A possible mitigation strategy is to use 54 * PRAGMA busy_timeout, but notice that might cause main-thread jank. 55 * Finally, if the database is using WAL journaling mode, other 56 * connections won't see the changes done in async committed transactions 57 * until commit is complete. 58 * 59 * For all of the above reasons, this should only be used as an interim 60 * solution and avoided completely if possible. 61 */ 62 class mozStorageTransaction { 63 using SQLiteMutexAutoLock = mozilla::storage::SQLiteMutexAutoLock; 64 65 public: 66 mozStorageTransaction( 67 mozIStorageConnection* aConnection, bool aCommitOnComplete, 68 int32_t aType = mozIStorageConnection::TRANSACTION_DEFAULT, 69 bool aAsyncCommit = false) 70 : mConnection(aConnection), 71 mType(aType), 72 mNestingLevel(0), 73 mHasTransaction(false), 74 mCommitOnComplete(aCommitOnComplete), 75 mCompleted(false), 76 mAsyncCommit(aAsyncCommit) {} 77 78 ~mozStorageTransaction() { 79 if (mConnection && mHasTransaction && !mCompleted) { 80 if (mCommitOnComplete) { 81 mozilla::DebugOnly<nsresult> rv = Commit(); 82 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), 83 "A transaction didn't commit correctly"); 84 } else { 85 mozilla::DebugOnly<nsresult> rv = Rollback(); 86 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), 87 "A transaction didn't rollback correctly"); 88 } 89 } 90 } 91 92 /** 93 * Starts the transaction. 94 */ 95 nsresult Start() { 96 // XXX We should probably get rid of mHasTransaction and use mConnection 97 // for checking if a transaction has been started. However, we need to 98 // first stop supporting null mConnection and also move aConnection from 99 // the constructor to Start. 100 MOZ_DIAGNOSTIC_ASSERT(!mHasTransaction); 101 102 // XXX We should probably stop supporting null mConnection. 103 104 // XXX We should probably get rid of mCompleted and allow to start the 105 // transaction again if it was already committed or rolled back. 106 if (!mConnection || mCompleted) { 107 return NS_OK; 108 } 109 110 SQLiteMutexAutoLock lock(mConnection->GetSharedDBMutex()); 111 112 // We nee to speculatively set the nesting level to be able to decide 113 // if this is a top level transaction and to be able to generate the 114 // savepoint name. 115 TransactionStarted(lock); 116 117 // If there's a failure we need to revert the speculatively set nesting 118 // level on the connection. 119 auto autoFinishTransaction = 120 mozilla::MakeScopeExit([&] { TransactionFinished(lock); }); 121 122 nsAutoCString query; 123 124 if (TopLevelTransaction(lock)) { 125 query.Assign("BEGIN"); 126 int32_t type = mType; 127 if (type == mozIStorageConnection::TRANSACTION_DEFAULT) { 128 MOZ_ALWAYS_SUCCEEDS(mConnection->GetDefaultTransactionType(&type)); 129 } 130 switch (type) { 131 case mozIStorageConnection::TRANSACTION_IMMEDIATE: 132 query.AppendLiteral(" IMMEDIATE"); 133 break; 134 case mozIStorageConnection::TRANSACTION_EXCLUSIVE: 135 query.AppendLiteral(" EXCLUSIVE"); 136 break; 137 case mozIStorageConnection::TRANSACTION_DEFERRED: 138 query.AppendLiteral(" DEFERRED"); 139 break; 140 default: 141 MOZ_ASSERT(false, "Unknown transaction type"); 142 } 143 } else { 144 query.Assign("SAVEPOINT sp"_ns + IntToCString(mNestingLevel)); 145 } 146 147 nsresult rv = mConnection->ExecuteSimpleSQL(query); 148 NS_ENSURE_SUCCESS(rv, rv); 149 150 autoFinishTransaction.release(); 151 152 return NS_OK; 153 } 154 155 /** 156 * Commits the transaction if one is in progress. If one is not in progress, 157 * this is a NOP since the actual owner of the transaction outside of our 158 * scope is in charge of finally committing or rolling back the transaction. 159 */ 160 nsresult Commit() { 161 // XXX Assert instead of returning NS_OK if the transaction hasn't been 162 // started. 163 if (!mConnection || mCompleted || !mHasTransaction) return NS_OK; 164 165 SQLiteMutexAutoLock lock(mConnection->GetSharedDBMutex()); 166 167 #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED 168 MOZ_DIAGNOSTIC_ASSERT(CurrentTransaction(lock)); 169 #else 170 if (!CurrentTransaction(lock)) { 171 return NS_ERROR_NOT_AVAILABLE; 172 } 173 #endif 174 175 mCompleted = true; 176 177 nsresult rv; 178 179 if (TopLevelTransaction(lock)) { 180 // TODO (bug 559659): this might fail with SQLITE_BUSY, but we don't 181 // handle it, thus the transaction might stay open until the next COMMIT. 182 if (mAsyncCommit) { 183 nsCOMPtr<mozIStoragePendingStatement> ps; 184 rv = mConnection->ExecuteSimpleSQLAsync("COMMIT"_ns, nullptr, 185 getter_AddRefs(ps)); 186 } else { 187 rv = mConnection->ExecuteSimpleSQL("COMMIT"_ns); 188 } 189 } else { 190 rv = mConnection->ExecuteSimpleSQL("RELEASE sp"_ns + 191 IntToCString(mNestingLevel)); 192 } 193 194 NS_ENSURE_SUCCESS(rv, rv); 195 196 TransactionFinished(lock); 197 198 return NS_OK; 199 } 200 201 /** 202 * Rolls back the transaction if one is in progress. If one is not in 203 * progress, this is a NOP since the actual owner of the transaction outside 204 * of our scope is in charge of finally rolling back the transaction. 205 */ 206 nsresult Rollback() { 207 // XXX Assert instead of returning NS_OK if the transaction hasn't been 208 // started. 209 if (!mConnection || mCompleted || !mHasTransaction) return NS_OK; 210 211 SQLiteMutexAutoLock lock(mConnection->GetSharedDBMutex()); 212 213 #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED 214 MOZ_DIAGNOSTIC_ASSERT(CurrentTransaction(lock)); 215 #else 216 if (!CurrentTransaction(lock)) { 217 return NS_ERROR_NOT_AVAILABLE; 218 } 219 #endif 220 221 mCompleted = true; 222 223 nsresult rv; 224 225 if (TopLevelTransaction(lock)) { 226 // TODO (bug 1062823): from Sqlite 3.7.11 on, rollback won't ever return 227 // a busy error, so this handling can be removed. 228 do { 229 rv = mConnection->ExecuteSimpleSQL("ROLLBACK"_ns); 230 if (rv == NS_ERROR_STORAGE_BUSY) (void)PR_Sleep(PR_INTERVAL_NO_WAIT); 231 } while (rv == NS_ERROR_STORAGE_BUSY); 232 } else { 233 const auto nestingLevelCString = IntToCString(mNestingLevel); 234 rv = mConnection->ExecuteSimpleSQL( 235 "ROLLBACK TO sp"_ns + nestingLevelCString + "; RELEASE sp"_ns + 236 nestingLevelCString); 237 } 238 239 NS_ENSURE_SUCCESS(rv, rv); 240 241 TransactionFinished(lock); 242 243 return NS_OK; 244 } 245 246 protected: 247 void TransactionStarted(const SQLiteMutexAutoLock& aProofOfLock) { 248 MOZ_ASSERT(mConnection); 249 MOZ_ASSERT(!mHasTransaction); 250 MOZ_ASSERT(mNestingLevel == 0); 251 mHasTransaction = true; 252 mNestingLevel = mConnection->IncreaseTransactionNestingLevel(aProofOfLock); 253 } 254 255 bool CurrentTransaction(const SQLiteMutexAutoLock& aProofOfLock) const { 256 MOZ_ASSERT(mConnection); 257 MOZ_ASSERT(mHasTransaction); 258 MOZ_ASSERT(mNestingLevel > 0); 259 return mNestingLevel == 260 mConnection->GetTransactionNestingLevel(aProofOfLock); 261 } 262 263 bool TopLevelTransaction(const SQLiteMutexAutoLock& aProofOfLock) const { 264 MOZ_ASSERT(mConnection); 265 MOZ_ASSERT(mHasTransaction); 266 MOZ_ASSERT(mNestingLevel > 0); 267 MOZ_ASSERT(CurrentTransaction(aProofOfLock)); 268 return mNestingLevel == 1; 269 } 270 271 void TransactionFinished(const SQLiteMutexAutoLock& aProofOfLock) { 272 MOZ_ASSERT(mConnection); 273 MOZ_ASSERT(mHasTransaction); 274 MOZ_ASSERT(mNestingLevel > 0); 275 MOZ_ASSERT(CurrentTransaction(aProofOfLock)); 276 mConnection->DecreaseTransactionNestingLevel(aProofOfLock); 277 mNestingLevel = 0; 278 mHasTransaction = false; 279 } 280 281 nsCOMPtr<mozIStorageConnection> mConnection; 282 int32_t mType; 283 uint32_t mNestingLevel; 284 bool mHasTransaction; 285 bool mCommitOnComplete; 286 bool mCompleted; 287 bool mAsyncCommit; 288 }; 289 290 /** 291 * This class wraps a statement so that it is guaraneed to be reset when 292 * this object goes out of scope. 293 * 294 * Note that this always just resets the statement. If the statement doesn't 295 * need resetting, the reset operation is inexpensive. 296 */ 297 class MOZ_STACK_CLASS mozStorageStatementScoper { 298 public: 299 explicit mozStorageStatementScoper(mozIStorageStatement* aStatement) 300 : mStatement(aStatement) {} 301 ~mozStorageStatementScoper() { 302 if (mStatement) mStatement->Reset(); 303 } 304 305 mozStorageStatementScoper(mozStorageStatementScoper&&) = default; 306 mozStorageStatementScoper& operator=(mozStorageStatementScoper&&) = default; 307 mozStorageStatementScoper(const mozStorageStatementScoper&) = delete; 308 mozStorageStatementScoper& operator=(const mozStorageStatementScoper&) = 309 delete; 310 311 /** 312 * Call this to make the statement not reset. You might do this if you know 313 * that the statement has been reset. 314 */ 315 void Abandon() { mStatement = nullptr; } 316 317 protected: 318 nsCOMPtr<mozIStorageStatement> mStatement; 319 }; 320 321 // Use this to make queries uniquely identifiable in telemetry 322 // statistics, especially PRAGMAs. We don't include __LINE__ so that 323 // queries are stable in the face of source code changes. 324 #define MOZ_STORAGE_UNIQUIFY_QUERY_STR "/* " __FILE__ " */ " 325 326 #endif /* MOZSTORAGEHELPER_H */