Spike: leased read-connection pool with global cap and idle reaper

Replace the per-thread cached read connections with a pool. Each
execute_fn()/execute() callback checks a connection out for its duration
and gets a LeasedConnection proxy that raises ConnectionLeaseError if
used after the callback returns.

New settings: max_open_connections (global LRU cap, default 128),
connection_idle_timeout (seconds, default 60, enforced by one
process-wide reaper thread plus opportunistic reaping on checkout),
connection_pool_wait_ms (soft cap: wait then exceed) and
pool_read_connections (off = fresh connection per query, a control).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwkLo2rJWidpkDVT3HkvNv
This commit is contained in:
Claude 2026-10-04 01:26:01 +00:00
commit 791ce7a3d9
No known key found for this signature in database
9 changed files with 900 additions and 15 deletions

View file

@ -251,6 +251,19 @@ These can be passed to ``datasette serve`` using ``datasette serve --setting nam
this limit (default=2097152)
num_sql_threads Number of threads in the thread pool for
executing SQLite queries (default=3)
max_open_connections Maximum number of pooled read connections open
across all databases - least recently used idle
connections are closed first, 0 for no limit
(default=128)
connection_idle_timeout Close pooled read connections that have been idle
for this many seconds - 0 to keep them open
(default=60)
connection_pool_wait_ms When max_open_connections is reached and every
connection is in use, wait this long for one to
be returned before opening one more (default=0)
pool_read_connections Reuse read connections between queries - turn off
to open a new connection for every query
(default=True)
sql_time_limit_ms Time limit for a SQL query in milliseconds
(default=1000)
default_facet_size Number of values to return for requested facets

View file

@ -2174,6 +2174,8 @@ Example usage:
version = await db.execute_fn(get_version)
The connection is borrowed from a pool for the duration of the call (see :ref:`setting_max_open_connections`) and is only valid inside the function. Using it after the function has returned - for example by storing it on an object - raises a ``datasette.database.ConnectionLeaseError``. Returning a cursor or a generator from the function raises the same error: fetch the rows you need before returning.
The call is traced as a ``db.query`` OpenTelemetry span carrying ``datasette.callback`` (the function's qualified name) rather than ``db.query.text``, since the SQL is whatever the function chooses to run - see :ref:`internals_telemetry`. Passing a named function gives the span a readable identity; a lambda reports ``<lambda>``.
.. _database_execute_write:

View file

@ -214,6 +214,72 @@ Maximum number of threads in the thread pool Datasette uses to execute SQLite qu
Setting this to 0 turns off threaded SQL queries entirely - useful for environments that do not support threading such as `Pyodide <https://pyodide.org/>`__.
.. _setting_max_open_connections:
max_open_connections
~~~~~~~~~~~~~~~~~~~~
.. [[[cog
setting_default(cog, "max_open_connections")
.. ]]]
Default: ``128``
.. [[[end]]]
Read queries borrow a connection from a pool for the duration of one query and return it afterwards. This setting caps how many pooled read connections can be open at once across all attached file databases. When a new connection is needed and the cap has been reached, the least recently used idle connection is closed first.
If every connection is in use the pool waits up to :ref:`setting_connection_pool_wait_ms` and then opens one more anyway, so the cap can be exceeded by at most :ref:`setting_num_sql_threads` connections. Set this to 0 for no limit.
::
datasette *.db --setting max_open_connections 64
.. _setting_connection_idle_timeout:
connection_idle_timeout
~~~~~~~~~~~~~~~~~~~~~~~
.. [[[cog
setting_default(cog, "connection_idle_timeout")
.. ]]]
Default: ``60``
.. [[[end]]]
Pooled read connections that have not been used for this many seconds are closed. Set to 0 to keep idle connections open until they are evicted by :ref:`setting_max_open_connections`.
.. _setting_connection_pool_wait_ms:
connection_pool_wait_ms
~~~~~~~~~~~~~~~~~~~~~~~
.. [[[cog
setting_default(cog, "connection_pool_wait_ms")
.. ]]]
Default: ``0``
.. [[[end]]]
How long a query waits for a connection to be returned when :ref:`setting_max_open_connections` has been reached and every pooled connection is in use, before opening one more connection anyway.
.. _setting_pool_read_connections:
pool_read_connections
~~~~~~~~~~~~~~~~~~~~~
.. [[[cog
setting_default(cog, "pool_read_connections")
.. ]]]
Default: ``on``
.. [[[end]]]
Reuse read connections between queries. Turn this off to open, prepare and close a new connection for every read query.
.. _setting_allow_facet:
allow_facet