diff --git a/datasette/database.py b/datasette/database.py index af1df0a8..dfca179c 100644 --- a/datasette/database.py +++ b/datasette/database.py @@ -47,6 +47,8 @@ class Database: # These are used when in non-threaded mode: self._read_connection = None self._write_connection = None + # This is used to track all file connections so they can be closed + self._all_file_connections = [] @property def cached_table_counts(self): @@ -91,9 +93,16 @@ class Database: assert not (write and not self.is_mutable) if write: qs = "" - return sqlite3.connect( + conn = sqlite3.connect( f"file:{self.path}{qs}", uri=True, check_same_thread=False ) + self._all_file_connections.append(conn) + return conn + + def close(self): + # Close all connections - useful to avoid running out of file handles in tests + for connection in self._all_file_connections: + connection.close() async def execute_write(self, sql, params=None, block=True): def _inner(conn): diff --git a/docs/internals.rst b/docs/internals.rst index c3892a7c..cc6de867 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -874,6 +874,13 @@ If your function raises an exception that exception will be propagated up to the If you specify ``block=False`` the method becomes fire-and-forget, queueing your function to be executed and then allowing your code after the call to ``.execute_write_fn()`` to continue running while the underlying thread waits for an opportunity to run your function. A UUID representing the queued task will be returned. Any exceptions in your code will be silently swallowed. +.. _database_close: + +db.close() +---------- + +Closes all of the open connections to file-backed databases. This is mainly intended to be used by large test suites, to avoid hitting limits on the number of open files. + .. _internals_database_introspection: Database introspection diff --git a/tests/fixtures.py b/tests/fixtures.py index 92a10da6..ba5f065e 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -1,5 +1,5 @@ from datasette.app import Datasette -from datasette.utils.sqlite import sqlite3, sqlite_version +from datasette.utils.sqlite import sqlite3 from datasette.utils.testing import TestClient import click import contextlib @@ -9,11 +9,9 @@ import os import pathlib import pytest import random -import sys import string import tempfile import textwrap -import time # This temp file is used by one of the plugin config tests @@ -167,7 +165,11 @@ def make_app_client( crossdb=crossdb, ) yield TestClient(ds) - os.remove(filepath) + # Close as many database connections as possible + # to try and avoid too many open files error + for db in ds.databases.values(): + if not db.is_memory: + db.close() @pytest.fixture(scope="session")