[pysqlite] Connection deadlock because of file lock

Joseph Wecker <[email protected]> Wed, 08 Apr 2009 15:10:18 -0600
Newsgroups gmane.comp.python.db.pysqlite.user
Message-ID <[email protected]>
Hi all.  I recently was experimenting with sqlite concurrency.  The 
commonly repeated meme is "don't use sqlite when you need concurrency"- 
the problem is I couldn't find any actual metrics anywhere, so I started 
benchmarking my own.  That is, I wanted to know at what point 
concurrency starts being a problem (5 simultaneous?  100 simultaneous? 
at what point do you get a 10% drop in performance versus serial access? 
etc.)

Initially, the core of my benchmarking suite was something like this:


#---------------------------
import sqlite3, time
def do_insert(conn):
    conn.execute("""
        insert into stocks values('2009-01-05', 'Test', 'Test', 100, 10.13)
        """)
    conn.commit()

conn = sqlite3.connect('for_testing.sqlite3')
for i in range(0,100):
    do_insert(conn)
#---------------------------


If I spawned many of these at the same time, one always finished and all 
the others always exit with (as expected) an OperationalError: database 
is locked.
The only surprise for me there was that only one finished- surely one of 
the others would, after some timeout, be able to acquire the lock and do 
its work as well?  So I increased the timeout on the sqlite3.connect() 
function.  No matter what I set it to, only one process succeeds, and 
the others wait for however long before failing.

OK, so at this point I'm surprised, but I figure I'll just do a quick 
workaround, since, again, I'm trying to measure sqlite concurrency:


#---------------------------
import sqlite3, time
def do_insert(conn):
    try:
        conn.execute("""
            insert into stocks values('2009-01-05', 'Test', 'Test', 100, 
10.13)
            """)
        conn.commit()
    except sqlite3.OperationalError:
        time.sleep(0.1)
        do_insert(conn)                       #  <-----   Wait for a bit 
and try again

conn = sqlite3.connect('for_testing.sqlite3')
for i in range(0,100):
    do_insert(conn)
#---------------------------

So here if it sees that the db is locked it simply waits for a bit and 
tries again (recursively- not pretty, but that's beside the point).

Again, one of these spawned processes is able to do it's things, but the 
rest continue to get OperationalErrors forever after- even after that 
first one is done and has released its lock.

Finally, the only thing that worked was for me to completely 
re-establish the connection each time, ala:

#---------------------------
def do_insert():
    try:
        conn = sqlite3.connect('example.db', timeout=0.1)
        conn.execute("""
                insert into stocks
                values('2006-01-05','BUY','RHAT',100,35.14)""")
        conn.commit()
        conn.close()
    except sqlite3.OperationalError:
        conn.close()
        time.sleep(0.1)
        do_insert()

for i in range(0,10):
    do_insert()
#---------------------------

Now, finally, processes keep trying to do their work until they succeed, 
and I was able to do my benchmarking.

I get the sinking feeling that a lot of the problem people have with 
sqlite's concurrency is not that it's large-grain locks make it that 
much less concurrent, but that it has a lot more to do with people's 
experience with how libraries like pysqlite handle and propagate the db 
lock message.  With my third solution above, I found that sqlite easily 
scales to hundreds of simultaneous users (_really_ simultaneous- as in 
all trying to write to the DB at the exact same time) in line with mysql 
and others- at 500+ simultaneous users you start to get about a 10% drop 
because of lock overhead.  Not spectacular, but not shabby either.

The problem is that anecdotally people using, say, django, encounter the 
database-is-locked error even at very, very minimal loads, because 
pysqlite requires the connection to be completely reset when it 
encounters a lock- even if the timeout parameter is set.  That gives 
people a sense that sqlite is not nearly as powerful as it actually is 
(which still may not match, say, Berkeley db or something, but still!)

I've gone through the django code and verified for myself that it will 
just blindly propagate the db-locked problem without trying to reset the 
connection.  It is, in fact, a bit unintuitive.

Is this something that can be addressed?  Am I missing something?  I 
_really_ appreciate the work that has gone into this library.  I feel 
that opening up this concurrency bottleneck will spread sqlite's 
adoption and this library's utility significantly.  We can stop hearing 
people say "oh, yeah, that's just sqlite for you- upgrade to mysql..." 
until much higher loads...

-Joseph