Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7fbee2f7 authored by Vasu Nori's avatar Vasu Nori Committed by Android (Google) Code Review
Browse files

Merge "unittests for DatabaseConnectionPool (and fix bugs)"

parents c9880beb 6ac21d30
Loading
Loading
Loading
Loading
+85 −33
Original line number Diff line number Diff line
@@ -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,51 +78,68 @@ 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--) {
                // 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);
                    if (!poolObj.isFree()) {
                        continue;
                        break;
                    }
                    // 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
                }
                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;
                        }
                    // haven't found a database object which has the given sql in its
                    // statement-cache
                    }
                }
                db = poolObj.mDb;
            }

            assert poolObj != null;
            assert poolObj.mDb == db;
@@ -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();
+364 −0
Original line number Diff line number Diff line
/*
 * 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());
    }
}