mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 22:35:08 +00:00
fix(core): font zip cache, monotonic timers, resolver back-off, and other core/common fixes (#654)
* fix(core): font zip cache, monotonic timers, resolver back-off, and other core/common fixes - font_manager: a .zip font URL is served as its extracted font after a restart (the cached-file check returned the archive first); downloads use requests with a 30s timeout into a temp file + os.replace. - api_helper / sync_manager: rate-limit and heartbeat/leader timeouts use time.monotonic(); last_request_time and the status file's ts stay wall-clock. set_on_new_cycle docstring no longer claims core uses it. - logo_helper: the placeholder uses the same scaled box as a real logo. - permission_utils: one _sudo_bash_candidates() helper (with the sudoers exact-argv rationale) shared by sudo_remove_directory, which now retries the next bash path on a sudo refusal, and install_requirements_file. - dynamic_team_resolver: failed/empty fetch backs off 5 min; duplicate INFO log and contradictory docstring example fixed. - element_style: scale default looked up through element aliases. - background_data_service: cache-hit callback runs outside the lock. - config_arrays: union-aware type check (["array","null"]); stale dotToNested() reference removed. - auto_update_setup: non-dict auto_update reads as off; temp result file unlinked when the write fails. - exceptions: constructors copy the caller's context dict. - logging_config: StructuredFormatter json.dumps(default=str). - error_aggregator: removed unused export_path/export_to_file/_auto_export. - Docstrings: validate_file_upload max_size_mb, raise_on_errors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sync): retry the status-file rename like the other atomic writers On Windows os.replace can fail with "Access is denied" while a scanner briefly holds the target open; config_manager_atomic._replace already retries that (and re-raises at once on other platforms). The sync status writer called os.replace directly, which made test_concurrent_writers_each_use_their_own_temp_file flaky on Windows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+3
-40
@@ -6,7 +6,8 @@ for the LEDMatrix system. Enables automatic bug detection by tracking
|
||||
error frequency, patterns, and context.
|
||||
|
||||
This is a local-only implementation with no external dependencies.
|
||||
Errors are stored in memory with optional JSON export.
|
||||
Errors are stored in memory; ErrorSnapshotPublisher shares a summary with the
|
||||
web process through the cache.
|
||||
"""
|
||||
|
||||
import math
|
||||
@@ -18,7 +19,6 @@ import uuid
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Any, Callable, Tuple
|
||||
import logging
|
||||
|
||||
@@ -105,7 +105,6 @@ class ErrorAggregator:
|
||||
max_records: int = 1000,
|
||||
pattern_threshold: int = 5,
|
||||
pattern_window_minutes: int = 60,
|
||||
export_path: Optional[Path] = None
|
||||
):
|
||||
"""
|
||||
Initialize the error aggregator.
|
||||
@@ -114,20 +113,18 @@ class ErrorAggregator:
|
||||
max_records: Maximum number of error records to keep in memory
|
||||
pattern_threshold: Number of occurrences to detect a pattern
|
||||
pattern_window_minutes: Time window for pattern detection
|
||||
export_path: Optional path for JSON export (auto-export on pattern detection)
|
||||
"""
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.max_records = max_records
|
||||
self.pattern_threshold = pattern_threshold
|
||||
self.pattern_window = timedelta(minutes=pattern_window_minutes)
|
||||
self.export_path = export_path
|
||||
|
||||
self._records: List[ErrorRecord] = []
|
||||
self._error_counts: Dict[str, int] = defaultdict(int)
|
||||
self._plugin_error_counts: Dict[str, Dict[str, int]] = defaultdict(lambda: defaultdict(int))
|
||||
self._patterns: Dict[str, ErrorPattern] = {}
|
||||
self._pattern_callbacks: List[Callable[[ErrorPattern], None]] = []
|
||||
self._lock = threading.RLock() # RLock allows nested acquisition for export_to_file
|
||||
self._lock = threading.RLock() # RLock: build_snapshot and pattern callbacks re-enter
|
||||
|
||||
# Track session start for relative timing
|
||||
self._session_start = datetime.now()
|
||||
@@ -248,10 +245,6 @@ class ErrorAggregator:
|
||||
callback(pattern)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Pattern callback failed: {e}")
|
||||
|
||||
# Auto-export if path configured
|
||||
if self.export_path:
|
||||
self._auto_export()
|
||||
else:
|
||||
# Update existing pattern
|
||||
self._patterns[pattern_key].count = count
|
||||
@@ -424,33 +417,6 @@ class ErrorAggregator:
|
||||
# turned into a string here rather than failing the write.
|
||||
return json.loads(json.dumps(summary, default=str))
|
||||
|
||||
def export_to_file(self, filepath: Path) -> None:
|
||||
"""
|
||||
Export error data to JSON file.
|
||||
|
||||
Args:
|
||||
filepath: Path to export file
|
||||
"""
|
||||
with self._lock:
|
||||
data = {
|
||||
"exported_at": datetime.now().isoformat(),
|
||||
"summary": self.get_error_summary(),
|
||||
"all_records": [r.to_dict() for r in self._records]
|
||||
}
|
||||
filepath.parent.mkdir(parents=True, exist_ok=True)
|
||||
filepath.write_text(json.dumps(data, indent=2))
|
||||
self.logger.info(f"Exported error data to {filepath}")
|
||||
|
||||
def _auto_export(self) -> None:
|
||||
"""Auto-export on pattern detection (if export_path configured)."""
|
||||
if self.export_path:
|
||||
try:
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filepath = self.export_path / f"errors_{timestamp}.json"
|
||||
self.export_to_file(filepath)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Auto-export failed: {e}")
|
||||
|
||||
|
||||
# Global singleton instance
|
||||
_error_aggregator: Optional[ErrorAggregator] = None
|
||||
@@ -461,7 +427,6 @@ def get_error_aggregator(
|
||||
max_records: int = 1000,
|
||||
pattern_threshold: int = 5,
|
||||
pattern_window_minutes: int = 60,
|
||||
export_path: Optional[Path] = None
|
||||
) -> ErrorAggregator:
|
||||
"""
|
||||
Get or create the global error aggregator instance.
|
||||
@@ -470,7 +435,6 @@ def get_error_aggregator(
|
||||
max_records: Maximum records to keep (only used on first call)
|
||||
pattern_threshold: Pattern detection threshold (only used on first call)
|
||||
pattern_window_minutes: Pattern detection window (only used on first call)
|
||||
export_path: Export path for auto-export (only used on first call)
|
||||
|
||||
Returns:
|
||||
The global ErrorAggregator instance
|
||||
@@ -483,7 +447,6 @@ def get_error_aggregator(
|
||||
max_records=max_records,
|
||||
pattern_threshold=pattern_threshold,
|
||||
pattern_window_minutes=pattern_window_minutes,
|
||||
export_path=export_path
|
||||
)
|
||||
return _error_aggregator
|
||||
|
||||
|
||||
Reference in New Issue
Block a user