1322 lines
54 KiB
Java
1322 lines
54 KiB
Java
![]() |
/*
|
||
|
* 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.annotation.optimization.NeverCompile;
|
||
|
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;
|
||
|
|
||
|
// Prepare statement cache statistics
|
||
|
public int mTotalPrepareStatementCacheMiss = 0;
|
||
|
public int mTotalPrepareStatements = 0;
|
||
|
|
||
|
@GuardedBy("mLock")
|
||
|
private IdleConnectionHandler mIdleConnectionHandler;
|
||
|
|
||
|
// The database schema sequence number. This counter is incremented every time a schema
|
||
|
// change is detected. Every prepared statement records its schema sequence when the
|
||
|
// statement is created. The prepared statement is not put back in the cache if the sequence
|
||
|
// number has changed. The counter starts at 1, which allows clients to use 0 as a
|
||
|
// distinguished value.
|
||
|
private long mDatabaseSeqNum = 1;
|
||
|
|
||
|
// whole execution time for this connection in milliseconds.
|
||
|
private final AtomicLong mTotalStatementsTime = new AtomicLong(0);
|
||
|
|
||
|
// total statements executed by this connection
|
||
|
private final AtomicLong mTotalStatementsCount = 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, null);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@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("SQLiteConnectionPool.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 isWalCurrentMode = mConfiguration.resolveJournalMode().equalsIgnoreCase(
|
||
|
SQLiteDatabase.JOURNAL_MODE_WAL);
|
||
|
boolean isWalNewMode = configuration.resolveJournalMode().equalsIgnoreCase(
|
||
|
SQLiteDatabase.JOURNAL_MODE_WAL);
|
||
|
boolean walModeChanged = isWalCurrentMode ^ isWalNewMode;
|
||
|
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) {
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
@VisibleForTesting
|
||
|
public boolean hasAnyAvailableNonPrimaryConnection() {
|
||
|
return mAvailableNonPrimaryConnections.size() > 0;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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);
|
||
|
}
|
||
|
|
||
|
// Global pool stats
|
||
|
DbStats poolStats = new DbStats(mConfiguration.path, 0, 0, 0,
|
||
|
mTotalPrepareStatements - mTotalPrepareStatementCacheMiss,
|
||
|
mTotalPrepareStatementCacheMiss, mTotalPrepareStatements, true);
|
||
|
dbStatsList.add(poolStats);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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) {
|
||
|
mTotalStatementsTime.addAndGet(executionTimeMs);
|
||
|
mTotalStatementsCount.incrementAndGet();
|
||
|
}
|
||
|
|
||
|
// 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.resolveJournalMode().equalsIgnoreCase(SQLiteDatabase.JOURNAL_MODE_WAL)) {
|
||
|
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, Runnable onAllConnectionsIdle) {
|
||
|
synchronized (mLock) {
|
||
|
mIdleConnectionHandler =
|
||
|
new IdleConnectionHandler(looper, timeoutMs, onAllConnectionsIdle);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
void clearAcquiredConnectionsPreparedStatementCache() {
|
||
|
// Invalidate prepared statements that have an earlier schema sequence number.
|
||
|
synchronized (mLock) {
|
||
|
mDatabaseSeqNum++;
|
||
|
if (!mAcquiredConnections.isEmpty()) {
|
||
|
for (SQLiteConnection connection : mAcquiredConnections.keySet()) {
|
||
|
connection.setDatabaseSeqNum(mDatabaseSeqNum);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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) {
|
||
|
String parent = new File(mConfiguration.path).getParent();
|
||
|
if (parent != null) {
|
||
|
directories.add(parent);
|
||
|
}
|
||
|
}
|
||
|
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 (ms): " + mTotalStatementsTime);
|
||
|
printer.println(" Total statements executed: " + mTotalStatementsCount);
|
||
|
if (mTotalStatementsCount.get() > 0) {
|
||
|
// Avoid division by 0 by filtering out logs where there are no statements executed.
|
||
|
printer.println(" Average time per statement (ms): "
|
||
|
+ mTotalStatementsTime.get() / mTotalStatementsCount.get());
|
||
|
}
|
||
|
printer.println(" Configuration: openFlags=" + mConfiguration.openFlags
|
||
|
+ ", isLegacyCompatibilityWalEnabled=" + isCompatibilityWalEnabled
|
||
|
+ ", journalMode=" + TextUtils.emptyIfNull(mConfiguration.resolveJournalMode())
|
||
|
+ ", syncMode=" + TextUtils.emptyIfNull(mConfiguration.resolveSyncMode()));
|
||
|
printer.println(" IsReadOnlyDatabase=" + mConfiguration.isReadOnlyDatabase());
|
||
|
|
||
|
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>");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
@NeverCompile
|
||
|
public double getStatementCacheMissRate() {
|
||
|
if (mTotalPrepareStatements == 0) {
|
||
|
// no statements executed thus no miss rate.
|
||
|
return 0;
|
||
|
}
|
||
|
return (double) mTotalPrepareStatementCacheMiss / (double) mTotalPrepareStatements;
|
||
|
}
|
||
|
|
||
|
public long getTotalStatementsTime() {
|
||
|
return mTotalStatementsTime.get();
|
||
|
}
|
||
|
|
||
|
public long getTotalStatementsCount() {
|
||
|
return mTotalStatementsCount.get();
|
||
|
}
|
||
|
|
||
|
@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;
|
||
|
private final Runnable mOnAllConnectionsIdle;
|
||
|
|
||
|
IdleConnectionHandler(Looper looper, long timeout, Runnable onAllConnectionsIdle) {
|
||
|
super(looper);
|
||
|
mTimeout = timeout;
|
||
|
this.mOnAllConnectionsIdle = onAllConnectionsIdle;
|
||
|
}
|
||
|
|
||
|
@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);
|
||
|
}
|
||
|
}
|
||
|
if (mOnAllConnectionsIdle != null) {
|
||
|
mOnAllConnectionsIdle.run();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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());
|
||
|
}
|
||
|
}
|
||
|
}
|