Skip to content

Commit df0bd45

Browse files
committed
Fixed threading.Lock held across await in async DI resolver
1 parent cf86b41 commit df0bd45

File tree

2 files changed

+10
-66
lines changed

2 files changed

+10
-66
lines changed

fastopenapi/core/dependency_resolver.py

Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -472,35 +472,20 @@ async def _execute_dependency_function_async(
472472
cache_key = self._make_cache_key(dependency_func, request_data)
473473
request_cache = self._get_request_cache(request_data)
474474

475-
# First check (without lock)
476475
hit, value = self._try_get_cached(cache_key, request_cache)
477476
if hit:
478477
return value
479478

480-
# Get or create lock for this function
481-
func_id = id(dependency_func)
482-
with self._execution_locks_lock:
483-
if func_id not in self._execution_locks:
484-
self._execution_locks[func_id] = threading.Lock()
485-
func_lock = self._execution_locks[func_id]
486-
487-
# Synchronize execution per function
488-
with func_lock:
489-
# Second check (double-checked locking pattern)
490-
hit, value = self._try_get_cached(cache_key, request_cache)
491-
if hit:
492-
return value
493-
494-
# Guard against circular dependencies
495-
with self._resolving_guard(request_cache, dependency_func, param_name):
496-
sub_dependencies = await self._resolve_sub_dependencies_async(
497-
dependency_func, request_data, security_scopes
498-
)
499-
result = await self._call_dependency_async(
500-
dependency_func, sub_dependencies or {}, request_data
501-
)
502-
self._cache_result(cache_key, result, request_cache)
503-
return result
479+
# Guard against circular dependencies
480+
with self._resolving_guard(request_cache, dependency_func, param_name):
481+
sub_dependencies = await self._resolve_sub_dependencies_async(
482+
dependency_func, request_data, security_scopes
483+
)
484+
result = await self._call_dependency_async(
485+
dependency_func, sub_dependencies or {}, request_data
486+
)
487+
self._cache_result(cache_key, result, request_cache)
488+
return result
504489

505490
async def _call_dependency_async(
506491
self,

tests/core/test_dependency_resolver.py

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1411,47 +1411,6 @@ def endpoint(s: str = Depends(sync_dep), a: str = Depends(async_dep)):
14111411
)
14121412
assert result == {"s": "sync", "a": "async"}
14131413

1414-
@pytest.mark.asyncio
1415-
async def test_async_double_checked_locking_second_check_hit(self):
1416-
"""Test second cache check (inside lock) finds value and returns it"""
1417-
1418-
async def test_dep():
1419-
return "should_not_execute"
1420-
1421-
# Initialize cache manually
1422-
with self.resolver._request_cache_lock:
1423-
self.resolver._request_cache[self.request_data] = {
1424-
"resolved": {},
1425-
"resolving": set(),
1426-
}
1427-
1428-
self.resolver._make_cache_key(test_dep, self.request_data)
1429-
self.resolver._get_request_cache(self.request_data)
1430-
1431-
# Mock _try_get_cached to return miss first, then hit
1432-
call_count = {"count": 0}
1433-
1434-
def mock_try_get_cached(key, cache):
1435-
call_count["count"] += 1
1436-
if call_count["count"] == 1:
1437-
# First check (before lock) - miss
1438-
return (False, None)
1439-
else:
1440-
# Second check (inside lock) - HIT
1441-
return (True, "from_second_check")
1442-
1443-
with patch.object(
1444-
self.resolver, "_try_get_cached", side_effect=mock_try_get_cached
1445-
):
1446-
# Call the internal method directly to avoid outer cleanup
1447-
result = await self.resolver._execute_dependency_function_async(
1448-
test_dep, self.request_data, "dep"
1449-
)
1450-
1451-
# Should return value from second check without executing function
1452-
assert result == "from_second_check"
1453-
assert call_count["count"] == 2 # Called twice
1454-
14551414
@pytest.mark.asyncio
14561415
async def test_async_cleanup_when_cache_already_deleted(self):
14571416
"""Test finally cleanup when request_data already removed from cache"""

0 commit comments

Comments
 (0)