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

Commit 6ac21d30 authored by Vasu Nori's avatar Vasu Nori
Browse files

unittests for DatabaseConnectionPool (and fix bugs)

Change-Id: I6f251b4bdd472bd840ea1be6497660f8c31cd3e3
parent 8cb62401
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());
    }
}