AtlatestRepositorysigil-sqlite
sigil-sqlite / tree / docsconcurrency.md
1
# Concurrency: one connection is one serial resource3
`sigil-sqlite` runs on a single-threaded cooperative scheduler. That makes two4
things true at once, and the interaction between them is the whole of this page.6
1. A contended `sqlite3_step` must never block, or it freezes every fiber —7
including the one holding the lock, which can then never run to commit. So8
the native layer forces SQLite's own `busy_timeout` to 0 and the Scheme layer9
retries cooperatively, yielding between attempts.10
2. **A SQLite connection is a serial resource.** SQLite ends a connection's11
implicit read transaction only when *no statement on it is active*.13
Put those together and a shared connection has a sharp edge. If fiber A has a14
statement open — parked in the busy-retry, or simply part-way through a result15
set — and fiber B runs a query on the *same* connection, B's read transaction16
cannot be released when B finalizes: A is still using the btree. The connection17
is pinned to B's snapshot until A completes, and every later read on it silently18
misses anything committed in between.20
## What the library does about it22
Every high-level primitive — `sqlite-query`, `sqlite-query-row`, `sqlite-run`,23
`sqlite-exec` — holds the connection exclusively for its whole duration, yields24
included. Concurrent calls on one connection serialise.26
**The lock spans the whole primitive, not just the busy window.** A fiber yields27
inside an *uncontended* query too: allocation-driven, roughly once per 450 rows.28
A 20000-row query yields about 44 times with no contention anywhere. Guarding29
only the retry would leave every one of those windows open.31
Statements are finalized on **every** exit path, including a raise. A statement32
leaked by a raise holds the connection's read transaction open for the life of33
the process, which turns a transient stale read into a permanent one.35
## What this costs you37
Concurrent database work on one connection now **queues**. A fiber waiting for a38
connection whose holder is parked on a contended write waits for that whole busy39
budget — by default 5 seconds. If you want database calls to proceed40
concurrently, **give each fiber its own connection**. That is what connections41
are for, and WAL readers are cheap.43
A fiber that waits more than `*sqlite-connection-wait-seconds*` (15s, three times44
the busy budget) for a connection **raises** rather than waiting forever. Two45
fibers each holding a connection the other wants is a deadlock; a bounded wait46
turns it into a loud, attributable error instead of a process that stops with no47
output.49
## What is NOT covered51
**The low-level API.** `sqlite-prepare` / `sqlite-step` / `sqlite-finalize` are52
not serialised and cannot be: the library does not know when your statement is53
finished. If you hold a statement across steps on a connection another fiber54
also uses, you own the hazard described at the top of this page. Use the55
high-level API, or give that work its own connection.57
**Multi-statement transactions.** `BEGIN` … `COMMIT` spans several primitives,58
and the lock is released between them, so another fiber's query can interleave59
inside your transaction. There is deliberately no exported "hold this connection60
across several calls" primitive, because holding the lock across caller code61
deadlocks the moment one of your database helpers calls another. **Give a62
transaction its own connection.**64
## busy_timeout66
`PRAGMA busy_timeout=N` cannot be passed to SQLite — a blocking step would67
freeze the scheduler, which is the thing point 1 above exists to prevent. It68
used to be accepted and silently ignored: dead configuration that read as live.69
It now sets the cooperative retry budget instead, which is the same intent70
expressed where this library can act on it.72
The budget is **process-wide**, not per-connection: `sqlite-step` receives a73
statement and has no connection to look a per-connection value up on. Prefer74
`sqlite-set-busy-budget!`, which says so in its name; read it back with75
`sqlite-busy-budget`.77
## Diagnostics79
`sqlite-connection-locked?` answers whether a fiber currently holds a80
connection. It is a diagnostic, not a synchronisation primitive: by the time you81
act on the answer it may have changed.83
## THE BUSY BUDGET NOW MEANS SOMETHING DIFFERENT85
Read this before tuning `*sqlite-busy-budget-seconds*` (5.0s), because the86
parameter did not change but **its job did**.88
**Before:** exhausting the budget was catastrophic. It raised, the raise leaked89
the statement, and the leaked statement pinned the connection for the life of90
the process. So the budget was a number chosen to make exhaustion *rare* — long91
enough that ordinary contention never reached it.93
**Now:** exhaustion is merely a raise the caller sees and can handle, and every94
statement is finalized on the way out. What the budget sets instead is the95
**worst-case stall every other database user on that connection pays**, because96
they queue behind the holder.98
Those are different jobs, and 5 seconds was chosen for the first one. A shorter99
budget now means faster failure and a shorter worst-case stall; a longer one100
means more patience under contention and a longer stall. Neither reading is101
available from the old number.103
It has deliberately not been changed here. Tuning it is a separate decision that104
should be made against the new meaning rather than inherited from the old.