lmcache-cli 0.4.5.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (399) hide show
  1. lmcache/__init__.py +84 -0
  2. lmcache/_version.py +24 -0
  3. lmcache/cli/__init__.py +1 -0
  4. lmcache/cli/commands/__init__.py +34 -0
  5. lmcache/cli/commands/base.py +157 -0
  6. lmcache/cli/commands/bench/__init__.py +557 -0
  7. lmcache/cli/commands/bench/engine_bench/__init__.py +1 -0
  8. lmcache/cli/commands/bench/engine_bench/config.py +245 -0
  9. lmcache/cli/commands/bench/engine_bench/interactive/__init__.py +274 -0
  10. lmcache/cli/commands/bench/engine_bench/interactive/config.json +10 -0
  11. lmcache/cli/commands/bench/engine_bench/interactive/schema.py +352 -0
  12. lmcache/cli/commands/bench/engine_bench/interactive/state.py +327 -0
  13. lmcache/cli/commands/bench/engine_bench/interactive/terminal.py +291 -0
  14. lmcache/cli/commands/bench/engine_bench/progress.py +145 -0
  15. lmcache/cli/commands/bench/engine_bench/request_sender.py +232 -0
  16. lmcache/cli/commands/bench/engine_bench/stats.py +275 -0
  17. lmcache/cli/commands/bench/engine_bench/workloads/__init__.py +153 -0
  18. lmcache/cli/commands/bench/engine_bench/workloads/base.py +122 -0
  19. lmcache/cli/commands/bench/engine_bench/workloads/long_doc_permutator.py +435 -0
  20. lmcache/cli/commands/bench/engine_bench/workloads/long_doc_qa.py +281 -0
  21. lmcache/cli/commands/bench/engine_bench/workloads/multi_round_chat.py +337 -0
  22. lmcache/cli/commands/bench/engine_bench/workloads/random_prefill.py +178 -0
  23. lmcache/cli/commands/describe.py +310 -0
  24. lmcache/cli/commands/kvcache.py +133 -0
  25. lmcache/cli/commands/mock.py +75 -0
  26. lmcache/cli/commands/ping.py +113 -0
  27. lmcache/cli/commands/query/__init__.py +155 -0
  28. lmcache/cli/commands/query/prompt.py +134 -0
  29. lmcache/cli/commands/query/request.py +357 -0
  30. lmcache/cli/commands/server.py +99 -0
  31. lmcache/cli/commands/tool/__init__.py +63 -0
  32. lmcache/cli/commands/tool/cache_simulator.py +113 -0
  33. lmcache/cli/commands/trace/__init__.py +505 -0
  34. lmcache/cli/commands/trace/dispatch.py +249 -0
  35. lmcache/cli/commands/trace/driver.py +372 -0
  36. lmcache/cli/commands/trace/stats.py +289 -0
  37. lmcache/cli/documents/lmcache.txt +11 -0
  38. lmcache/cli/main.py +42 -0
  39. lmcache/cli/metrics/__init__.py +29 -0
  40. lmcache/cli/metrics/formatter.py +171 -0
  41. lmcache/cli/metrics/handler.py +94 -0
  42. lmcache/cli/metrics/metrics.py +161 -0
  43. lmcache/cli/metrics/section.py +77 -0
  44. lmcache/connections.py +173 -0
  45. lmcache/integration/__init__.py +2 -0
  46. lmcache/integration/base_service_factory.py +165 -0
  47. lmcache/integration/request_telemetry/__init__.py +1 -0
  48. lmcache/integration/request_telemetry/base.py +51 -0
  49. lmcache/integration/request_telemetry/factory.py +113 -0
  50. lmcache/integration/request_telemetry/fastapi.py +109 -0
  51. lmcache/integration/request_telemetry/noop.py +35 -0
  52. lmcache/integration/sglang/__init__.py +2 -0
  53. lmcache/integration/sglang/sglang_adapter.py +326 -0
  54. lmcache/integration/sglang/utils.py +39 -0
  55. lmcache/integration/vllm/__init__.py +1 -0
  56. lmcache/integration/vllm/lmcache_connector_v1.py +213 -0
  57. lmcache/integration/vllm/lmcache_connector_v1_085.py +150 -0
  58. lmcache/integration/vllm/lmcache_mp_connector_0180.py +1072 -0
  59. lmcache/integration/vllm/tests/test_mm_hash_utils.py +112 -0
  60. lmcache/integration/vllm/utils.py +433 -0
  61. lmcache/integration/vllm/vllm_multi_process_adapter.py +1090 -0
  62. lmcache/integration/vllm/vllm_service_factory.py +339 -0
  63. lmcache/integration/vllm/vllm_v1_adapter.py +1713 -0
  64. lmcache/logging.py +107 -0
  65. lmcache/native_storage_ops.pyi +230 -0
  66. lmcache/non_cuda_equivalents.py +1424 -0
  67. lmcache/observability.py +1958 -0
  68. lmcache/storage_backend/serde/__init__.py +1 -0
  69. lmcache/storage_backend/serde/cachegen_basics.py +210 -0
  70. lmcache/storage_backend/serde/cachegen_decoder.py +207 -0
  71. lmcache/storage_backend/serde/cachegen_encoder.py +394 -0
  72. lmcache/storage_backend/serde/serde.py +75 -0
  73. lmcache/tools/__init__.py +1 -0
  74. lmcache/tools/cache_simulator/README.md +392 -0
  75. lmcache/tools/cache_simulator/__init__.py +1 -0
  76. lmcache/tools/cache_simulator/docs/simulate_example.png +0 -0
  77. lmcache/tools/cache_simulator/docs/sweep_example.png +0 -0
  78. lmcache/tools/cache_simulator/gen_bench_dataset.py +360 -0
  79. lmcache/tools/cache_simulator/lru_cache.py +124 -0
  80. lmcache/tools/cache_simulator/plot_hit_rate.py +231 -0
  81. lmcache/tools/cache_simulator/simulator.py +795 -0
  82. lmcache/tools/controller_benchmark/README.md +161 -0
  83. lmcache/tools/controller_benchmark/__init__.py +1 -0
  84. lmcache/tools/controller_benchmark/__main__.py +331 -0
  85. lmcache/tools/controller_benchmark/benchmark.py +660 -0
  86. lmcache/tools/controller_benchmark/config.py +44 -0
  87. lmcache/tools/controller_benchmark/constants.py +10 -0
  88. lmcache/tools/controller_benchmark/handlers/__init__.py +46 -0
  89. lmcache/tools/controller_benchmark/handlers/admit.py +52 -0
  90. lmcache/tools/controller_benchmark/handlers/base.py +47 -0
  91. lmcache/tools/controller_benchmark/handlers/deregister.py +49 -0
  92. lmcache/tools/controller_benchmark/handlers/evict.py +52 -0
  93. lmcache/tools/controller_benchmark/handlers/heartbeat.py +56 -0
  94. lmcache/tools/controller_benchmark/handlers/p2p_lookup.py +47 -0
  95. lmcache/tools/controller_benchmark/handlers/register.py +56 -0
  96. lmcache/tools/mp_status_viewer/__init__.py +1 -0
  97. lmcache/tools/mp_status_viewer/__main__.py +95 -0
  98. lmcache/usage_context.py +417 -0
  99. lmcache/utils.py +665 -0
  100. lmcache/v1/__init__.py +2 -0
  101. lmcache/v1/api_server/__init__.py +2 -0
  102. lmcache/v1/api_server/__main__.py +537 -0
  103. lmcache/v1/basic_check.py +112 -0
  104. lmcache/v1/cache_controller/__init__.py +9 -0
  105. lmcache/v1/cache_controller/commands/__init__.py +15 -0
  106. lmcache/v1/cache_controller/commands/base.py +35 -0
  107. lmcache/v1/cache_controller/commands/full_sync.py +49 -0
  108. lmcache/v1/cache_controller/config.py +176 -0
  109. lmcache/v1/cache_controller/controller_manager.py +535 -0
  110. lmcache/v1/cache_controller/controllers/__init__.py +11 -0
  111. lmcache/v1/cache_controller/controllers/full_sync_tracker.py +473 -0
  112. lmcache/v1/cache_controller/controllers/kv_controller.py +439 -0
  113. lmcache/v1/cache_controller/controllers/registration_controller.py +282 -0
  114. lmcache/v1/cache_controller/executor.py +463 -0
  115. lmcache/v1/cache_controller/frontend/static/css/style.css +201 -0
  116. lmcache/v1/cache_controller/frontend/static/img/logo.png +0 -0
  117. lmcache/v1/cache_controller/frontend/static/index.html +234 -0
  118. lmcache/v1/cache_controller/frontend/static/js/controller_app.js +660 -0
  119. lmcache/v1/cache_controller/full_sync_sender.py +475 -0
  120. lmcache/v1/cache_controller/locks.py +149 -0
  121. lmcache/v1/cache_controller/message.py +828 -0
  122. lmcache/v1/cache_controller/observability.py +208 -0
  123. lmcache/v1/cache_controller/utils.py +679 -0
  124. lmcache/v1/cache_controller/worker.py +665 -0
  125. lmcache/v1/cache_engine.py +2058 -0
  126. lmcache/v1/cache_interface.py +19 -0
  127. lmcache/v1/check/__init__.py +74 -0
  128. lmcache/v1/check/check_mode_gen.py +86 -0
  129. lmcache/v1/check/check_mode_test_l2_adapter.py +284 -0
  130. lmcache/v1/check/check_mode_test_remote.py +155 -0
  131. lmcache/v1/check/check_mode_test_storage_manager.py +142 -0
  132. lmcache/v1/check/utils.py +571 -0
  133. lmcache/v1/compute/__init__.py +2 -0
  134. lmcache/v1/compute/attention/__init__.py +0 -0
  135. lmcache/v1/compute/attention/abstract.py +39 -0
  136. lmcache/v1/compute/attention/flash_attn.py +129 -0
  137. lmcache/v1/compute/attention/flash_infer_sparse.py +284 -0
  138. lmcache/v1/compute/attention/metadata.py +85 -0
  139. lmcache/v1/compute/attention/utils.py +14 -0
  140. lmcache/v1/compute/blend/__init__.py +7 -0
  141. lmcache/v1/compute/blend/blender.py +168 -0
  142. lmcache/v1/compute/blend/metadata.py +34 -0
  143. lmcache/v1/compute/blend/utils.py +63 -0
  144. lmcache/v1/compute/models/__init__.py +0 -0
  145. lmcache/v1/compute/models/base.py +141 -0
  146. lmcache/v1/compute/models/llama.py +9 -0
  147. lmcache/v1/compute/models/qwen3.py +24 -0
  148. lmcache/v1/compute/models/utils.py +68 -0
  149. lmcache/v1/compute/positional_encoding.py +199 -0
  150. lmcache/v1/config.py +848 -0
  151. lmcache/v1/config_base.py +848 -0
  152. lmcache/v1/distributed/api.py +248 -0
  153. lmcache/v1/distributed/config.py +321 -0
  154. lmcache/v1/distributed/error.py +64 -0
  155. lmcache/v1/distributed/eviction.py +192 -0
  156. lmcache/v1/distributed/eviction_policy/__init__.py +21 -0
  157. lmcache/v1/distributed/eviction_policy/factory.py +27 -0
  158. lmcache/v1/distributed/eviction_policy/lru.py +244 -0
  159. lmcache/v1/distributed/eviction_policy/noop.py +50 -0
  160. lmcache/v1/distributed/internal_api.py +170 -0
  161. lmcache/v1/distributed/l1_manager.py +835 -0
  162. lmcache/v1/distributed/l2_adapters/__init__.py +67 -0
  163. lmcache/v1/distributed/l2_adapters/base.py +360 -0
  164. lmcache/v1/distributed/l2_adapters/config.py +385 -0
  165. lmcache/v1/distributed/l2_adapters/factory.py +205 -0
  166. lmcache/v1/distributed/l2_adapters/fs_l2_adapter.py +747 -0
  167. lmcache/v1/distributed/l2_adapters/fs_native_l2_adapter.py +167 -0
  168. lmcache/v1/distributed/l2_adapters/mock_l2_adapter.py +516 -0
  169. lmcache/v1/distributed/l2_adapters/mooncake_store_l2_adapter.py +135 -0
  170. lmcache/v1/distributed/l2_adapters/native_connector_l2_adapter.py +468 -0
  171. lmcache/v1/distributed/l2_adapters/native_plugin_l2_adapter.py +199 -0
  172. lmcache/v1/distributed/l2_adapters/nixl_store_dynamic_l2_adapter.py +831 -0
  173. lmcache/v1/distributed/l2_adapters/nixl_store_l2_adapter.py +983 -0
  174. lmcache/v1/distributed/l2_adapters/plugin_l2_adapter.py +210 -0
  175. lmcache/v1/distributed/l2_adapters/resp_l2_adapter.py +176 -0
  176. lmcache/v1/distributed/memory_manager.py +179 -0
  177. lmcache/v1/distributed/storage_controller.py +39 -0
  178. lmcache/v1/distributed/storage_controllers/__init__.py +43 -0
  179. lmcache/v1/distributed/storage_controllers/eviction_controller.py +242 -0
  180. lmcache/v1/distributed/storage_controllers/prefetch_controller.py +830 -0
  181. lmcache/v1/distributed/storage_controllers/prefetch_policy.py +193 -0
  182. lmcache/v1/distributed/storage_controllers/store_controller.py +452 -0
  183. lmcache/v1/distributed/storage_controllers/store_policy.py +213 -0
  184. lmcache/v1/distributed/storage_manager.py +532 -0
  185. lmcache/v1/event_manager.py +145 -0
  186. lmcache/v1/exceptions/__init__.py +16 -0
  187. lmcache/v1/gpu_connector/__init__.py +126 -0
  188. lmcache/v1/gpu_connector/gpu_connectors.py +1906 -0
  189. lmcache/v1/gpu_connector/gpu_ops.py +85 -0
  190. lmcache/v1/gpu_connector/hpu_connector.py +326 -0
  191. lmcache/v1/gpu_connector/mock_gpu_connector.py +67 -0
  192. lmcache/v1/gpu_connector/utils.py +890 -0
  193. lmcache/v1/gpu_connector/xpu_connectors.py +916 -0
  194. lmcache/v1/health_monitor/__init__.py +1 -0
  195. lmcache/v1/health_monitor/base.py +587 -0
  196. lmcache/v1/health_monitor/checks/__init__.py +1 -0
  197. lmcache/v1/health_monitor/checks/remote_backend_check.py +304 -0
  198. lmcache/v1/health_monitor/constants.py +36 -0
  199. lmcache/v1/internal_api_server/__init__.py +0 -0
  200. lmcache/v1/internal_api_server/api_registry.py +59 -0
  201. lmcache/v1/internal_api_server/api_server.py +120 -0
  202. lmcache/v1/internal_api_server/common/__init__.py +1 -0
  203. lmcache/v1/internal_api_server/common/env_api.py +22 -0
  204. lmcache/v1/internal_api_server/common/loglevel_api.py +57 -0
  205. lmcache/v1/internal_api_server/common/metrics_api.py +29 -0
  206. lmcache/v1/internal_api_server/common/periodic_thread_api.py +138 -0
  207. lmcache/v1/internal_api_server/common/run_script_api.py +73 -0
  208. lmcache/v1/internal_api_server/common/thread_api.py +63 -0
  209. lmcache/v1/internal_api_server/controller/__init__.py +1 -0
  210. lmcache/v1/internal_api_server/controller/key_stats_api.py +81 -0
  211. lmcache/v1/internal_api_server/controller/worker_info_api.py +136 -0
  212. lmcache/v1/internal_api_server/utils.py +43 -0
  213. lmcache/v1/internal_api_server/vllm/__init__.py +1 -0
  214. lmcache/v1/internal_api_server/vllm/backend_api.py +221 -0
  215. lmcache/v1/internal_api_server/vllm/bypass_api.py +204 -0
  216. lmcache/v1/internal_api_server/vllm/cache_api.py +895 -0
  217. lmcache/v1/internal_api_server/vllm/chunk_statistics_api.py +141 -0
  218. lmcache/v1/internal_api_server/vllm/conf_api.py +147 -0
  219. lmcache/v1/internal_api_server/vllm/freeze_api.py +172 -0
  220. lmcache/v1/internal_api_server/vllm/hot_cache_api.py +184 -0
  221. lmcache/v1/internal_api_server/vllm/inference_api.py +65 -0
  222. lmcache/v1/internal_api_server/vllm/load_fs_chunks_api.py +320 -0
  223. lmcache/v1/internal_api_server/vllm/lookup_api.py +145 -0
  224. lmcache/v1/internal_api_server/vllm/version_api.py +25 -0
  225. lmcache/v1/kv_layer_groups.py +267 -0
  226. lmcache/v1/lazy_memory_allocator.py +284 -0
  227. lmcache/v1/lookup_client/__init__.py +25 -0
  228. lmcache/v1/lookup_client/abstract_client.py +77 -0
  229. lmcache/v1/lookup_client/async_lookup_message.py +50 -0
  230. lmcache/v1/lookup_client/chunk_statistics_lookup_client.py +200 -0
  231. lmcache/v1/lookup_client/factory.py +251 -0
  232. lmcache/v1/lookup_client/hit_limit_lookup_client.py +86 -0
  233. lmcache/v1/lookup_client/lmcache_async_lookup_client.py +407 -0
  234. lmcache/v1/lookup_client/lmcache_lookup_client.py +285 -0
  235. lmcache/v1/lookup_client/lmcache_lookup_client_bypass.py +99 -0
  236. lmcache/v1/lookup_client/mooncake_lookup_client.py +87 -0
  237. lmcache/v1/lookup_client/record_strategies/__init__.py +77 -0
  238. lmcache/v1/lookup_client/record_strategies/base.py +327 -0
  239. lmcache/v1/lookup_client/record_strategies/file_hash.py +130 -0
  240. lmcache/v1/lookup_client/record_strategies/memory_bloom_filter.py +81 -0
  241. lmcache/v1/manager.py +539 -0
  242. lmcache/v1/memory_management.py +2619 -0
  243. lmcache/v1/metadata.py +114 -0
  244. lmcache/v1/mp_observability/AGENTS.override.md +21 -0
  245. lmcache/v1/mp_observability/README.md +204 -0
  246. lmcache/v1/mp_observability/config.py +340 -0
  247. lmcache/v1/mp_observability/event.py +100 -0
  248. lmcache/v1/mp_observability/event_bus.py +313 -0
  249. lmcache/v1/mp_observability/otel_init.py +145 -0
  250. lmcache/v1/mp_observability/subscribers/__init__.py +28 -0
  251. lmcache/v1/mp_observability/subscribers/logging/__init__.py +19 -0
  252. lmcache/v1/mp_observability/subscribers/logging/l1.py +56 -0
  253. lmcache/v1/mp_observability/subscribers/logging/l2.py +73 -0
  254. lmcache/v1/mp_observability/subscribers/logging/lookup_hash.py +209 -0
  255. lmcache/v1/mp_observability/subscribers/logging/mp_server.py +90 -0
  256. lmcache/v1/mp_observability/subscribers/logging/sm.py +59 -0
  257. lmcache/v1/mp_observability/subscribers/metrics/__init__.py +20 -0
  258. lmcache/v1/mp_observability/subscribers/metrics/l0_lifecycle.py +290 -0
  259. lmcache/v1/mp_observability/subscribers/metrics/l1.py +55 -0
  260. lmcache/v1/mp_observability/subscribers/metrics/l1_lifecycle.py +166 -0
  261. lmcache/v1/mp_observability/subscribers/metrics/l2.py +121 -0
  262. lmcache/v1/mp_observability/subscribers/metrics/sm.py +69 -0
  263. lmcache/v1/mp_observability/subscribers/tracing/__init__.py +12 -0
  264. lmcache/v1/mp_observability/subscribers/tracing/mp_server.py +333 -0
  265. lmcache/v1/mp_observability/subscribers/tracing/span_registry.py +148 -0
  266. lmcache/v1/mp_observability/trace/__init__.py +50 -0
  267. lmcache/v1/mp_observability/trace/codecs.py +255 -0
  268. lmcache/v1/mp_observability/trace/decorator.py +147 -0
  269. lmcache/v1/mp_observability/trace/format.py +132 -0
  270. lmcache/v1/mp_observability/trace/lifecycle.py +83 -0
  271. lmcache/v1/mp_observability/trace/reader.py +167 -0
  272. lmcache/v1/mp_observability/trace/recorder.py +300 -0
  273. lmcache/v1/multiprocess/__init__.py +0 -0
  274. lmcache/v1/multiprocess/affinity_pool.py +102 -0
  275. lmcache/v1/multiprocess/blend_server_v2.py +891 -0
  276. lmcache/v1/multiprocess/config.py +253 -0
  277. lmcache/v1/multiprocess/custom_types.py +281 -0
  278. lmcache/v1/multiprocess/futures.py +194 -0
  279. lmcache/v1/multiprocess/gpu_context.py +511 -0
  280. lmcache/v1/multiprocess/http_server.py +235 -0
  281. lmcache/v1/multiprocess/mp_runtime_plugin_launcher.py +130 -0
  282. lmcache/v1/multiprocess/mq.py +732 -0
  283. lmcache/v1/multiprocess/protocol.py +86 -0
  284. lmcache/v1/multiprocess/protocols/README.md +213 -0
  285. lmcache/v1/multiprocess/protocols/__init__.py +127 -0
  286. lmcache/v1/multiprocess/protocols/base.py +89 -0
  287. lmcache/v1/multiprocess/protocols/blend.py +109 -0
  288. lmcache/v1/multiprocess/protocols/blend_v2.py +57 -0
  289. lmcache/v1/multiprocess/protocols/controller.py +53 -0
  290. lmcache/v1/multiprocess/protocols/debug.py +34 -0
  291. lmcache/v1/multiprocess/protocols/engine.py +146 -0
  292. lmcache/v1/multiprocess/protocols/observability.py +39 -0
  293. lmcache/v1/multiprocess/server.py +1134 -0
  294. lmcache/v1/multiprocess/session.py +190 -0
  295. lmcache/v1/multiprocess/token_hasher.py +441 -0
  296. lmcache/v1/offload_server/__init__.py +17 -0
  297. lmcache/v1/offload_server/abstract_server.py +37 -0
  298. lmcache/v1/offload_server/message.py +30 -0
  299. lmcache/v1/offload_server/zmq_server.py +122 -0
  300. lmcache/v1/periodic_thread.py +579 -0
  301. lmcache/v1/pin_monitor.py +246 -0
  302. lmcache/v1/plugin/__init__.py +0 -0
  303. lmcache/v1/plugin/runtime_plugin_launcher.py +211 -0
  304. lmcache/v1/protocol.py +317 -0
  305. lmcache/v1/rpc/__init__.py +17 -0
  306. lmcache/v1/rpc/transport.py +105 -0
  307. lmcache/v1/rpc/zmq_transport.py +213 -0
  308. lmcache/v1/rpc_utils.py +165 -0
  309. lmcache/v1/server/__init__.py +2 -0
  310. lmcache/v1/server/__main__.py +170 -0
  311. lmcache/v1/server/storage_backend/__init__.py +21 -0
  312. lmcache/v1/server/storage_backend/abstract_backend.py +80 -0
  313. lmcache/v1/server/storage_backend/local_backend.py +75 -0
  314. lmcache/v1/server/utils.py +21 -0
  315. lmcache/v1/standalone/__init__.py +1 -0
  316. lmcache/v1/standalone/__main__.py +583 -0
  317. lmcache/v1/standalone/manager.py +80 -0
  318. lmcache/v1/standalone/standalone_service_factory.py +86 -0
  319. lmcache/v1/storage_backend/__init__.py +313 -0
  320. lmcache/v1/storage_backend/abstract_backend.py +445 -0
  321. lmcache/v1/storage_backend/audit_backend.py +233 -0
  322. lmcache/v1/storage_backend/batched_message_sender.py +222 -0
  323. lmcache/v1/storage_backend/cache_policy/__init__.py +45 -0
  324. lmcache/v1/storage_backend/cache_policy/base_policy.py +87 -0
  325. lmcache/v1/storage_backend/cache_policy/fifo.py +58 -0
  326. lmcache/v1/storage_backend/cache_policy/lfu.py +105 -0
  327. lmcache/v1/storage_backend/cache_policy/lru.py +81 -0
  328. lmcache/v1/storage_backend/cache_policy/mru.py +61 -0
  329. lmcache/v1/storage_backend/connector/__init__.py +443 -0
  330. lmcache/v1/storage_backend/connector/audit_adapter.py +77 -0
  331. lmcache/v1/storage_backend/connector/audit_connector.py +320 -0
  332. lmcache/v1/storage_backend/connector/base_connector.py +379 -0
  333. lmcache/v1/storage_backend/connector/blackhole_adapter.py +21 -0
  334. lmcache/v1/storage_backend/connector/blackhole_connector.py +37 -0
  335. lmcache/v1/storage_backend/connector/eic_adapter.py +31 -0
  336. lmcache/v1/storage_backend/connector/eic_connector.py +757 -0
  337. lmcache/v1/storage_backend/connector/external_adapter.py +79 -0
  338. lmcache/v1/storage_backend/connector/fs_adapter.py +51 -0
  339. lmcache/v1/storage_backend/connector/fs_connector.py +403 -0
  340. lmcache/v1/storage_backend/connector/infinistore_adapter.py +56 -0
  341. lmcache/v1/storage_backend/connector/infinistore_connector.py +177 -0
  342. lmcache/v1/storage_backend/connector/instrumented_connector.py +219 -0
  343. lmcache/v1/storage_backend/connector/lm_adapter.py +31 -0
  344. lmcache/v1/storage_backend/connector/lm_connector.py +176 -0
  345. lmcache/v1/storage_backend/connector/mock_adapter.py +57 -0
  346. lmcache/v1/storage_backend/connector/mock_connector.py +349 -0
  347. lmcache/v1/storage_backend/connector/mooncakestore_adapter.py +43 -0
  348. lmcache/v1/storage_backend/connector/mooncakestore_connector.py +614 -0
  349. lmcache/v1/storage_backend/connector/redis_adapter.py +181 -0
  350. lmcache/v1/storage_backend/connector/redis_connector.py +828 -0
  351. lmcache/v1/storage_backend/connector/s3_adapter.py +59 -0
  352. lmcache/v1/storage_backend/connector/s3_connector.py +699 -0
  353. lmcache/v1/storage_backend/connector/sagemaker_hyperpod_adapter.py +233 -0
  354. lmcache/v1/storage_backend/connector/sagemaker_hyperpod_connector.py +987 -0
  355. lmcache/v1/storage_backend/connector/valkey_adapter.py +114 -0
  356. lmcache/v1/storage_backend/connector/valkey_connector.py +627 -0
  357. lmcache/v1/storage_backend/gds_backend.py +1199 -0
  358. lmcache/v1/storage_backend/job_executor/__init__.py +0 -0
  359. lmcache/v1/storage_backend/job_executor/base_executor.py +34 -0
  360. lmcache/v1/storage_backend/job_executor/pq_executor.py +235 -0
  361. lmcache/v1/storage_backend/local_cpu_backend.py +810 -0
  362. lmcache/v1/storage_backend/local_disk_backend.py +656 -0
  363. lmcache/v1/storage_backend/maru_backend.py +734 -0
  364. lmcache/v1/storage_backend/naive_serde/__init__.py +50 -0
  365. lmcache/v1/storage_backend/naive_serde/cachegen_basics.py +133 -0
  366. lmcache/v1/storage_backend/naive_serde/cachegen_decoder.py +135 -0
  367. lmcache/v1/storage_backend/naive_serde/cachegen_encoder.py +83 -0
  368. lmcache/v1/storage_backend/naive_serde/kivi_serde.py +22 -0
  369. lmcache/v1/storage_backend/naive_serde/naive_serde.py +18 -0
  370. lmcache/v1/storage_backend/naive_serde/serde.py +37 -0
  371. lmcache/v1/storage_backend/native_clients/connector_client_base.py +165 -0
  372. lmcache/v1/storage_backend/native_clients/resp_client.py +35 -0
  373. lmcache/v1/storage_backend/nixl_storage_backend.py +1400 -0
  374. lmcache/v1/storage_backend/p2p_backend.py +788 -0
  375. lmcache/v1/storage_backend/path_sharder.py +117 -0
  376. lmcache/v1/storage_backend/pd_backend.py +646 -0
  377. lmcache/v1/storage_backend/plugins/dax_backend.py +1443 -0
  378. lmcache/v1/storage_backend/plugins/rust_raw_block_backend.py +1361 -0
  379. lmcache/v1/storage_backend/remote_backend.py +624 -0
  380. lmcache/v1/storage_backend/resp_client.py +227 -0
  381. lmcache/v1/storage_backend/storage_backend_listener.py +19 -0
  382. lmcache/v1/storage_backend/storage_manager.py +1352 -0
  383. lmcache/v1/system_detection.py +110 -0
  384. lmcache/v1/token_database.py +551 -0
  385. lmcache/v1/transfer_channel/__init__.py +83 -0
  386. lmcache/v1/transfer_channel/abstract.py +285 -0
  387. lmcache/v1/transfer_channel/mock_memory_channel.py +156 -0
  388. lmcache/v1/transfer_channel/nixl_channel.py +639 -0
  389. lmcache/v1/transfer_channel/py_socket_channel.py +260 -0
  390. lmcache/v1/transfer_channel/transfer_utils.py +63 -0
  391. lmcache/v1/utils/__init__.py +1 -0
  392. lmcache/v1/utils/bloom_filter.py +109 -0
  393. lmcache/v1/utils/cache_utils.py +125 -0
  394. lmcache_cli-0.4.5.dev0.dist-info/METADATA +185 -0
  395. lmcache_cli-0.4.5.dev0.dist-info/RECORD +399 -0
  396. lmcache_cli-0.4.5.dev0.dist-info/WHEEL +5 -0
  397. lmcache_cli-0.4.5.dev0.dist-info/entry_points.txt +2 -0
  398. lmcache_cli-0.4.5.dev0.dist-info/licenses/LICENSE +201 -0
  399. lmcache_cli-0.4.5.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,249 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ """Dispatcher mapping recorded ``qualname`` strings to live callables.
4
+
5
+ The recorder writes one :class:`~lmcache.v1.mp_observability.trace.format.Record`
6
+ per decorated call, tagged by the function's fully-qualified name.
7
+ The dispatcher translates those strings back into concrete calls on a
8
+ live :class:`~lmcache.v1.distributed.storage_manager.StorageManager`.
9
+
10
+ Adding support for a new traced operation is a two-line change: put
11
+ ``@enable_tracing`` on the function, then register a handler here with
12
+ a matching ``qualname``. No per-op schemas, no new event types.
13
+ """
14
+
15
+ # Future
16
+ from __future__ import annotations
17
+
18
+ # Standard
19
+ from collections import deque
20
+ from contextlib import AbstractContextManager
21
+ from dataclasses import dataclass, field
22
+ from typing import Any, Callable
23
+
24
+ # First Party
25
+ from lmcache.logging import init_logger
26
+ from lmcache.v1.distributed.api import ObjectKey
27
+ from lmcache.v1.distributed.storage_manager import StorageManager
28
+
29
+ logger = init_logger(__name__)
30
+
31
+ #: Fully-qualified name of the ``StorageManager`` class. Used to
32
+ #: build the qualnames of all its traced methods. Kept as a constant
33
+ #: so tests and dispatcher registrations stay in lock-step if the class
34
+ #: is ever renamed or moved.
35
+ _SM_PREFIX = "lmcache.v1.distributed.storage_manager.StorageManager"
36
+
37
+
38
+ @dataclass
39
+ class ReplayContext:
40
+ """State carried across dispatcher invocations during one replay.
41
+
42
+ The driver creates exactly one ``ReplayContext`` per trace file,
43
+ hands it to every dispatched handler, and closes the StorageManager
44
+ when replay finishes.
45
+
46
+ Attributes:
47
+ sm: The live StorageManager that receives replayed calls.
48
+ open_read_contexts: FIFO queue of
49
+ ``read_prefetched_results`` contexts entered but not yet
50
+ exited. Matching ``__enter__``/``__exit__`` records are
51
+ popped in the order they were entered for the same
52
+ ``keys`` tuple. A dict-of-deques keyed on
53
+ ``tuple(keys)`` supports interleaved contexts across
54
+ different key sets.
55
+ """
56
+
57
+ sm: StorageManager
58
+ open_read_contexts: dict[tuple[ObjectKey, ...], deque[AbstractContextManager]] = (
59
+ field(default_factory=dict)
60
+ )
61
+
62
+
63
+ #: Type of a dispatcher handler: takes a :class:`ReplayContext` and an
64
+ #: already-decoded ``args`` dict (keys = parameter names, values =
65
+ #: native Python values restored by
66
+ #: :mod:`lmcache.v1.mp_observability.trace.codecs`). Handlers return
67
+ #: nothing; any return value from the live call is discarded.
68
+ Handler = Callable[[ReplayContext, dict[str, Any]], None]
69
+
70
+
71
+ class CallDispatcher:
72
+ """Registry mapping recorded qualnames to replay handlers.
73
+
74
+ Handlers are plain callables — no per-op subclass hierarchy. The
75
+ default factory :func:`build_default_dispatcher` populates the
76
+ registry with every v1 ``StorageManager`` operation.
77
+ """
78
+
79
+ def __init__(self) -> None:
80
+ self._handlers: dict[str, Handler] = {}
81
+
82
+ def register(self, qualname: str, handler: Handler) -> None:
83
+ """Register a handler for *qualname*.
84
+
85
+ Args:
86
+ qualname: Fully-qualified call-site name exactly matching
87
+ the ``qualname`` field written by the recorder.
88
+ handler: Callable invoked on each matching record.
89
+
90
+ Raises:
91
+ ValueError: If a handler is already registered for
92
+ *qualname*.
93
+ """
94
+ if qualname in self._handlers:
95
+ raise ValueError(f"handler already registered for {qualname!r}")
96
+ self._handlers[qualname] = handler
97
+
98
+ def has(self, qualname: str) -> bool:
99
+ """Return ``True`` if a handler is registered for *qualname*."""
100
+ return qualname in self._handlers
101
+
102
+ def registered_qualnames(self) -> list[str]:
103
+ """Return the list of currently registered qualnames.
104
+
105
+ Returns:
106
+ A new list of qualname strings, suitable for inspection or
107
+ assertion in tests.
108
+ """
109
+ return list(self._handlers)
110
+
111
+ def dispatch(
112
+ self,
113
+ qualname: str,
114
+ context: ReplayContext,
115
+ args: dict[str, Any],
116
+ ) -> None:
117
+ """Invoke the handler for *qualname*.
118
+
119
+ Args:
120
+ qualname: The recorded qualname.
121
+ context: Replay context passed through to the handler.
122
+ args: Decoded argument dict for the call.
123
+
124
+ Raises:
125
+ KeyError: If no handler is registered for *qualname*. The
126
+ driver catches this and logs a warning so unknown
127
+ qualnames (e.g. from a future trace level) do not stop
128
+ replay.
129
+ """
130
+ handler = self._handlers.get(qualname)
131
+ if handler is None:
132
+ raise KeyError(qualname)
133
+ handler(context, args)
134
+
135
+
136
+ # ---------------------------------------------------------------------------
137
+ # Default handlers for v1 StorageManager operations
138
+ # ---------------------------------------------------------------------------
139
+
140
+
141
+ def _call_sm_method(method_name: str) -> Handler:
142
+ """Build a handler that forwards a record to ``sm.<method_name>``.
143
+
144
+ The returned callable invokes ``getattr(ctx.sm, method_name)(**args)``
145
+ and discards the result. Used for every "plain" traced method on
146
+ StorageManager — ``reserve_write``, ``finish_write``,
147
+ ``submit_prefetch_task``, ``finish_read_prefetched``.
148
+
149
+ Args:
150
+ method_name: Attribute name on the live StorageManager.
151
+
152
+ Returns:
153
+ A :data:`Handler` closure.
154
+ """
155
+
156
+ def _handler(ctx: ReplayContext, args: dict[str, Any]) -> None:
157
+ method = getattr(ctx.sm, method_name)
158
+ method(**args)
159
+
160
+ _handler.__name__ = f"_call_sm_{method_name}"
161
+ return _handler
162
+
163
+
164
+ def _enter_read_prefetched(ctx: ReplayContext, args: dict[str, Any]) -> None:
165
+ """Handle a ``read_prefetched_results.__enter__`` record.
166
+
167
+ Enters the live context manager and stashes it under
168
+ ``tuple(keys)``. The matching ``__exit__`` handler pops the top
169
+ entry for that key tuple, preserving FIFO order when multiple
170
+ contexts are simultaneously open for identical key lists.
171
+
172
+ Args:
173
+ ctx: Active replay context.
174
+ args: Decoded record arguments. Must contain ``"keys"``.
175
+ """
176
+ keys = args["keys"]
177
+ cm = ctx.sm.read_prefetched_results(keys)
178
+ cm.__enter__()
179
+ key_tuple = tuple(keys)
180
+ ctx.open_read_contexts.setdefault(key_tuple, deque()).append(cm)
181
+
182
+
183
+ def _exit_read_prefetched(ctx: ReplayContext, args: dict[str, Any]) -> None:
184
+ """Handle a ``read_prefetched_results.__exit__`` record.
185
+
186
+ Pops and exits the oldest context opened for ``tuple(keys)``. If
187
+ no matching open context exists — typically because the trace was
188
+ truncated between the enter and exit events — logs a warning and
189
+ continues so replay does not abort.
190
+
191
+ Args:
192
+ ctx: Active replay context.
193
+ args: Decoded record arguments. Must contain ``"keys"``.
194
+ """
195
+ keys = args["keys"]
196
+ key_tuple = tuple(keys)
197
+ pending = ctx.open_read_contexts.get(key_tuple)
198
+ if not pending:
199
+ logger.warning(
200
+ "trace replay: read_prefetched_results.__exit__ with no "
201
+ "matching __enter__ (keys=%d); ignoring",
202
+ len(keys),
203
+ )
204
+ return
205
+ cm = pending.popleft()
206
+ if not pending:
207
+ del ctx.open_read_contexts[key_tuple]
208
+ # Pass a clean exit — replay does not reproduce caller-side
209
+ # exceptions. The context manager's ``finally`` block runs
210
+ # regardless, so read locks are released.
211
+ cm.__exit__(None, None, None)
212
+
213
+
214
+ def build_default_dispatcher() -> CallDispatcher:
215
+ """Return a :class:`CallDispatcher` populated with v1 handlers.
216
+
217
+ Covers every qualname the storage-level recorder emits:
218
+
219
+ * ``StorageManager.reserve_write``
220
+ * ``StorageManager.finish_write``
221
+ * ``StorageManager.submit_prefetch_task``
222
+ * ``StorageManager.finish_read_prefetched``
223
+ * ``StorageManager.read_prefetched_results.__enter__``
224
+ * ``StorageManager.read_prefetched_results.__exit__``
225
+
226
+ Returns:
227
+ A ready-to-use dispatcher. Callers may further register
228
+ additional handlers on it for future trace levels.
229
+ """
230
+ dispatcher = CallDispatcher()
231
+ for method_name in (
232
+ "reserve_write",
233
+ "finish_write",
234
+ "submit_prefetch_task",
235
+ "finish_read_prefetched",
236
+ ):
237
+ dispatcher.register(
238
+ f"{_SM_PREFIX}.{method_name}",
239
+ _call_sm_method(method_name),
240
+ )
241
+ dispatcher.register(
242
+ f"{_SM_PREFIX}.read_prefetched_results.__enter__",
243
+ _enter_read_prefetched,
244
+ )
245
+ dispatcher.register(
246
+ f"{_SM_PREFIX}.read_prefetched_results.__exit__",
247
+ _exit_read_prefetched,
248
+ )
249
+ return dispatcher
@@ -0,0 +1,372 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ """High-level replay driver for storage-level traces.
4
+
5
+ Usage::
6
+
7
+ driver = StorageReplayDriver(sm_config, trace_path)
8
+ result = driver.run()
9
+ driver.close()
10
+
11
+ The driver:
12
+
13
+ 1. Opens the trace file via :class:`TraceReader`.
14
+ 2. Constructs a fresh :class:`StorageManager` from the supplied
15
+ :class:`StorageManagerConfig`. Notably, the replay-side config is
16
+ chosen by the *caller*, not copied from the trace's header. This
17
+ lets the same trace exercise different L1/L2 configurations —
18
+ e.g., record with a Redis L2 adapter and replay with a
19
+ local-filesystem adapter.
20
+ 3. Iterates records, decodes their argument dicts via the trace
21
+ codec registry, and dispatches each to a live StorageManager call
22
+ through a :class:`CallDispatcher`. Each dispatch is aligned to
23
+ the recorded ``t_mono`` offset via ``time.sleep`` — the replay
24
+ never runs ahead of the recording. Dispatching as-fast-as-
25
+ possible is unsafe because reads and writes are async in
26
+ ``StorageManager`` and carry cross-call dependencies; collapsing
27
+ the recorded gaps races the async queues.
28
+ 4. Records per-qualname timings into a :class:`ReplayStatsCollector`.
29
+
30
+ Replay is deliberately single-threaded: the recorder captures calls
31
+ in the order the EventBus drained them, which is already a
32
+ linearization of the concurrent production calls. Replaying
33
+ in that same order preserves the observed interleaving without
34
+ needing to reconstruct thread identities.
35
+ """
36
+
37
+ # Future
38
+ from __future__ import annotations
39
+
40
+ # Standard
41
+ from dataclasses import dataclass
42
+ from typing import TYPE_CHECKING, Callable
43
+ import hashlib
44
+ import json
45
+ import time
46
+
47
+ # First Party
48
+ from lmcache.cli.commands.trace.dispatch import (
49
+ CallDispatcher,
50
+ ReplayContext,
51
+ build_default_dispatcher,
52
+ )
53
+ from lmcache.cli.commands.trace.stats import ReplayStatsCollector
54
+ from lmcache.logging import init_logger
55
+ from lmcache.v1.distributed.config import StorageManagerConfig
56
+ from lmcache.v1.distributed.storage_manager import StorageManager
57
+ from lmcache.v1.mp_observability.config import (
58
+ ObservabilityConfig,
59
+ init_observability,
60
+ )
61
+ from lmcache.v1.mp_observability.trace import codecs
62
+ from lmcache.v1.mp_observability.trace.reader import TraceReader
63
+ from lmcache.v1.mp_observability.trace.recorder import safe_storage_config_dict
64
+
65
+ if TYPE_CHECKING:
66
+ # First Party
67
+ from lmcache.v1.mp_observability.event_bus import EventBus
68
+
69
+ logger = init_logger(__name__)
70
+
71
+ #: Default :class:`ObservabilityConfig` for replay sessions.
72
+ #:
73
+ #: Enables the EventBus and its logging subscribers so users can see
74
+ #: SM/L1/L2 log output during replay. Metrics and tracing are off:
75
+ #: the OTel / Prometheus pipelines bind a port, which would collide
76
+ #: across concurrent replays. Declared at module scope to avoid
77
+ #: Ruff B008 (mutable default argument).
78
+ DEFAULT_REPLAY_OBS_CONFIG: ObservabilityConfig = ObservabilityConfig(
79
+ enabled=True,
80
+ metrics_enabled=False,
81
+ logging_enabled=True,
82
+ tracing_enabled=False,
83
+ )
84
+
85
+
86
+ @dataclass
87
+ class ReplayResult:
88
+ """Summary returned from :meth:`StorageReplayDriver.run`.
89
+
90
+ Attributes:
91
+ records_replayed: Successful dispatches.
92
+ records_skipped: Records with no registered handler (likely
93
+ from a newer trace level).
94
+ records_failed: Records whose handler raised.
95
+ stats: The per-qualname timing collector. Exposed so the
96
+ caller can export CSV/JSON or inspect individual
97
+ percentiles.
98
+ header_level: ``level`` field read from the trace header.
99
+ header_digest: ``sm_config_digest`` from the trace header.
100
+ replay_config_digest: SHA-256 of the replay-side
101
+ StorageManagerConfig, for mismatch comparisons. Empty
102
+ string if the driver could not compute it.
103
+ """
104
+
105
+ records_replayed: int
106
+ records_skipped: int
107
+ records_failed: int
108
+ stats: ReplayStatsCollector
109
+ header_level: str
110
+ header_digest: str
111
+ replay_config_digest: str
112
+
113
+
114
+ class StorageReplayDriver:
115
+ """Replays a storage-level trace against a live StorageManager.
116
+
117
+ The driver owns the StorageManager for its lifetime; call
118
+ :meth:`close` (or use as a context manager) to shut it down.
119
+ """
120
+
121
+ def __init__(
122
+ self,
123
+ sm_config: StorageManagerConfig,
124
+ trace_path: str,
125
+ dispatcher: CallDispatcher | None = None,
126
+ obs_config: ObservabilityConfig = DEFAULT_REPLAY_OBS_CONFIG,
127
+ ) -> None:
128
+ """Construct a driver.
129
+
130
+ Initializes the global observability EventBus **before**
131
+ constructing the StorageManager so internal events
132
+ (L0/L1/L2 lifecycle, eviction, etc.) flow through a live bus
133
+ during replay. This lets the same logging and monitoring
134
+ subscribers that run in the real server attach to the replay
135
+ session — e.g. operators can eyeball L1/L2 log output to
136
+ spot eviction churn or L2 bottlenecks.
137
+
138
+ Metrics (OTel / Prometheus) are **off by default** because
139
+ the metrics pipeline binds a Prometheus port; running two
140
+ replays concurrently with the same port would fail. Callers
141
+ who want metrics can pass their own ``obs_config``.
142
+
143
+ Args:
144
+ sm_config: Replay-side StorageManager configuration.
145
+ Determines L1 size, eviction policy, and L2 adapters
146
+ used during replay. Typically different from the
147
+ recording-side config (the original deployment may
148
+ have used adapters unavailable on the replay host).
149
+ trace_path: Path to a ``.lct`` trace file written by
150
+ :class:`StorageTraceRecorder`.
151
+ dispatcher: Custom dispatcher. When omitted, a fresh
152
+ :func:`build_default_dispatcher` is used; passing one
153
+ explicitly is useful for tests that register extra
154
+ handlers.
155
+ obs_config: Observability configuration for the replay
156
+ session. Defaults to an enabled bus with logging
157
+ subscribers and no metrics/tracing. The driver
158
+ installs this config as the global singleton via
159
+ :func:`init_observability` and stops the resulting
160
+ bus on :meth:`close`.
161
+ """
162
+ self._sm_config = sm_config
163
+ self._trace_path = trace_path
164
+ self._dispatcher = dispatcher or build_default_dispatcher()
165
+ self._closed = False
166
+
167
+ # Each resource is acquired under its own try/except so that a
168
+ # failure partway through __init__ still releases what has
169
+ # already been opened. Without this, ``__exit__`` / ``close``
170
+ # never runs (the caller never got a valid instance), and the
171
+ # reader / bus would leak — see Cursor bugbot comment on
172
+ # PR #3075.
173
+ reader: TraceReader | None = None
174
+ bus: EventBus | None = None
175
+ try:
176
+ reader = TraceReader(trace_path)
177
+ bus = init_observability(obs_config)
178
+ self._sm = StorageManager(sm_config)
179
+ except BaseException:
180
+ # ``BaseException`` to also cover KeyboardInterrupt /
181
+ # SystemExit raised from inside StorageManager setup —
182
+ # the resources below must still be released.
183
+ if bus is not None:
184
+ try:
185
+ bus.stop()
186
+ except Exception:
187
+ logger.warning(
188
+ "trace replay: error stopping bus during failed driver init",
189
+ exc_info=True,
190
+ )
191
+ if reader is not None:
192
+ try:
193
+ reader.close()
194
+ except Exception:
195
+ logger.warning(
196
+ "trace replay: error closing reader during failed driver init",
197
+ exc_info=True,
198
+ )
199
+ raise
200
+
201
+ self._reader = reader
202
+ self._bus = bus
203
+
204
+ def __enter__(self) -> StorageReplayDriver:
205
+ return self
206
+
207
+ def __exit__(self, *_exc: object) -> None:
208
+ self.close()
209
+
210
+ # ------------------------------------------------------------------
211
+
212
+ @property
213
+ def trace_path(self) -> str:
214
+ """Path of the trace file being replayed."""
215
+ return self._trace_path
216
+
217
+ @property
218
+ def storage_manager(self) -> StorageManager:
219
+ """The live StorageManager driving replay.
220
+
221
+ Exposed primarily for tests that need to introspect residency
222
+ or eviction state after ``run()`` returns.
223
+ """
224
+ return self._sm
225
+
226
+ def close(self) -> None:
227
+ """Close the StorageManager, stop the bus, and close the reader.
228
+
229
+ Idempotent. The order matters: the StorageManager may
230
+ publish teardown events, so the bus outlives it briefly.
231
+ """
232
+ if self._closed:
233
+ return
234
+ self._closed = True
235
+ try:
236
+ self._sm.close()
237
+ finally:
238
+ try:
239
+ self._bus.stop()
240
+ finally:
241
+ self._reader.close()
242
+
243
+ # ------------------------------------------------------------------
244
+
245
+ def run(
246
+ self,
247
+ on_record: RecordCallback | None = None,
248
+ ) -> ReplayResult:
249
+ """Replay every record in the trace.
250
+
251
+ Dispatch is always paced to the recorded ``t_mono`` offsets
252
+ via ``time.sleep``: the replay never runs *ahead* of the
253
+ recording. Running ahead is unsafe because ``StorageManager``
254
+ reads and writes are async — collapsing the recorded gaps
255
+ races the async queues and leads to retrieve misses. If the
256
+ replay host is slower than recording, the loop simply lags
257
+ the recorded schedule.
258
+
259
+ Args:
260
+ on_record: Optional per-record callback invoked after
261
+ dispatch with ``(qualname, latency_s, failed)``.
262
+ Used by the CLI's ``--jsonl-out`` feature.
263
+
264
+ Returns:
265
+ A :class:`ReplayResult` summarizing the run.
266
+ """
267
+ stats = ReplayStatsCollector()
268
+ context = ReplayContext(sm=self._sm)
269
+ header = self._reader.header
270
+ t_start = time.time()
271
+ stats.mark_start(t_start)
272
+
273
+ replayed = skipped = failed = 0
274
+ t_wall_origin = time.monotonic()
275
+
276
+ for record in self._reader.records():
277
+ # Sleep just long enough to align to the recorded
278
+ # offset from the start of replay. No speedup — if
279
+ # the replay machine is slower than recording, the
280
+ # loop simply runs behind.
281
+ target = t_wall_origin + record.t_mono
282
+ now = time.monotonic()
283
+ if now < target:
284
+ time.sleep(target - now)
285
+
286
+ try:
287
+ decoded_args = codecs.decode_args(record.args)
288
+ except Exception:
289
+ skipped += 1
290
+ logger.warning(
291
+ "trace replay: failed to decode args for %s; skipping",
292
+ record.qualname,
293
+ exc_info=True,
294
+ )
295
+ if on_record is not None:
296
+ on_record(record.qualname, 0.0, True)
297
+ continue
298
+
299
+ if not self._dispatcher.has(record.qualname):
300
+ skipped += 1
301
+ logger.warning(
302
+ "trace replay: no handler for qualname %r; skipping",
303
+ record.qualname,
304
+ )
305
+ if on_record is not None:
306
+ on_record(record.qualname, 0.0, True)
307
+ continue
308
+
309
+ t0 = time.monotonic()
310
+ try:
311
+ self._dispatcher.dispatch(record.qualname, context, decoded_args)
312
+ latency = time.monotonic() - t0
313
+ stats.record(record.qualname, latency, failed=False)
314
+ replayed += 1
315
+ if on_record is not None:
316
+ on_record(record.qualname, latency, False)
317
+ except Exception:
318
+ latency = time.monotonic() - t0
319
+ stats.record(record.qualname, latency, failed=True)
320
+ failed += 1
321
+ logger.warning(
322
+ "trace replay: handler for %s raised",
323
+ record.qualname,
324
+ exc_info=True,
325
+ )
326
+ if on_record is not None:
327
+ on_record(record.qualname, latency, True)
328
+
329
+ # Close any contexts the trace left open (truncated trace,
330
+ # or missing __exit__ records). Releasing these keeps the
331
+ # StorageManager in a consistent state for a follow-up run
332
+ # or inspection.
333
+ for key_tuple, pending in list(context.open_read_contexts.items()):
334
+ while pending:
335
+ cm = pending.popleft()
336
+ try:
337
+ cm.__exit__(None, None, None)
338
+ except Exception:
339
+ logger.warning(
340
+ "trace replay: forced exit of dangling "
341
+ "read_prefetched_results raised (keys=%d)",
342
+ len(key_tuple),
343
+ exc_info=True,
344
+ )
345
+ context.open_read_contexts.pop(key_tuple, None)
346
+
347
+ stats.mark_end(time.time())
348
+
349
+ # Digest of the replay-side config so callers can compare
350
+ # against ``header.sm_config_digest``. Same hashing used by
351
+ # the recorder — see :func:`safe_storage_config_dict`.
352
+ safe = safe_storage_config_dict(self._sm_config)
353
+ replay_digest = hashlib.sha256(
354
+ json.dumps(safe, sort_keys=True).encode("utf-8")
355
+ ).hexdigest()
356
+
357
+ return ReplayResult(
358
+ records_replayed=replayed,
359
+ records_skipped=skipped,
360
+ records_failed=failed,
361
+ stats=stats,
362
+ header_level=header.level,
363
+ header_digest=header.sm_config_digest,
364
+ replay_config_digest=replay_digest,
365
+ )
366
+
367
+
368
+ #: Callback signature for per-record hooks during replay. Arguments
369
+ #: are ``(qualname, latency_seconds, failed)``. Declared at module
370
+ #: scope so callers can type-annotate their hooks without importing
371
+ #: the driver class.
372
+ RecordCallback = Callable[[str, float, bool], None]