blob: 852f8f2a4204a7793377339927fab0ea6c47aafa [file] [log] [blame]
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.database.sqlite;
import android.database.sqlite.SQLiteDebug.DbStats;
import android.os.CancellationSignal;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.OperationCanceledException;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
import android.util.PrefixPrinter;
import android.util.Printer;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import dalvik.system.CloseGuard;
import java.io.Closeable;
import java.io.File;
import java.util.ArrayList;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.LockSupport;
/**
* Maintains a pool of active SQLite database connections.
* <p>
* At any given time, a connection is either owned by the pool, or it has been
* acquired by a {@link SQLiteSession}. When the {@link SQLiteSession} is
* finished with the connection it is using, it must return the connection
* back to the pool.
* </p><p>
* The pool holds strong references to the connections it owns. However,
* it only holds <em>weak references</em> to the connections that sessions
* have acquired from it. Using weak references in the latter case ensures
* that the connection pool can detect when connections have been improperly
* abandoned so that it can create new connections to replace them if needed.
* </p><p>
* The connection pool is thread-safe (but the connections themselves are not).
* </p>
*
* <h2>Exception safety</h2>
* <p>
* This code attempts to maintain the invariant that opened connections are
* always owned. Unfortunately that means it needs to handle exceptions
* all over to ensure that broken connections get cleaned up. Most
* operations invokving SQLite can throw {@link SQLiteException} or other
* runtime exceptions. This is a bit of a pain to deal with because the compiler
* cannot help us catch missing exception handling code.
* </p><p>
* The general rule for this file: If we are making calls out to
* {@link SQLiteConnection} then we must be prepared to handle any
* runtime exceptions it might throw at us. Note that out-of-memory
* is an {@link Error}, not a {@link RuntimeException}. We don't trouble ourselves
* handling out of memory because it is hard to do anything at all sensible then
* and most likely the VM is about to crash.
* </p>
*
* @hide
*/
public final class SQLiteConnectionPool implements Closeable {
private static final String TAG = "SQLiteConnectionPool";
// Amount of time to wait in milliseconds before unblocking acquireConnection
// and logging a message about the connection pool being busy.
private static final long CONNECTION_POOL_BUSY_MILLIS = 30 * 1000; // 30 seconds
private final CloseGuard mCloseGuard = CloseGuard.get();
private final Object mLock = new Object();
private final AtomicBoolean mConnectionLeaked = new AtomicBoolean();
private final SQLiteDatabaseConfiguration mConfiguration;
private int mMaxConnectionPoolSize;
private boolean mIsOpen;
private int mNextConnectionId;
private ConnectionWaiter mConnectionWaiterPool;
private ConnectionWaiter mConnectionWaiterQueue;
// Strong references to all available connections.
private final ArrayList<SQLiteConnection> mAvailableNonPrimaryConnections =
new ArrayList<SQLiteConnection>();
private SQLiteConnection mAvailablePrimaryConnection;
@GuardedBy("mLock")
private IdleConnectionHandler mIdleConnectionHandler;
private final AtomicLong mTotalExecutionTimeCounter = new AtomicLong(0);
// Describes what should happen to an acquired connection when it is returned to the pool.
enum AcquiredConnectionStatus {
// The connection should be returned to the pool as usual.
NORMAL,
// The connection must be reconfigured before being returned.
RECONFIGURE,
// The connection must be closed and discarded.
DISCARD,
}
// Weak references to all acquired connections. The associated value
// indicates whether the connection must be reconfigured before being
// returned to the available connection list or discarded.
// For example, the prepared statement cache size may have changed and
// need to be updated in preparation for the next client.
private final WeakHashMap<SQLiteConnection, AcquiredConnectionStatus> mAcquiredConnections =
new WeakHashMap<SQLiteConnection, AcquiredConnectionStatus>();
/**
* Connection flag: Read-only.
* <p>
* This flag indicates that the connection will only be used to
* perform read-only operations.
* </p>
*/
public static final int CONNECTION_FLAG_READ_ONLY = 1 << 0;
/**
* Connection flag: Primary connection affinity.
* <p>
* This flag indicates that the primary connection is required.
* This flag helps support legacy applications that expect most data modifying
* operations to be serialized by locking the primary database connection.
* Setting this flag essentially implements the old "db lock" concept by preventing
* an operation from being performed until it can obtain exclusive access to
* the primary connection.
* </p>
*/
public static final int CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY = 1 << 1;
/**
* Connection flag: Connection is being used interactively.
* <p>
* This flag indicates that the connection is needed by the UI thread.
* The connection pool can use this flag to elevate the priority
* of the database connection request.
* </p>
*/
public static final int CONNECTION_FLAG_INTERACTIVE = 1 << 2;
private SQLiteConnectionPool(SQLiteDatabaseConfiguration configuration) {
mConfiguration = new SQLiteDatabaseConfiguration(configuration);
setMaxConnectionPoolSizeLocked();
// If timeout is set, setup idle connection handler
// In case of MAX_VALUE - idle connections are never closed
if (mConfiguration.idleConnectionTimeoutMs != Long.MAX_VALUE) {
setupIdleConnectionHandler(Looper.getMainLooper(),
mConfiguration.idleConnectionTimeoutMs);
}
}
@Override
protected void finalize() throws Throwable {
try {
dispose(true);
} finally {
super.finalize();
}
}
/**
* Opens a connection pool for the specified database.
*
* @param configuration The database configuration.
* @return The connection pool.
*
* @throws SQLiteException if a database error occurs.
*/
public static SQLiteConnectionPool open(SQLiteDatabaseConfiguration configuration) {
if (configuration == null) {
throw new IllegalArgumentException("configuration must not be null.");
}
// Create the pool.
SQLiteConnectionPool pool = new SQLiteConnectionPool(configuration);
pool.open(); // might throw
return pool;
}
// Might throw
private void open() {
// Open the primary connection.
// This might throw if the database is corrupt.
mAvailablePrimaryConnection = openConnectionLocked(mConfiguration,
true /*primaryConnection*/); // might throw
// Mark it released so it can be closed after idle timeout
synchronized (mLock) {
if (mIdleConnectionHandler != null) {
mIdleConnectionHandler.connectionReleased(mAvailablePrimaryConnection);
}
}
// Mark the pool as being open for business.
mIsOpen = true;
mCloseGuard.open("close");
}
/**
* Closes the connection pool.
* <p>
* When the connection pool is closed, it will refuse all further requests
* to acquire connections. All connections that are currently available in
* the pool are closed immediately. Any connections that are still in use
* will be closed as soon as they are returned to the pool.
* </p>
*
* @throws IllegalStateException if the pool has been closed.
*/
public void close() {
dispose(false);
}
private void dispose(boolean finalized) {
if (mCloseGuard != null) {
if (finalized) {
mCloseGuard.warnIfOpen();
}
mCloseGuard.close();
}
if (!finalized) {
// Close all connections. We don't need (or want) to do this
// when finalized because we don't know what state the connections
// themselves will be in. The finalizer is really just here for CloseGuard.
// The connections will take care of themselves when their own finalizers run.
synchronized (mLock) {
throwIfClosedLocked();
mIsOpen = false;
closeAvailableConnectionsAndLogExceptionsLocked();
final int pendingCount = mAcquiredConnections.size();
if (pendingCount != 0) {
Log.i(TAG, "The connection pool for " + mConfiguration.label
+ " has been closed but there are still "
+ pendingCount + " connections in use. They will be closed "
+ "as they are released back to the pool.");
}
wakeConnectionWaitersLocked();
}
}
}
/**
* Reconfigures the database configuration of the connection pool and all of its
* connections.
* <p>
* Configuration changes are propagated down to connections immediately if
* they are available or as soon as they are released. This includes changes
* that affect the size of the pool.
* </p>
*
* @param configuration The new configuration.
*
* @throws IllegalStateException if the pool has been closed.
*/
public void reconfigure(SQLiteDatabaseConfiguration configuration) {
if (configuration == null) {
throw new IllegalArgumentException("configuration must not be null.");
}
synchronized (mLock) {
throwIfClosedLocked();
boolean walModeChanged = ((configuration.openFlags ^ mConfiguration.openFlags)
& SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) != 0;
if (walModeChanged) {
// WAL mode can only be changed if there are no acquired connections
// because we need to close all but the primary connection first.
if (!mAcquiredConnections.isEmpty()) {
throw new IllegalStateException("Write Ahead Logging (WAL) mode cannot "
+ "be enabled or disabled while there are transactions in "
+ "progress. Finish all transactions and release all active "
+ "database connections first.");
}
// Close all non-primary connections. This should happen immediately
// because none of them are in use.
closeAvailableNonPrimaryConnectionsAndLogExceptionsLocked();
assert mAvailableNonPrimaryConnections.isEmpty();
}
boolean foreignKeyModeChanged = configuration.foreignKeyConstraintsEnabled
!= mConfiguration.foreignKeyConstraintsEnabled;
if (foreignKeyModeChanged) {
// Foreign key constraints can only be changed if there are no transactions
// in progress. To make this clear, we throw an exception if there are
// any acquired connections.
if (!mAcquiredConnections.isEmpty()) {
throw new IllegalStateException("Foreign Key Constraints cannot "
+ "be enabled or disabled while there are transactions in "
+ "progress. Finish all transactions and release all active "
+ "database connections first.");
}
}
// We should do in-place switching when transitioning from compatibility WAL
// to rollback journal. Otherwise transient connection state will be lost
boolean onlyCompatWalChanged = (mConfiguration.openFlags ^ configuration.openFlags)
== SQLiteDatabase.ENABLE_LEGACY_COMPATIBILITY_WAL;
if (!onlyCompatWalChanged && mConfiguration.openFlags != configuration.openFlags) {
// If we are changing open flags and WAL mode at the same time, then
// we have no choice but to close the primary connection beforehand
// because there can only be one connection open when we change WAL mode.
if (walModeChanged) {
closeAvailableConnectionsAndLogExceptionsLocked();
}
// Try to reopen the primary connection using the new open flags then
// close and discard all existing connections.
// This might throw if the database is corrupt or cannot be opened in
// the new mode in which case existing connections will remain untouched.
SQLiteConnection newPrimaryConnection = openConnectionLocked(configuration,
true /*primaryConnection*/); // might throw
closeAvailableConnectionsAndLogExceptionsLocked();
discardAcquiredConnectionsLocked();
mAvailablePrimaryConnection = newPrimaryConnection;
mConfiguration.updateParametersFrom(configuration);
setMaxConnectionPoolSizeLocked();
} else {
// Reconfigure the database connections in place.
mConfiguration.updateParametersFrom(configuration);
setMaxConnectionPoolSizeLocked();
closeExcessConnectionsAndLogExceptionsLocked();
reconfigureAllConnectionsLocked();
}
wakeConnectionWaitersLocked();
}
}
/**
* Acquires a connection from the pool.
* <p>
* The caller must call {@link #releaseConnection} to release the connection
* back to the pool when it is finished. Failure to do so will result
* in much unpleasantness.
* </p>
*
* @param sql If not null, try to find a connection that already has
* the specified SQL statement in its prepared statement cache.
* @param connectionFlags The connection request flags.
* @param cancellationSignal A signal to cancel the operation in progress, or null if none.
* @return The connection that was acquired, never null.
*
* @throws IllegalStateException if the pool has been closed.
* @throws SQLiteException if a database error occurs.
* @throws OperationCanceledException if the operation was canceled.
*/
public SQLiteConnection acquireConnection(String sql, int connectionFlags,
CancellationSignal cancellationSignal) {
SQLiteConnection con = waitForConnection(sql, connectionFlags, cancellationSignal);
synchronized (mLock) {
if (mIdleConnectionHandler != null) {
mIdleConnectionHandler.connectionAcquired(con);
}
}
return con;
}
/**
* Releases a connection back to the pool.
* <p>
* It is ok to call this method after the pool has closed, to release
* connections that were still in use at the time of closure.
* </p>
*
* @param connection The connection to release. Must not be null.
*
* @throws IllegalStateException if the connection was not acquired
* from this pool or if it has already been released.
*/
public void releaseConnection(SQLiteConnection connection) {
synchronized (mLock) {
if (mIdleConnectionHandler != null) {
mIdleConnectionHandler.connectionReleased(connection);
}
AcquiredConnectionStatus status = mAcquiredConnections.remove(connection);
if (status == null) {
throw new IllegalStateException("Cannot perform this operation "
+ "because the specified connection was not acquired "
+ "from this pool or has already been released.");
}
if (!mIsOpen) {
closeConnectionAndLogExceptionsLocked(connection);
} else if (connection.isPrimaryConnection()) {
if (recycleConnectionLocked(connection, status)) {
assert mAvailablePrimaryConnection == null;
mAvailablePrimaryConnection = connection;
}
wakeConnectionWaitersLocked();
} else if (mAvailableNonPrimaryConnections.size() >= mMaxConnectionPoolSize - 1) {
closeConnectionAndLogExceptionsLocked(connection);
} else {
if (recycleConnectionLocked(connection, status)) {
mAvailableNonPrimaryConnections.add(connection);
}
wakeConnectionWaitersLocked();
}
}
}
// Can't throw.
@GuardedBy("mLock")
private boolean recycleConnectionLocked(SQLiteConnection connection,
AcquiredConnectionStatus status) {
if (status == AcquiredConnectionStatus.RECONFIGURE) {
try {
connection.reconfigure(mConfiguration); // might throw
} catch (RuntimeException ex) {
Log.e(TAG, "Failed to reconfigure released connection, closing it: "
+ connection, ex);
status = AcquiredConnectionStatus.DISCARD;
}
}
if (status == AcquiredConnectionStatus.DISCARD) {
closeConnectionAndLogExceptionsLocked(connection);
return false;
}
return true;
}
/**
* Returns true if the session should yield the connection due to
* contention over available database connections.
*
* @param connection The connection owned by the session.
* @param connectionFlags The connection request flags.
* @return True if the session should yield its connection.
*
* @throws IllegalStateException if the connection was not acquired
* from this pool or if it has already been released.
*/
public boolean shouldYieldConnection(SQLiteConnection connection, int connectionFlags) {
synchronized (mLock) {
if (!mAcquiredConnections.containsKey(connection)) {
throw new IllegalStateException("Cannot perform this operation "
+ "because the specified connection was not acquired "
+ "from this pool or has already been released.");
}
if (!mIsOpen) {
return false;
}
return isSessionBlockingImportantConnectionWaitersLocked(
connection.isPrimaryConnection(), connectionFlags);
}
}
/**
* Collects statistics about database connection memory usage.
*
* @param dbStatsList The list to populate.
*/
public void collectDbStats(ArrayList<DbStats> dbStatsList) {
synchronized (mLock) {
if (mAvailablePrimaryConnection != null) {
mAvailablePrimaryConnection.collectDbStats(dbStatsList);
}
for (SQLiteConnection connection : mAvailableNonPrimaryConnections) {
connection.collectDbStats(dbStatsList);
}
for (SQLiteConnection connection : mAcquiredConnections.keySet()) {
connection.collectDbStatsUnsafe(dbStatsList);
}
}
}
// Might throw.
private SQLiteConnection openConnectionLocked(SQLiteDatabaseConfiguration configuration,
boolean primaryConnection) {
final int connectionId = mNextConnectionId++;
return SQLiteConnection.open(this, configuration,
connectionId, primaryConnection); // might throw
}
void onConnectionLeaked() {
// This code is running inside of the SQLiteConnection finalizer.
//
// We don't know whether it is just the connection that has been finalized (and leaked)
// or whether the connection pool has also been or is about to be finalized.
// Consequently, it would be a bad idea to try to grab any locks or to
// do any significant work here. So we do the simplest possible thing and
// set a flag. waitForConnection() periodically checks this flag (when it
// times out) so that it can recover from leaked connections and wake
// itself or other threads up if necessary.
//
// You might still wonder why we don't try to do more to wake up the waiters
// immediately. First, as explained above, it would be hard to do safely
// unless we started an extra Thread to function as a reference queue. Second,
// this is never supposed to happen in normal operation. Third, there is no
// guarantee that the GC will actually detect the leak in a timely manner so
// it's not all that important that we recover from the leak in a timely manner
// either. Fourth, if a badly behaved application finds itself hung waiting for
// several seconds while waiting for a leaked connection to be detected and recreated,
// then perhaps its authors will have added incentive to fix the problem!
Log.w(TAG, "A SQLiteConnection object for database '"
+ mConfiguration.label + "' was leaked! Please fix your application "
+ "to end transactions in progress properly and to close the database "
+ "when it is no longer needed.");
mConnectionLeaked.set(true);
}
void onStatementExecuted(long executionTimeMs) {
mTotalExecutionTimeCounter.addAndGet(executionTimeMs);
}
// Can't throw.
@GuardedBy("mLock")
private void closeAvailableConnectionsAndLogExceptionsLocked() {
closeAvailableNonPrimaryConnectionsAndLogExceptionsLocked();
if (mAvailablePrimaryConnection != null) {
closeConnectionAndLogExceptionsLocked(mAvailablePrimaryConnection);
mAvailablePrimaryConnection = null;
}
}
// Can't throw.
@GuardedBy("mLock")
private boolean closeAvailableConnectionLocked(int connectionId) {
final int count = mAvailableNonPrimaryConnections.size();
for (int i = count - 1; i >= 0; i--) {
SQLiteConnection c = mAvailableNonPrimaryConnections.get(i);
if (c.getConnectionId() == connectionId) {
closeConnectionAndLogExceptionsLocked(c);
mAvailableNonPrimaryConnections.remove(i);
return true;
}
}
if (mAvailablePrimaryConnection != null
&& mAvailablePrimaryConnection.getConnectionId() == connectionId) {
closeConnectionAndLogExceptionsLocked(mAvailablePrimaryConnection);
mAvailablePrimaryConnection = null;
return true;
}
return false;
}
// Can't throw.
@GuardedBy("mLock")
private void closeAvailableNonPrimaryConnectionsAndLogExceptionsLocked() {
final int count = mAvailableNonPrimaryConnections.size();
for (int i = 0; i < count; i++) {
closeConnectionAndLogExceptionsLocked(mAvailableNonPrimaryConnections.get(i));
}
mAvailableNonPrimaryConnections.clear();
}
/**
* Close non-primary connections that are not currently in use. This method is safe to use
* in finalize block as it doesn't throw RuntimeExceptions.
*/
void closeAvailableNonPrimaryConnectionsAndLogExceptions() {
synchronized (mLock) {
closeAvailableNonPrimaryConnectionsAndLogExceptionsLocked();
}
}
// Can't throw.
@GuardedBy("mLock")
private void closeExcessConnectionsAndLogExceptionsLocked() {
int availableCount = mAvailableNonPrimaryConnections.size();
while (availableCount-- > mMaxConnectionPoolSize - 1) {
SQLiteConnection connection =
mAvailableNonPrimaryConnections.remove(availableCount);
closeConnectionAndLogExceptionsLocked(connection);
}
}
// Can't throw.
@GuardedBy("mLock")
private void closeConnectionAndLogExceptionsLocked(SQLiteConnection connection) {
try {
connection.close(); // might throw
if (mIdleConnectionHandler != null) {
mIdleConnectionHandler.connectionClosed(connection);
}
} catch (RuntimeException ex) {
Log.e(TAG, "Failed to close connection, its fate is now in the hands "
+ "of the merciful GC: " + connection, ex);
}
}
// Can't throw.
private void discardAcquiredConnectionsLocked() {
markAcquiredConnectionsLocked(AcquiredConnectionStatus.DISCARD);
}
// Can't throw.
@GuardedBy("mLock")
private void reconfigureAllConnectionsLocked() {
if (mAvailablePrimaryConnection != null) {
try {
mAvailablePrimaryConnection.reconfigure(mConfiguration); // might throw
} catch (RuntimeException ex) {
Log.e(TAG, "Failed to reconfigure available primary connection, closing it: "
+ mAvailablePrimaryConnection, ex);
closeConnectionAndLogExceptionsLocked(mAvailablePrimaryConnection);
mAvailablePrimaryConnection = null;
}
}
int count = mAvailableNonPrimaryConnections.size();
for (int i = 0; i < count; i++) {
final SQLiteConnection connection = mAvailableNonPrimaryConnections.get(i);
try {
connection.reconfigure(mConfiguration); // might throw
} catch (RuntimeException ex) {
Log.e(TAG, "Failed to reconfigure available non-primary connection, closing it: "
+ connection, ex);
closeConnectionAndLogExceptionsLocked(connection);
mAvailableNonPrimaryConnections.remove(i--);
count -= 1;
}
}
markAcquiredConnectionsLocked(AcquiredConnectionStatus.RECONFIGURE);
}
// Can't throw.
private void markAcquiredConnectionsLocked(AcquiredConnectionStatus status) {
if (!mAcquiredConnections.isEmpty()) {
ArrayList<SQLiteConnection> keysToUpdate = new ArrayList<SQLiteConnection>(
mAcquiredConnections.size());
for (Map.Entry<SQLiteConnection, AcquiredConnectionStatus> entry
: mAcquiredConnections.entrySet()) {
AcquiredConnectionStatus oldStatus = entry.getValue();
if (status != oldStatus
&& oldStatus != AcquiredConnectionStatus.DISCARD) {
keysToUpdate.add(entry.getKey());
}
}
final int updateCount = keysToUpdate.size();
for (int i = 0; i < updateCount; i++) {
mAcquiredConnections.put(keysToUpdate.get(i), status);
}
}
}
// Might throw.
private SQLiteConnection waitForConnection(String sql, int connectionFlags,
CancellationSignal cancellationSignal) {
final boolean wantPrimaryConnection =
(connectionFlags & CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY) != 0;
final ConnectionWaiter waiter;
final int nonce;
synchronized (mLock) {
throwIfClosedLocked();
// Abort if canceled.
if (cancellationSignal != null) {
cancellationSignal.throwIfCanceled();
}
// Try to acquire a connection.
SQLiteConnection connection = null;
if (!wantPrimaryConnection) {
connection = tryAcquireNonPrimaryConnectionLocked(
sql, connectionFlags); // might throw
}
if (connection == null) {
connection = tryAcquirePrimaryConnectionLocked(connectionFlags); // might throw
}
if (connection != null) {
return connection;
}
// No connections available. Enqueue a waiter in priority order.
final int priority = getPriority(connectionFlags);
final long startTime = SystemClock.uptimeMillis();
waiter = obtainConnectionWaiterLocked(Thread.currentThread(), startTime,
priority, wantPrimaryConnection, sql, connectionFlags);
ConnectionWaiter predecessor = null;
ConnectionWaiter successor = mConnectionWaiterQueue;
while (successor != null) {
if (priority > successor.mPriority) {
waiter.mNext = successor;
break;
}
predecessor = successor;
successor = successor.mNext;
}
if (predecessor != null) {
predecessor.mNext = waiter;
} else {
mConnectionWaiterQueue = waiter;
}
nonce = waiter.mNonce;
}
// Set up the cancellation listener.
if (cancellationSignal != null) {
cancellationSignal.setOnCancelListener(new CancellationSignal.OnCancelListener() {
@Override
public void onCancel() {
synchronized (mLock) {
if (waiter.mNonce == nonce) {
cancelConnectionWaiterLocked(waiter);
}
}
}
});
}
try {
// Park the thread until a connection is assigned or the pool is closed.
// Rethrow an exception from the wait, if we got one.
long busyTimeoutMillis = CONNECTION_POOL_BUSY_MILLIS;
long nextBusyTimeoutTime = waiter.mStartTime + busyTimeoutMillis;
for (;;) {
// Detect and recover from connection leaks.
if (mConnectionLeaked.compareAndSet(true, false)) {
synchronized (mLock) {
wakeConnectionWaitersLocked();
}
}
// Wait to be unparked (may already have happened), a timeout, or interruption.
LockSupport.parkNanos(this, busyTimeoutMillis * 1000000L);
// Clear the interrupted flag, just in case.
Thread.interrupted();
// Check whether we are done waiting yet.
synchronized (mLock) {
throwIfClosedLocked();
final SQLiteConnection connection = waiter.mAssignedConnection;
final RuntimeException ex = waiter.mException;
if (connection != null || ex != null) {
recycleConnectionWaiterLocked(waiter);
if (connection != null) {
return connection;
}
throw ex; // rethrow!
}
final long now = SystemClock.uptimeMillis();
if (now < nextBusyTimeoutTime) {
busyTimeoutMillis = now - nextBusyTimeoutTime;
} else {
logConnectionPoolBusyLocked(now - waiter.mStartTime, connectionFlags);
busyTimeoutMillis = CONNECTION_POOL_BUSY_MILLIS;
nextBusyTimeoutTime = now + busyTimeoutMillis;
}
}
}
} finally {
// Remove the cancellation listener.
if (cancellationSignal != null) {
cancellationSignal.setOnCancelListener(null);
}
}
}
// Can't throw.
@GuardedBy("mLock")
private void cancelConnectionWaiterLocked(ConnectionWaiter waiter) {
if (waiter.mAssignedConnection != null || waiter.mException != null) {
// Waiter is done waiting but has not woken up yet.
return;
}
// Waiter must still be waiting. Dequeue it.
ConnectionWaiter predecessor = null;
ConnectionWaiter current = mConnectionWaiterQueue;
while (current != waiter) {
assert current != null;
predecessor = current;
current = current.mNext;
}
if (predecessor != null) {
predecessor.mNext = waiter.mNext;
} else {
mConnectionWaiterQueue = waiter.mNext;
}
// Send the waiter an exception and unpark it.
waiter.mException = new OperationCanceledException();
LockSupport.unpark(waiter.mThread);
// Check whether removing this waiter will enable other waiters to make progress.
wakeConnectionWaitersLocked();
}
// Can't throw.
private void logConnectionPoolBusyLocked(long waitMillis, int connectionFlags) {
final Thread thread = Thread.currentThread();
StringBuilder msg = new StringBuilder();
msg.append("The connection pool for database '").append(mConfiguration.label);
msg.append("' has been unable to grant a connection to thread ");
msg.append(thread.getId()).append(" (").append(thread.getName()).append(") ");
msg.append("with flags 0x").append(Integer.toHexString(connectionFlags));
msg.append(" for ").append(waitMillis * 0.001f).append(" seconds.\n");
ArrayList<String> requests = new ArrayList<String>();
int activeConnections = 0;
int idleConnections = 0;
if (!mAcquiredConnections.isEmpty()) {
for (SQLiteConnection connection : mAcquiredConnections.keySet()) {
String description = connection.describeCurrentOperationUnsafe();
if (description != null) {
requests.add(description);
activeConnections += 1;
} else {
idleConnections += 1;
}
}
}
int availableConnections = mAvailableNonPrimaryConnections.size();
if (mAvailablePrimaryConnection != null) {
availableConnections += 1;
}
msg.append("Connections: ").append(activeConnections).append(" active, ");
msg.append(idleConnections).append(" idle, ");
msg.append(availableConnections).append(" available.\n");
if (!requests.isEmpty()) {
msg.append("\nRequests in progress:\n");
for (String request : requests) {
msg.append(" ").append(request).append("\n");
}
}
Log.w(TAG, msg.toString());
}
// Can't throw.
@GuardedBy("mLock")
private void wakeConnectionWaitersLocked() {
// Unpark all waiters that have requests that we can fulfill.
// This method is designed to not throw runtime exceptions, although we might send
// a waiter an exception for it to rethrow.
ConnectionWaiter predecessor = null;
ConnectionWaiter waiter = mConnectionWaiterQueue;
boolean primaryConnectionNotAvailable = false;
boolean nonPrimaryConnectionNotAvailable = false;
while (waiter != null) {
boolean unpark = false;
if (!mIsOpen) {
unpark = true;
} else {
try {
SQLiteConnection connection = null;
if (!waiter.mWantPrimaryConnection && !nonPrimaryConnectionNotAvailable) {
connection = tryAcquireNonPrimaryConnectionLocked(
waiter.mSql, waiter.mConnectionFlags); // might throw
if (connection == null) {
nonPrimaryConnectionNotAvailable = true;
}
}
if (connection == null && !primaryConnectionNotAvailable) {
connection = tryAcquirePrimaryConnectionLocked(
waiter.mConnectionFlags); // might throw
if (connection == null) {
primaryConnectionNotAvailable = true;
}
}
if (connection != null) {
waiter.mAssignedConnection = connection;
unpark = true;
} else if (nonPrimaryConnectionNotAvailable && primaryConnectionNotAvailable) {
// There are no connections available and the pool is still open.
// We cannot fulfill any more connection requests, so stop here.
break;
}
} catch (RuntimeException ex) {
// Let the waiter handle the exception from acquiring a connection.
waiter.mException = ex;
unpark = true;
}
}
final ConnectionWaiter successor = waiter.mNext;
if (unpark) {
if (predecessor != null) {
predecessor.mNext = successor;
} else {
mConnectionWaiterQueue = successor;
}
waiter.mNext = null;
LockSupport.unpark(waiter.mThread);
} else {
predecessor = waiter;
}
waiter = successor;
}
}
// Might throw.
@GuardedBy("mLock")
private SQLiteConnection tryAcquirePrimaryConnectionLocked(int connectionFlags) {
// If the primary connection is available, acquire it now.
SQLiteConnection connection = mAvailablePrimaryConnection;
if (connection != null) {
mAvailablePrimaryConnection = null;
finishAcquireConnectionLocked(connection, connectionFlags); // might throw
return connection;
}
// Make sure that the primary connection actually exists and has just been acquired.
for (SQLiteConnection acquiredConnection : mAcquiredConnections.keySet()) {
if (acquiredConnection.isPrimaryConnection()) {
return null;
}
}
// Uhoh. No primary connection! Either this is the first time we asked
// for it, or maybe it leaked?
connection = openConnectionLocked(mConfiguration,
true /*primaryConnection*/); // might throw
finishAcquireConnectionLocked(connection, connectionFlags); // might throw
return connection;
}
// Might throw.
@GuardedBy("mLock")
private SQLiteConnection tryAcquireNonPrimaryConnectionLocked(
String sql, int connectionFlags) {
// Try to acquire the next connection in the queue.
SQLiteConnection connection;
final int availableCount = mAvailableNonPrimaryConnections.size();
if (availableCount > 1 && sql != null) {
// If we have a choice, then prefer a connection that has the
// prepared statement in its cache.
for (int i = 0; i < availableCount; i++) {
connection = mAvailableNonPrimaryConnections.get(i);
if (connection.isPreparedStatementInCache(sql)) {
mAvailableNonPrimaryConnections.remove(i);
finishAcquireConnectionLocked(connection, connectionFlags); // might throw
return connection;
}
}
}
if (availableCount > 0) {
// Otherwise, just grab the next one.
connection = mAvailableNonPrimaryConnections.remove(availableCount - 1);
finishAcquireConnectionLocked(connection, connectionFlags); // might throw
return connection;
}
// Expand the pool if needed.
int openConnections = mAcquiredConnections.size();
if (mAvailablePrimaryConnection != null) {
openConnections += 1;
}
if (openConnections >= mMaxConnectionPoolSize) {
return null;
}
connection = openConnectionLocked(mConfiguration,
false /*primaryConnection*/); // might throw
finishAcquireConnectionLocked(connection, connectionFlags); // might throw
return connection;
}
// Might throw.
@GuardedBy("mLock")
private void finishAcquireConnectionLocked(SQLiteConnection connection, int connectionFlags) {
try {
final boolean readOnly = (connectionFlags & CONNECTION_FLAG_READ_ONLY) != 0;
connection.setOnlyAllowReadOnlyOperations(readOnly);
mAcquiredConnections.put(connection, AcquiredConnectionStatus.NORMAL);
} catch (RuntimeException ex) {
Log.e(TAG, "Failed to prepare acquired connection for session, closing it: "
+ connection +", connectionFlags=" + connectionFlags);
closeConnectionAndLogExceptionsLocked(connection);
throw ex; // rethrow!
}
}
private boolean isSessionBlockingImportantConnectionWaitersLocked(
boolean holdingPrimaryConnection, int connectionFlags) {
ConnectionWaiter waiter = mConnectionWaiterQueue;
if (waiter != null) {
final int priority = getPriority(connectionFlags);
do {
// Only worry about blocked connections that have same or lower priority.
if (priority > waiter.mPriority) {
break;
}
// If we are holding the primary connection then we are blocking the waiter.
// Likewise, if we are holding a non-primary connection and the waiter
// would accept a non-primary connection, then we are blocking the waier.
if (holdingPrimaryConnection || !waiter.mWantPrimaryConnection) {
return true;
}
waiter = waiter.mNext;
} while (waiter != null);
}
return false;
}
private static int getPriority(int connectionFlags) {
return (connectionFlags & CONNECTION_FLAG_INTERACTIVE) != 0 ? 1 : 0;
}
private void setMaxConnectionPoolSizeLocked() {
if (!mConfiguration.isInMemoryDb()
&& (mConfiguration.openFlags & SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) != 0) {
mMaxConnectionPoolSize = SQLiteGlobal.getWALConnectionPoolSize();
} else {
// We don't actually need to always restrict the connection pool size to 1
// for non-WAL databases. There might be reasons to use connection pooling
// with other journal modes. However, we should always keep pool size of 1 for in-memory
// databases since every :memory: db is separate from another.
// For now, enabling connection pooling and using WAL are the same thing in the API.
mMaxConnectionPoolSize = 1;
}
}
/**
* Set up the handler based on the provided looper and timeout.
*/
@VisibleForTesting
public void setupIdleConnectionHandler(Looper looper, long timeoutMs) {
synchronized (mLock) {
mIdleConnectionHandler = new IdleConnectionHandler(looper, timeoutMs);
}
}
void disableIdleConnectionHandler() {
synchronized (mLock) {
mIdleConnectionHandler = null;
}
}
private void throwIfClosedLocked() {
if (!mIsOpen) {
throw new IllegalStateException("Cannot perform this operation "
+ "because the connection pool has been closed.");
}
}
private ConnectionWaiter obtainConnectionWaiterLocked(Thread thread, long startTime,
int priority, boolean wantPrimaryConnection, String sql, int connectionFlags) {
ConnectionWaiter waiter = mConnectionWaiterPool;
if (waiter != null) {
mConnectionWaiterPool = waiter.mNext;
waiter.mNext = null;
} else {
waiter = new ConnectionWaiter();
}
waiter.mThread = thread;
waiter.mStartTime = startTime;
waiter.mPriority = priority;
waiter.mWantPrimaryConnection = wantPrimaryConnection;
waiter.mSql = sql;
waiter.mConnectionFlags = connectionFlags;
return waiter;
}
private void recycleConnectionWaiterLocked(ConnectionWaiter waiter) {
waiter.mNext = mConnectionWaiterPool;
waiter.mThread = null;
waiter.mSql = null;
waiter.mAssignedConnection = null;
waiter.mException = null;
waiter.mNonce += 1;
mConnectionWaiterPool = waiter;
}
/**
* Dumps debugging information about this connection pool.
*
* @param printer The printer to receive the dump, not null.
* @param verbose True to dump more verbose information.
*/
public void dump(Printer printer, boolean verbose, ArraySet<String> directories) {
Printer indentedPrinter = PrefixPrinter.create(printer, " ");
synchronized (mLock) {
if (directories != null) {
directories.add(new File(mConfiguration.path).getParent());
}
boolean isCompatibilityWalEnabled = mConfiguration.isLegacyCompatibilityWalEnabled();
printer.println("Connection pool for " + mConfiguration.path + ":");
printer.println(" Open: " + mIsOpen);
printer.println(" Max connections: " + mMaxConnectionPoolSize);
printer.println(" Total execution time: " + mTotalExecutionTimeCounter);
printer.println(" Configuration: openFlags=" + mConfiguration.openFlags
+ ", isLegacyCompatibilityWalEnabled=" + isCompatibilityWalEnabled
+ ", journalMode=" + TextUtils.emptyIfNull(mConfiguration.journalMode)
+ ", syncMode=" + TextUtils.emptyIfNull(mConfiguration.syncMode));
if (isCompatibilityWalEnabled) {
printer.println(" Compatibility WAL enabled: wal_syncmode="
+ SQLiteCompatibilityWalFlags.getWALSyncMode());
}
if (mConfiguration.isLookasideConfigSet()) {
printer.println(" Lookaside config: sz=" + mConfiguration.lookasideSlotSize
+ " cnt=" + mConfiguration.lookasideSlotCount);
}
if (mConfiguration.idleConnectionTimeoutMs != Long.MAX_VALUE) {
printer.println(
" Idle connection timeout: " + mConfiguration.idleConnectionTimeoutMs);
}
printer.println(" Available primary connection:");
if (mAvailablePrimaryConnection != null) {
mAvailablePrimaryConnection.dump(indentedPrinter, verbose);
} else {
indentedPrinter.println("<none>");
}
printer.println(" Available non-primary connections:");
if (!mAvailableNonPrimaryConnections.isEmpty()) {
final int count = mAvailableNonPrimaryConnections.size();
for (int i = 0; i < count; i++) {
mAvailableNonPrimaryConnections.get(i).dump(indentedPrinter, verbose);
}
} else {
indentedPrinter.println("<none>");
}
printer.println(" Acquired connections:");
if (!mAcquiredConnections.isEmpty()) {
for (Map.Entry<SQLiteConnection, AcquiredConnectionStatus> entry :
mAcquiredConnections.entrySet()) {
final SQLiteConnection connection = entry.getKey();
connection.dumpUnsafe(indentedPrinter, verbose);
indentedPrinter.println(" Status: " + entry.getValue());
}
} else {
indentedPrinter.println("<none>");
}
printer.println(" Connection waiters:");
if (mConnectionWaiterQueue != null) {
int i = 0;
final long now = SystemClock.uptimeMillis();
for (ConnectionWaiter waiter = mConnectionWaiterQueue; waiter != null;
waiter = waiter.mNext, i++) {
indentedPrinter.println(i + ": waited for "
+ ((now - waiter.mStartTime) * 0.001f)
+ " ms - thread=" + waiter.mThread
+ ", priority=" + waiter.mPriority
+ ", sql='" + waiter.mSql + "'");
}
} else {
indentedPrinter.println("<none>");
}
}
}
@Override
public String toString() {
return "SQLiteConnectionPool: " + mConfiguration.path;
}
public String getPath() {
return mConfiguration.path;
}
private static final class ConnectionWaiter {
public ConnectionWaiter mNext;
public Thread mThread;
public long mStartTime;
public int mPriority;
public boolean mWantPrimaryConnection;
public String mSql;
public int mConnectionFlags;
public SQLiteConnection mAssignedConnection;
public RuntimeException mException;
public int mNonce;
}
private class IdleConnectionHandler extends Handler {
private final long mTimeout;
IdleConnectionHandler(Looper looper, long timeout) {
super(looper);
mTimeout = timeout;
}
@Override
public void handleMessage(Message msg) {
// Skip the (obsolete) message if the handler has changed
synchronized (mLock) {
if (this != mIdleConnectionHandler) {
return;
}
if (closeAvailableConnectionLocked(msg.what)) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Closed idle connection " + mConfiguration.label + " " + msg.what
+ " after " + mTimeout);
}
}
}
}
void connectionReleased(SQLiteConnection con) {
sendEmptyMessageDelayed(con.getConnectionId(), mTimeout);
}
void connectionAcquired(SQLiteConnection con) {
// Remove any pending close operations
removeMessages(con.getConnectionId());
}
void connectionClosed(SQLiteConnection con) {
removeMessages(con.getConnectionId());
}
}
}