You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
GET /api/v2/videos/list is declared async def, but its entire body is synchronous
filesystem work executed directly on the event loop
(src/youtube_extension/backend/real_api_endpoints.py):
cache_dir.exists() — a blocking stat
cache_dir.glob("*_processed.json") — a blocking directory walk
one blocking open() + json.load()per cached video
There is no upper bound on the number of cache entries. The handler never awaits, so
from the moment the request is dispatched until the listing is built, the event loop
cannot run any other task: no other request is served, no heartbeat fires, no timeout
is honoured. The stall grows linearly with the number of processed videos, which grows
monotonically over the lifetime of the deployment.
Measured on a 2,000-entry cache with realistic transcript and analysis payloads, the
handler stalls the loop for ~176–215 ms in a single call.
Expected
The cache scan runs in a worker thread, matching the pattern already established across
this codebase (#1194, #1196, #1228, #1233, #1240, #1245, #1251): a module-level *_sync
helper invoked via await asyncio.to_thread(...).
Acceptance criteria
The blocking scan no longer executes on the event loop thread.
Response payload is unchanged: same 12 keys per entry, same newest-first ordering,
same silent-skip behaviour for corrupt entries, same empty-list result for a
missing cache directory, same [] on unexpected error.
A regression test asserts the scan runs off the loop thread and fails against the
pre-change source.
All existing tests in tests/unit/test_real_api_endpoints.py pass unmodified.
Out of scope
GET /api/v2/videos/{video_id} (get_video_analysis), which has the same blocking open() + json.load() defect. Separate issue.
clear_processing_cache, which calls blocking shutil.rmtree() + mkdir().
Separate issue.
Defect
GET /api/v2/videos/listis declaredasync def, but its entire body is synchronousfilesystem work executed directly on the event loop
(
src/youtube_extension/backend/real_api_endpoints.py):cache_dir.exists()— a blockingstatcache_dir.glob("*_processed.json")— a blocking directory walkopen()+json.load()per cached videoThere is no upper bound on the number of cache entries. The handler never awaits, so
from the moment the request is dispatched until the listing is built, the event loop
cannot run any other task: no other request is served, no heartbeat fires, no timeout
is honoured. The stall grows linearly with the number of processed videos, which grows
monotonically over the lifetime of the deployment.
Measured on a 2,000-entry cache with realistic transcript and analysis payloads, the
handler stalls the loop for ~176–215 ms in a single call.
Expected
The cache scan runs in a worker thread, matching the pattern already established across
this codebase (#1194, #1196, #1228, #1233, #1240, #1245, #1251): a module-level
*_synchelper invoked via
await asyncio.to_thread(...).Acceptance criteria
same silent-skip behaviour for corrupt entries, same empty-list result for a
missing cache directory, same
[]on unexpected error.pre-change source.
tests/unit/test_real_api_endpoints.pypass unmodified.Out of scope
GET /api/v2/videos/{video_id}(get_video_analysis), which has the same blockingopen()+json.load()defect. Separate issue.clear_processing_cache, which calls blockingshutil.rmtree()+mkdir().Separate issue.
real_video_processor.pycache handling, already claimed by open PR perf: offload cache-directory scan off the event loop (#1231) #1237.