test_filament_info_single_flight_2572.py 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. """Filament-info cloud lookups single-flight concurrent misses (#2572).
  2. The printer overview mounts one ``/cloud/filament-info`` request per printer
  3. card, so at farm scale several browsers ask for the same uncached preset in the
  4. same instant. Without coalescing each request issued its own Bambu Cloud
  5. round-trip for the same id — a thundering herd against a rate-limited API.
  6. ``_resolve_cloud_filament`` makes the first caller the leader and has the rest
  7. await its result.
  8. """
  9. import asyncio
  10. import pytest
  11. import backend.app.api.routes.cloud as cloud_mod
  12. from backend.app.api.routes.cloud import _resolve_cloud_filament
  13. class _FakeCloud:
  14. """Counts cloud calls; blocks in get_setting_detail until released."""
  15. def __init__(self, gate: asyncio.Event, *, fail: bool = False):
  16. self.calls = 0
  17. self._gate = gate
  18. self._fail = fail
  19. async def get_setting_detail(self, api_setting_id):
  20. self.calls += 1
  21. await self._gate.wait()
  22. if self._fail:
  23. raise RuntimeError("cloud down")
  24. return {"name": "PLA Matte", "setting": {"pressure_advance": "0.021"}}
  25. @pytest.fixture(autouse=True)
  26. def _clear_state():
  27. cloud_mod._filament_cache.clear()
  28. cloud_mod._filament_inflight.clear()
  29. yield
  30. cloud_mod._filament_cache.clear()
  31. cloud_mod._filament_inflight.clear()
  32. @pytest.mark.asyncio
  33. async def test_concurrent_misses_share_one_cloud_call():
  34. gate = asyncio.Event()
  35. cloud = _FakeCloud(gate)
  36. leader = asyncio.create_task(_resolve_cloud_filament("GFSA00", cloud))
  37. await asyncio.sleep(0) # let the leader register its in-flight future
  38. follower = asyncio.create_task(_resolve_cloud_filament("GFSA00", cloud))
  39. await asyncio.sleep(0) # let the follower attach to the leader's future
  40. gate.set()
  41. leader_info, follower_info = await asyncio.gather(leader, follower)
  42. assert cloud.calls == 1, "the two concurrent misses did not coalesce"
  43. assert leader_info == {"name": "PLA Matte", "k": 0.021}
  44. assert follower_info == leader_info
  45. assert cloud_mod._filament_cache["GFSA00"] == leader_info
  46. assert "GFSA00" not in cloud_mod._filament_inflight # cleaned up
  47. @pytest.mark.asyncio
  48. async def test_cache_hit_skips_cloud_entirely():
  49. gate = asyncio.Event()
  50. gate.set()
  51. cloud = _FakeCloud(gate)
  52. cloud_mod._filament_cache["GFSA00"] = {"name": "cached", "k": None}
  53. info = await _resolve_cloud_filament("GFSA00", cloud)
  54. assert info == {"name": "cached", "k": None}
  55. assert cloud.calls == 0
  56. @pytest.mark.asyncio
  57. async def test_failed_fetch_returns_none_and_leaves_no_inflight():
  58. gate = asyncio.Event()
  59. gate.set()
  60. cloud = _FakeCloud(gate, fail=True)
  61. info = await _resolve_cloud_filament("GFSA00", cloud)
  62. assert info is None
  63. assert "GFSA00" not in cloud_mod._filament_cache
  64. assert "GFSA00" not in cloud_mod._filament_inflight