Fix deadlock in spatial_scene — lock re-entrancy

observe() held self._lock, called _check_anomaly, which called
get_usual_direction, which tried to acquire self._lock again → deadlock.
Split into _usual_direction_unlocked (no lock) for internal use.

This caused /scene and all other API endpoints to hang after the first
sound classification with spatial data.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alex
2026-04-12 21:28:36 -05:00
parent 9f9796ddb6
commit 8caa9ee57e

View File

@@ -168,8 +168,8 @@ class SpatialScene:
if now - self._last_anomaly.get(category, 0) < 30.0:
return None
# Find the usual direction for this category
usual_angle = self.get_usual_direction(category)
# Find the usual direction for this category (already holding lock)
usual_angle = self._usual_direction_unlocked(category)
if usual_angle is None:
return None
@@ -192,14 +192,12 @@ class SpatialScene:
return None
def get_usual_direction(self, category: str) -> Optional[float]:
"""Get the most common direction for a sound category (weighted average)."""
with self._lock:
def _usual_direction_unlocked(self, category: str) -> Optional[float]:
"""Get the most common direction for a category. Caller must hold self._lock."""
bins = self.scene_map.get(category)
if not bins:
return None
# Weighted circular mean
total_weight = sum(bins.values())
if total_weight == 0:
return None
@@ -211,15 +209,19 @@ class SpatialScene:
sin_sum += count * math.sin(angle_rad)
cos_sum += count * math.cos(angle_rad)
mean_angle = math.degrees(math.atan2(sin_sum, cos_sum)) % 360
return mean_angle
return math.degrees(math.atan2(sin_sum, cos_sum)) % 360
def get_usual_direction(self, category: str) -> Optional[float]:
"""Get the most common direction for a sound category (thread-safe)."""
with self._lock:
return self._usual_direction_unlocked(category)
def get_scene_summary(self) -> dict:
"""Get a summary of the learned spatial scene."""
with self._lock:
summary = {}
for category in sorted(self.scene_map.keys()):
usual = self.get_usual_direction(category)
usual = self._usual_direction_unlocked(category)
total = self.category_totals[category]
if usual is not None:
summary[category] = {