Add /-/tasks introspection endpoint for supervised background tasks (#2892)

Co-authored-by: Simon Willison <swillison@gmail.com>
This commit is contained in:
Alex Garcia 2026-09-15 11:56:53 -07:00 committed by GitHub
commit 374b194ff5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 218 additions and 52 deletions

View file

@ -12,6 +12,17 @@ Unreleased
- Fixed incorrect counts when clicking **count all** on filtered table pages. The button now uses a new :ref:`POST count endpoint <TableCountView>`. (:issue:`2914`)
- Datasette now uses `httpx2 <https://httpx2.pydantic.dev/>`__, the Pydantic-maintained continuation of `httpx <https://www.python-httpx.org/>`__, in place of ``httpx``. The public API is the same, but responses returned by :ref:`internals_datasette_client` are now ``httpx2.Response`` objects rather than ``httpx.Response``. Plugins that use ``isinstance()`` checks against ``httpx.Response`` should be updated to use ``httpx2``. **Plugins that use httpx without explicitly depending on it** will need to add an explicit dependency or switch to `httpx2`.
Background tasks
~~~~~~~~~~~~~~~~
Datasette plugins can now use **background tasks** to run code independent of the Datasette request/response cycle.
- New :ref:`datasette_add_background_task` API: plugins register supervised, long-lived background work - typically from a ``startup`` hook - and these will be launched after every ``startup`` hook has run. Tasks are cancelled (with a five-second grace period) on shutdown.
- New ``/-/tasks`` JSON debug endpoint lists every supervised background task and its state, in the style of ``/-/threads``. See :ref:`JsonDataView_tasks`. It requires the ``permissions-debug`` permission.
- New :ref:`plugin_hook_shutdown` plugin hook, called during graceful shutdown (Ctrl-C, ``SIGTERM``) before background tasks are cancelled and before database connections are closed. It is not called on a hard kill (``SIGKILL``).
- Plugin ``asgi_wrapper`` middleware now always runs *after* startup has completed.
- If your plugin uses ``asgi_wrapper`` to start background tasks on the first incoming request, you should migrate to ``datasette.add_background_task()`` instead. `datasette-cron <https://datasette.io/plugins/datasette-cron>`__ and `datasette-enrichments <https://datasette.io/plugins/datasette-enrichments>`__ are being migrated to this pattern.
.. _v1_0_a39:
1.0a39 (2026-09-10)

View file

@ -1440,7 +1440,7 @@ All three paths call the same idempotent internal methods, so it is safe for mor
A coroutine function taking one positional argument, the ``Datasette`` instance. Core calls ``await func(datasette)``.
``name`` - string, optional
A name for the task, used to identify it in log messages. Defaults to ``func.__qualname__``. If the resulting name collides with an already-registered task, a ``-2``, ``-3``, ... suffix is appended.
A name for the task, used to identify it in the ``/-/tasks`` introspection endpoint (:ref:`JsonDataView_tasks`) and in log messages. Defaults to ``func.__qualname__``. If the resulting name collides with an already-registered task, a ``-2``, ``-3``, ... suffix is appended.
Registers a piece of supervised, long-lived background work — typically called from a :ref:`plugin_hook_startup` hook, though it can be called at any point after the instance exists, including from a request handler. Returns a :ref:`BackgroundTask <BackgroundTask>` handle.
@ -1470,8 +1470,6 @@ Core owns the task for the rest of the process's life:
- **A crash is logged, not swallowed.** If ``func`` raises anything other than ``asyncio.CancelledError``, the exception (with its traceback) is logged to the ``datasette.background_tasks`` logger and recorded on the handle's ``.exception``, and the task's ``.state`` becomes ``crashed``.
- **Cancellation is coordinated.** On shutdown, every task that is still running is cancelled and given a grace period to stop — see :ref:`datasette_lifecycle`.
Raw ``asyncio.create_task()`` inside a ``startup`` hook now works correctly, because ``startup`` hooks run on the serving event loop (see the admonition in :ref:`datasette_lifecycle`) — the bug that made this unsafe is fixed. But a task created that way is unsupervised: nothing keeps a reference to it, nothing logs its exceptions, and nothing cancels it on shutdown. Prefer ``add_background_task()`` for anything long-lived.
Launch matrix
~~~~~~~~~~~~~
@ -1515,12 +1513,14 @@ BackgroundTask objects
``.started_at`` - string or ``None``
ISO 8601 UTC timestamp of when the task was launched.
``.plugin`` - string or ``None``
Best-effort name of the plugin that registered the task, resolved from the module ``func`` was defined in. Used in log messages; ``None`` if it cannot be determined.
``.function`` - string
The callable's dotted module and qualified name, for example ``my_plugin.jobs.poll_for_updates``.
``.cancel()``
Cancel the task. If it has already launched, this cancels the underlying ``asyncio.Task````.state`` becomes ``cancelled`` once the cancellation is observed. If it has not launched yet, it is removed from the queue so it never runs.
This is also the shape of each entry returned by the ``/-/tasks`` JSON introspection endpoint — see :ref:`JsonDataView_tasks`.
.. _datasette_start_background_tasks:
await .start_background_tasks()

View file

@ -9,7 +9,7 @@ Each of these pages can be viewed in your browser. Add ``.json`` to the URL to g
JSON responses that return an object include an ``"ok": true`` key, consistent with the rest of the :ref:`JSON API <json_api>`.
The introspection endpoints documented on this page are covered by the :ref:`JSON API stability promise <json_api_stability>`, with the exception of the debug endpoints ``/-/threads`` and ``/-/actions``, whose shapes may change in future releases.
The introspection endpoints documented on this page are covered by the :ref:`JSON API stability promise <json_api_stability>`, with the exception of the debug endpoints ``/-/threads``, ``/-/tasks`` and ``/-/actions``, whose shapes may change in future releases.
.. _JsonDataView_metadata:
@ -278,6 +278,42 @@ Shows details of threads and ``asyncio`` tasks. This endpoint requires the ``per
]
}
.. _JsonDataView_tasks:
/-/tasks
--------
Shows the state of every supervised background task registered with :ref:`datasette.add_background_task() <datasette_add_background_task>`; see also :ref:`BackgroundTask <BackgroundTask>` for what each field below means, and :ref:`datasette_lifecycle` for when tasks are launched. This endpoint requires the ``permissions-debug`` permission, since a crashed task's ``exception`` field can reveal internals such as file paths or query text:
.. code-block:: json
{
"ok": true,
"tasks": [
{
"name": "my_plugin.poll_for_updates",
"state": "running",
"function": "my_plugin.poll_for_updates",
"started_at": "2026-07-30T12:00:00+00:00",
"exception": null
},
{
"name": "my_plugin.broken_task",
"state": "crashed",
"function": "my_plugin.broken_task",
"started_at": "2026-07-30T12:00:00+00:00",
"exception": "ValueError('something went wrong')"
}
],
"launched": true
}
Each entry's ``function`` identifies the callable by its dotted module and qualified name.
Each entry's ``state`` is one of ``registered`` (added but not yet launched), ``running``, ``completed``, ``crashed`` or ``cancelled``. ``exception`` is a one-line ``repr()`` of the exception for a ``crashed`` task, or ``null`` otherwise - the full traceback is written to the ``datasette.background_tasks`` logger instead, to keep this payload skimmable.
The top-level ``launched`` flag reports whether the instance has run its one-time background task launch (after ``startup`` hooks finish, or via lifespan/first-request/:ref:`start_background_tasks() <datasette_start_background_tasks>`). It distinguishes "no tasks have been registered" (``tasks`` is empty either way) from "tasks are registered but nothing has armed the launch yet" (``launched`` is ``false`` and every task's ``state`` is still ``registered``) - useful when debugging a host that never triggers Datasette's lifespan events.
.. _JsonDataView_actor:
/-/actor

View file

@ -48,6 +48,7 @@ Some JSON endpoints are **exempt** from this promise:
debug playground.
- Debug and support endpoints are documented so you can use them, but their
JSON shapes are not frozen: :ref:`/-/threads <JsonDataView_threads>`,
:ref:`/-/tasks <JsonDataView_tasks>`,
:ref:`/-/actions <JsonDataView_actions>`,
the :ref:`permission debug endpoints <PermissionsDebugView>`
(``/-/allowed``, ``/-/rules``, ``/-/check``) and the