Loading core/java/android/database/sqlite/SQLiteConnectionPool.java +112 −1 Original line number Diff line number Diff line Loading @@ -19,12 +19,19 @@ package android.database.sqlite; import android.app.ActivityManager; 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.os.SystemProperties; 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; Loading Loading @@ -77,6 +84,15 @@ public final class SQLiteConnectionPool implements Closeable { // and logging a message about the connection pool being busy. private static final long CONNECTION_POOL_BUSY_MILLIS = 30 * 1000; // 30 seconds // TODO b/63398887 Move to SQLiteGlobal private static final long IDLE_CONNECTION_CLOSE_DELAY_MILLIS = SystemProperties .getInt("persist.debug.sqlite.idle_connection_close_delay", 30000); // TODO b/63398887 STOPSHIP. // Temporarily enabled for testing across a broader set of dogfood devices. private static final boolean CLOSE_IDLE_CONNECTIONS = SystemProperties .getBoolean("persist.debug.sqlite.close_idle_connections", true); private final CloseGuard mCloseGuard = CloseGuard.get(); private final Object mLock = new Object(); Loading @@ -94,6 +110,9 @@ public final class SQLiteConnectionPool implements Closeable { new ArrayList<SQLiteConnection>(); private SQLiteConnection mAvailablePrimaryConnection; @GuardedBy("mLock") private IdleConnectionHandler mIdleConnectionHandler; // 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. Loading Loading @@ -154,6 +173,11 @@ public final class SQLiteConnectionPool implements Closeable { mConfiguration.lookasideSlotSize = 0; } setMaxConnectionPoolSizeLocked(); // Do not close idle connections for in-memory databases if (CLOSE_IDLE_CONNECTIONS && !configuration.isInMemoryDb()) { setupIdleConnectionHandler(Looper.getMainLooper(), IDLE_CONNECTION_CLOSE_DELAY_MILLIS); } } @Override Loading Loading @@ -351,7 +375,13 @@ public final class SQLiteConnectionPool implements Closeable { */ public SQLiteConnection acquireConnection(String sql, int connectionFlags, CancellationSignal cancellationSignal) { return waitForConnection(sql, connectionFlags, cancellationSignal); SQLiteConnection con = waitForConnection(sql, connectionFlags, cancellationSignal); synchronized (mLock) { if (mIdleConnectionHandler != null) { mIdleConnectionHandler.connectionAcquired(con); } } return con; } /** Loading @@ -368,6 +398,9 @@ public final class SQLiteConnectionPool implements Closeable { */ 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 " Loading Loading @@ -509,6 +542,27 @@ public final class SQLiteConnectionPool implements Closeable { } } // Can't throw. 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. private void closeAvailableNonPrimaryConnectionsAndLogExceptionsLocked() { final int count = mAvailableNonPrimaryConnections.size(); Loading @@ -532,6 +586,9 @@ public final class SQLiteConnectionPool implements Closeable { 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); Loading Loading @@ -963,6 +1020,22 @@ public final class SQLiteConnectionPool implements Closeable { } } /** * Set up the handler based on the provided looper and delay. */ @VisibleForTesting public void setupIdleConnectionHandler(Looper looper, long delayMs) { synchronized (mLock) { mIdleConnectionHandler = new IdleConnectionHandler(looper, delayMs); } } void disableIdleConnectionHandler() { synchronized (mLock) { mIdleConnectionHandler = null; } } private void throwIfClosedLocked() { if (!mIsOpen) { throw new IllegalStateException("Cannot perform this operation " Loading Loading @@ -1078,4 +1151,42 @@ public final class SQLiteConnectionPool implements Closeable { public RuntimeException mException; public int mNonce; } private class IdleConnectionHandler extends Handler { private final long mDelay; IdleConnectionHandler(Looper looper, long delay) { super(looper); mDelay = delay; } @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 " + mDelay); } } } } void connectionReleased(SQLiteConnection con) { sendEmptyMessageDelayed(con.getConnectionId(), mDelay); } void connectionAcquired(SQLiteConnection con) { // Remove any pending close operations removeMessages(con.getConnectionId()); } void connectionClosed(SQLiteConnection con) { removeMessages(con.getConnectionId()); } } } core/java/android/database/sqlite/SQLiteDatabase.java +1 −0 Original line number Diff line number Diff line Loading @@ -1713,6 +1713,7 @@ public final class SQLiteDatabase extends SQLiteClosable { if (!mHasAttachedDbsLocked) { mHasAttachedDbsLocked = true; disableWal = true; mConnectionPoolLocked.disableIdleConnectionHandler(); } } if (disableWal) { Loading core/tests/coretests/src/android/database/sqlite/SQLiteConnectionPoolTest.java 0 → 100644 +91 −0 Original line number Diff line number Diff line /* * Copyright (C) 2017 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 static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import android.content.Context; import android.os.HandlerThread; import android.support.test.InstrumentationRegistry; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; import android.util.Log; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.io.File; /** * Tests for {@link SQLiteConnectionPool} * * <p>Run with: bit FrameworksCoreTests:android.database.sqlite.SQLiteConnectionPoolTest */ @RunWith(AndroidJUnit4.class) @SmallTest public class SQLiteConnectionPoolTest { private static final String TAG = "SQLiteConnectionPoolTest"; private Context mContext; private File mTestDatabase; private SQLiteDatabaseConfiguration mTestConf; @Before public void setup() { mContext = InstrumentationRegistry.getContext(); SQLiteDatabase db = SQLiteDatabase .openOrCreateDatabase(mContext.getDatabasePath("pool_test"), null); mTestDatabase = new File(db.getPath()); Log.i(TAG, "setup: created " + mTestDatabase); db.close(); mTestConf = new SQLiteDatabaseConfiguration(mTestDatabase.getPath(), 0); } @After public void teardown() { if (mTestDatabase != null) { Log.i(TAG, "teardown: deleting " + mTestDatabase); SQLiteDatabase.deleteDatabase(mTestDatabase); } } @Test public void testCloseIdleConnections() throws InterruptedException { HandlerThread thread = new HandlerThread("test-close-idle-connections-thread"); Log.i(TAG, "Starting " + thread.getName()); thread.start(); SQLiteConnectionPool pool = SQLiteConnectionPool.open(mTestConf); pool.setupIdleConnectionHandler(thread.getLooper(), 100); SQLiteConnection c1 = pool.acquireConnection("pragma user_version", 0, null); assertEquals("First connection should be returned", 0, c1.getConnectionId()); pool.releaseConnection(c1); SQLiteConnection c2 = pool.acquireConnection("pragma user_version", 0, null); assertTrue("Returned connection should be the same", c1 == c2); pool.releaseConnection(c2); Thread.sleep(200); SQLiteConnection c3 = pool.acquireConnection("pragma user_version", 0, null); assertTrue("New connection should be returned", c1 != c3); assertEquals("New connection should be returned", 1, c3.getConnectionId()); pool.releaseConnection(c3); pool.close(); thread.quit(); } } Loading
core/java/android/database/sqlite/SQLiteConnectionPool.java +112 −1 Original line number Diff line number Diff line Loading @@ -19,12 +19,19 @@ package android.database.sqlite; import android.app.ActivityManager; 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.os.SystemProperties; 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; Loading Loading @@ -77,6 +84,15 @@ public final class SQLiteConnectionPool implements Closeable { // and logging a message about the connection pool being busy. private static final long CONNECTION_POOL_BUSY_MILLIS = 30 * 1000; // 30 seconds // TODO b/63398887 Move to SQLiteGlobal private static final long IDLE_CONNECTION_CLOSE_DELAY_MILLIS = SystemProperties .getInt("persist.debug.sqlite.idle_connection_close_delay", 30000); // TODO b/63398887 STOPSHIP. // Temporarily enabled for testing across a broader set of dogfood devices. private static final boolean CLOSE_IDLE_CONNECTIONS = SystemProperties .getBoolean("persist.debug.sqlite.close_idle_connections", true); private final CloseGuard mCloseGuard = CloseGuard.get(); private final Object mLock = new Object(); Loading @@ -94,6 +110,9 @@ public final class SQLiteConnectionPool implements Closeable { new ArrayList<SQLiteConnection>(); private SQLiteConnection mAvailablePrimaryConnection; @GuardedBy("mLock") private IdleConnectionHandler mIdleConnectionHandler; // 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. Loading Loading @@ -154,6 +173,11 @@ public final class SQLiteConnectionPool implements Closeable { mConfiguration.lookasideSlotSize = 0; } setMaxConnectionPoolSizeLocked(); // Do not close idle connections for in-memory databases if (CLOSE_IDLE_CONNECTIONS && !configuration.isInMemoryDb()) { setupIdleConnectionHandler(Looper.getMainLooper(), IDLE_CONNECTION_CLOSE_DELAY_MILLIS); } } @Override Loading Loading @@ -351,7 +375,13 @@ public final class SQLiteConnectionPool implements Closeable { */ public SQLiteConnection acquireConnection(String sql, int connectionFlags, CancellationSignal cancellationSignal) { return waitForConnection(sql, connectionFlags, cancellationSignal); SQLiteConnection con = waitForConnection(sql, connectionFlags, cancellationSignal); synchronized (mLock) { if (mIdleConnectionHandler != null) { mIdleConnectionHandler.connectionAcquired(con); } } return con; } /** Loading @@ -368,6 +398,9 @@ public final class SQLiteConnectionPool implements Closeable { */ 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 " Loading Loading @@ -509,6 +542,27 @@ public final class SQLiteConnectionPool implements Closeable { } } // Can't throw. 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. private void closeAvailableNonPrimaryConnectionsAndLogExceptionsLocked() { final int count = mAvailableNonPrimaryConnections.size(); Loading @@ -532,6 +586,9 @@ public final class SQLiteConnectionPool implements Closeable { 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); Loading Loading @@ -963,6 +1020,22 @@ public final class SQLiteConnectionPool implements Closeable { } } /** * Set up the handler based on the provided looper and delay. */ @VisibleForTesting public void setupIdleConnectionHandler(Looper looper, long delayMs) { synchronized (mLock) { mIdleConnectionHandler = new IdleConnectionHandler(looper, delayMs); } } void disableIdleConnectionHandler() { synchronized (mLock) { mIdleConnectionHandler = null; } } private void throwIfClosedLocked() { if (!mIsOpen) { throw new IllegalStateException("Cannot perform this operation " Loading Loading @@ -1078,4 +1151,42 @@ public final class SQLiteConnectionPool implements Closeable { public RuntimeException mException; public int mNonce; } private class IdleConnectionHandler extends Handler { private final long mDelay; IdleConnectionHandler(Looper looper, long delay) { super(looper); mDelay = delay; } @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 " + mDelay); } } } } void connectionReleased(SQLiteConnection con) { sendEmptyMessageDelayed(con.getConnectionId(), mDelay); } void connectionAcquired(SQLiteConnection con) { // Remove any pending close operations removeMessages(con.getConnectionId()); } void connectionClosed(SQLiteConnection con) { removeMessages(con.getConnectionId()); } } }
core/java/android/database/sqlite/SQLiteDatabase.java +1 −0 Original line number Diff line number Diff line Loading @@ -1713,6 +1713,7 @@ public final class SQLiteDatabase extends SQLiteClosable { if (!mHasAttachedDbsLocked) { mHasAttachedDbsLocked = true; disableWal = true; mConnectionPoolLocked.disableIdleConnectionHandler(); } } if (disableWal) { Loading
core/tests/coretests/src/android/database/sqlite/SQLiteConnectionPoolTest.java 0 → 100644 +91 −0 Original line number Diff line number Diff line /* * Copyright (C) 2017 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 static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import android.content.Context; import android.os.HandlerThread; import android.support.test.InstrumentationRegistry; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; import android.util.Log; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.io.File; /** * Tests for {@link SQLiteConnectionPool} * * <p>Run with: bit FrameworksCoreTests:android.database.sqlite.SQLiteConnectionPoolTest */ @RunWith(AndroidJUnit4.class) @SmallTest public class SQLiteConnectionPoolTest { private static final String TAG = "SQLiteConnectionPoolTest"; private Context mContext; private File mTestDatabase; private SQLiteDatabaseConfiguration mTestConf; @Before public void setup() { mContext = InstrumentationRegistry.getContext(); SQLiteDatabase db = SQLiteDatabase .openOrCreateDatabase(mContext.getDatabasePath("pool_test"), null); mTestDatabase = new File(db.getPath()); Log.i(TAG, "setup: created " + mTestDatabase); db.close(); mTestConf = new SQLiteDatabaseConfiguration(mTestDatabase.getPath(), 0); } @After public void teardown() { if (mTestDatabase != null) { Log.i(TAG, "teardown: deleting " + mTestDatabase); SQLiteDatabase.deleteDatabase(mTestDatabase); } } @Test public void testCloseIdleConnections() throws InterruptedException { HandlerThread thread = new HandlerThread("test-close-idle-connections-thread"); Log.i(TAG, "Starting " + thread.getName()); thread.start(); SQLiteConnectionPool pool = SQLiteConnectionPool.open(mTestConf); pool.setupIdleConnectionHandler(thread.getLooper(), 100); SQLiteConnection c1 = pool.acquireConnection("pragma user_version", 0, null); assertEquals("First connection should be returned", 0, c1.getConnectionId()); pool.releaseConnection(c1); SQLiteConnection c2 = pool.acquireConnection("pragma user_version", 0, null); assertTrue("Returned connection should be the same", c1 == c2); pool.releaseConnection(c2); Thread.sleep(200); SQLiteConnection c3 = pool.acquireConnection("pragma user_version", 0, null); assertTrue("New connection should be returned", c1 != c3); assertEquals("New connection should be returned", 1, c3.getConnectionId()); pool.releaseConnection(c3); pool.close(); thread.quit(); } }