mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 21:47:39 +02:00
fix(asyncio): track strong refs on orphan create_task sites
asyncio holds only a weak reference to tasks returned by ``create_task``. Fire-and-forget callers that discard the return value let the event loop GC the task before it finishes, logging ``Task was destroyed but it is pending!`` with no traceback. The #1648 support-bundle review surfaced 94 such warnings in 8 days of v0.2.4.5 -- the silently-vanished exceptions reach support bundles as opaque GC notices instead of actionable errors. New backend/app/core/tasks.py::spawn_background_task(coro, *, name=None) is the one place in the codebase that calls asyncio.create_task. It stores the task in a module-level set, attaches a done-callback that auto-removes on completion AND surfaces any uncaught exception via the logger with the originating traceback, and accepts name= so a leak source is traceable through /tracebacks and the log line. Cancelled tasks don't log (a shutting-down service is not an error). Migrated the 16 truly-orphan create_task call sites to the helper: main.py (8): reconcile-stale, cooldown-poweroff, energy calc, smart-plug, maintenance-check, photo-then-notify, layer-timelapse, scan-timelapse, print-scheduler, notify-no-archive (the last one was hand-rolling the same pattern with task + no-op done_callback) printers.py:3123 apply-pa-after-refresh print_queue.py:1034 queue cooldown-poweroff firmware_update.py:261 firmware upload archive.py:1514 timelapse mp4 convert print_scheduler.py:2199 watchdog print-start library.py:1614 STL backfill smart_plugs.py:259 tasmota scan discovery.py:159 subnet scan smart_plug_manager.py x3 plug auto-off-pending background_dispatch.py x2 (lambda-wrapped inside loop.call_soon_threadsafe) upload progress Sites that already kept strong refs are unchanged: self._tasks.append(asyncio.create_task(...)) -- VP manager, tcp_proxy, mqtt_server self._x_task = asyncio.create_task(...) on service instances -- mqtt_bridge, obico_detection, github_backup, archive_purge, local_backup, library_trash, discovery service Locally assigned + awaited/gathered -- tcp_proxy bidirectional pumps, camera_fanout, slice_dispatch, slicer_api progress_task, manager._finish_release_task, main.py module-level cleanup loops
This commit is contained in:
@@ -12,6 +12,7 @@ from pydantic import BaseModel
|
||||
|
||||
from backend.app.core.auth import RequirePermissionIfAuthEnabled
|
||||
from backend.app.core.permissions import Permission
|
||||
from backend.app.core.tasks import spawn_background_task
|
||||
from backend.app.models.user import User
|
||||
from backend.app.services.discovery import (
|
||||
discovery_service,
|
||||
@@ -154,9 +155,10 @@ async def start_subnet_scan(
|
||||
request: Subnet to scan in CIDR notation (e.g., "192.168.1.0/24")
|
||||
"""
|
||||
# Start scan in background
|
||||
import asyncio
|
||||
|
||||
asyncio.create_task(subnet_scanner.scan_subnet(request.subnet, request.timeout))
|
||||
spawn_background_task(
|
||||
subnet_scanner.scan_subnet(request.subnet, request.timeout),
|
||||
name=f"subnet-scan-{request.subnet}",
|
||||
)
|
||||
|
||||
# Return immediate status
|
||||
scanned, total = subnet_scanner.progress
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""API routes for File Manager (Library) functionality."""
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import binascii
|
||||
import contextlib
|
||||
@@ -30,6 +29,7 @@ from backend.app.core.auth import (
|
||||
from backend.app.core.config import settings as app_settings
|
||||
from backend.app.core.database import async_session, get_db
|
||||
from backend.app.core.permissions import Permission
|
||||
from backend.app.core.tasks import spawn_background_task
|
||||
from backend.app.models.archive import PrintArchive
|
||||
from backend.app.models.library import LibraryFile, LibraryFolder
|
||||
from backend.app.models.print_queue import PrintQueueItem
|
||||
@@ -1611,7 +1611,7 @@ async def scan_external_folder(
|
||||
# folder_cache.values() covers the root + every pre-existing subfolder
|
||||
# + every subfolder created during this scan. all_folder_ids on its own
|
||||
# would miss the newly-created ones (it's snapshotted before the walk).
|
||||
asyncio.create_task(
|
||||
spawn_background_task(
|
||||
_backfill_external_stl_thumbnails(list(set(folder_cache.values()))),
|
||||
name=f"stl-backfill-folder-{folder_id}",
|
||||
)
|
||||
|
||||
@@ -16,6 +16,7 @@ from backend.app.core.auth import RequirePermissionIfAuthEnabled, require_owners
|
||||
from backend.app.core.config import settings
|
||||
from backend.app.core.database import get_db
|
||||
from backend.app.core.permissions import Permission
|
||||
from backend.app.core.tasks import spawn_background_task
|
||||
from backend.app.models.archive import PrintArchive
|
||||
from backend.app.models.library import LibraryFile
|
||||
from backend.app.models.print_batch import PrintBatch
|
||||
@@ -961,7 +962,6 @@ async def stop_queue_item(
|
||||
_: User | None = RequirePermissionIfAuthEnabled(Permission.QUEUE_UPDATE_ALL),
|
||||
):
|
||||
"""Stop an actively printing queue item."""
|
||||
import asyncio
|
||||
|
||||
from backend.app.models.smart_plug import SmartPlug
|
||||
from backend.app.services.printer_manager import printer_manager
|
||||
@@ -1031,7 +1031,7 @@ async def stop_queue_item(
|
||||
logger.info("Auto-off: Powering off printer %s", printer_id)
|
||||
await tasmota_service.turn_off(plug)
|
||||
|
||||
asyncio.create_task(cooldown_and_poweroff())
|
||||
spawn_background_task(cooldown_and_poweroff(), name=f"queue-cooldown-poweroff-{printer_id}")
|
||||
|
||||
return {"message": "Print stopped" if stop_sent else "Queue item cancelled (printer was offline)"}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from backend.app.core.auth import RequireCameraStreamTokenIfAuthEnabled, Require
|
||||
from backend.app.core.config import settings
|
||||
from backend.app.core.database import get_db
|
||||
from backend.app.core.permissions import Permission
|
||||
from backend.app.core.tasks import spawn_background_task
|
||||
from backend.app.models.ams_label import AmsLabel
|
||||
from backend.app.models.printer import Printer
|
||||
from backend.app.models.slot_preset import SlotPresetMapping
|
||||
@@ -3120,7 +3121,10 @@ async def refresh_ams_slot(
|
||||
raise HTTPException(400, message)
|
||||
|
||||
# Apply PA profile after delay (RFID re-read takes a few seconds)
|
||||
asyncio.create_task(_apply_pa_after_refresh(printer_id, ams_id, slot_id))
|
||||
spawn_background_task(
|
||||
_apply_pa_after_refresh(printer_id, ams_id, slot_id),
|
||||
name=f"apply-pa-after-refresh-{printer_id}-{ams_id}-{slot_id}",
|
||||
)
|
||||
|
||||
return {"success": True, "message": message}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from backend.app.api.routes.settings import get_setting
|
||||
from backend.app.core.auth import RequirePermissionIfAuthEnabled
|
||||
from backend.app.core.database import get_db
|
||||
from backend.app.core.permissions import Permission
|
||||
from backend.app.core.tasks import spawn_background_task
|
||||
from backend.app.models.printer import Printer
|
||||
from backend.app.models.smart_plug import SmartPlug
|
||||
from backend.app.models.user import User
|
||||
@@ -249,14 +250,16 @@ async def start_tasmota_scan(
|
||||
|
||||
Auto-detects local network if no IP range provided.
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
# Auto-detect network
|
||||
from_ip, to_ip = get_local_network_range()
|
||||
timeout = request.timeout if request else 1.0
|
||||
|
||||
# Start scan in background
|
||||
asyncio.create_task(tasmota_scanner.scan_range(from_ip, to_ip, timeout))
|
||||
spawn_background_task(
|
||||
tasmota_scanner.scan_range(from_ip, to_ip, timeout),
|
||||
name="tasmota-scan",
|
||||
)
|
||||
|
||||
# Return immediate status
|
||||
scanned, total = tasmota_scanner.progress
|
||||
|
||||
Reference in New Issue
Block a user