Concurrency

Single operations

Every operation takes an instance-level lock, so operations from several threads on one instance are serialised. They take turns; they do not run in parallel.

db = mysql('localhost', 'user', 'password', 'mydb')

# The four threads share the instance and each query waits its turn.
for _ in range(4):
    threading.Thread(target=lambda: db.select('users')).start()

Transactions belong to one thread

A transaction opened with transaction() or begin() belongs to the thread that opened it, until the outermost commit or rollback. While it is open, an operation from any other thread on that instance raises RuntimeError before SQL is sent and before count() and getLastId() change.

# thread A
with db.transaction():
    db.insert('orders', {'customer_id': 7})
    ...

# thread B, same instance, while A is still inside
db.select('orders')      # RuntimeError

The guard covers execute, query, insert, update, delete, select, the batch operations, begin, commit, rollback, close, connect, reconnect and resetSession. A foreign thread cannot enter or exit the owner's transaction() block either.

Nested blocks in the owning thread keep working through savepoints. Ownership is released when the outermost transaction ends.

Ownership covers transactions opened through transaction() and begin(). A transaction opened with raw SQL — db.execute('BEGIN') — takes no owner.

One instance per thread

For parallel work, and for threads that each need their own transaction, give every thread its own instance:

import threading

local = threading.local()

def db():
    if not hasattr(local, 'conn'):
        local.conn = mysql('localhost', 'user', 'password', 'mydb')
    return local.conn

def work():
    with db().transaction():
        db().insert('events', {'name': 'signup'})

Or an external connection pool. The library does not ship one.

Metadata is per instance

count() and getLastId() describe the last successful operation on the instance. On a shared instance, an operation from another thread replaces them between your write and your read.

result = db.upsert('stock', {'sku': 'A-1', 'units': 10}, update_columns=['units'])
result.affected_rows    # not affected by later operations
result.last_id

upsert(), insert_many() and upsert_many() return a WriteResult, which holds its values independently of the cursor.