mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-25 18:40:06 -04:00
videos: deflake event-loop-blocking search test (#699)
test_search_videos_does_not_block_event_loop asserted a wall-clock threshold (echo < 0.4s), which fails on loaded CI machines from scheduler noise alone. Replace the timing with ordering: the mocked search parks its thread on an Event, the echo is sent only once the handler is provably inside the query, and the test asserts the echo completes while the query is still held open. Events are only waited on off the event loop, so a regression fails the ordering assertion instead of deadlocking. Verified both directions: passes repeatedly against the real endpoint, and fails when the endpoint is changed to run the search on the loop. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
d5b933920a
commit
9bc067886f
1 changed files with 27 additions and 15 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import asyncio
|
||||
import time
|
||||
import threading
|
||||
from unittest import mock
|
||||
import json
|
||||
from http import HTTPStatus
|
||||
|
|
@ -598,28 +598,40 @@ async def test_search_videos_does_not_block_event_loop(async_client):
|
|||
"""A slow video search must not stall other requests on the same Sanic worker.
|
||||
|
||||
The search runs a synchronous SQLite query; on a large library it takes seconds cold. If it
|
||||
runs on the event loop, every other request on the worker waits for it."""
|
||||
# The first request pays the test app's startup cost; keep that out of the measurement.
|
||||
runs on the event loop, every other request on the worker waits for it.
|
||||
|
||||
The search is held open with an Event and the test asserts the echo completes while the search
|
||||
is still inside the query, rather than timing the echo against a wall-clock threshold, so the
|
||||
test cannot flake on a loaded CI machine. The events are only ever waited on off the event
|
||||
loop, so a regression (query on the loop) cannot deadlock the test; it fails the ordering
|
||||
assertion instead."""
|
||||
await async_client.get('/api/echo')
|
||||
|
||||
entered = threading.Event() # the handler has reached the search query.
|
||||
release = threading.Event() # the test is done measuring; let the query return.
|
||||
finished = threading.Event() # the search query has returned.
|
||||
|
||||
def slow_search(*_, **__):
|
||||
time.sleep(0.5) # a synchronous, blocking query
|
||||
entered.set()
|
||||
release.wait(timeout=30)
|
||||
finished.set()
|
||||
return [], 0
|
||||
|
||||
async def search():
|
||||
return await async_client.post('/api/videos/search', content=json.dumps({}))
|
||||
|
||||
async def echo():
|
||||
start = time.perf_counter()
|
||||
_, response = await async_client.get('/api/echo')
|
||||
return response, time.perf_counter() - start
|
||||
|
||||
with mock.patch('modules.videos.video.lib.search_videos', slow_search):
|
||||
(_, search_response), (echo_response, echo_elapsed) = await asyncio.gather(search(), echo())
|
||||
search_task = asyncio.create_task(
|
||||
async_client.post('/api/videos/search', content=json.dumps({})))
|
||||
try:
|
||||
# Do not send the echo until the search request is provably inside the query.
|
||||
assert await asyncio.to_thread(entered.wait, 30), 'the search request never started'
|
||||
_, echo_response = await asyncio.wait_for(async_client.get('/api/echo'), timeout=30)
|
||||
assert not finished.is_set(), \
|
||||
'echo only completed after the search query returned; the search blocked the event loop'
|
||||
finally:
|
||||
release.set()
|
||||
_, search_response = await search_task
|
||||
|
||||
assert search_response.status_code == HTTPStatus.OK
|
||||
assert echo_response.status_code == HTTPStatus.OK
|
||||
assert echo_elapsed < 0.4, f'echo waited {echo_elapsed:.2f}s behind a blocking search'
|
||||
assert search_response.status_code == HTTPStatus.OK
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue