Browse Source

Set dependency parallelization default opt in. (#639)

pull/13756/head
Fodor Zoltan 11 months ago
parent
commit
b123c5f2ef
  1. 11
      fastapi/applications.py
  2. 2
      fastapi/dependencies/models.py
  3. 23
      fastapi/dependencies/utils.py
  4. 33
      fastapi/param_functions.py
  5. 5
      fastapi/params.py
  6. 2
      tests/test_dependency_cache.py

11
fastapi/applications.py

@ -836,6 +836,16 @@ class FastAPI(Starlette):
""" """
), ),
] = None, ] = None,
depends_default_parallelizable: Annotated[
bool,
Doc(
"""
Default for whether dependencies are allowed to run in parallel when not
explicitly specified per dependency. Projects can opt in by setting this
to True when creating the application. The default is False.
"""
),
] = False,
**extra: Annotated[ **extra: Annotated[
Any, Any,
Doc( Doc(
@ -865,6 +875,7 @@ class FastAPI(Starlette):
self.servers = servers or [] self.servers = servers or []
self.separate_input_output_schemas = separate_input_output_schemas self.separate_input_output_schemas = separate_input_output_schemas
self.openapi_external_docs = openapi_external_docs self.openapi_external_docs = openapi_external_docs
self.depends_default_parallelizable = depends_default_parallelizable
self.extra = extra self.extra = extra
self.openapi_version: Annotated[ self.openapi_version: Annotated[
str, str,

2
fastapi/dependencies/models.py

@ -32,7 +32,7 @@ class Dependant:
use_cache: bool = True use_cache: bool = True
path: Optional[str] = None path: Optional[str] = None
cache_key: Tuple[Optional[Callable[..., Any]], Tuple[str, ...]] = field(init=False) cache_key: Tuple[Optional[Callable[..., Any]], Tuple[str, ...]] = field(init=False)
parallelizable: bool = True parallelizable: Optional[bool] = None
def __post_init__(self) -> None: def __post_init__(self) -> None:
self.cache_key = (self.call, tuple(sorted(set(self.security_scopes or [])))) self.cache_key = (self.call, tuple(sorted(set(self.security_scopes or []))))

23
fastapi/dependencies/utils.py

@ -281,7 +281,7 @@ def get_dependant(
name: Optional[str] = None, name: Optional[str] = None,
security_scopes: Optional[List[str]] = None, security_scopes: Optional[List[str]] = None,
use_cache: bool = True, use_cache: bool = True,
parallelizable: bool = True, parallelizable: Optional[bool] = None,
) -> Dependant: ) -> Dependant:
path_param_names = get_path_param_names(path) path_param_names = get_path_param_names(path)
endpoint_signature = get_typed_signature(call) endpoint_signature = get_typed_signature(call)
@ -589,13 +589,19 @@ class DependencySolveException(Exception):
self.errors = errors self.errors = errors
def is_context_sensitive(dependant: Dependant) -> bool: def is_context_sensitive(dependant: Dependant, *, default_parallelizable: bool) -> bool:
if dependant.parallelizable is False or ( effective_parallelizable = (
default_parallelizable if dependant.parallelizable is None else dependant.parallelizable
)
if effective_parallelizable is False or (
dependant.call is not None dependant.call is not None
and (is_gen_callable(dependant.call) or is_async_gen_callable(dependant.call)) and (is_gen_callable(dependant.call) or is_async_gen_callable(dependant.call))
): ):
return True return True
return any(is_context_sensitive(sub) for sub in dependant.dependencies) return any(
is_context_sensitive(sub, default_parallelizable=default_parallelizable)
for sub in dependant.dependencies
)
def is_context_with_background_task(dependant: Dependant) -> bool: def is_context_with_background_task(dependant: Dependant) -> bool:
@ -659,6 +665,7 @@ async def solve_dependencies(
call=call, call=call,
name=sub_dependant.name, name=sub_dependant.name,
security_scopes=sub_dependant.security_scopes, security_scopes=sub_dependant.security_scopes,
parallelizable=sub_dependant.parallelizable,
) )
def resolve_cached() -> Optional["Future[Any]"]: def resolve_cached() -> Optional["Future[Any]"]:
@ -750,10 +757,16 @@ async def solve_dependencies(
if name is not None: if name is not None:
values[name] = value values[name] = value
app = request.app # type: ignore[attr-defined]
default_parallelizable = bool(
getattr(app, "depends_default_parallelizable", False)
)
sequential_deps = [] sequential_deps = []
parallel_deps = [] parallel_deps = []
for sub in dependant.dependencies: for sub in dependant.dependencies:
if is_context_sensitive(sub): if is_context_sensitive(sub, default_parallelizable=default_parallelizable):
sequential_deps.append(sub) sequential_deps.append(sub)
else: else:
parallel_deps.append(sub) parallel_deps.append(sub)

33
fastapi/param_functions.py

@ -2244,6 +2244,17 @@ def Depends( # noqa: N802
""" """
), ),
] = True, ] = True,
parallelizable: Annotated[
Optional[bool],
Doc(
"""
Whether this dependency may be resolved in parallel.
If not provided, the application-wide default will be used (configured with
`FastAPI(depends_default_parallelizable=...)`).
"""
),
] = None,
) -> Any: ) -> Any:
""" """
Declare a FastAPI dependency. Declare a FastAPI dependency.
@ -2274,7 +2285,9 @@ def Depends( # noqa: N802
return commons return commons
``` ```
""" """
return params.Depends(dependency=dependency, use_cache=use_cache) return params.Depends(
dependency=dependency, use_cache=use_cache, parallelizable=parallelizable
)
def Security( # noqa: N802 def Security( # noqa: N802
@ -2321,6 +2334,17 @@ def Security( # noqa: N802
""" """
), ),
] = True, ] = True,
parallelizable: Annotated[
Optional[bool],
Doc(
"""
Whether this dependency may be resolved in parallel.
If not provided, the application-wide default will be used (configured with
`FastAPI(depends_default_parallelizable=...)`).
"""
),
] = None,
) -> Any: ) -> Any:
""" """
Declare a FastAPI Security dependency. Declare a FastAPI Security dependency.
@ -2357,4 +2381,9 @@ def Security( # noqa: N802
return [{"item_id": "Foo", "owner": current_user.username}] return [{"item_id": "Foo", "owner": current_user.username}]
``` ```
""" """
return params.Security(dependency=dependency, scopes=scopes, use_cache=use_cache) return params.Security(
dependency=dependency,
scopes=scopes,
use_cache=use_cache,
parallelizable=parallelizable,
)

5
fastapi/params.py

@ -767,10 +767,11 @@ class Depends:
dependency: Optional[Callable[..., Any]] = None, dependency: Optional[Callable[..., Any]] = None,
*, *,
use_cache: bool = True, use_cache: bool = True,
parallelizable: bool = True, parallelizable: Optional[bool] = None,
): ):
self.dependency = dependency self.dependency = dependency
self.use_cache = use_cache self.use_cache = use_cache
# None means: use application default at resolution time
self.parallelizable = parallelizable self.parallelizable = parallelizable
def __repr__(self) -> str: def __repr__(self) -> str:
@ -786,7 +787,7 @@ class Security(Depends):
*, *,
scopes: Optional[Sequence[str]] = None, scopes: Optional[Sequence[str]] = None,
use_cache: bool = True, use_cache: bool = True,
parallelizable: bool = True, parallelizable: Optional[bool] = None,
): ):
super().__init__( super().__init__(
dependency=dependency, use_cache=use_cache, parallelizable=parallelizable dependency=dependency, use_cache=use_cache, parallelizable=parallelizable

2
tests/test_dependency_cache.py

@ -29,7 +29,7 @@ async def get_sub_counter(
@app.get("/sub-counter-no-cache/") @app.get("/sub-counter-no-cache/")
async def get_sub_counter_no_cache( async def get_sub_counter_no_cache(
subcount: int = Depends(dep_counter), subcount: int = Depends(super_dep),
count: int = Depends(dep_counter, use_cache=False), count: int = Depends(dep_counter, use_cache=False),
): ):
return {"counter": count, "subcounter": subcount} return {"counter": count, "subcounter": subcount}

Loading…
Cancel
Save