redis_ttl_bug_test.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. Redis TTL不同步Bug单元测试
  5. 验证问题:store_file_info函数只更新meta的TTL,不更新content的TTL,
  6. 导致两个键的过期时间不同步,造成间歇性缓存失败。
  7. """
  8. import asyncio
  9. import json
  10. import time
  11. import pytest
  12. from typing import Dict, Any
  13. # 导入被测试的模块
  14. import sys
  15. from pathlib import Path
  16. root_dir = Path(__file__).parent.parent
  17. sys.path.append(str(root_dir))
  18. from foundation.utils.redis_utils import store_file_info, get_file_info
  19. from foundation.infrastructure.cache.redis_connection import RedisConnectionFactory
  20. class TestRedisTTLSync:
  21. """Redis TTL同步问题测试类"""
  22. @pytest.fixture
  23. async def redis_store(self):
  24. """Redis连接fixture"""
  25. redis_store = await RedisConnectionFactory.get_redis_store()
  26. yield redis_store
  27. # 测试后清理
  28. await RedisConnectionFactory.close_all()
  29. @pytest.fixture
  30. async def test_file_data(self):
  31. """测试文件数据fixture"""
  32. file_id = "test_ttl_bug_file_123"
  33. file_info = {
  34. "file_name": "测试文档.pdf",
  35. "file_size": 1024,
  36. "file_content": "这是一个测试文件内容,用于验证TTL不同步问题。" * 50,
  37. "callback_task_id": "original_task_id",
  38. "upload_time": int(time.time())
  39. }
  40. return file_id, file_info
  41. async def test_ttl_sync_bug_reproduction(self, redis_store, test_file_data):
  42. """
  43. 测试1: 复现TTL不同步Bug
  44. 1. 存储完整文件信息
  45. 2. 更新callback_task_id(触发TTL不同步)
  46. 3. 等待原始content过期
  47. 4. 验证获取文件内容失败
  48. """
  49. file_id, file_info = test_file_data
  50. print("\n=== 开始测试TTL不同步Bug复现 ===")
  51. # 步骤1: 存储完整文件信息,TTL=5秒(便于测试)
  52. print("1. 存储完整文件信息...")
  53. success = await store_file_info(file_id, file_info, expire_seconds=5)
  54. assert success, "存储文件信息失败"
  55. # 验证初始状态
  56. print("2. 验证初始状态...")
  57. retrieved_info = await get_file_info(file_id, include_content=True)
  58. assert retrieved_info is not None, "初始获取文件信息失败"
  59. assert retrieved_info['file_content'] is not None, "初始文件内容为空"
  60. print(f" [OK] 文件内容长度: {len(retrieved_info['file_content'])}")
  61. # 检查键的TTL
  62. meta_ttl = await redis_store.ttl(f"meta:{file_id}")
  63. content_ttl = await redis_store.ttl(f"content:{file_id}")
  64. print(f" [OK] 初始TTL - meta: {meta_ttl}s, content: {content_ttl}s")
  65. assert abs(meta_ttl - content_ttl) <= 1, "初始TTL应该基本同步"
  66. # 步骤3: 等待3秒,让TTL减少一些
  67. print("3. 等待3秒...")
  68. await asyncio.sleep(3)
  69. # 步骤4: 更新callback_task_id(触发Bug)
  70. print("4. 更新callback_task_id(触发TTL不同步Bug)...")
  71. update_success = await store_file_info(
  72. file_id,
  73. {'callback_task_id': 'updated_task_id'},
  74. expire_seconds=10 # 设置更长的TTL
  75. )
  76. assert update_success, "更新callback_task_id失败"
  77. # 步骤5: 检查TTL不同步问题
  78. print("5. 检查TTL不同步问题...")
  79. new_meta_ttl = await redis_store.ttl(f"meta:{file_id}")
  80. new_content_ttl = await redis_store.ttl(f"content:{file_id}")
  81. print(f" [OK] 更新后TTL - meta: {new_meta_ttl}s, content: {new_content_ttl}s")
  82. # 验证TTL不同步:meta的TTL应该重置为10秒,content应该继续倒计时(约2秒)
  83. assert new_meta_ttl > 8, f"meta的TTL应该被重置为约10秒,实际为{new_meta_ttl}"
  84. assert new_content_ttl < 5, f"content的TTL应该继续倒计时,实际为{new_content_ttl}"
  85. assert abs(new_meta_ttl - new_content_ttl) > 3, "TTL应该明显不同步"
  86. print(" [PASS] TTL不同步Bug已复现!")
  87. # 步骤6: 等待content过期
  88. print("6. 等待content过期...")
  89. await asyncio.sleep(new_content_ttl + 1)
  90. # 步骤7: 验证获取文件内容失败
  91. print("7. 验证content过期后获取失败...")
  92. final_info = await get_file_info(file_id, include_content=True)
  93. # 根据当前bug逻辑,这里应该返回None(因为content缺失)
  94. assert final_info is None, f"content过期后应该返回None,实际返回: {final_info}"
  95. print(" [PASS] Bug验证成功:content过期后无法获取文件信息")
  96. # 清理测试数据
  97. await redis_store.delete(f"meta:{file_id}", f"content:{file_id}")
  98. async def test_only_meta_retrieval_works(self, redis_store, test_file_data):
  99. """
  100. 测试2: 验证不包含content的获取仍然正常工作
  101. 即使content过期,meta信息应该仍然可以获取
  102. """
  103. file_id, file_info = test_file_data
  104. print("\n=== 测试meta单独获取功能 ===")
  105. # 存储文件信息
  106. await store_file_info(file_id, file_info, expire_seconds=3)
  107. # 更新callback_task_id
  108. await store_file_info(file_id, {'callback_task_id': 'updated'}, expire_seconds=10)
  109. # 等待content过期
  110. await asyncio.sleep(4)
  111. # 测试不包含content的获取
  112. meta_only_info = await get_file_info(file_id, include_content=False)
  113. assert meta_only_info is not None, "meta信息应该可以正常获取"
  114. assert meta_only_info['callback_task_id'] == 'updated', "callback_task_id应该被正确更新"
  115. assert 'file_content' not in meta_only_info, "不应该包含file_content"
  116. print(" [PASS] meta信息单独获取正常")
  117. # 清理
  118. await redis_store.delete(f"meta:{file_id}")
  119. async def test_current_bug_detection_in_logs(self, redis_store, test_file_data):
  120. """
  121. 测试3: 模拟日志中观察到的实际场景
  122. 复现日志中的时间序列:
  123. - 11:41:53 文件信息存储
  124. - 11:42:01 获取失败(8秒后)
  125. """
  126. file_id, file_info = test_file_data
  127. print("\n=== 模拟日志中的实际场景 ===")
  128. # 步骤1: 模拟文件上传(11:41:53)
  129. print("1. 模拟文件上传(11:41:53)...")
  130. upload_time = time.time()
  131. await store_file_info(file_id, file_info, expire_seconds=10) # 10秒TTL
  132. # 步骤2: 模拟一些处理时间
  133. await asyncio.sleep(2)
  134. # 步骤3: 模拟callback_task_id更新(可能发生在中间某个时间点)
  135. print("3. 模拟callback_task_id更新...")
  136. await store_file_info(file_id, {'callback_task_id': 'updated_task'}, expire_seconds=15)
  137. # 步骤4: 等待到11:42:01(约8秒后)
  138. elapsed_time = time.time() - upload_time
  139. wait_time = 8 - elapsed_time
  140. if wait_time > 0:
  141. print(f"4. 等待到11:42:01(还需要{wait_time:.1f}秒)...")
  142. await asyncio.sleep(wait_time)
  143. # 步骤5: 验证获取失败(如日志所示)
  144. print("5. 验证11:42:01时的获取失败...")
  145. failed_info = await get_file_info(file_id, include_content=True)
  146. assert failed_info is None, "模拟11:42:01时应该获取失败"
  147. print(" [PASS] 成功复现日志中的失败场景")
  148. # 清理
  149. await redis_store.delete(f"meta:{file_id}")
  150. async def run_ttl_bug_tests():
  151. """运行TTL Bug测试"""
  152. print("[TEST] 开始Redis TTL不同步Bug测试")
  153. test_instance = TestRedisTTLSync()
  154. try:
  155. # 获取redis连接
  156. redis_store = await RedisConnectionFactory.get_redis_store()
  157. # 准备测试数据
  158. file_id = "test_ttl_bug_file_123"
  159. file_info = {
  160. "file_name": "测试文档.pdf",
  161. "file_size": 1024,
  162. "file_content": "这是一个测试文件内容,用于验证TTL不同步问题。" * 50,
  163. "callback_task_id": "original_task_id",
  164. "upload_time": int(time.time())
  165. }
  166. test_file_data = (file_id, file_info)
  167. # 运行测试
  168. await test_instance.test_ttl_sync_bug_reproduction(redis_store, test_file_data)
  169. await test_instance.test_only_meta_retrieval_works(redis_store, test_file_data)
  170. await test_instance.test_current_bug_detection_in_logs(redis_store, test_file_data)
  171. print("\n[PASS] 所有TTL Bug测试通过!")
  172. except Exception as e:
  173. print(f"\n[FAIL] 测试失败: {e}")
  174. import traceback
  175. traceback.print_exc()
  176. finally:
  177. await RedisConnectionFactory.close_all()
  178. if __name__ == "__main__":
  179. # 直接运行测试
  180. asyncio.run(run_ttl_bug_tests())