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/. */
6 #ifndef MOZSTORAGEHELPER_H
7 #define MOZSTORAGEHELPER_H
11 #include "mozilla/DebugOnly.h"
12 #include "mozilla/ScopeExit.h"
14 #include "mozilla/storage/SQLiteMutex.h"
15 #include "mozIStorageConnection.h"
16 #include "mozIStorageStatement.h"
17 #include "mozIStoragePendingStatement.h"
18 #include "mozilla/DebugOnly.h"
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
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.
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.
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.
50 * WARNING: YOU SHOULD _NOT_ WRITE NEW MAIN-THREAD CODE USING THIS!
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.
59 * For all of the above reasons, this should only be used as an interim
60 * solution and avoided completely if possible.
62 class mozStorageTransaction
{
63 using SQLiteMutexAutoLock
= mozilla::storage::SQLiteMutexAutoLock
;
66 mozStorageTransaction(
67 mozIStorageConnection
* aConnection
, bool aCommitOnComplete
,
68 int32_t aType
= mozIStorageConnection::TRANSACTION_DEFAULT
,
69 bool aAsyncCommit
= false)
70 : mConnection(aConnection
),
73 mHasTransaction(false),
74 mCommitOnComplete(aCommitOnComplete
),
76 mAsyncCommit(aAsyncCommit
) {}
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");
85 mozilla::DebugOnly
<nsresult
> rv
= Rollback();
86 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv
),
87 "A transaction didn't rollback correctly");
93 * Starts the transaction.
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
);
102 // XXX We should probably stop supporting null mConnection.
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
) {
110 SQLiteMutexAutoLock
lock(mConnection
->GetSharedDBMutex());
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
115 TransactionStarted(lock
);
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
); });
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
));
131 case mozIStorageConnection::TRANSACTION_IMMEDIATE
:
132 query
.AppendLiteral(" IMMEDIATE");
134 case mozIStorageConnection::TRANSACTION_EXCLUSIVE
:
135 query
.AppendLiteral(" EXCLUSIVE");
137 case mozIStorageConnection::TRANSACTION_DEFERRED
:
138 query
.AppendLiteral(" DEFERRED");
141 MOZ_ASSERT(false, "Unknown transaction type");
144 query
.Assign("SAVEPOINT sp"_ns
+ IntToCString(mNestingLevel
));
147 nsresult rv
= mConnection
->ExecuteSimpleSQL(query
);
148 NS_ENSURE_SUCCESS(rv
, rv
);
150 autoFinishTransaction
.release();
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.
161 // XXX Assert instead of returning NS_OK if the transaction hasn't been
163 if (!mConnection
|| mCompleted
|| !mHasTransaction
) return NS_OK
;
165 SQLiteMutexAutoLock
lock(mConnection
->GetSharedDBMutex());
167 #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
168 MOZ_DIAGNOSTIC_ASSERT(CurrentTransaction(lock
));
170 if (!CurrentTransaction(lock
)) {
171 return NS_ERROR_NOT_AVAILABLE
;
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.
183 nsCOMPtr
<mozIStoragePendingStatement
> ps
;
184 rv
= mConnection
->ExecuteSimpleSQLAsync("COMMIT"_ns
, nullptr,
187 rv
= mConnection
->ExecuteSimpleSQL("COMMIT"_ns
);
190 rv
= mConnection
->ExecuteSimpleSQL("RELEASE sp"_ns
+
191 IntToCString(mNestingLevel
));
194 NS_ENSURE_SUCCESS(rv
, rv
);
196 TransactionFinished(lock
);
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.
206 nsresult
Rollback() {
207 // XXX Assert instead of returning NS_OK if the transaction hasn't been
209 if (!mConnection
|| mCompleted
|| !mHasTransaction
) return NS_OK
;
211 SQLiteMutexAutoLock
lock(mConnection
->GetSharedDBMutex());
213 #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
214 MOZ_DIAGNOSTIC_ASSERT(CurrentTransaction(lock
));
216 if (!CurrentTransaction(lock
)) {
217 return NS_ERROR_NOT_AVAILABLE
;
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.
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
);
233 const auto nestingLevelCString
= IntToCString(mNestingLevel
);
234 rv
= mConnection
->ExecuteSimpleSQL(
235 "ROLLBACK TO sp"_ns
+ nestingLevelCString
+ "; RELEASE sp"_ns
+
236 nestingLevelCString
);
239 NS_ENSURE_SUCCESS(rv
, rv
);
241 TransactionFinished(lock
);
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
);
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
);
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;
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
);
278 mHasTransaction
= false;
281 nsCOMPtr
<mozIStorageConnection
> mConnection
;
283 uint32_t mNestingLevel
;
284 bool mHasTransaction
;
285 bool mCommitOnComplete
;
291 * This class wraps a statement so that it is guaraneed to be reset when
292 * this object goes out of scope.
294 * Note that this always just resets the statement. If the statement doesn't
295 * need resetting, the reset operation is inexpensive.
297 class MOZ_STACK_CLASS mozStorageStatementScoper
{
299 explicit mozStorageStatementScoper(mozIStorageStatement
* aStatement
)
300 : mStatement(aStatement
) {}
301 ~mozStorageStatementScoper() {
302 if (mStatement
) mStatement
->Reset();
305 mozStorageStatementScoper(mozStorageStatementScoper
&&) = default;
306 mozStorageStatementScoper
& operator=(mozStorageStatementScoper
&&) = default;
307 mozStorageStatementScoper(const mozStorageStatementScoper
&) = delete;
308 mozStorageStatementScoper
& operator=(const mozStorageStatementScoper
&) =
312 * Call this to make the statement not reset. You might do this if you know
313 * that the statement has been reset.
315 void Abandon() { mStatement
= nullptr; }
318 nsCOMPtr
<mozIStorageStatement
> mStatement
;
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__ " */ "
326 #endif /* MOZSTORAGEHELPER_H */