Skip to content

Commit

Permalink
Bug 1355561 - Add a new API to spinningly close the database when str…
Browse files Browse the repository at this point in the history
…ictly needed, and ensure Close() does what it's named after. r=asuth

Introduce a new SpinningSynchronousClose API that allows to synchronously close
a connection that executed asynchronous statements, by spinning the events loop.
This is expected to be used rarely in particular cases like database corruption.
It is currently [noscript] since the only consumer is cpp, in the future we can
evaluate removing that, if we find more uses for it.

MozReview-Commit-ID: 7SPqutoF9jJ

--HG--
extra : rebase_source : d053e308192f335dfdca089668e56115b068ff8c
  • Loading branch information
mak77 committed Apr 12, 2017
1 parent 4486e0a commit 8d20cc6
Show file tree
Hide file tree
Showing 13 changed files with 249 additions and 83 deletions.
7 changes: 7 additions & 0 deletions dom/cache/Connection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ Connection::AsyncClose(mozIStorageCompletionCallback*)
return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP
Connection::SpinningSynchronousClose()
{
// not supported
return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP
Connection::AsyncClone(bool, mozIStorageCompletionCallback*)
{
Expand Down
14 changes: 14 additions & 0 deletions storage/mozIStorageAsyncConnection.idl
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,23 @@ interface mozIStorageAsyncConnection : nsISupports {
* If called on a connection that has already been closed or was
* never properly opened. The callback will still be dispatched
* to the main thread despite the returned error.
* @note If this call should fail, the callback won't be invoked.
*/
void asyncClose([optional] in mozIStorageCompletionCallback aCallback);

/**
* Forcibly closes a database connection synchronously.
* This should only be used when it's required to close and replace the
* database synchronously to return control to the consumer, for example in
* case of a detected corruption on database opening.
* Since this spins the events loop, it should be used only in very particular
* and rare situations, or it may cause unexpected consequences (crashes).
*
* @throws NS_ERROR_NOT_SAME_THREAD
* If called on a thread other than the one that opened it.
*/
[noscript] void spinningSynchronousClose();

/**
* Clone a database and make the clone read only if needed.
* SQL Functions and attached on-disk databases are applied to the new clone.
Expand Down
85 changes: 73 additions & 12 deletions storage/mozStorageConnection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -369,13 +369,9 @@ WaitForUnlockNotify(sqlite3* aDatabase)
return srv;
}

} // namespace

////////////////////////////////////////////////////////////////////////////////
//// Local Classes

namespace {

class AsyncCloseConnection final: public Runnable
{
public:
Expand Down Expand Up @@ -488,6 +484,32 @@ class AsyncInitializeClone final: public Runnable
nsCOMPtr<mozIStorageCompletionCallback> mCallback;
};

/**
* A listener for async connection closing.
*/
class CloseListener final : public mozIStorageCompletionCallback
{
public:
NS_DECL_ISUPPORTS
CloseListener()
: mClosed(false)
{
}

NS_IMETHOD Complete(nsresult, nsISupports*) override
{
mClosed = true;
return NS_OK;
}

bool mClosed;

private:
~CloseListener() = default;
};

NS_IMPL_ISUPPORTS(CloseListener, mozIStorageCompletionCallback)

} // namespace

////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -517,8 +539,7 @@ Connection::Connection(Service *aService,

Connection::~Connection()
{
(void)Close();

Unused << Close();
MOZ_ASSERT(!mAsyncExecutionThread,
"The async thread has not been shutdown properly!");
}
Expand Down Expand Up @@ -1250,11 +1271,22 @@ Connection::Close()
#endif // DEBUG

// Make sure we have not executed any asynchronous statements.
// If this fails, the mDBConn will be left open, resulting in a leak.
// Ideally we'd schedule some code to destroy the mDBConn once all its
// async statements have finished executing; see bug 704030.
bool asyncCloseWasCalled = !mAsyncExecutionThread;
NS_ENSURE_TRUE(asyncCloseWasCalled, NS_ERROR_UNEXPECTED);
// If this fails, the mDBConn may be left open, resulting in a leak.
// We'll try to finalize the pending statements and close the connection.
if (isAsyncExecutionThreadAvailable()) {
#ifdef DEBUG
if (NS_IsMainThread()) {
nsCOMPtr<nsIXPConnect> xpc = do_GetService(nsIXPConnect::GetCID());
Unused << xpc->DebugDumpJSStack(false, false, false);
}
#endif
MOZ_ASSERT(false,
"Close() was invoked on a connection that executed asynchronous statements. "
"Should have used asyncClose().");
// Try to close the database regardless, to free up resources.
Unused << SpinningSynchronousClose();
return NS_ERROR_UNEXPECTED;
}

// setClosedState nullifies our connection pointer, so we take a raw pointer
// off it, to pass it through the close procedure.
Expand All @@ -1265,6 +1297,32 @@ Connection::Close()
return internalClose(nativeConn);
}

NS_IMETHODIMP
Connection::SpinningSynchronousClose()
{
if (threadOpenedOn != NS_GetCurrentThread()) {
return NS_ERROR_NOT_SAME_THREAD;
}

// As currently implemented, we can't spin to wait for an existing AsyncClose.
// Our only existing caller will never have called close; assert if misused
// so that no new callers assume this works after an AsyncClose.
MOZ_DIAGNOSTIC_ASSERT(connectionReady());
if (!connectionReady()) {
return NS_ERROR_UNEXPECTED;
}

RefPtr<CloseListener> listener = new CloseListener();
nsresult rv = AsyncClose(listener);
NS_ENSURE_SUCCESS(rv, rv);
MOZ_ALWAYS_TRUE(SpinEventLoopUntil([&]() {
return listener->mClosed;
}));
MOZ_ASSERT(isClosed(), "The connection should be closed at this point");

return rv;
}

NS_IMETHODIMP
Connection::AsyncClose(mozIStorageCompletionCallback *aCallback)
{
Expand Down Expand Up @@ -1344,7 +1402,10 @@ Connection::AsyncClose(mozIStorageCompletionCallback *aCallback)
// callers ignore our return value.
Unused << NS_DispatchToMainThread(completeEvent.forget());
}
return Close();
MOZ_ALWAYS_SUCCEEDS(Close());
// Return a success inconditionally here, since Close() is unlikely to fail
// and we want to reassure the consumer that its callback will be invoked.
return NS_OK;
}

