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,699 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Standard
3
+ from enum import IntEnum, auto
4
+ from typing import List, Optional
5
+ from urllib.parse import quote as url_quote
6
+ import asyncio
7
+ import ctypes
8
+
9
+ # Third Party
10
+ from awscrt import auth, io, s3
11
+ from awscrt.http import HttpHeaders, HttpRequest
12
+ from awscrt.io import ClientTlsContext, TlsConnectionOptions, TlsContextOptions
13
+
14
+ # First Party
15
+ from lmcache.logging import init_logger
16
+ from lmcache.utils import CacheEngineKey
17
+ from lmcache.v1.memory_management import MemoryObj
18
+ from lmcache.v1.storage_backend.connector.base_connector import RemoteConnector
19
+ from lmcache.v1.storage_backend.job_executor.pq_executor import AsyncPQExecutor
20
+ from lmcache.v1.storage_backend.local_cpu_backend import LocalCPUBackend
21
+
22
+ logger = init_logger(__name__)
23
+
24
+
25
+ class Priorities(IntEnum):
26
+ PEEK = auto()
27
+ PREFETCH = auto()
28
+ GET = auto()
29
+ PUT = auto()
30
+
31
+
32
+ # zero copy helper for S3 upload
33
+ class MemoryViewStream:
34
+ def __init__(self, mv: bytes):
35
+ # casting does not copy
36
+ # we just get a uint8 view
37
+ self.mv = memoryview(mv).cast("B")
38
+ self.offset = 0
39
+
40
+ def read(self, size=None):
41
+ if size is None:
42
+ size = len(self.mv) - self.offset
43
+ if size < 0:
44
+ size = 0
45
+
46
+ end = min(self.offset + size, len(self.mv))
47
+ result = self.mv[self.offset : end]
48
+ self.offset = end
49
+ # CRT/Python accepts memoryview
50
+ return result
51
+
52
+ def seek(self, offset, whence=0):
53
+ if whence == 0:
54
+ self.offset = offset
55
+ elif whence == 1:
56
+ self.offset += offset
57
+ elif whence == 2:
58
+ self.offset = len(self.mv) + offset
59
+ return self.offset
60
+
61
+ def tell(self):
62
+ return self.offset
63
+
64
+ def __len__(self):
65
+ return len(self.mv)
66
+
67
+
68
+ class S3Connector(RemoteConnector):
69
+ """
70
+ S3 remote connector
71
+ """
72
+
73
+ def __init__(
74
+ self,
75
+ s3_endpoint: str,
76
+ loop: asyncio.AbstractEventLoop,
77
+ local_cpu_backend: LocalCPUBackend,
78
+ s3_num_io_threads: int,
79
+ s3_prefer_http2: bool,
80
+ s3_region: str,
81
+ s3_enable_s3express: bool,
82
+ disable_tls: bool,
83
+ aws_access_key_id: Optional[str] = None,
84
+ aws_secret_access_key: Optional[str] = None,
85
+ ):
86
+ # initialize base class, which includes some common attributes
87
+ super().__init__(local_cpu_backend.config, local_cpu_backend.metadata)
88
+
89
+ if not s3_endpoint.startswith("s3://"):
90
+ raise ValueError("S3 url must start with 's3://'")
91
+
92
+ self.s3_part_size = self.full_chunk_size_bytes
93
+
94
+ self.s3_endpoint = s3_endpoint.removeprefix("s3://")
95
+ self.loop = loop
96
+ self.local_cpu_backend = local_cpu_backend
97
+
98
+ self.s3_num_io_threads = s3_num_io_threads
99
+ self.s3_prefer_http2 = s3_prefer_http2
100
+ self.s3_region = s3_region
101
+ self.s3_enable_s3express = s3_enable_s3express
102
+
103
+ event_loop_group = io.EventLoopGroup(s3_num_io_threads)
104
+ host_resolver = io.DefaultHostResolver(event_loop_group)
105
+ client_bootstrap = io.ClientBootstrap(event_loop_group, host_resolver)
106
+ if aws_access_key_id and aws_secret_access_key:
107
+ logger.info("Using explicit AWS credentials passed to S3Connector")
108
+ self.credentials_provider = auth.AwsCredentialsProvider.new_static(
109
+ aws_access_key_id,
110
+ aws_secret_access_key,
111
+ )
112
+ else:
113
+ logger.info(
114
+ "No credentials provider, trying to use credentials from environment"
115
+ )
116
+ self.credentials_provider = auth.AwsCredentialsProvider.new_default_chain(
117
+ client_bootstrap
118
+ )
119
+
120
+ tls_opts = None
121
+
122
+ if self.s3_prefer_http2:
123
+ # Use HTTP/2 multiplexing if possible.
124
+ tls_ctx = ClientTlsContext(TlsContextOptions())
125
+ tls_opts = TlsConnectionOptions(tls_ctx)
126
+ try:
127
+ tls_opts.set_alpn_list(["h2", "http/1.1"])
128
+ except Exception:
129
+ tls_opts = None
130
+
131
+ signing_config = None
132
+ if self.s3_enable_s3express:
133
+ signing_config = auth.AwsSigningConfig(
134
+ algorithm=auth.AwsSigningAlgorithm.V4_S3EXPRESS,
135
+ region=self.s3_region,
136
+ service="s3",
137
+ credentials_provider=self.credentials_provider,
138
+ )
139
+
140
+ # turn off TLS for non-AWS services
141
+ # regular and directory/express buckets both use TLS by default
142
+ turn_off_tls = (
143
+ s3.S3RequestTlsMode.DISABLED if disable_tls else s3.S3RequestTlsMode.ENABLED
144
+ )
145
+ logger.info("Initializing S3 client")
146
+ self.s3_client = s3.S3Client(
147
+ bootstrap=client_bootstrap,
148
+ region=s3_region,
149
+ enable_s3express=s3_enable_s3express,
150
+ tls_connection_options=tls_opts,
151
+ tls_mode=turn_off_tls,
152
+ signing_config=signing_config,
153
+ )
154
+
155
+ # TODO(Jiayi): We need to handle cache consistency issues in a systematic way
156
+ # across all connectors.
157
+ # We assume S3 cache is never evicted and read-only for now.
158
+ # the object size cache does not need protection because
159
+ # asyncio scheduling is cooperative and not preemptive
160
+ self.object_size_cache: dict[str, int] = {}
161
+
162
+ # Circuit breaker for connection failures
163
+ self.connection_failures = 0
164
+ self.max_connection_failures = 3
165
+ self.connection_disabled = False
166
+
167
+ self.pq_executor = AsyncPQExecutor(loop)
168
+
169
+ def _format_safe_path(self, key_str: str) -> str:
170
+ """
171
+ Generate a safe HTTP path for the S3 key.
172
+ Flattens the key by replacing slashes with underscores and URL-encodes
173
+ any special characters.
174
+ """
175
+ flat_key_str = key_str.replace("/", "_")
176
+ return "/" + url_quote(flat_key_str)
177
+
178
+ # TODO(Jiayi): optimize this with async
179
+ def _get_object_size(self, key_str: str) -> int:
180
+ headers = HttpHeaders()
181
+ headers.add("Host", self.s3_endpoint)
182
+ req = HttpRequest("HEAD", self._format_safe_path(key_str), headers)
183
+
184
+ got = {"len": None, "status": None, "err": None}
185
+
186
+ def on_headers(status_code, headers, **kwargs):
187
+ got["status"] = status_code
188
+ for name, value in headers:
189
+ if name.lower() == "content-length":
190
+ try:
191
+ got["len"] = int(value)
192
+ except Exception:
193
+ pass
194
+
195
+ def on_done(error=None, **kwargs):
196
+ got["err"] = error
197
+
198
+ s3_req = s3.S3Request(
199
+ client=self.s3_client,
200
+ type=s3.S3RequestType.DEFAULT,
201
+ request=req,
202
+ operation_name="HeadObject",
203
+ on_headers=on_headers,
204
+ on_done=on_done,
205
+ credential_provider=self.credentials_provider,
206
+ region=self.s3_region,
207
+ )
208
+
209
+ try:
210
+ s3_req.finished_future.result()
211
+ except Exception as e:
212
+ # 404 (not found) is expected when checking if object exists
213
+ if got["status"] == 404:
214
+ logger.debug(f"Object not found: {key_str}")
215
+ else:
216
+ logger.debug(f"Exception in `_get_object_size`: {e}")
217
+ return 0
218
+ if got["err"] or got["status"] != 200:
219
+ if got["status"] != 404: # Don't warn for 404, it's expected
220
+ logger.warning(
221
+ "Encountering error in S3 HEAD request "
222
+ f"with error code: {got['status']}"
223
+ )
224
+ return 0
225
+ return got["len"] if got["len"] is not None else 0
226
+
227
+ # exactly the same as _get_object_size just awaiting an asyncio.Future
228
+ # instead of a concurrent.futures.Future
229
+ async def _get_object_size_async(self, key_str: str) -> int:
230
+ headers = HttpHeaders()
231
+ headers.add("Host", self.s3_endpoint)
232
+ req = HttpRequest("HEAD", self._format_safe_path(key_str), headers)
233
+
234
+ got = {"len": None, "status": None, "err": None}
235
+
236
+ def on_headers(status_code, headers, **kwargs):
237
+ got["status"] = status_code
238
+ for name, value in headers:
239
+ if name.lower() == "content-length":
240
+ try:
241
+ got["len"] = int(value)
242
+ except Exception:
243
+ pass
244
+
245
+ def on_done(error=None, **kwargs):
246
+ got["err"] = error
247
+
248
+ s3_req = s3.S3Request(
249
+ client=self.s3_client,
250
+ type=s3.S3RequestType.DEFAULT,
251
+ request=req,
252
+ operation_name="HeadObject",
253
+ on_headers=on_headers,
254
+ on_done=on_done,
255
+ credential_provider=self.credentials_provider,
256
+ region=self.s3_region,
257
+ )
258
+
259
+ try:
260
+ await asyncio.wrap_future(s3_req.finished_future)
261
+ except Exception as e:
262
+ # 404 (not found) is expected when checking if object exists
263
+ if got["status"] == 404:
264
+ logger.debug(f"Object not found: {key_str}")
265
+ else:
266
+ logger.debug(f"Exception in `_get_object_size_async`: {e}")
267
+ return 0
268
+ if got["err"] or got["status"] != 200:
269
+ if got["status"] != 404: # Don't warn for 404, it's expected
270
+ logger.warning(
271
+ "Encountering error in S3 HEAD request "
272
+ f"with error code: {got['status']}"
273
+ )
274
+ return 0
275
+ return got["len"] if got["len"] is not None else 0
276
+
277
+ async def exists(self, key: CacheEngineKey) -> bool:
278
+ return self.exists_sync(key)
279
+
280
+ def exists_sync(self, key: CacheEngineKey) -> bool:
281
+ # Circuit breaker: if connection is disabled, return False
282
+ if self.connection_disabled:
283
+ return False
284
+
285
+ key_str = key.to_string()
286
+ if key_str in self.object_size_cache:
287
+ return self.object_size_cache[key_str] > 0
288
+ cache_size = self._get_object_size(key_str)
289
+ if cache_size > 0:
290
+ self.object_size_cache[key_str] = cache_size
291
+ return True
292
+ return False
293
+
294
+ def _write_mem_obj(self, mem_obj: MemoryObj, data: bytes, offset: int):
295
+ ctypes.memmove(mem_obj.data_ptr + offset, data, len(data))
296
+
297
+ def _s3_download(
298
+ self,
299
+ key_str: str,
300
+ mem_obj: MemoryObj,
301
+ ) -> "s3.S3Request":
302
+ """
303
+ Download a file from S3.
304
+ """
305
+ headers = HttpHeaders()
306
+ headers.add("Host", self.s3_endpoint)
307
+
308
+ # TODO(Jiayi): Enable more finegrained data partition
309
+ # range_header = f"bytes={start_byte}-{end_byte}"
310
+ # headers.add("Range", range_header)
311
+
312
+ req = HttpRequest("GET", self._format_safe_path(key_str), headers)
313
+
314
+ def on_body(chunk, offset, **kwargs):
315
+ # Directly write chunk to the memory object at the correct offset
316
+ self._write_mem_obj(mem_obj, chunk, offset)
317
+
318
+ # NOTE(Jiayi): Run in crt threads (not this thread) with GIL
319
+ # See https://github.com/awslabs/aws-crt-python/blob/4250709624119de1af3ca86816e1a154fcac7cc8/source/common.c#L51
320
+ def on_done(error=None, status_code=None, **kwargs):
321
+ ok = (status_code in (200, 206)) or (status_code is None)
322
+ if error or not ok:
323
+ raise RuntimeError(
324
+ f"Failed to download {key_str} from S3: {error or status_code}"
325
+ )
326
+
327
+ # TODO(Jiayi): Need to support offset to enable zero-copy
328
+ # More concretely, we need to get the shared memory offset.
329
+ s3_req = s3.S3Request(
330
+ client=self.s3_client,
331
+ type=s3.S3RequestType.GET_OBJECT,
332
+ request=req,
333
+ on_body=on_body,
334
+ credential_provider=self.credentials_provider,
335
+ region=self.s3_region,
336
+ on_done=on_done,
337
+ )
338
+
339
+ return s3_req
340
+
341
+ async def get(self, key: CacheEngineKey) -> Optional[MemoryObj]:
342
+ # Circuit breaker: if connection is disabled, return None immediately
343
+ if self.connection_disabled:
344
+ logger.debug(
345
+ f"S3 connection disabled. Skipping download for {key.to_string()}"
346
+ )
347
+ return None
348
+
349
+ key_str = key.to_string()
350
+
351
+ obj_size = self.object_size_cache.get(key_str, None)
352
+
353
+ if obj_size is None:
354
+ obj_size = await self._get_object_size_async(key_str)
355
+ if obj_size <= 0:
356
+ self.object_size_cache[key_str] = 0
357
+ return None
358
+ self.object_size_cache[key_str] = obj_size
359
+
360
+ memory_obj = self.local_cpu_backend.allocate(
361
+ self.meta_shapes,
362
+ self.meta_dtypes,
363
+ self.meta_fmt,
364
+ )
365
+
366
+ if memory_obj is None:
367
+ return None
368
+
369
+ # Check if stored size matches expected size
370
+ if obj_size != memory_obj.get_size():
371
+ logger.error(
372
+ f"Size mismatch for {key_str}: S3 has {obj_size} bytes, "
373
+ f"but current config expects {memory_obj.get_size()} bytes. "
374
+ f"This usually means the data was stored with different chunk_size "
375
+ f"or model configuration. Please use matching config or clear S3."
376
+ )
377
+ memory_obj.ref_count_down()
378
+ return None
379
+
380
+ s3_req = self._s3_download(
381
+ key_str=key_str,
382
+ mem_obj=memory_obj,
383
+ )
384
+
385
+ try:
386
+ # use blocking_timeout_sec in config to control the timeout
387
+ await asyncio.wrap_future(s3_req.finished_future)
388
+
389
+ # Reset failure counter on success
390
+ self._reset_connection_failures()
391
+
392
+ return memory_obj
393
+ except Exception as e:
394
+ error_msg = str(e)
395
+
396
+ # Update connection failures and check if it's a connection error
397
+ is_connection_error = self._update_connection_failures(error_msg)
398
+
399
+ if not is_connection_error:
400
+ # Log non-connection errors
401
+ logger.error(f"Failed to download {key_str} from S3: {e}")
402
+
403
+ memory_obj.ref_count_down()
404
+ return None
405
+
406
+ async def batched_get(
407
+ self, keys: List[CacheEngineKey]
408
+ ) -> List[Optional[MemoryObj]]:
409
+ # Circuit breaker: if connection is disabled, return all None
410
+ if self.connection_disabled:
411
+ logger.debug(
412
+ f"S3 connection disabled. "
413
+ f"Skipping batched download for {len(keys)} keys"
414
+ )
415
+ return [None] * len(keys)
416
+
417
+ memory_objs: List[Optional[MemoryObj]] = []
418
+ futures = []
419
+ future_to_memobj_idx = []
420
+
421
+ for idx, key in enumerate(keys):
422
+ key_str = key.to_string()
423
+
424
+ obj_size = self.object_size_cache.get(key_str, None)
425
+
426
+ if obj_size is None:
427
+ obj_size = await self._get_object_size_async(key_str)
428
+ if obj_size <= 0:
429
+ self.object_size_cache[key_str] = 0
430
+ memory_objs.append(None)
431
+ continue
432
+ self.object_size_cache[key_str] = obj_size
433
+
434
+ memory_obj = self.local_cpu_backend.allocate(
435
+ self.meta_shapes,
436
+ self.meta_dtypes,
437
+ self.meta_fmt,
438
+ )
439
+
440
+ if not memory_obj:
441
+ memory_objs.append(None)
442
+ continue
443
+
444
+ # Check if stored size matches expected size
445
+ if obj_size != memory_obj.get_size():
446
+ logger.error(
447
+ f"Size mismatch for {key_str}: S3 has {obj_size} bytes, "
448
+ f"but current config expects {memory_obj.get_size()} bytes. "
449
+ f"Skipping this key."
450
+ )
451
+ memory_obj.ref_count_down()
452
+ memory_objs.append(None)
453
+ continue
454
+
455
+ memory_objs.append(memory_obj)
456
+
457
+ s3_req = self._s3_download(
458
+ key_str=key_str,
459
+ mem_obj=memory_obj,
460
+ )
461
+ fut = asyncio.wrap_future(s3_req.finished_future)
462
+ futures.append(fut)
463
+ future_to_memobj_idx.append(len(memory_objs) - 1)
464
+
465
+ # Use return_exceptions to prevent one failure from stopping all downloads
466
+ results = await asyncio.gather(*futures, return_exceptions=True)
467
+
468
+ had_success = False
469
+
470
+ for future_idx, result in enumerate(results):
471
+ memobj_idx = future_to_memobj_idx[future_idx]
472
+
473
+ if isinstance(result, Exception):
474
+ error_msg = str(result)
475
+
476
+ is_connection_error = self._update_connection_failures(error_msg)
477
+
478
+ if not is_connection_error:
479
+ # Log non-connection errors
480
+ logger.error(
481
+ f"Failed to download key at index {memobj_idx}: {error_msg}"
482
+ )
483
+ # Release the memory object for failed download
484
+ memobj = memory_objs[memobj_idx]
485
+ if memobj is not None:
486
+ memobj.ref_count_down()
487
+ memory_objs[memobj_idx] = None
488
+ else:
489
+ had_success = True
490
+
491
+ if had_success:
492
+ self._reset_connection_failures()
493
+
494
+ return memory_objs
495
+
496
+ def _s3_upload(
497
+ self,
498
+ key_str: str,
499
+ memory_obj: MemoryObj,
500
+ ) -> "s3.S3Request":
501
+ """
502
+ Upload a file to S3.
503
+ """
504
+ # Zero-copy approach using MemoryViewStream
505
+ stream = MemoryViewStream(memory_obj.byte_array)
506
+ # Calculate total length from the memoryview
507
+ total_len = len(stream)
508
+
509
+ headers = HttpHeaders()
510
+ headers.add("Host", self.s3_endpoint)
511
+ headers.add("Content-Length", str(total_len))
512
+ headers.add("Content-Type", "application/octet-stream")
513
+
514
+ req = HttpRequest(
515
+ "PUT", self._format_safe_path(key_str), headers, body_stream=stream
516
+ )
517
+
518
+ done = {"err": None, "status": None}
519
+
520
+ def on_done(error=None, status_code=None, **kwargs):
521
+ done["err"] = error
522
+ done["status"] = status_code
523
+
524
+ if done["err"] or done["status"] not in (200, 201):
525
+ raise RuntimeError(f"Upload failed in S3Connector: {done}")
526
+
527
+ s3_req = s3.S3Request(
528
+ client=self.s3_client,
529
+ type=s3.S3RequestType.PUT_OBJECT,
530
+ request=req,
531
+ credential_provider=self.credentials_provider,
532
+ region=self.s3_region,
533
+ on_done=on_done,
534
+ )
535
+ return s3_req
536
+
537
+ async def _put(self, key: CacheEngineKey, memory_obj: MemoryObj):
538
+ """
539
+ Store data to S3
540
+ """
541
+ # Circuit breaker: if connection is disabled, just log and return
542
+ if self.connection_disabled:
543
+ logger.debug(
544
+ f"S3 connection disabled due to repeated failures. "
545
+ f"Skipping upload for {key.to_string()}"
546
+ )
547
+ return
548
+
549
+ key_str = key.to_string()
550
+
551
+ # Check if the chunk size matches expected S3 part size
552
+ if memory_obj.get_physical_size() != self.s3_part_size:
553
+ logger.error(
554
+ f"Cannot upload {key_str}: chunk size {memory_obj.get_physical_size()} "
555
+ f"bytes does not match S3 part size {self.s3_part_size} bytes. "
556
+ f"Partial/unfull chunks are not supported."
557
+ )
558
+ return
559
+
560
+ try:
561
+ logger.debug(f"Uploading {key_str} to S3")
562
+ s3_req = self._s3_upload(key_str, memory_obj)
563
+ await asyncio.wrap_future(s3_req.finished_future)
564
+
565
+ self.object_size_cache[key_str] = memory_obj.get_physical_size()
566
+ logger.debug(f"Uploaded {key_str} to S3 successfully")
567
+
568
+ # Reset failure counter on success
569
+ self._reset_connection_failures()
570
+ except Exception as e:
571
+ error_msg = str(e)
572
+
573
+ # Update connection failures and check if it's a connection error
574
+ is_connection_error = self._update_connection_failures(error_msg)
575
+
576
+ if not is_connection_error:
577
+ # Log non-connection errors
578
+ logger.error(f"Failed to upload {key_str} to S3: {e}")
579
+
580
+ async def put(self, key: CacheEngineKey, memory_obj: MemoryObj):
581
+ return await self.pq_executor.submit_job(
582
+ self._put,
583
+ key=key,
584
+ memory_obj=memory_obj,
585
+ priority=Priorities.PUT,
586
+ )
587
+
588
+ def support_batched_async_contains(self) -> bool:
589
+ return True
590
+
591
+ async def _batched_async_contains(
592
+ self, lookup_id: str, keys: List[CacheEngineKey], pin: bool = False
593
+ ) -> int:
594
+ # Circuit breaker: if connection is disabled, return 0
595
+ if self.connection_disabled:
596
+ return 0
597
+
598
+ num_hit_counts = 0
599
+ for key in keys:
600
+ key_str = key.to_string()
601
+ cached_size = self.object_size_cache.get(key_str, None)
602
+ if cached_size is not None:
603
+ if cached_size > 0:
604
+ num_hit_counts += 1
605
+ continue
606
+ else:
607
+ return num_hit_counts
608
+
609
+ obj_size = await self._get_object_size_async(key_str)
610
+ if not obj_size > 0:
611
+ self.object_size_cache[key_str] = 0
612
+ return num_hit_counts
613
+
614
+ self.object_size_cache[key_str] = obj_size
615
+ num_hit_counts += 1
616
+
617
+ return num_hit_counts
618
+
619
+ async def batched_async_contains(
620
+ self, lookup_id: str, keys: List[CacheEngineKey], pin: bool = False
621
+ ) -> int:
622
+ return await self.pq_executor.submit_job(
623
+ self._batched_async_contains,
624
+ lookup_id=lookup_id,
625
+ keys=keys,
626
+ pin=pin,
627
+ priority=Priorities.PEEK,
628
+ )
629
+
630
+ def support_batched_get_non_blocking(self) -> bool:
631
+ return True
632
+
633
+ async def _batched_get_non_blocking(
634
+ self,
635
+ lookup_id: str,
636
+ keys: List[CacheEngineKey],
637
+ ) -> List[MemoryObj]:
638
+ # batched get is already a coroutine
639
+ result = await self.batched_get(keys)
640
+ return [r for r in result if r is not None]
641
+
642
+ async def batched_get_non_blocking(
643
+ self, lookup_id: str, keys: List[CacheEngineKey]
644
+ ) -> List[MemoryObj]:
645
+ return await self.pq_executor.submit_job(
646
+ self._batched_get_non_blocking,
647
+ lookup_id=lookup_id,
648
+ keys=keys,
649
+ priority=Priorities.PREFETCH,
650
+ )
651
+
652
+ async def list(self) -> List[str]:
653
+ raise NotImplementedError
654
+
655
+ def support_ping(self) -> bool:
656
+ return False
657
+
658
+ # TODO(Jiayi): This needs to be implemented.
659
+ async def ping(self) -> int:
660
+ raise NotImplementedError
661
+
662
+ def support_batched_get(self) -> bool:
663
+ return True
664
+
665
+ def _update_connection_failures(self, error_msg: str) -> bool:
666
+ # Check if it's a connection error
667
+ is_connection_error = (
668
+ "CONNECTION_REFUSED" in error_msg
669
+ or "SOCKET" in error_msg
670
+ or "DNS" in error_msg
671
+ or "TIMEOUT" in error_msg
672
+ )
673
+
674
+ if is_connection_error:
675
+ self.connection_failures += 1
676
+ logger.error(
677
+ f"S3 connection error ({self.connection_failures}/"
678
+ f"{self.max_connection_failures}): {error_msg}"
679
+ )
680
+
681
+ if self.connection_failures >= self.max_connection_failures:
682
+ self.connection_disabled = True
683
+ logger.error(
684
+ f"S3 connection disabled after "
685
+ f"{self.max_connection_failures} "
686
+ f"consecutive failures. "
687
+ f"All future S3 operations will be skipped."
688
+ )
689
+
690
+ return is_connection_error
691
+
692
+ def _reset_connection_failures(self):
693
+ """Reset connection failure counter on successful operation."""
694
+ if self.connection_failures > 0:
695
+ logger.info("S3 connection recovered")
696
+ self.connection_failures = 0
697
+
698
+ async def close(self):
699
+ await self.pq_executor.shutdown(wait=True)