SqliteQ
The playhouse.sqliteq
module provides a subclass of SqliteExtDatabase
, that will serialize concurrent writes to a SQLite database. SqliteQueueDatabase
can be used as a drop-in replacement for the regular SqliteDatabase
if you want simple read and write access to a SQLite database from multiple threads.
SQLite only allows one connection to write to the database at any given time. As a result, if you have a multi-threaded application (like a web-server, for example) that needs to write to the database, you may see occasional errors when one or more of the threads attempting to write cannot acquire the lock.
SqliteQueueDatabase
is designed to simplify things by sending all write queries through a single, long-lived connection. The benefit is that you get the appearance of multiple threads writing to the database without conflicts or timeouts. The downside, however, is that you cannot issue write transactions that encompass multiple queries – all writes run in autocommit mode, essentially.
Note
The module gets its name from the fact that all write queries get put into a thread-safe queue. A single worker thread listens to the queue and executes all queries that are sent to it.
Transactions
Because all queries are serialized and executed by a single worker thread, it is possible for transactional SQL from separate threads to be executed out-of-order. In the example below, the transaction started by thread “B” is rolled back by thread “A” (with bad consequences!):
- Thread A: UPDATE transplants SET organ=’liver’, …;
- Thread B: BEGIN TRANSACTION;
- Thread B: UPDATE life_support_system SET timer += 60 …;
- Thread A: ROLLBACK; – Oh no….
Since there is a potential for queries from separate transactions to be interleaved, the transaction()
and atomic()
methods are disabled on SqliteQueueDatabase
.
For cases when you wish to temporarily write to the database from a different thread, you can use the pause()
and unpause()
methods. These methods block the caller until the writer thread is finished with its current workload. The writer then disconnects and the caller takes over until unpause
is called.
The stop()
, start()
, and is_stopped()
methods can also be used to control the writer thread.
Note
Take a look at SQLite’s isolation documentation for more information about how SQLite handles concurrent connections.
Code sample
Creating a database instance does not require any special handling. The SqliteQueueDatabase
accepts some special parameters which you should be aware of, though. If you are using gevent, you must specify use_gevent=True
when instantiating your database – this way Peewee will know to use the appropriate objects for handling queueing, thread creation, and locking.
from playhouse.sqliteq import SqliteQueueDatabase
db = SqliteQueueDatabase(
'my_app.db',
use_gevent=False, # Use the standard library "threading" module.
autostart=False, # The worker thread now must be started manually.
queue_max_size=64, # Max. # of pending writes that can accumulate.
results_timeout=5.0) # Max. time to wait for query to be executed.
If autostart=False
, as in the above example, you will need to call start()
to bring up the worker threads that will do the actual write query execution.
@app.before_first_request
def _start_worker_threads():
db.start()
If you plan on performing SELECT queries or generally wanting to access the database, you will need to call connect()
and close()
as you would with any other database instance.
When your application is ready to terminate, use the stop()
method to shut down the worker thread. If there was a backlog of work, then this method will block until all pending work is finished (though no new work is allowed).
import atexit
@atexit.register
def _stop_worker_threads():
db.stop()
Lastly, the is_stopped()
method can be used to determine whether the database writer is up and running.