// setClosedState nullifies our connection pointer, so we take a raw pointer
Expand Down
7 changes: 7 additions & 0 deletions storage/mozStorageHelper.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include "mozIStorageStatement.h"
#include "mozIStoragePendingStatement.h"
#include "nsError.h"
#include "nsIXPConnect.h"

/**
* This class wraps a transaction inside a given C++ scope, guaranteeing that
Expand Down Expand Up @@ -227,6 +228,12 @@ class MOZ_STACK_CLASS mozStorageStatementScoper
} \
} \
} \
if (NS_IsMainThread()) { \
nsCOMPtr<nsIXPConnect> xpc = do_GetService(nsIXPConnect::GetCID()); \
if (xpc) { \
mozilla::Unused << xpc->DebugDumpJSStack(false, false, false); \
} \
} \
MOZ_ASSERT(false, "You are trying to use a deprecated mozStorage method. " \
"Check error message in console to identify the method name.");\
PR_END_MACRO
Expand Down
1 change: 1 addition & 0 deletions storage/test/gtest/moz.build
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ UNIFIED_SOURCES += [
'test_file_perms.cpp',
'test_mutex.cpp',
'test_service_init_background_thread.cpp',
'test_spinningSynchronousClose.cpp',
'test_statement_scoper.cpp',
'test_StatementCache.cpp',
'test_transaction_helper.cpp',
Expand Down
78 changes: 78 additions & 0 deletions storage/test/gtest/test_spinningSynchronousClose.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
* vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "storage_test_harness.h"
#include "prinrval.h"

/**
* Helper to verify that the event loop was spun. As long as this is dispatched
* prior to a call to Close()/SpinningSynchronousClose() we are guaranteed this
* will be run if the event loop is spun to perform a close. This is because
* SpinningSynchronousClose must spin the event loop to realize the close
* completed and our runnable will already be enqueued and therefore run before
* the AsyncCloseConnection's callback. Note that this invariant may be
* violated if our runnables end up in different queues thanks to Quantum
* changes, so this test may need to be updated if the close dispatch changes.
*/
class CompletionRunnable final : public Runnable
{
public:
explicit CompletionRunnable()
: mDone(false)
{
}

NS_IMETHOD Run() override
{
mDone = true;
return NS_OK;
}

bool mDone;
};

// Can only run in optimized builds, or it would assert.
#ifndef DEBUG
TEST(storage_spinningSynchronousClose, CloseOnAsync)
{
nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
// Run an async statement.
nsCOMPtr<mozIStorageAsyncStatement> stmt;
do_check_success(db->CreateAsyncStatement(
NS_LITERAL_CSTRING("CREATE TABLE test (id INTEGER PRIMARY KEY)"),
getter_AddRefs(stmt)
));
nsCOMPtr<mozIStoragePendingStatement> p;
do_check_success(stmt->ExecuteAsync(nullptr, getter_AddRefs(p)));
do_check_success(stmt->Finalize());

// Wrongly use Close() instead of AsyncClose().
RefPtr<CompletionRunnable> event = new CompletionRunnable();
NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL);
do_check_false(NS_SUCCEEDED(db->Close()));
do_check_true(event->mDone);
}
#endif

