Root cause: Frontend was firing multiple concurrent PUT requests when toggling MQTT, causing SQLite "database is locked" errors after 6-second timeout.

Fixes:
1. frontend/src/pages/SettingsPage.tsx - Added isSavingRef to prevent concurrent saves while a mutation is in progress
2. backend/app/services/mqtt_relay.py - Added 3-second timeout for MQTT connect to prevent blocking on unreachable brokers
This commit is contained in:
maziggy
2026-01-19 13:29:37 +01:00
committed by MartinNYHC
parent 01fb1c3b53
commit 5d6ba0f875
5 changed files with 98 additions and 72 deletions
+9 -3
View File
@@ -92,11 +92,17 @@ class MQTTRelayService:
self.client.tls_set(cert_reqs=ssl.CERT_NONE)
self.client.tls_insecure_set(True) # Allow self-signed certs
# Connect (non-blocking with loop_start)
self.client.connect_async(broker, port, keepalive=60)
# Run connect_async in thread pool with timeout to avoid blocking
# on unreachable brokers (connect_async does synchronous socket creation)
try:
await asyncio.wait_for(asyncio.to_thread(self.client.connect_async, broker, port, 60), timeout=3.0)
except TimeoutError:
logger.warning(f"MQTT relay connection to {broker}:{port} timed out")
return False
self.client.loop_start()
# Wait briefly for connection
# Wait briefly for connection callback
await asyncio.sleep(1.0)
if self.connected:
+15
View File
@@ -300,6 +300,7 @@ export function SettingsPage() {
// Ref for debounce timeout
const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const isSavingRef = useRef(false);
const isInitialLoadRef = useRef(true);
// Sync local state when settings load
@@ -324,6 +325,10 @@ export function SettingsPage() {
onError: (error: Error) => {
showToast(`Failed to save: ${error.message}`, 'error');
},
onSettled: () => {
// Reset saving flag when mutation completes (success or error)
isSavingRef.current = false;
},
});
// Debounced auto-save when localSettings change
@@ -369,6 +374,11 @@ export function SettingsPage() {
return;
}
// Don't queue more saves while one is in progress
if (isSavingRef.current) {
return;
}
// Clear existing timeout
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current);
@@ -376,6 +386,11 @@ export function SettingsPage() {
// Set new debounced save (500ms delay)
saveTimeoutRef.current = setTimeout(() => {
// Skip if a save is already in progress
if (isSavingRef.current) {
return;
}
isSavingRef.current = true;
// Only send the fields we manage on this page (exclude virtual_printer_* which are managed separately)
const settingsToSave: AppSettingsUpdate = {
auto_archive: localSettings.auto_archive,
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -23,7 +23,7 @@
<!-- Splash screens for iOS -->
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
<script type="module" crossorigin src="/assets/index-Yr6VeTyb.js"></script>
<script type="module" crossorigin src="/assets/index-BEswNysN.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Dzh7xD3q.css">
</head>
<body>
+5
View File
@@ -15,4 +15,9 @@ git add .
git commit -m "Updated Wiki"
git push
cd ../spoolbuddy-website
git add .
git commit -m "Updated website"
git push
cd ../bambuddy