queue_announcements.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. """Telling the outside world that jobs were added to the queue.
  2. Every path that creates queue jobs calls this once, after its commit: the print
  3. dialog, File Manager's "Add to queue", the webhook, a virtual printer, a
  4. pipeline run and an order's "dispatch remaining". The "job added" notification
  5. is one summary per printer target, so a bulk add is one message, not one per
  6. job, and a provider scoped to one printer hears only of that printer's jobs.
  7. The MQTT relay is a machine event and gets one per job.
  8. """
  9. from __future__ import annotations
  10. import logging
  11. from collections.abc import Sequence
  12. from sqlalchemy import select
  13. from sqlalchemy.ext.asyncio import AsyncSession
  14. from backend.app.models.archive import PrintArchive
  15. from backend.app.models.library import LibraryFile
  16. from backend.app.models.print_queue import PrintQueueItem
  17. from backend.app.models.printer import Printer
  18. logger = logging.getLogger(__name__)
  19. # Names listed in a summary of different files; the rest is "…".
  20. _NAMES_LISTED = 3
  21. def _job_name(filename: str | None, item_id: int) -> str:
  22. if not filename:
  23. return f"Job #{item_id}"
  24. return filename.replace(".gcode.3mf", "").replace(".3mf", "")
  25. def _summary(names: list[str]) -> str:
  26. distinct = list(dict.fromkeys(names))
  27. if len(distinct) == 1:
  28. return f"{distinct[0]} ×{len(names)}" if len(names) > 1 else distinct[0]
  29. listed = ", ".join(distinct[:_NAMES_LISTED]) + (", …" if len(distinct) > _NAMES_LISTED else "")
  30. return f"{len(names)} jobs ({listed})"
  31. def _target(printer_name: str | None, models: list[str]) -> str:
  32. if printer_name:
  33. return printer_name
  34. distinct = list(dict.fromkeys(models))
  35. return f"Any {' / '.join(distinct)}" if distinct else "Unassigned"
  36. async def announce_queue_jobs_added(db: AsyncSession, item_ids: Sequence[int]) -> None:
  37. """Send the "job added" notification and relay events for new queue jobs.
  38. Never raises: a queue add must not fail because a notification did.
  39. """
  40. if not item_ids:
  41. return
  42. try:
  43. await _announce(db, item_ids)
  44. except Exception as e:
  45. logger.warning("Could not announce new queue jobs: %s", e)
  46. async def _announce(db: AsyncSession, item_ids: Sequence[int]) -> None:
  47. try:
  48. rows = (
  49. await db.execute(
  50. select(
  51. PrintQueueItem.id,
  52. PrintQueueItem.printer_id,
  53. PrintQueueItem.target_model,
  54. Printer.name.label("printer_name"),
  55. PrintArchive.filename.label("archive_filename"),
  56. LibraryFile.filename.label("library_filename"),
  57. )
  58. .outerjoin(Printer, Printer.id == PrintQueueItem.printer_id)
  59. .outerjoin(PrintArchive, PrintArchive.id == PrintQueueItem.archive_id)
  60. .outerjoin(LibraryFile, LibraryFile.id == PrintQueueItem.library_file_id)
  61. .where(PrintQueueItem.id.in_(list(item_ids)))
  62. .order_by(PrintQueueItem.position, PrintQueueItem.id)
  63. )
  64. ).all()
  65. except Exception as e:
  66. logger.warning("Could not announce new queue jobs: %s", e)
  67. return
  68. from backend.app.services.mqtt_relay import mqtt_relay
  69. from backend.app.services.notification_service import notification_service
  70. groups: dict[int | None, list] = {}
  71. for row in rows:
  72. groups.setdefault(row.printer_id, []).append(row)
  73. try:
  74. await mqtt_relay.on_queue_job_added(
  75. job_id=row.id,
  76. filename=row.archive_filename or row.library_filename or "",
  77. printer_id=row.printer_id,
  78. printer_name=row.printer_name,
  79. )
  80. except Exception:
  81. pass # Don't fail queue add if MQTT fails
  82. for printer_id, group in groups.items():
  83. printer_name = group[0].printer_name
  84. try:
  85. await notification_service.on_queue_job_added(
  86. job_name=_summary([_job_name(r.archive_filename or r.library_filename, r.id) for r in group]),
  87. target=_target(printer_name, [r.target_model for r in group if r.target_model]),
  88. db=db,
  89. printer_id=printer_id,
  90. printer_name=printer_name,
  91. )
  92. except Exception as e:
  93. logger.warning("Queue job added notification failed: %s", e)
  94. # Its log write commits on this session: a failed one must not leave
  95. # the request's session unusable after the job was committed.
  96. try:
  97. await db.rollback()
  98. except Exception:
  99. pass