TEST(storage_spinningSynchronousClose, spinningSynchronousCloseOnAsync)
{
nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
// Run an async statement.
nsCOMPtr<mozIStorageAsyncStatement> stmt;
do_check_success(db->CreateAsyncStatement(
NS_LITERAL_CSTRING("CREATE TABLE test (id INTEGER PRIMARY KEY)"),
getter_AddRefs(stmt)
));
nsCOMPtr<mozIStoragePendingStatement> p;
do_check_success(stmt->ExecuteAsync(nullptr, getter_AddRefs(p)));
do_check_success(stmt->Finalize());

// Use the spinning close API.
RefPtr<CompletionRunnable> event = new CompletionRunnable();
NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL);
do_check_success(db->SpinningSynchronousClose());
do_check_true(event->mDone);
}
32 changes: 17 additions & 15 deletions storage/test/unit/test_connection_asyncClose.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,23 +53,25 @@ add_task(function* test_asyncClose_does_not_complete_before_statements() {
* async thread is not available and fall back to invoking Close() which will
* notice the mDBConn is already gone.
*/
add_task(function* test_double_asyncClose_throws() {
let db = yield openAsyncDatabase(getTestDB());
if (!AppConstants.DEBUG) {
add_task(function* test_double_asyncClose_throws() {
let db = yield openAsyncDatabase(getTestDB());

// (Don't yield control flow yet, save the promise for after we make the
// second call.)
// Branch coverage: (asyncThread && mDBConn)
let realClosePromise = yield asyncClose(db);
try {
// Branch coverage: (!asyncThread && !mDBConn)
db.asyncClose();
ok(false, "should have thrown");
} catch (e) {
equal(e.result, Cr.NS_ERROR_NOT_INITIALIZED);
}
// (Don't yield control flow yet, save the promise for after we make the
// second call.)
// Branch coverage: (asyncThread && mDBConn)
let realClosePromise = yield asyncClose(db);
try {
// Branch coverage: (!asyncThread && !mDBConn)
db.asyncClose();
ok(false, "should have thrown");
} catch (e) {
equal(e.result, Cr.NS_ERROR_NOT_INITIALIZED);
}

yield realClosePromise;
});
yield realClosePromise;
});
}

/**
* Create a sync db connection and never take it asynchronous and then call
Expand Down
23 changes: 9 additions & 14 deletions storage/test/unit/test_storage_connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,24 +265,19 @@ if (!AppConstants.DEBUG) {
});
}

add_task(function* test_close_fails_with_async_statement_ran() {
let deferred = Promise.defer();
let stmt = createStatement("SELECT * FROM test");
stmt.executeAsync();
stmt.finalize();

let db = getOpenedDatabase();
Assert.throws(() => db.close(), /NS_ERROR_UNEXPECTED/);
// In debug builds this would cause a fatal assertion.
if (!AppConstants.DEBUG) {
add_task(function* test_close_fails_with_async_statement_ran() {
let stmt = createStatement("SELECT * FROM test");
stmt.executeAsync();
stmt.finalize();

// Clean up after ourselves.
db.asyncClose(function() {
let db = getOpenedDatabase();
Assert.throws(() => db.close(), /NS_ERROR_UNEXPECTED/);
// Reset gDBConn so that later tests will get a new connection object.
gDBConn = null;
deferred.resolve();
});

yield deferred.promise;
});
}

add_task(function* test_clone_optional_param() {
let db1 = getService().openUnsharedDatabase(getTestDB());
Expand Down
4 changes: 3 additions & 1 deletion toolkit/components/contentprefs/nsContentPrefService.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,9 @@ ContentPrefService.prototype = {
if (this._contentPrefService2)
this._contentPrefService2.destroy();

this._dbConnection.asyncClose();
this._dbConnection.asyncClose(() => {
Services.obs.notifyObservers(null, "content-prefs-db-closed");
});

// Delete references to XPCOM components to make sure we don't leak them
// (although we haven't observed leakage in tests). Also delete references
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ var Cu = Components.utils;

Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/ContentPrefInstance.jsm");
Cu.import("resource://testing-common/TestUtils.jsm");

const CONTENT_PREFS_DB_FILENAME = "content-prefs.sqlite";
const CONTENT_PREFS_BACKUP_DB_FILENAME = "content-prefs.sqlite.corrupt";
Expand Down
Loading

0 comments on commit 8d20cc6

Please sign in to comment.