/*
 * 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.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.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.

At any given time, a connection is either owned by the pool, or it has been acquired by a SQLiteSession. When the SQLiteSession is finished with the connection it is using, it must return the connection back to the pool.

The pool holds strong references to the connections it owns. However, it only holds weak references 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.

The connection pool is thread-safe (but the connections themselves are not).

Exception safety

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 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.

The general rule for this file: If we are making calls out to SQLiteConnection then we must be prepared to handle any runtime exceptions it might throw at us. Note that out-of-memory is an Error, not a 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.

@hide
/** * 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.

This flag indicates that the connection will only be used to perform read-only operations.

/** * 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.

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.

/** * 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.

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.

/** * 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.
Params:
  • configuration – The database configuration.
Throws:
Returns:The connection pool.
/** * 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.

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.

Throws:
  • IllegalStateException – if the pool has been closed.
/** * 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.

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.

Params:
  • configuration – The new configuration.
Throws:
/** * 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.DISABLE_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.

The caller must call releaseConnection to release the connection back to the pool when it is finished. Failure to do so will result in much unpleasantness.

Params:
  • sql – If not null, try to find a connection that already has the specified SQL statement in its prepared statement cache.
  • connectionFlags – The connection request flags.
  • cancellationSignal – A signal to cancel the operation in progress, or null if none.
Throws:
Returns:The connection that was acquired, never null.
/** * 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.

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.

Params:
  • 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.
/** * 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.
Params:
  • connection – The connection owned by the session.
  • connectionFlags – The connection request flags.
Throws:
  • IllegalStateException – if the connection was not acquired from this pool or if it has already been released.
Returns:True if the session should yield its connection.
/** * 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.
Params:
  • dbStatsList – The list to populate.
/** * 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.
/** * 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.
/** * 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.
Params:
  • printer – The printer to receive the dump, not null.
  • verbose – True to dump more verbose information.
/** * 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) { Printer indentedPrinter = PrefixPrinter.create(printer, " "); synchronized (mLock) { 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 + ", useCompatibilityWal=" + mConfiguration.useCompatibilityWal() + ", journalMode=" + TextUtils.emptyIfNull(mConfiguration.journalMode) + ", syncMode=" + TextUtils.emptyIfNull(mConfiguration.syncMode)); if (SQLiteCompatibilityWalFlags.areFlagsSet()) { printer.println(" Compatibility WAL settings: compatibility_wal_supported=" + SQLiteCompatibilityWalFlags .isCompatibilityWalSupported() + ", 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; } 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()); } } }