spoolman_slot_sync.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. """Writing a Spoolman AMS sync's results to the local slot table.
  2. Shared by the automatic pass in ``on_ams_change`` and the Sync buttons in
  3. Settings. A sync reads the slot table, then awaits Spoolman for every tray, so
  4. by the time it writes, a slot may hold a spool the user assigned meanwhile.
  5. Writing every synced slot back, the unchanged ones included, overwrote that
  6. assignment with what the sync had read (SSA-3). Only a slot whose spool the
  7. sync changed is written, and only while it still holds what the sync read.
  8. What it did change is announced, its grace hold dropped, as a hand assignment
  9. does; the Sync buttons did neither (SSA-2).
  10. """
  11. import logging
  12. from collections.abc import Iterable, Mapping
  13. from typing import cast
  14. from sqlalchemy import delete, text, update
  15. from sqlalchemy.engine import CursorResult
  16. from sqlalchemy.ext.asyncio import AsyncSession
  17. from backend.app.core.websocket import ws_manager
  18. from backend.app.models.spoolman_slot_assignment import SpoolmanSlotAssignment
  19. from backend.app.services import slot_unlink_grace
  20. logger = logging.getLogger(__name__)
  21. Slot = tuple[int, int, int] # (printer_id, ams_id, tray_id)
  22. async def write_slot_results(
  23. db: AsyncSession,
  24. read: Mapping[Slot, int] | None,
  25. synced: Iterable[tuple[int, int, int, int]],
  26. emptied: Iterable[Slot],
  27. ) -> list[Slot]:
  28. """Write the slots the sync changed and commit; returns them.
  29. ``read`` is the slot table as the sync read it, ``synced`` the
  30. (printer_id, ams_id, tray_id, spoolman_spool_id) it found in the slots, and
  31. ``emptied`` the slots it found empty. Raises on a database error, as the
  32. callers report it.
  33. ``read`` None: the table could not be read, so there is nothing to compare
  34. against, and every slot is written as it was before the compare existed. A
  35. swap and a removal must not be dropped for want of a snapshot.
  36. """
  37. if read is None:
  38. return await _write_unconditionally(db, synced, emptied)
  39. changed: list[Slot] = []
  40. assigned: list[Slot] = []
  41. for printer_id, ams_id, tray_id, spool_id in synced:
  42. slot = (printer_id, ams_id, tray_id)
  43. before = read.get(slot)
  44. if before == spool_id:
  45. continue
  46. where = (
  47. SpoolmanSlotAssignment.printer_id == printer_id,
  48. SpoolmanSlotAssignment.ams_id == ams_id,
  49. SpoolmanSlotAssignment.tray_id == tray_id,
  50. )
  51. if before is None:
  52. statement = text(
  53. "INSERT INTO spoolman_slot_assignments (printer_id, ams_id, tray_id, spoolman_spool_id)"
  54. " VALUES (:printer_id, :ams_id, :tray_id, :spool_id)"
  55. " ON CONFLICT(printer_id, ams_id, tray_id) DO NOTHING"
  56. ).bindparams(printer_id=printer_id, ams_id=ams_id, tray_id=tray_id, spool_id=spool_id)
  57. else:
  58. statement = (
  59. update(SpoolmanSlotAssignment)
  60. .where(*where, SpoolmanSlotAssignment.spoolman_spool_id == before)
  61. .values(spoolman_spool_id=spool_id)
  62. .execution_options(synchronize_session=False)
  63. )
  64. if cast(CursorResult, await db.execute(statement)).rowcount:
  65. changed.append(slot)
  66. assigned.append(slot)
  67. for slot in emptied:
  68. # A slot the sync read without a spool matches no row below: there is
  69. # nothing of the sync's own to remove.
  70. before = read.get(slot)
  71. printer_id, ams_id, tray_id = slot
  72. result = cast(
  73. CursorResult,
  74. await db.execute(
  75. delete(SpoolmanSlotAssignment)
  76. .where(
  77. SpoolmanSlotAssignment.printer_id == printer_id,
  78. SpoolmanSlotAssignment.ams_id == ams_id,
  79. SpoolmanSlotAssignment.tray_id == tray_id,
  80. SpoolmanSlotAssignment.spoolman_spool_id == before,
  81. )
  82. .execution_options(synchronize_session=False)
  83. ),
  84. )
  85. if result.rowcount:
  86. changed.append(slot)
  87. await db.commit()
  88. await _announce(assigned, changed)
  89. return changed
  90. async def _write_unconditionally(
  91. db: AsyncSession, synced: Iterable[tuple[int, int, int, int]], emptied: Iterable[Slot]
  92. ) -> list[Slot]:
  93. assigned: list[Slot] = []
  94. for printer_id, ams_id, tray_id, spool_id in synced:
  95. await db.execute(
  96. text(
  97. "INSERT INTO spoolman_slot_assignments (printer_id, ams_id, tray_id, spoolman_spool_id)"
  98. " VALUES (:printer_id, :ams_id, :tray_id, :spool_id)"
  99. " ON CONFLICT(printer_id, ams_id, tray_id)"
  100. " DO UPDATE SET spoolman_spool_id = excluded.spoolman_spool_id"
  101. ).bindparams(printer_id=printer_id, ams_id=ams_id, tray_id=tray_id, spool_id=spool_id)
  102. )
  103. assigned.append((printer_id, ams_id, tray_id))
  104. removed: list[Slot] = []
  105. for printer_id, ams_id, tray_id in emptied:
  106. await db.execute(
  107. delete(SpoolmanSlotAssignment)
  108. .where(
  109. SpoolmanSlotAssignment.printer_id == printer_id,
  110. SpoolmanSlotAssignment.ams_id == ams_id,
  111. SpoolmanSlotAssignment.tray_id == tray_id,
  112. )
  113. .execution_options(synchronize_session=False)
  114. )
  115. removed.append((printer_id, ams_id, tray_id))
  116. await db.commit()
  117. changed = assigned + removed
  118. await _announce(assigned, changed)
  119. return changed
  120. async def _announce(assigned: list[Slot], changed: list[Slot]) -> None:
  121. for printer_id, ams_id, tray_id in assigned:
  122. slot_unlink_grace.forget_slot(printer_id, ams_id, tray_id)
  123. for printer_id, ams_id, tray_id in changed:
  124. await ws_manager.broadcast(
  125. {"type": "spool_assignment_changed", "printer_id": printer_id, "ams_id": ams_id, "tray_id": tray_id}
  126. )