diff --git a/fasthtml/core.py b/fasthtml/core.py index 50658ec5..f734d97d 100644 --- a/fasthtml/core.py +++ b/fasthtml/core.py @@ -775,13 +775,12 @@ async def __call__(self, scope, receive, send) -> None: # %% ../nbs/api/00_core.ipynb class FtResponse: "Wrap an FT response with any Starlette `Response`" - def __init__(self, content, status_code:int=200, headers=None, cls=HTMLResponse, media_type:str|None=None, background: BackgroundTask | None = None): + def __init__(self, content, status_code:int=200, headers=None, cls=HTMLResponse, media_type:str|None=None): self.content,self.status_code,self.headers = content,status_code,headers - self.cls,self.media_type,self.background = cls,media_type,background + self.cls,self.media_type = cls,media_type def __response__(self, req): cts,httphdrs,tasks = _xt_cts(req, self.content) - if not tasks.tasks: tasks = self.background headers = {**(self.headers or {}), **httphdrs} return self.cls(cts, status_code=self.status_code, headers=headers, media_type=self.media_type, background=tasks) diff --git a/nbs/api/00_core.ipynb b/nbs/api/00_core.ipynb index 4d1af4b8..217039cf 100644 --- a/nbs/api/00_core.ipynb +++ b/nbs/api/00_core.ipynb @@ -131,7 +131,7 @@ { "data": { "text/plain": [ - "datetime.datetime(2025, 3, 20, 14, 0)" + "datetime.datetime(2025, 3, 16, 14, 0)" ] }, "execution_count": null, @@ -1231,7 +1231,7 @@ { "data": { "text/plain": [ - "'a2597a39-e99d-4460-90b9-2ca21e87bc8f'" + "'cc87253c-bfc1-4544-bbc0-58dd8d3291bc'" ] }, "execution_count": null, @@ -2578,13 +2578,13 @@ "name": "stdout", "output_type": "stream", "text": [ - "Set to 2025-03-20 09:00:21.844393\n" + "Set to 2025-03-16 04:41:09.011712\n" ] }, { "data": { "text/plain": [ - "'Session time: 2025-03-20 09:00:21.844393'" + "'Session time: 2025-03-16 04:41:09.011712'" ] }, "execution_count": null, @@ -3105,7 +3105,7 @@ { "data": { "text/plain": [ - "'Cookie was set at time 09:00:22.051066'" + "'Cookie was set at time 04:41:09.620176'" ] }, "execution_count": null, @@ -3236,13 +3236,12 @@ "#| export\n", "class FtResponse:\n", " \"Wrap an FT response with any Starlette `Response`\"\n", - " def __init__(self, content, status_code:int=200, headers=None, cls=HTMLResponse, media_type:str|None=None, background: BackgroundTask | None = None):\n", + " def __init__(self, content, status_code:int=200, headers=None, cls=HTMLResponse, media_type:str|None=None):\n", " self.content,self.status_code,self.headers = content,status_code,headers\n", - " self.cls,self.media_type,self.background = cls,media_type,background\n", + " self.cls,self.media_type = cls,media_type\n", "\n", " def __response__(self, req):\n", " cts,httphdrs,tasks = _xt_cts(req, self.content)\n", - " if not tasks.tasks: tasks = self.background\n", " headers = {**(self.headers or {}), **httphdrs}\n", " return self.cls(cts, status_code=self.status_code, headers=headers, media_type=self.media_type, background=tasks)" ] @@ -3267,88 +3266,6 @@ "assert 'Foo' in txt and '

bar

