diff --git a/README.md b/README.md index 655ef8e4e..3320a799c 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@
+
diff --git a/backend/app/services/print_scheduler.py b/backend/app/services/print_scheduler.py
index e60e7efa7..3b0ac0e35 100644
--- a/backend/app/services/print_scheduler.py
+++ b/backend/app/services/print_scheduler.py
@@ -5,7 +5,7 @@ import json
import logging
import xml.etree.ElementTree as ET
import zipfile
-from datetime import datetime
+from datetime import datetime, timedelta
from pathlib import Path
from sqlalchemy import func, select
@@ -76,6 +76,18 @@ class PrintScheduler:
if item.scheduled_time and item.scheduled_time > datetime.utcnow():
continue
+ # Safety: Skip stale items (older than 24 hours) to prevent phantom reprints
+ # This protects against items that got stuck in "pending" status due to
+ # crashes/restarts after the print already started
+ stale_threshold = timedelta(hours=24)
+ if item.created_at and datetime.utcnow() - item.created_at.replace(tzinfo=None) > stale_threshold:
+ logger.warning(f"Queue item {item.id} is stale (created {item.created_at}), marking as expired")
+ item.status = "expired"
+ item.error_message = "Queue item expired - older than 24 hours"
+ item.completed_at = datetime.utcnow()
+ await db.commit()
+ continue
+
# Skip items that require manual start
if item.manual_start:
continue
@@ -809,6 +821,28 @@ class PrintScheduler:
logger.error(f"Queue item {item.id}: Archive {item.archive_id} not found")
await self._power_off_if_needed(db, item)
return
+
+ # Safety: Check if this archive was printed recently (within 4 hours)
+ # This prevents phantom reprints if a queue item got stuck in "pending"
+ # after its print already started due to a crash/restart
+ if archive.status == "completed" and archive.completed_at:
+ completed_at = (
+ archive.completed_at.replace(tzinfo=None) if archive.completed_at.tzinfo else archive.completed_at
+ )
+ time_since_completed = datetime.utcnow() - completed_at
+ if time_since_completed < timedelta(hours=4):
+ logger.warning(
+ f"Queue item {item.id}: Archive {item.archive_id} was already printed "
+ f"{time_since_completed.total_seconds() / 3600:.1f} hours ago, skipping to prevent duplicate"
+ )
+ item.status = "skipped"
+ item.error_message = (
+ f"Archive was already printed {time_since_completed.total_seconds() / 3600:.1f} hours ago"
+ )
+ item.completed_at = datetime.utcnow()
+ await db.commit()
+ return
+
file_path = settings.base_dir / archive.file_path
filename = archive.filename
@@ -953,6 +987,17 @@ class PrintScheduler:
except json.JSONDecodeError:
logger.warning(f"Queue item {item.id}: Invalid AMS mapping JSON, ignoring")
+ # IMPORTANT: Set status to "printing" BEFORE sending the print command.
+ # This prevents phantom reprints if the backend crashes/restarts after the
+ # print command is sent but before the status update is committed.
+ # If we crash after this commit but before start_print(), the item will be
+ # in "printing" status without actually printing - but that's safer than
+ # accidentally reprinting the same file hours later.
+ item.status = "printing"
+ item.started_at = datetime.utcnow()
+ await db.commit()
+ logger.info(f"Queue item {item.id}: Status set to 'printing', sending print command...")
+
# Start the print with AMS mapping, plate_id and print options
started = printer_manager.start_print(
item.printer_id,
@@ -968,10 +1013,7 @@ class PrintScheduler:
)
if started:
- item.status = "printing"
- item.started_at = datetime.utcnow()
- await db.commit()
- logger.info(f"Queue item {item.id}: Print started - {filename}")
+ logger.info(f"Queue item {item.id}: Print started successfully - {filename}")
# Get estimated time for notification
estimated_time = None
@@ -1003,6 +1045,7 @@ class PrintScheduler:
except Exception:
pass # Don't fail if MQTT fails
else:
+ # Print command failed - revert status
item.status = "failed"
item.error_message = "Failed to send print command to printer"
item.completed_at = datetime.utcnow()