Hi,

I think I have found a regression/bug in Sqlite with respect to
the sqlite3_get_autocommit() function.

Sometimes, when multiple connections are used in multiple threads,
sqlite3_get_autocommit()  reports that the connection has a transaction
open (i.e. not in auto commit mode), even though sqlite3_step() returns
SQLITE_BUSY.  I have verified that sqlite3_get_autocommit()  reports that
there is no transaction open before sqlite3_step() is called.  The problem
seems to be specific to the WAL journal mode, as I haven't been able to
reproduce it for

Initially, the problem only showed up under Linux, but I have now been able
to reproduce the problem on Windows too.  For me it shows up quicker under
linux, but that may just be because of the difference in environments.  I
have attached a single-file C++11 application that consistently reproduces
the problem for me.

I was also able to bisect the introduction of the problem to sqlite
v3.17.0.  v3.16.2 does not have the problem.  The latest 3.24.0 release
does.  I am not an expert on the sqlite source code, but during my
debugging, I was able to determine that the auto commit flag was being
cleared in "case OP_AutoCommit", which returns without error.  Obviously,
something else must be causing the  SQLITE_BUSY afterwards, but is not
resetting the auto commit flag.

In my code, I have worked around the problem by issuing a "rollback"
whenever I detect the problem described.

I hope this is enough information to be able to solve the bug.  If,
however, this behaviour turns out not to be a bug, but something that is
expected to happen occasionally, then the "rollback" provides a reasonable
way to mitigate the issue.

Regards
Nic
#include <assert.h>
#include <stdio.h>

#include <chrono>
#include <thread>

#include "sqlite3.h"


static const char DATABASE_NAME[] = "test.db";


using namespace std::chrono;


static volatile bool finished = false;
static int Update (sqlite3 *db)
{
        printf ("Entering Update() for db = %p\n", db);

        int rc = SQLITE_OK;
        unsigned loops = 0;
        const char *msg = "UNKNOWN";
        while (finished == false && rc == SQLITE_OK)
        {
                ++loops;

                // We should not be in a transaction.
                assert (sqlite3_get_autocommit (db) != 0);


                // The failure always occurs on "begin immediate", so
                // prepare and step it manually.
                sqlite3_stmt *st = nullptr;
                rc = sqlite3_prepare_v2 (db, "begin immediate", 15, &st, 
nullptr);
                if (rc != SQLITE_OK)
                {
                        msg = "sqlite3_prepare_v2()";
                        break;
                }


                // Repeat while SQLITE_BUSY
                do
                {
                        rc = sqlite3_step (st);
                        if (rc == SQLITE_DONE)
                        {
                                break;
                        }

                        msg = "sqlite3_step()";
                        if (rc == SQLITE_BUSY)
                        {
                                // Since sqlite3_step() failed, we do not expect
                                // to be in a transaction.
                                if (sqlite3_get_autocommit (db) == 0)
                                {
                                        // Oops, this is unexpected!
                                        printf ("Failed!\n");

                                        msg = "sqlite3_get_autocommit()";
                                        break;
                                }

                                // Back off for a while.
                                std::this_thread::sleep_for (milliseconds 
(rand() % 15));
                        }
                }
                while (rc == SQLITE_BUSY);


                sqlite3_finalize (st);
                if (rc != SQLITE_DONE)
                {
                        break;
                }


                // UPDATE should not fail, as we have our lock,
                // so just use sqlite3_exec() for simplicity.
                rc = sqlite3_exec (db, "UPDATE t SET i = i + 1", nullptr, 
nullptr, nullptr);
                if (rc != SQLITE_OK)
                {
                        msg = "'UPDATE'";
                        break;
                }


                // COMMIT can cause a SQLITE_BUSY
                do
                {
                        msg = "'COMMIT'";
                        rc = sqlite3_exec (db, "commit", nullptr, nullptr, 
nullptr);
                        if (rc == SQLITE_BUSY)
                        {
                                // Back off for a while.
                                std::this_thread::sleep_for (milliseconds 
(rand() % 15));
                        }
                }
                while (rc == SQLITE_BUSY);
        }


        if (rc != SQLITE_OK && rc != SQLITE_INTERRUPT)
        {
                printf ("%s failed on db %p, with %d\n", msg, db, rc);
        }

        printf ("Exiting Update() for db = %p, loops = %u\n", db, loops);

        // Tell the other threads to terminate their loops too.
        finished = true;

        return rc;
}


static sqlite3 *OpenDb()
{
        sqlite3 *db;
        int rc = sqlite3_open_v2 (DATABASE_NAME, &db, SQLITE_OPEN_READWRITE | 
SQLITE_OPEN_CREATE, nullptr);
        assert (db);
        assert (rc == SQLITE_OK);

        return db;
}


// Create the initial table that we need for the test.
bool InitialiseDb (sqlite3 *db)
{
        // I have been able to reproduce the problem with the WAL journal mode,
        // but it may affect other modes too.
        int rc = sqlite3_exec (db, "PRAGMA journal_mode=WAL", nullptr, nullptr, 
nullptr);
        if (rc != SQLITE_OK)
        {
                printf ("'PRAGMA journal_mode=WAL' failed with %d\n", rc);
                return false;
        }

        sqlite3_exec (db, "DROP TABLE t", nullptr, nullptr, nullptr);

        sqlite3_exec (db, "CREATE TABLE t (i int)", nullptr, nullptr, nullptr);
        if (rc != SQLITE_OK)
        {
                printf ("'CREATE TABLE t (i int)' failed with %d\n", rc);
                return false;
        }

        sqlite3_exec (db, "INSERT INTO t values (0)", nullptr, nullptr, 
nullptr);
        if (rc != SQLITE_OK)
        {
                printf ("'INSERT INTO t values (0)' failed with %d\n", rc);
                return false;
        }

        return true;
}


int main()
{
        // Setup
        finished = false;
        sqlite3 *db1 = OpenDb();
        if (InitialiseDb (db1) == false)
        {
                sqlite3_close (db1);
                return 1;
        }

        sqlite3 *db2 = OpenDb();
        sqlite3 *db3 = OpenDb();
        sqlite3 *db4 = OpenDb();
        assert (db1 && db2 && db3 && db4);


        // Run the Update() function in multple threads
        std::thread thread1 (&Update, db1);
        std::thread thread2 (&Update, db2);
        std::thread thread3 (&Update, db3);
        std::thread thread4 (&Update, db4);


        // Run the test for 30 seconds.
        // Comment this out to loop indefinitely
        for (int i = 0;  finished == false && i < 30;  ++i)
        {
                std::this_thread::sleep_for (seconds (1));
        }


        // Force the threads to terminate
        finished = true;
        sqlite3_interrupt (db1);
        sqlite3_interrupt (db2);
        sqlite3_interrupt (db3);
        sqlite3_interrupt (db4);


        // Wait for the threads and close the connections
        thread1.join();
        thread2.join();
        thread3.join();
        thread4.join();

        sqlite3_close (db4);
        sqlite3_close (db3);
        sqlite3_close (db2);
        sqlite3_close (db1);

        return 0;
}
_______________________________________________
sqlite-users mailing list
sqlite-users@mailinglists.sqlite.org
http://mailinglists.sqlite.org/cgi-bin/mailman/listinfo/sqlite-users

Reply via email to