test_unified_lock_safety.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. """
  2. Tests for UnifiedLock safety when lock is None.
  3. This test module verifies that get_internal_lock() and get_data_init_lock()
  4. raise RuntimeError when shared data is not initialized, preventing false
  5. security and potential race conditions.
  6. Design: The None check has been moved from UnifiedLock.__aenter__/__enter__
  7. to the lock factory functions (get_internal_lock, get_data_init_lock) for
  8. early failure detection.
  9. Critical Bug 1 (Fixed): When self._lock is None, the code would fail with
  10. AttributeError. Now the check is in factory functions for clearer errors.
  11. Critical Bug 2: In __aexit__, when async_lock.release() fails, the error
  12. recovery logic would attempt to release it again, causing double-release issues.
  13. """
  14. from unittest.mock import MagicMock, AsyncMock
  15. import pytest
  16. from lightrag.kg.shared_storage import (
  17. UnifiedLock,
  18. get_internal_lock,
  19. get_data_init_lock,
  20. finalize_share_data,
  21. )
  22. class TestUnifiedLockSafety:
  23. """Test suite for UnifiedLock None safety checks."""
  24. def setup_method(self):
  25. """Ensure shared data is finalized before each test."""
  26. finalize_share_data()
  27. def teardown_method(self):
  28. """Clean up after each test."""
  29. finalize_share_data()
  30. def test_get_internal_lock_raises_when_not_initialized(self):
  31. """
  32. Test that get_internal_lock() raises RuntimeError when shared data is not initialized.
  33. Scenario: Call get_internal_lock() before initialize_share_data() is called.
  34. Expected: RuntimeError raised with clear error message.
  35. This test verifies the None check has been moved to the factory function.
  36. """
  37. with pytest.raises(
  38. RuntimeError, match="Shared data not initialized.*initialize_share_data"
  39. ):
  40. get_internal_lock()
  41. def test_get_data_init_lock_raises_when_not_initialized(self):
  42. """
  43. Test that get_data_init_lock() raises RuntimeError when shared data is not initialized.
  44. Scenario: Call get_data_init_lock() before initialize_share_data() is called.
  45. Expected: RuntimeError raised with clear error message.
  46. This test verifies the None check has been moved to the factory function.
  47. """
  48. with pytest.raises(
  49. RuntimeError, match="Shared data not initialized.*initialize_share_data"
  50. ):
  51. get_data_init_lock()
  52. @pytest.mark.offline
  53. async def test_aexit_no_double_release_on_async_lock_failure(self):
  54. """
  55. Test that __aexit__ doesn't attempt to release async_lock twice when it fails.
  56. Scenario: async_lock.release() fails during normal release.
  57. Expected: Recovery logic should NOT attempt to release async_lock again,
  58. preventing double-release issues.
  59. This tests Bug 2 fix: async_lock_released tracking prevents double release.
  60. """
  61. # Create mock locks
  62. main_lock = MagicMock()
  63. main_lock.acquire = MagicMock()
  64. main_lock.release = MagicMock()
  65. async_lock = AsyncMock()
  66. async_lock.acquire = AsyncMock()
  67. # Make async_lock.release() fail
  68. release_call_count = 0
  69. def mock_release_fail():
  70. nonlocal release_call_count
  71. release_call_count += 1
  72. raise RuntimeError("Async lock release failed")
  73. async_lock.release = MagicMock(side_effect=mock_release_fail)
  74. # Create UnifiedLock with both locks (sync mode with async_lock)
  75. lock = UnifiedLock(
  76. lock=main_lock,
  77. is_async=False,
  78. name="test_double_release",
  79. enable_logging=False,
  80. )
  81. lock._async_lock = async_lock
  82. # Try to use the lock - should fail during __aexit__
  83. try:
  84. async with lock:
  85. pass
  86. except RuntimeError as e:
  87. # Should get the async lock release error
  88. assert "Async lock release failed" in str(e)
  89. # Verify async_lock.release() was called only ONCE, not twice
  90. assert (
  91. release_call_count == 1
  92. ), f"async_lock.release() should be called only once, but was called {release_call_count} times"
  93. # Main lock should have been released successfully
  94. main_lock.release.assert_called_once()