test_cover_coalescing_2572.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. """Concurrent cover requests for the same print coalesce into one download (#2572).
  2. The farm dashboard mounts a cover tile per printer card, so several browsers
  3. request the same printer's cover in the same instant. Before this fix each miss
  4. ran the full multi-path FTP lookup + 3MF extraction independently (one observed
  5. live transfer pulled an 81 MB 3MF while real uploads were in flight). Now the
  6. first miss becomes the leader and the rest await its result, then serve from the
  7. cache it filled.
  8. """
  9. import asyncio
  10. from types import SimpleNamespace
  11. from unittest.mock import MagicMock, patch
  12. import pytest
  13. import backend.app.api.routes.printers as printers_mod
  14. from backend.app.api.routes.printers import get_printer_cover
  15. class _FakeSession:
  16. def __init__(self, printer):
  17. self._printer = printer
  18. async def __aenter__(self):
  19. return self
  20. async def __aexit__(self, *exc):
  21. return False
  22. async def execute(self, *args, **kwargs):
  23. return SimpleNamespace(scalar_one_or_none=lambda: self._printer)
  24. @pytest.fixture(autouse=True)
  25. def _clear_cover_state():
  26. printers_mod._cover_cache.clear()
  27. printers_mod._cover_404_cache.clear()
  28. printers_mod._cover_inflight.clear()
  29. yield
  30. printers_mod._cover_cache.clear()
  31. printers_mod._cover_404_cache.clear()
  32. printers_mod._cover_inflight.clear()
  33. @pytest.mark.asyncio
  34. async def test_concurrent_cover_requests_download_once():
  35. printer = SimpleNamespace(id=1, ip_address="127.0.0.1", access_code="x", model="X1C", name="P")
  36. state = SimpleNamespace(subtask_name="job", state="RUNNING")
  37. produce_calls = {"n": 0}
  38. async def slow_produce(
  39. printer_row, printer_id, subtask_name, view, view_key, plate_num, cache_key, archive_path=None
  40. ):
  41. produce_calls["n"] += 1
  42. await asyncio.sleep(0.1) # hold leadership long enough for followers to attach
  43. printers_mod._cover_cache.setdefault(printer_id, {})[cache_key] = b"PNGDATA"
  44. return b"PNGDATA"
  45. with (
  46. patch("backend.app.core.database.async_session", lambda: _FakeSession(printer)),
  47. patch.object(printers_mod.printer_manager, "get_status", MagicMock(return_value=state)),
  48. patch.object(printers_mod, "resolve_plate_id", MagicMock(return_value=1)),
  49. patch.object(printers_mod, "_produce_cover_image", slow_produce),
  50. ):
  51. responses = await asyncio.gather(*[get_printer_cover(1, None, None) for _ in range(5)])
  52. assert produce_calls["n"] == 1, "concurrent cover requests each ran their own FTP download"
  53. assert {bytes(r.body) for r in responses} == {b"PNGDATA"}
  54. @pytest.mark.asyncio
  55. async def test_second_request_serves_from_positive_cache():
  56. """A follower arriving after the leader filled the cache serves it directly."""
  57. printer = SimpleNamespace(id=1, ip_address="127.0.0.1", access_code="x", model="X1C", name="P")
  58. state = SimpleNamespace(subtask_name="job", state="RUNNING")
  59. produce_calls = {"n": 0}
  60. async def produce(printer_row, printer_id, subtask_name, view, view_key, plate_num, cache_key, archive_path=None):
  61. produce_calls["n"] += 1
  62. printers_mod._cover_cache.setdefault(printer_id, {})[cache_key] = b"PNGDATA"
  63. return b"PNGDATA"
  64. with (
  65. patch("backend.app.core.database.async_session", lambda: _FakeSession(printer)),
  66. patch.object(printers_mod.printer_manager, "get_status", MagicMock(return_value=state)),
  67. patch.object(printers_mod, "resolve_plate_id", MagicMock(return_value=1)),
  68. patch.object(printers_mod, "_produce_cover_image", produce),
  69. ):
  70. first = await get_printer_cover(1, None, None)
  71. second = await get_printer_cover(1, None, None)
  72. assert produce_calls["n"] == 1 # second hit the positive cache
  73. assert bytes(first.body) == bytes(second.body) == b"PNGDATA"