From 4fd8bb79af5f9abc4bce0a111b453f81fc080b1d Mon Sep 17 00:00:00 2001 From: saphid <4596216+saphid@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:28:45 +1000 Subject: [PATCH] Store: publish a search's completion and hand over its queue atomically A query arriving between the queue handover and the completion event could be queued with nobody to start it, leaving the source 'loading' forever. Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/test_apk_search.py | 22 ++++++++++++++++++++++ ui/apk_sources/search.py | 4 ++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/tests/test_apk_search.py b/tests/test_apk_search.py index ab9c2e7..ec520a9 100644 --- a/tests/test_apk_search.py +++ b/tests/test_apk_search.py @@ -95,6 +95,28 @@ class SearchTests(SettingsTest): finally: release.set() + def test_query_arriving_as_a_search_finishes_is_not_stranded(self): + mod = fake() + source = mod.sources()[0] + arrived = [] + + class Event(threading.Event): + def set(self): + if not arrived: # a request lands just as the first search completes + arrived.append(None) + t = threading.Thread(target=lambda: arrived.append(search._launch(mod, source, 'second', 50))) + t.start() + t.join(.3) # blocks on search._lock if completion is published atomically + super().set() + with patch.object(search, 'threading', types.SimpleNamespace(Event=Event, Thread=threading.Thread)): + search._launch(mod, source, 'first', 50) + for _ in range(200): + if len(arrived) == 2: + break + time.sleep(.01) + self.assertTrue(arrived[1]['event'].wait(2)) + self.assertEqual(arrived[1]['query'], ('second', 50)) + def test_set_enabled_does_not_hold_search_lock_in_source(self): free = [] def set_enabled(source_id, enabled): diff --git a/ui/apk_sources/search.py b/ui/apk_sources/search.py index 619be8f..c8fd6d0 100644 --- a/ui/apk_sources/search.py +++ b/ui/apk_sources/search.py @@ -249,11 +249,11 @@ def _start(key, task): task['error'] = str(e) task['limited'] = isinstance(e, SourceLimited) finally: - with _lock: + with _lock: # completion and queue handover are one step for _launch queued = _pending.pop(key, None) if queued: _running[key] = queued - task['event'].set() + task['event'].set() if queued: _start(key, queued) threading.Thread(target=run, daemon=True).start()