' in txt and '' in txt" ] }, - { - "cell_type": "markdown", - "id": "3f506103", - "metadata": {}, - "source": [ - "Test on a single background task:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4ea66093", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Starting slow task\n", - "Finished slow task\n" - ] - } - ], - "source": [ - "def my_slow_task():\n", - " print('Starting slow task') \n", - " time.sleep(3)\n", - " print('Finished slow task') \n", - "\n", - "@rt('/background')\n", - "def get():\n", - " return FtResponse(P('BG Task'), background=BackgroundTask(my_slow_task))\n", - "\n", - "r = cli.get('/background')\n", - "\n", - "test_eq(r.status_code, 200)\n" - ] - }, - { - "cell_type": "markdown", - "id": "881a88bb", - "metadata": {}, - "source": [ - "Test multiple background tasks:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1764a6aa", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Sleeping for 0s\n", - "Slept for 0s\n", - "Sleeping for 1s\n", - "Slept for 1s\n", - "Sleeping for 2s\n", - "Slept for 2s\n" - ] - } - ], - "source": [ - "def increment(amount):\n", - " print(f'Sleeping for {amount}s') \n", - " time.sleep(amount)\n", - " print(f'Slept for {amount}s') \n", - "\n", - "@rt('/backgrounds')\n", - "def get():\n", - " tasks = BackgroundTasks()\n", - " for i in range(3): tasks.add_task(increment, i)\n", - " return FtResponse(P('BG Tasks'), background=tasks)\n", - "\n", - "r = cli.get('/backgrounds')\n", - "\n", - "test_eq(r.status_code, 200)" - ] - }, { "cell_type": "code", "execution_count": null, diff --git a/nbs/explains/background_tasks.ipynb b/nbs/explains/background_tasks.ipynb deleted file mode 100644 index c16123ea..00000000 --- a/nbs/explains/background_tasks.ipynb +++ /dev/null @@ -1,288 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Background Tasks\n", - "\n", - "> Background tasks are functions run after handlers return a response." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Useful for operations where the users gets a response quickly but doesn't need to wait for the operation to finish. Typical scenarios include:\n", - "\n", - "- User setup in complex systems where you can inform the user and other people later in email that their account is complete\n", - "- Batch processes that can take a significant amount of time (bulk email or API calls)\n", - "- Any other process where the user can be notified later by email, websocket, webhook, or pop-up" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "::: {.callout-note}\n", - "Background tasks are an easy-to-use wrapper over Python's async and threading libraries. Used to improve user experience, background tasks often making apps feel faster to the end user.\n", - ":::" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## A simple background task example" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In this example attaching a task to FtResponse by assigning it via the background argument. When the page is visited, it will display 'Simple Background Task Example' almost instantly, while in the terminal it will slowly count upward from 0." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "``` {.python filename=\"main.py\" code-line-numbers=\"true\"}\n", - "from fasthtml.common import *\n", - "from starlette.background import BackgroundTask\n", - "from time import time, sleep\n", - "\n", - "app, rt = fast_app()\n", - "\n", - "def counter(loops:int): # <1>\n", - " \"\"\"Slowly print integers to the terminal\"\"\"\n", - " for i in range(loops):\n", - " print(i)\n", - " sleep(i)\n", - "\n", - "@rt\n", - "def index():\n", - " task = BackgroundTask(counter, loops=5) # <2>\n", - " cts = Titled('Simple Background Task Example')\n", - " return FtResponse(cts, background=task) # <3>\n", - "\n", - "serve()\n", - "```\n", - "\n", - "1. `counter` is our task function. There is nothing special about it, although it is a good practice for its arguments to be serializable as JSON\n", - "2. We use `starlette.background.BackgroundTask` to turn `counter()` into a background task\n", - "3. `FtResponse` is called explicitly so we can attach `task`. Normally we don't need to call `FtResponse` explicitly; setting background tasks is the exception." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## A more realistic example" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's imagine that we are accessing a slow-to-process critical service. We don't want our users to have to wait. While we could set up SSE to notify on completion, instead we decide to periodically check to see if the status of their record has changed." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Simulated Slow API Service" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "First, create a very simple slow timestamp API. All it does is stall requests for a few seconds before returning JSON containing timestamps." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "```python\n", - "# slow_api.py\n", - "from fasthtml.common import *\n", - "from time import sleep, time\n", - "\n", - "app, rt = fast_app()\n", - "\n", - "@rt('/slow/{ts}')\n", - "def slow(ts: int):\n", - " sleep(3) # <1>\n", - " return dict(request_time=ts, response_time=int(time())) # <2>\n", - "\n", - "serve(port=8123)\n", - "```\n", - "\n", - "1. This represents slow processing.\n", - "2. Returns both the task's original timestamp and the time after completion" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Main FastHTML app" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now let's create a user-facing app that uses this API to fetch the timestamp from the glacially slow service." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "```python\n", - "# main.py\n", - "from fasthtml.common import *\n", - "from fastlite import *\n", - "from starlette.background import BackgroundTask\n", - "import time\n", - "import httpx\n", - "\n", - "app, rt = fast_app()\n", - "\n", - "db = database(':memory:')\n", - "\n", - "class TStamp: # <1>\n", - " request_time: int # <2> \n", - " response_time: int # <3>\n", - "\n", - "tstamps = db.create(TStamp, pk='request_time')\n", - "\n", - "def task_submit(request_time: int): # <4>\n", - " client = httpx.Client()\n", - " response = client.post(f'http://127.0.0.1:8123/slow/{request_time}') # <5>\n", - " tstamps.insert(**response.json()) # <6>\n", - "\n", - "@rt\n", - "def submit():\n", - " \"\"\"Route that initiates a background task and returns immediately.\"\"\"\n", - " request_time = int(time.time())\n", - " resp_content = P(f'Request submitted at: {request_time}') \n", - " task = BackgroundTask(task_submit, request_time=request_time) # <7>\n", - " return FtResponse(resp_content, background=task) # <8>\n", - "\n", - "@rt\n", - "def show_tstamps(): return Ul(*[Li(t) for t in tstamps()]) # <9> \n", - "\n", - "@rt\n", - "def index():\n", - " return Titled('Background Task Dashboard',\n", - " P(Button('Press to call slow service', # <10> \n", - " hx_post='/submit', hx_target='#res')),\n", - " H2('Responses from Tasks'),\n", - " P('', id='res'),\n", - " Div(Ul(*[Li(t) for t in tstamps()]),\n", - " hx_get=show_tstamps, hx_trigger='every 5s'), # <11>\n", - " )\n", - "\n", - "serve()\n", - "```\n", - "\n", - "1. Tracks when requests are sent and responses received\n", - "2. When the request was initiated\n", - "3. When the response was received\n", - "4. Task function calling slow service to be run in the background of a route handler. It is common but not necessary to prefix task functions with 'task_'\n", - "5. Call the slow API service (simulating a time-consuming operation)\n", - "6. Store both timestamps in our database\n", - "7. Create a background task by passing in the function to a BackgroundTask object, followed by any arguments.\n", - "8. In FtResponse, use the background keyword argument to set the task to be run after the HTTP response is generated.\n", - "9. Endpoint that displays all recorded timestamp pairs.\n", - "10. When this button is pressed, the 'submit' handler will respond instantly. The task_submit function will insert the slow API response into the db later. \n", - "11. Every 5 seconds get the tstamps stored in the DB." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "::: {.callout-tip}\n", - "\n", - "In the example above we use a synchronous background task function set in the `FtResponse` of a synchronous handler. However, we can also use asynchronous functions and handlers.\n", - "\n", - ":::" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Multiple background tasks in a handler" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "It is possible to add multiple background tasks to an FtResponse." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "::: {.callout-warning}\n", - "Multiple background tasks on a background task are executed in order. In the case a task raises an exception, following tasks will not get the opportunity to be executed.\n", - ":::" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "```python\n", - "from fasthtml.common import *\n", - "from starlette.background import BackgroundTasks\n", - "\n", - "@rt\n", - "async def signup(email, username):\n", - " tasks = BackgroundTasks()\n", - " tasks.add_task(send_welcome_email, to_address=email)\n", - " tasks.add_task(send_admin_notification, username=username)\n", - " cts = Titled('Signup successful!')\n", - " return FtResponse(cts, background=tasks)\n", - "\n", - "async def send_welcome_email(to_address):\n", - " ...\n", - "\n", - "async def send_admin_notification(username):\n", - " ...\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Background tasks are not distributed task queues\n", - "\n", - "While background tasks often provide the user with a faster experience, the server itself isn't accelerated. What that means is that processes are still happening, but are hidden. So if a server is struggling under the load of a lot of user activity, so long as it isn't an issue with open HTTP connections consuming resources, background tasks won't help with server load.\n", - "\n", - "This is where full-fledged distributed task queue libraries like Celery and Dramatiq come into play. At the cost of dramatically increased complexity over background tasks, they allow for the distribution of tasks over additional servers. These libraries also provide improved observability, retry mechanisms, and persistence in case of server shutdown.\n", - "\n", - "In our experience, most of the time background tasks like the ones built into FastHTML suffice. We recommend trying out background tasks before using a distributed task queue. If background task functions are written with JSON serializable arguments, then converting the functions to work in a distributed task queue is a straight-forward process." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "python3", - "language": "python", - "name": "python3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -}