fix(heatmap): interpolation thread is killed only on exit, logger for dandling thread

This commit is contained in:
2025-12-09 15:41:15 +01:00
committed by Jan Wyzula
parent 01114c72d6
commit d041ac251a
2 changed files with 23 additions and 13 deletions
+19 -10
View File
@@ -127,6 +127,12 @@ class _StepInterpolationWorker(QObject):
def __init__(self, parent: QObject | None = None):
super().__init__(parent=parent)
self._active_request: _InterpolationRequest | None = None
self._processing = False
@property
def is_processing(self) -> bool:
"""Return whether the worker is currently processing a request."""
return self._processing
@SafeSlot(object, int)
def process(self, request: _InterpolationRequest, data_version: int):
@@ -138,6 +144,7 @@ class _StepInterpolationWorker(QObject):
data_version(int): The data version for the request.
"""
self._active_request = request
self._processing = True
try:
image, transform = Heatmap.compute_step_scan_image(
x_data=np.asarray(request.x_data, dtype=float),
@@ -149,7 +156,9 @@ class _StepInterpolationWorker(QObject):
except Exception as exc: # pragma: no cover - defensive
logger.warning(f"Step-scan interpolation failed with: {exc}")
self.failed.emit(str(exc), data_version, request.scan_id)
self._processing = False
return
self._processing = False
self.finished.emit(image, transform, data_version, request.scan_id)
@@ -700,7 +709,7 @@ class Heatmap(ImageBase):
oversampling_factor=self._image_config.oversampling_factor,
)
if self._interpolation_thread is not None and self._interpolation_thread.isRunning():
if self._interpolation_worker is not None and self._interpolation_worker.is_processing:
self._pending_interpolation_request = request
return
@@ -739,16 +748,10 @@ class Heatmap(ImageBase):
self._apply_image_update(img, transform)
else:
logger.info("Discarding outdated interpolation result.")
if self._interpolation_thread is not None and self._interpolation_thread.isRunning():
self._interpolation_thread.quit()
self._interpolation_thread.wait()
self._maybe_start_pending_interpolation()
def _on_interpolation_failed(self, error: str, data_version: int, scan_id: str):
logger.warning(f"Interpolation failed for scan {scan_id} (version {data_version}): {error}")
if self._interpolation_thread is not None and self._interpolation_thread.isRunning():
self._interpolation_thread.quit()
self._interpolation_thread.wait()
self._maybe_start_pending_interpolation()
def _finish_interpolation_thread(self):
@@ -756,17 +759,21 @@ class Heatmap(ImageBase):
if self._interpolation_worker is not None:
try:
self.interpolation_requested.disconnect(self._interpolation_worker.process)
except (TypeError, RuntimeError):
# Defensive: disconnect may fail if already disconnected or during shutdown.
except (TypeError, RuntimeError) as ext:
logger.warning(f"Processing thread already disconnected: {ext}")
pass
self._interpolation_worker.deleteLater()
self._interpolation_worker = None
if self._interpolation_thread is not None:
if self._interpolation_thread.isRunning():
self._interpolation_thread.quit()
self._interpolation_thread.wait()
if not self._interpolation_thread.wait(3000): # 3s timeout
logger.error(
f"Interpolation thread of widget {self.gui_id} did not stop within timeout 3s; leaving it dangling."
)
self._interpolation_thread.deleteLater()
self._interpolation_thread = None
logger.info(f"Interpolation thread finished of widget {self.gui_id}")
def _maybe_start_pending_interpolation(self):
if self._pending_interpolation_request is None:
@@ -774,6 +781,8 @@ class Heatmap(ImageBase):
if self._pending_interpolation_request.scan_id != self.scan_id:
self._pending_interpolation_request = None
return
if self._interpolation_worker is not None and self._interpolation_worker.is_processing:
return
pending = self._pending_interpolation_request
self._pending_interpolation_request = None
+4 -3
View File
@@ -446,8 +446,9 @@ def test_pending_request_queueing_and_start(heatmap_widget):
metadata={},
info={"positions": [[0, 0], [1, 1], [2, 2], [3, 3]]},
)
heatmap_widget._interpolation_thread = mock.MagicMock()
heatmap_widget._interpolation_thread.isRunning.return_value = True
# Simulate an active worker processing a job so new requests are queued.
heatmap_widget._interpolation_worker = mock.MagicMock()
heatmap_widget._interpolation_worker.is_processing = True
with mock.patch.object(heatmap_widget, "_start_step_scan_interpolation") as start_mock:
heatmap_widget._request_step_scan_interpolation(
@@ -459,7 +460,7 @@ def test_pending_request_queueing_and_start(heatmap_widget):
assert heatmap_widget._pending_interpolation_request is not None
# Now simulate worker finished and thread cleaned up
heatmap_widget._interpolation_thread = None
heatmap_widget._interpolation_worker.is_processing = False
pending = heatmap_widget._pending_interpolation_request
heatmap_widget._pending_interpolation_request = pending
heatmap_widget._maybe_start_pending_interpolation()