diff options
| -rw-r--r-- | core/java/android/database/sqlite/DatabaseConnectionPool.java | 118 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/database/sqlite/DatabaseConnectionPoolTest.java | 364 |
2 files changed, 449 insertions, 33 deletions
diff --git a/core/java/android/database/sqlite/DatabaseConnectionPool.java b/core/java/android/database/sqlite/DatabaseConnectionPool.java index 3f7018fe14ff..50b291949974 100644 --- a/core/java/android/database/sqlite/DatabaseConnectionPool.java +++ b/core/java/android/database/sqlite/DatabaseConnectionPool.java @@ -48,6 +48,9 @@ import java.util.Random; /** the main database connection to which this connection pool is attached */ private final SQLiteDatabase mParentDbObj; + /** Random number generator used to pick a free connection out of the pool */ + private Random rand; // lazily initialized + /* package */ DatabaseConnectionPool(SQLiteDatabase db) { this.mParentDbObj = db; if (Log.isLoggable(TAG, Log.DEBUG)) { @@ -75,50 +78,67 @@ import java.util.Random; * @return the Database connection that the caller can use */ /* package */ SQLiteDatabase get(String sql) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - doAsserts(); - } - SQLiteDatabase db = null; PoolObj poolObj = null; synchronized(mParentDbObj) { + int poolSize = mPool.size(); + if (Log.isLoggable(TAG, Log.DEBUG)) { + assert sql != null; + doAsserts(); + } if (getFreePoolSize() == 0) { - if (mMaxPoolSize == mPool.size()) { + // no free ( = available) connections + if (mMaxPoolSize == poolSize) { // maxed out. can't open any more connections. // let the caller wait on one of the pooled connections + // preferably a connection caching the pre-compiled statement of the given SQL if (mMaxPoolSize == 1) { poolObj = mPool.get(0); } else { - // get a random number between 0 and (mMaxPoolSize-1) - poolObj = mPool.get( - new Random(SystemClock.elapsedRealtime()).nextInt(mMaxPoolSize-1)); + for (int i = 0; i < mMaxPoolSize; i++) { + if (mPool.get(i).mDb.isSqlInStatementCache(sql)) { + poolObj = mPool.get(i); + break; + } + } + if (poolObj == null) { + // there are no database connections with the given SQL pre-compiled. + // ok to return any of the connections. + if (rand == null) { + rand = new Random(SystemClock.elapsedRealtime()); + } + poolObj = mPool.get(rand.nextInt(mMaxPoolSize)); + } } db = poolObj.mDb; } else { // create a new connection and add it to the pool, since we haven't reached // max pool size allowed - int poolSize = getPoolSize(); db = mParentDbObj.createPoolConnection((short)(poolSize + 1)); poolObj = new PoolObj(db); mPool.add(poolSize, poolObj); } } else { // there are free connections available. pick one - for (int i = mPool.size() - 1; i >= 0; i--) { - poolObj = mPool.get(i); - if (!poolObj.isFree()) { - continue; - } - // it is free - but does its database object already have the given sql in its - // statement-cache? - db = poolObj.mDb; - if (sql == null || db.isSqlInStatementCache(sql)) { - // found a free connection we can use + // preferably a connection caching the pre-compiled statement of the given SQL + for (int i = 0; i < poolSize; i++) { + if (mPool.get(i).isFree() && mPool.get(i).mDb.isSqlInStatementCache(sql)) { + poolObj = mPool.get(i); break; } - // haven't found a database object which has the given sql in its - // statement-cache } + if (poolObj == null) { + // didn't find a free database connection with the given SQL already + // pre-compiled. return a free connection (this means, the same SQL could be + // pre-compiled on more than one database connection. potential wasted memory.) + for (int i = 0; i < poolSize; i++) { + if (mPool.get(i).isFree()) { + poolObj = mPool.get(i); + break; + } + } + } + db = poolObj.mDb; } assert poolObj != null; @@ -181,13 +201,10 @@ import java.util.Random; return list; } - /* package */ int getPoolSize() { - synchronized(mParentDbObj) { - return mPool.size(); - } - } - - private int getFreePoolSize() { + /** + * package level access for testing purposes only. otherwise, private should be sufficient. + */ + /* package */ int getFreePoolSize() { int count = 0; for (int i = mPool.size() - 1; i >= 0; i--) { if (mPool.get(i).isFree()) { @@ -197,12 +214,29 @@ import java.util.Random; return count++; } + /** + * only for testing purposes + */ + /* package */ ArrayList<PoolObj> getPool() { + return mPool; + } + @Override public String toString() { - return "db: " + mParentDbObj.getPath() + - ", threadid = " + Thread.currentThread().getId() + - ", totalsize = " + mPool.size() + ", #free = " + getFreePoolSize() + - ", maxpoolsize = " + mMaxPoolSize; + StringBuilder buff = new StringBuilder(); + buff.append("db: "); + buff.append(mParentDbObj.getPath()); + buff.append(", totalsize = "); + buff.append(mPool.size()); + buff.append(", #free = "); + buff.append(getFreePoolSize()); + buff.append(", maxpoolsize = "); + buff.append(mMaxPoolSize); + for (PoolObj p : mPool) { + buff.append("\n"); + buff.append(p.toString()); + } + return buff.toString(); } private void doAsserts() { @@ -224,10 +258,21 @@ import java.util.Random; } } + /** only used for testing purposes. */ + /* package */ boolean isDatabaseObjFree(SQLiteDatabase db) { + return mPool.get(db.mConnectionNum - 1).isFree(); + } + + /** only used for testing purposes. */ + /* package */ int getSize() { + return mPool.size(); + } + /** * represents objects in the connection pool. + * package-level access for testing purposes only. */ - private static class PoolObj { + /* package */ static class PoolObj { private final SQLiteDatabase mDb; private boolean mFreeBusyFlag = FREE; @@ -289,6 +334,13 @@ import java.util.Random; } } + /** + * only for testing purposes + */ + /* package */ synchronized int getNumHolders() { + return mNumHolders; + } + @Override public String toString() { StringBuilder buff = new StringBuilder(); diff --git a/core/tests/coretests/src/android/database/sqlite/DatabaseConnectionPoolTest.java b/core/tests/coretests/src/android/database/sqlite/DatabaseConnectionPoolTest.java new file mode 100644 index 000000000000..bb5e02465501 --- /dev/null +++ b/core/tests/coretests/src/android/database/sqlite/DatabaseConnectionPoolTest.java @@ -0,0 +1,364 @@ +/* + * Copyright (C) 2006 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.content.Context; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; +import android.util.Log; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; + +public class DatabaseConnectionPoolTest extends AndroidTestCase { + private static final String TAG = "DatabaseConnectionPoolTest"; + + private static final int MAX_CONN = 5; + private static final String TEST_SQL = "select * from test where i = ? AND j = 1"; + private static final String[] TEST_SQLS = new String[] { + TEST_SQL, TEST_SQL + 1, TEST_SQL + 2, TEST_SQL + 3, TEST_SQL + 4 + }; + + private SQLiteDatabase mDatabase; + private File mDatabaseFile; + private DatabaseConnectionPool mTestPool; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + File dbDir = getContext().getDir(this.getClass().getName(), Context.MODE_PRIVATE); + mDatabaseFile = new File(dbDir, "database_test.db"); + if (mDatabaseFile.exists()) { + mDatabaseFile.delete(); + } + mDatabase = SQLiteDatabase.openOrCreateDatabase(mDatabaseFile.getPath(), null); + assertNotNull(mDatabase); + mDatabase.execSQL("create table test (i int, j int);"); + mTestPool = new DatabaseConnectionPool(mDatabase); + assertNotNull(mTestPool); + } + + @Override + protected void tearDown() throws Exception { + mTestPool.close(); + mDatabase.close(); + mDatabaseFile.delete(); + super.tearDown(); + } + + @SmallTest + public void testGetAndRelease() { + mTestPool.setMaxPoolSize(MAX_CONN); + // connections should be lazily created. + assertEquals(0, mTestPool.getSize()); + // MAX pool size should be set to MAX_CONN + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + // get a connection + SQLiteDatabase db = mTestPool.get(TEST_SQL); + // pool size should be one - since only one should be allocated for the above get() + assertEquals(1, mTestPool.getSize()); + // no free connections should be available + assertEquals(0, mTestPool.getFreePoolSize()); + assertFalse(mTestPool.isDatabaseObjFree(db)); + // release the connection + mTestPool.release(db); + assertEquals(1, mTestPool.getFreePoolSize()); + assertEquals(1, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + assertTrue(mTestPool.isDatabaseObjFree(db)); + // release the same object again and expect IllegalStateException + try { + mTestPool.release(db); + fail("illegalStateException expected"); + } catch (IllegalStateException e ) { + // expected. + } + } + + /** + * get all connections from the pool and ask for one more. + * should get one of the connections already got so far. + */ + @SmallTest + public void testGetAllConnAndOneMore() { + mTestPool.setMaxPoolSize(MAX_CONN); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + ArrayList<SQLiteDatabase> dbObjs = new ArrayList<SQLiteDatabase>(); + for (int i = 0; i < MAX_CONN; i++) { + SQLiteDatabase db = mTestPool.get(TEST_SQL); + assertFalse(dbObjs.contains(db)); + dbObjs.add(db); + } + assertEquals(0, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + // pool is maxed out and no free connections. ask for one more connection + SQLiteDatabase db1 = mTestPool.get(TEST_SQL); + // make sure db1 is one of the existing ones + assertTrue(dbObjs.contains(db1)); + // pool size should remain at MAX_CONN + assertEquals(0, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + // release db1 but since it is allocated 2 times, it should still remain 'busy' + mTestPool.release(db1); + assertFalse(mTestPool.isDatabaseObjFree(db1)); + assertEquals(0, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + // release all connections + for (int i = 0; i < MAX_CONN; i++) { + mTestPool.release(dbObjs.get(i)); + } + // all objects in the pool should be freed now + assertEquals(MAX_CONN, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + } + + /** + * same as above except that each connection has different SQL statement associated with it. + */ + @SmallTest + public void testConnRetrievalForPreviouslySeenSql() { + mTestPool.setMaxPoolSize(MAX_CONN); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + + HashMap<String, SQLiteDatabase> dbObjs = new HashMap<String, SQLiteDatabase>(); + for (int i = 0; i < MAX_CONN; i++) { + SQLiteDatabase db = mTestPool.get(TEST_SQLS[i]); + executeSqlOnDatabaseConn(db, TEST_SQLS[i]); + assertFalse(dbObjs.values().contains(db)); + dbObjs.put(TEST_SQLS[i], db); + } + assertEquals(0, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + // pool is maxed out and no free connections. ask for one more connection + // use a previously seen SQL statement + String testSql = TEST_SQLS[MAX_CONN - 1]; + SQLiteDatabase db1 = mTestPool.get(testSql); + assertEquals(0, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + // make sure db1 is one of the existing ones + assertTrue(dbObjs.values().contains(db1)); + assertEquals(db1, dbObjs.get(testSql)); + // do the same again + SQLiteDatabase db2 = mTestPool.get(testSql); + // make sure db1 is one of the existing ones + assertEquals(db2, dbObjs.get(testSql)); + + // pool size should remain at MAX_CONN + assertEquals(0, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + + // release db1 but since the same connection is allocated 3 times, + // it should still remain 'busy' + mTestPool.release(db1); + assertFalse(mTestPool.isDatabaseObjFree(dbObjs.get(testSql))); + assertEquals(0, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + + // release db2 but since the same connection is allocated 2 times, + // it should still remain 'busy' + mTestPool.release(db2); + assertFalse(mTestPool.isDatabaseObjFree(dbObjs.get(testSql))); + assertEquals(0, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + + // release all connections + for (int i = 0; i < MAX_CONN; i++) { + mTestPool.release(dbObjs.get(TEST_SQLS[i])); + } + // all objects in the pool should be freed now + assertEquals(MAX_CONN, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + } + + private void executeSqlOnDatabaseConn(SQLiteDatabase db, String sql) { + // execute the given SQL on the given database connection so that the prepared + // statement for SQL is cached by the given database connection + // this will help DatabaseConenctionPool figure out if a given SQL statement + // is already cached by a database connection. + db.execSQL(sql, new String[]{1+""}); + } + + /** + * get a connection for a SQL statement 'blah'. (connection_s) + * make sure the pool has at least one free connection even after this get(). + * and get a connection for the same SQL again. + * this connection should be different from connection_s. + * even though there is a connection with the given SQL pre-compiled, since is it not free + * AND since the pool has free connections available, should get a new connection. + */ + @SmallTest + public void testGetConnForTheSameSql() { + mTestPool.setMaxPoolSize(MAX_CONN); + + SQLiteDatabase db = mTestPool.get(TEST_SQL); + executeSqlOnDatabaseConn(db, TEST_SQL); + assertEquals(0, mTestPool.getFreePoolSize()); + assertEquals(1, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + + assertFalse(mTestPool.isDatabaseObjFree(db)); + + SQLiteDatabase db1 = mTestPool.get(TEST_SQL); + assertEquals(0, mTestPool.getFreePoolSize()); + assertEquals(2, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + + assertFalse(mTestPool.isDatabaseObjFree(db1)); + assertFalse(db1.equals(db)); + + mTestPool.release(db); + assertEquals(1, mTestPool.getFreePoolSize()); + assertEquals(2, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + + mTestPool.release(db1); + assertEquals(2, mTestPool.getFreePoolSize()); + assertEquals(2, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + } + + /** + * get the same connection N times and release it N times. + * this tests DatabaseConnectionPool.PoolObj.mNumHolders + */ + @SmallTest + public void testGetSameConnNtimesAndReleaseItNtimes() { + mTestPool.setMaxPoolSize(MAX_CONN); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + + HashMap<String, SQLiteDatabase> dbObjs = new HashMap<String, SQLiteDatabase>(); + for (int i = 0; i < MAX_CONN; i++) { + SQLiteDatabase db = mTestPool.get(TEST_SQLS[i]); + executeSqlOnDatabaseConn(db, TEST_SQLS[i]); + assertFalse(dbObjs.values().contains(db)); + dbObjs.put(TEST_SQLS[i], db); + } + assertEquals(0, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + // every connection in the pool should have numHolders = 1 + for (int i = 0; i < MAX_CONN; i ++) { + assertEquals(1, mTestPool.getPool().get(i).getNumHolders()); + } + // pool is maxed out and no free connections. ask for one more connection + // use a previously seen SQL statement + String testSql = TEST_SQLS[MAX_CONN - 1]; + SQLiteDatabase db1 = mTestPool.get(testSql); + assertEquals(0, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + // make sure db1 is one of the existing ones + assertTrue(dbObjs.values().contains(db1)); + assertEquals(db1, dbObjs.get(testSql)); + assertFalse(mTestPool.isDatabaseObjFree(db1)); + DatabaseConnectionPool.PoolObj poolObj = mTestPool.getPool().get(db1.mConnectionNum - 1); + int numHolders = poolObj.getNumHolders(); + assertEquals(2, numHolders); + assertEquals(0, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + // get the same connection N times more + int N = 100; + for (int i = 0; i < N; i++) { + SQLiteDatabase db2 = mTestPool.get(testSql); + assertEquals(db1, db2); + assertFalse(mTestPool.isDatabaseObjFree(db2)); + // numHolders for this object should be now up by 1 + int prev = numHolders; + numHolders = poolObj.getNumHolders(); + assertEquals(prev + 1, numHolders); + } + // release it N times + for (int i = 0; i < N; i++) { + mTestPool.release(db1); + int prev = numHolders; + numHolders = poolObj.getNumHolders(); + assertEquals(prev - 1, numHolders); + assertFalse(mTestPool.isDatabaseObjFree(db1)); + } + // the connection should still have 2 more holders + assertFalse(mTestPool.isDatabaseObjFree(db1)); + assertEquals(2, poolObj.getNumHolders()); + assertEquals(0, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + // release 2 more times + mTestPool.release(db1); + mTestPool.release(db1); + assertEquals(0, poolObj.getNumHolders()); + assertEquals(1, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + assertTrue(mTestPool.isDatabaseObjFree(db1)); + } + + @SmallTest + public void testStressTest() { + mTestPool.setMaxPoolSize(MAX_CONN); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + + HashMap<SQLiteDatabase, Integer> dbMap = new HashMap<SQLiteDatabase, Integer>(); + for (int i = 0; i < MAX_CONN; i++) { + SQLiteDatabase db = mTestPool.get(TEST_SQLS[i]); + assertFalse(dbMap.containsKey(db)); + dbMap.put(db, 1); + executeSqlOnDatabaseConn(db, TEST_SQLS[i]); + } + assertEquals(0, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + // ask for lot more connections but since the pool is maxed out, we should start receiving + // connections that we already got so far + for (int i = MAX_CONN; i < 1000; i++) { + SQLiteDatabase db = mTestPool.get(TEST_SQL + i); + assertTrue(dbMap.containsKey(db)); + int k = dbMap.get(db); + dbMap.put(db, ++k); + } + assertEquals(0, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + // print the distribution of the database connection handles received, should be uniform. + for (SQLiteDatabase d : dbMap.keySet()) { + Log.i(TAG, "connection # " + d.mConnectionNum + ", numHolders: " + dbMap.get(d)); + } + // print the pool info + Log.i(TAG, mTestPool.toString()); + // release all + for (SQLiteDatabase d : dbMap.keySet()) { + int num = dbMap.get(d); + for (int i = 0; i < num; i++) { + mTestPool.release(d); + } + } + assertEquals(MAX_CONN, mTestPool.getFreePoolSize()); + assertEquals(MAX_CONN, mTestPool.getSize()); + assertEquals(MAX_CONN, mTestPool.getMaxPoolSize()); + } +} |