test_scheduler_snapshot_races.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. """An edit made while the scheduler's pass runs is not overwritten (Q2).
  2. ``check_queue`` loads the pending items once and works on that copy. A queue
  3. item edited in between (a mapping set by hand, a move to another printer) was
  4. overwritten when the pass computed and saved its AMS mapping, or dispatched
  5. to the printer it was on before. The edit lands where the pass waits on
  6. something; the previous-print check is such a place.
  7. """
  8. from unittest.mock import AsyncMock, patch
  9. import pytest
  10. from sqlalchemy import update
  11. from backend.app.models.print_queue import PrintQueueItem
  12. from backend.app.models.printer import Printer
  13. from backend.tests.unit.test_scheduler_busy_reasons_3018 import _scheduler, one_printer # noqa: F401
  14. async def _pinned_item(ctx, ams_mapping: str | None = None) -> int:
  15. item_id = await ctx.add_item()
  16. async with ctx.session_maker() as db:
  17. await db.execute(
  18. update(PrintQueueItem)
  19. .where(PrintQueueItem.id == item_id)
  20. .values(require_previous_success=True, ams_mapping=ams_mapping)
  21. )
  22. await db.commit()
  23. return item_id
  24. def _edit_during_the_pass(ctx, scheduler, **values):
  25. async def edit(_db, item, *_args, **_kwargs):
  26. async with ctx.session_maker() as other:
  27. await other.execute(update(PrintQueueItem).where(PrintQueueItem.id == item.id).values(**values))
  28. await other.commit()
  29. return True
  30. return patch.object(scheduler, "_check_previous_success", AsyncMock(side_effect=edit))
  31. @pytest.mark.asyncio
  32. async def test_a_mapping_set_meanwhile_is_kept(one_printer): # noqa: F811
  33. """The pass would clear the bogus all-unresolved mapping it loaded."""
  34. item_id = await _pinned_item(one_printer, ams_mapping="[-1]")
  35. async with _scheduler(one_printer, idle=True) as scheduler:
  36. with _edit_during_the_pass(one_printer, scheduler, ams_mapping="[2]"):
  37. await scheduler.check_queue()
  38. async with one_printer.session_maker() as db:
  39. assert (await db.get(PrintQueueItem, item_id)).ams_mapping == "[2]"
  40. @pytest.mark.asyncio
  41. async def test_an_item_moved_meanwhile_is_not_sent_to_its_old_printer(one_printer): # noqa: F811
  42. """Nor given a mapping worked out for its old printer: the pass would
  43. clear the bogus all-unresolved one it loaded."""
  44. item_id = await _pinned_item(one_printer, ams_mapping="[-1]")
  45. async with one_printer.session_maker() as db:
  46. other = Printer(name="Other", serial_number="OTHER", ip_address="10.0.0.2", access_code="code", model="X1C")
  47. db.add(other)
  48. await db.commit()
  49. other_id = other.id
  50. async with _scheduler(one_printer, idle=True) as scheduler:
  51. with _edit_during_the_pass(one_printer, scheduler, printer_id=other_id):
  52. await scheduler.check_queue()
  53. assert not scheduler._inflight
  54. async with one_printer.session_maker() as db:
  55. item = await db.get(PrintQueueItem, item_id)
  56. assert (item.status, item.printer_id, item.ams_mapping) == ("pending", other_id, "[-1]")
  57. @pytest.mark.asyncio
  58. async def test_the_claim_requires_the_printer_the_item_was_selected_for(one_printer): # noqa: F811
  59. """A move landing after the re-read: the dispatch's claim still refuses it."""
  60. from backend.app.services.print_scheduler import PrintScheduler
  61. item_id = await one_printer.add_item()
  62. scheduler = PrintScheduler()
  63. async with one_printer.session_maker() as db:
  64. assert not await scheduler._claim_for_dispatch(db, item_id, one_printer.printer_id + 1)
  65. assert await scheduler._claim_for_dispatch(db, item_id, one_printer.printer_id)
  66. @pytest.mark.asyncio
  67. async def test_the_power_on_wait_holds_no_transaction(one_printer): # noqa: F811
  68. """RC-C8: 30-180 s of waiting for a printer to boot with the pass's
  69. transaction open pins a connection (PostgreSQL) or a stale snapshot."""
  70. from unittest.mock import MagicMock
  71. from backend.app.services import print_scheduler as scheduler_module
  72. from backend.app.services.print_scheduler import PrintScheduler
  73. scheduler = PrintScheduler()
  74. order: list[str] = []
  75. plug = MagicMock(name="plug")
  76. plug.name = "Plug"
  77. service = MagicMock(
  78. get_status=AsyncMock(return_value={"reachable": True, "state": "ON"}), turn_on=AsyncMock(return_value=True)
  79. )
  80. async def sleep(_seconds):
  81. order.append("sleep")
  82. async with one_printer.session_maker() as db:
  83. real_commit = db.commit
  84. async def commit():
  85. order.append("commit")
  86. await real_commit()
  87. with (
  88. patch.object(scheduler_module.smart_plug_manager, "get_service_for_plug", AsyncMock(return_value=service)),
  89. patch.object(scheduler_module.asyncio, "sleep", sleep),
  90. patch.object(scheduler_module.printer_manager, "connect_printer", AsyncMock(return_value=True)),
  91. patch.object(db, "commit", commit),
  92. ):
  93. assert await scheduler._power_on_and_wait(plug, one_printer.printer_id, db)
  94. assert order[:2] == ["commit", "sleep"]
  95. @pytest.mark.asyncio
  96. async def test_an_item_cancelled_meanwhile_is_left_as_cancelled(one_printer): # noqa: F811
  97. item_id = await _pinned_item(one_printer, ams_mapping="[-1]")
  98. async with _scheduler(one_printer, idle=True) as scheduler:
  99. with _edit_during_the_pass(one_printer, scheduler, status="cancelled"):
  100. await scheduler.check_queue()
  101. async with one_printer.session_maker() as db:
  102. item = await db.get(PrintQueueItem, item_id)
  103. assert (item.status, item.ams_mapping) == ("cancelled", "[-1]")
  104. @pytest.mark.asyncio
  105. async def test_another_plate_chosen_meanwhile_is_not_mapped_from_the_old_one(one_printer): # noqa: F811
  106. """The new mapping belongs to the new plate; checked against the old
  107. plate's filaments it would be thrown away."""
  108. item_id = await _pinned_item(one_printer, ams_mapping="[-1]")
  109. async with _scheduler(one_printer, idle=True) as scheduler:
  110. with _edit_during_the_pass(one_printer, scheduler, plate_id=2, ams_mapping="[3]"):
  111. await scheduler.check_queue()
  112. assert not scheduler._inflight
  113. async with one_printer.session_maker() as db:
  114. item = await db.get(PrintQueueItem, item_id)
  115. assert (item.plate_id, item.ams_mapping) == (2, "[3]")
  116. @pytest.mark.asyncio
  117. async def test_a_model_job_pinned_meanwhile_is_not_sent_to_the_printer_it_was_offered(one_printer): # noqa: F811
  118. item_id = await _pinned_item(one_printer)
  119. async with one_printer.session_maker() as db:
  120. other = Printer(name="Other", serial_number="OTHER", ip_address="10.0.0.2", access_code="code", model="X1C")
  121. db.add(other)
  122. await db.commit()
  123. other_id = other.id
  124. await db.execute(
  125. update(PrintQueueItem).where(PrintQueueItem.id == item_id).values(printer_id=None, target_model="X1C")
  126. )
  127. await db.commit()
  128. async with _scheduler(one_printer, idle=True) as scheduler:
  129. with _edit_during_the_pass(one_printer, scheduler, printer_id=other_id, target_model=None):
  130. await scheduler.check_queue()
  131. assert not scheduler._inflight
  132. async with one_printer.session_maker() as db:
  133. item = await db.get(PrintQueueItem, item_id)
  134. assert (item.status, item.printer_id, item.target_model) == ("pending", other_id, None)
  135. @pytest.mark.asyncio
  136. @pytest.mark.parametrize(
  137. "edit",
  138. [
  139. {"plate_id": 2},
  140. {"filament_overrides": '[{"slot_id": 1, "type": "PETG"}]'},
  141. {"skip_filament_check": True},
  142. {"target_model": "P1S"},
  143. {"target_location": "Lab"},
  144. ],
  145. )
  146. async def test_any_edit_to_what_the_dispatch_is_worked_out_from_waits_a_round(one_printer, edit): # noqa: F811
  147. await _pinned_item(one_printer)
  148. async with _scheduler(one_printer, idle=True) as scheduler:
  149. with _edit_during_the_pass(one_printer, scheduler, **edit):
  150. await scheduler.check_queue()
  151. assert not scheduler._inflight