mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-22 05:24:18 +02:00
Expose minimum_check_interval, reduce task pickup latency (#3500)
* perf: expose minimum_check_interval, reduce task pickup latency The Docket Worker polls for new tasks every minimum_check_interval (previously hardcoded to 250ms in pydocket). Expose this setting so users can tune it, default to 50ms, and override to 10ms in tests. This cuts average task pickup latency from ~125ms to ~5ms per task. * perf: reduce task test overhead and eliminate cross-test contamination - Expose minimum_check_interval setting (default 50ms, 10ms in tests) to reduce Docket Worker task pickup latency - Isolate fakeredis per test via unique memory:// URLs to prevent stale _async_blocking tasks from contaminating subsequent tests - Make client disconnect timeout configurable (default 5s, 1s in tests) - Add --durations=50 to CI for passive performance regression detection - Remove 15s timeout band-aids from task test conftest files - Add explicit @pytest.mark.timeout(10) to cancellation tests - Fix deprecated FastMCP.as_proxy() usage in test_task_proxy.py
This commit is contained in:
parent
cedf734f15
commit
32f1118a11
11 changed files with 51 additions and 34 deletions
1
.github/actions/run-pytest/action.yml
vendored
1
.github/actions/run-pytest/action.yml
vendored
|
|
@ -38,6 +38,7 @@ runs:
|
|||
uv run --no-sync pytest \
|
||||
--inline-snapshot=disable \
|
||||
--timeout=$TIMEOUT \
|
||||
--durations=50 \
|
||||
-m "$MARKER" \
|
||||
$PARALLEL_FLAGS \
|
||||
$EXTRA_FLAGS \
|
||||
|
|
|
|||
|
|
@ -336,6 +336,11 @@ class Client(
|
|||
elicitation_handler
|
||||
)
|
||||
|
||||
# Maximum time to wait for a clean disconnect before giving up.
|
||||
# Normally disconnects complete in <100ms; this is a safety net for
|
||||
# unresponsive servers.
|
||||
self._disconnect_timeout: float = fastmcp.settings.client_disconnect_timeout
|
||||
|
||||
# Session context management - see class docstring for detailed explanation
|
||||
self._session_state = ClientSessionState()
|
||||
|
||||
|
|
@ -511,7 +516,7 @@ class Client(
|
|||
# Use a timeout to prevent hanging during cleanup if the connection is in a bad
|
||||
# state (e.g., rate-limited). The MCP SDK's transport may try to terminate the
|
||||
# session which can hang if the server is unresponsive.
|
||||
with anyio.move_on_after(5):
|
||||
with anyio.move_on_after(self._disconnect_timeout):
|
||||
await self._disconnect()
|
||||
|
||||
async def _connect(self):
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ class LifespanMixin:
|
|||
"concurrency": settings.docket.concurrency,
|
||||
"redelivery_timeout": settings.docket.redelivery_timeout,
|
||||
"reconnection_delay": settings.docket.reconnection_delay,
|
||||
"minimum_check_interval": settings.docket.minimum_check_interval,
|
||||
}
|
||||
if settings.docket.worker_name:
|
||||
worker_kwargs["name"] = settings.docket.worker_name
|
||||
|
|
|
|||
|
|
@ -117,6 +117,21 @@ class DocketSettings(BaseSettings):
|
|||
),
|
||||
] = timedelta(seconds=5)
|
||||
|
||||
minimum_check_interval: Annotated[
|
||||
timedelta,
|
||||
Field(
|
||||
description=inspect.cleandoc(
|
||||
"""
|
||||
How frequently the worker polls for new tasks. Lower
|
||||
values reduce latency for task pickup at the cost of
|
||||
more CPU usage. The default of 50ms is a good balance;
|
||||
increase for high-volume production deployments where
|
||||
tasks are long-running.
|
||||
"""
|
||||
),
|
||||
),
|
||||
] = timedelta(milliseconds=50)
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""FastMCP settings."""
|
||||
|
|
@ -231,6 +246,13 @@ class Settings(BaseSettings):
|
|||
),
|
||||
] = None
|
||||
|
||||
client_disconnect_timeout: Annotated[
|
||||
float,
|
||||
Field(
|
||||
description="Maximum time to wait for a clean disconnect before giving up, in seconds.",
|
||||
),
|
||||
] = 5
|
||||
|
||||
# Transport settings
|
||||
transport: Literal["stdio", "http", "sse", "streamable-http"] = "stdio"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1 @@
|
|||
"""Configuration for client task tests.
|
||||
|
||||
Task tests require Docket infrastructure (Redis-backed task queue) which can
|
||||
take significant time to initialize, especially under parallel test execution.
|
||||
The default 5s timeout is too tight for these tests.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(items: list[pytest.Item]) -> None:
|
||||
"""Increase timeout for task tests that need Docket infrastructure."""
|
||||
for item in items:
|
||||
if not item.get_closest_marker("timeout"):
|
||||
item.add_marker(pytest.mark.timeout(15))
|
||||
"""Configuration for client task tests."""
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import asyncio
|
||||
import logging
|
||||
import secrets
|
||||
import socket
|
||||
import sys
|
||||
from collections.abc import Callable, Generator
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
|
@ -60,11 +62,20 @@ def isolate_settings_home(tmp_path: Path):
|
|||
|
||||
This prevents file locking issues when multiple tests share the same
|
||||
storage directory in settings.home / "oauth-proxy".
|
||||
|
||||
Also sets a fast Docket polling interval for tests — the default 50ms
|
||||
is fine for production but still adds ~25ms average pickup latency per
|
||||
task. 10ms makes task tests near-instant.
|
||||
"""
|
||||
test_home = tmp_path / "fastmcp-test-home"
|
||||
test_home.mkdir(exist_ok=True)
|
||||
|
||||
with temporary_settings(home=test_home):
|
||||
with temporary_settings(
|
||||
home=test_home,
|
||||
docket__minimum_check_interval=timedelta(milliseconds=10),
|
||||
docket__url=f"memory://{secrets.token_hex(4)}",
|
||||
client_disconnect_timeout=1,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1 @@
|
|||
"""Configuration for server task tests.
|
||||
|
||||
Task tests require Docket infrastructure (Redis-backed task queue) which can
|
||||
take significant time to initialize, especially under parallel test execution.
|
||||
The default 5s timeout is too tight for these tests.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(items: list[pytest.Item]) -> None:
|
||||
"""Increase timeout for task tests that need Docket infrastructure."""
|
||||
for item in items:
|
||||
if not item.get_closest_marker("timeout"):
|
||||
item.add_marker(pytest.mark.timeout(15))
|
||||
"""Configuration for server task tests."""
|
||||
|
|
|
|||
|
|
@ -6,10 +6,13 @@ components (tools, prompts, resources), and that explicit component-level
|
|||
settings properly override the server default.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.client import Client
|
||||
|
||||
|
||||
@pytest.mark.timeout(10)
|
||||
async def test_server_tasks_true_defaults_all_components():
|
||||
"""Server with tasks=True makes all components default to supporting tasks."""
|
||||
mcp = FastMCP("test", tasks=True)
|
||||
|
|
|
|||
|
|
@ -174,6 +174,7 @@ async def test_task_cancellation_workflow(endpoint_server):
|
|||
assert status.status == "cancelled"
|
||||
|
||||
|
||||
@pytest.mark.timeout(10)
|
||||
async def test_task_cancellation_interrupts_running_coroutine(endpoint_server):
|
||||
"""Task cancellation actually interrupts the running coroutine.
|
||||
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ class TestMountedToolTasks:
|
|||
status = await task.status()
|
||||
assert status.status == "completed"
|
||||
|
||||
@pytest.mark.timeout(10)
|
||||
async def test_mounted_tool_task_cancellation(self, parent_server):
|
||||
"""Can cancel a mounted tool task."""
|
||||
async with Client(parent_server) as client:
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ from mcp.types import TextContent, TextResourceContents
|
|||
from fastmcp import FastMCP
|
||||
from fastmcp.client import Client
|
||||
from fastmcp.client.transports import FastMCPTransport
|
||||
from fastmcp.server.providers.proxy import ProxyClient
|
||||
from fastmcp.server import create_proxy
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -60,7 +60,7 @@ def backend_server() -> FastMCP:
|
|||
@pytest.fixture
|
||||
def proxy_server(backend_server: FastMCP) -> FastMCP:
|
||||
"""Create a proxy server that forwards to the backend."""
|
||||
return FastMCP.as_proxy(ProxyClient(transport=FastMCPTransport(backend_server)))
|
||||
return create_proxy(FastMCPTransport(backend_server))
|
||||
|
||||
|
||||
class TestProxyToolsSyncExecution:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue