diff --git a/constitution.py b/constitution.py index d4ace9591f035d67721178a7c17e6053039800d1..7b7a61de87f7872ff9c66755200cf5103920eec6 100644 --- a/constitution.py +++ b/constitution.py @@ -1238,6 +1238,9 @@ async def pairwise_rank(n: int, compare_fn, progress_fn=None) -> list: results = await compare_fn(i, j) pairs.extend(results) compared.add(frozenset((i, j))) + # Resumed comparisons may be satisfied entirely from RocksDB without + # an I/O await. Yield explicitly so ranking never starves HTTP/SSE. + await asyncio.sleep(0) return results # --- Phase 1: spanning tree --- diff --git a/tests/test_pair_selection.py b/tests/test_pair_selection.py index 13040ef4a6789ef74e7449e8a39df3b67d1c2e8c..8a5614da2ba6062af961f2bb942af70fd7c69813 100644 --- a/tests/test_pair_selection.py +++ b/tests/test_pair_selection.py @@ -329,6 +329,30 @@ def test_progress_no_duplicate_spanning_steps(): assert spanning_steps == sorted(set(spanning_steps)) +def test_cached_comparisons_yield_to_other_event_loop_work(): + async def scenario(): + heartbeat_ticks = 0 + ranking_done = False + + async def cached_compare(i, j): + # Deliberately contains no await, matching a RocksDB cache hit. + return [(min(i, j), max(i, j), 2.0, 1.0)] + + async def heartbeat(): + nonlocal heartbeat_ticks + while not ranking_done: + heartbeat_ticks += 1 + await asyncio.sleep(0) + + heartbeat_task = asyncio.create_task(heartbeat()) + await pairwise_rank(20, cached_compare) + ranking_done = True + await heartbeat_task + return heartbeat_ticks + + assert run(scenario()) >= 19 + + # --------------------------------------------------------------------------- # rank_centrality accumulates (no overwrite bug) # ---------------------------------------------------------------------------