I sometimes randomly encounter:
OperationalError: database is locked
in a process that updates a SQLite database, but I find it difficult to reproduce the error:
- no other process is inserting / deleting rows at the same time
- just one process might do some read-only queries (
SELECT
, etc.) here and there, but no committing
I've already read OperationalError: database is locked
Question: Is there a way, when this error happens, to log which other process ID is responsible for the lock?
More generally, how to debug a OperationalError: database is locked
?
Solution: Always close the
cursor
for (even read-only) queries!First, here is a way to reproduce the problem:
First run this code, once:
to initialize the test.
Then begin a read-only query:
and keep this script running while executing the next step:
Then try to delete some content and commit:
It will trigger this error indeed:
Why? Because it's not possible to delete data that is currently accessed by a read-query: if the cursor it's still open, it means the data could still be fetched with
fetchone
orfetchall
.Here is how to solve the error: in step #2, just add:
Then while this is still running, start script #3, you will see there is no more error.
No, that information is not recorded when the exception occurs. The
OperationalError: database is locked
exception is usually raised after a timeout (5 minutes is the default) when trying to obtain a mutex and and a file lock in the SQLite internals, at which point SQLite returnsSQLITE_BUSY
, butSQLITE_BUSY
can also be reported at other points. SQLite error codes do not carry any further context such as the PID of another process that held a lock, and it is conceivable that the lock was passed between two other processes before the current process gave up trying to get hold of it!At best you can enumerate what processes are currently accessing the file by using
lsof <filename of database>
, but that won't get you any closer to figuring out which one of these is actually taking too long to commit.I would, instead, instrument your code with explicit transactions and detailed logging as to when you start and commit transactions. Then, when you do run into
OperationalError
exceptions, you can check the logs for what was happening where within that time window.A Python context manager that could be used for this is:
The above will create start and end entries, include exception information if there is any, optionally trace all statements that are being executed on the connection and can include a stack trace that will tell you the location where the context manager was used. Do make sure to include the date and time in when formatting log messages so you can track when transactions started.
I'd use it around any code using the connection, so you can time selects as well:
It may be that just using this context manager makes your issue disappear, at which point you'd have to analyse why the code without this context manager leaves open a transaction without committing.
You may also want to use a lower
timeout
value insqlite3.connect()
calls to speed up the process; you may not have to wait a full 5 minutes to detect the situation.Note on threading: when enabling tracing it is assumed that you use separate connections for separate threads. If this is not the case, then you'll need to permanently register a trace callback that then sorts out what transaction id to use for the current thread.