couchbase-lm-cache 0.2.0__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.
- couchbase_lm_cache/__init__.py +15 -0
- couchbase_lm_cache/config.py +166 -0
- couchbase_lm_cache/connector.py +319 -0
- couchbase_lm_cache/l2_adapter.py +351 -0
- couchbase_lm_cache/store.py +220 -0
- couchbase_lm_cache-0.2.0.dist-info/METADATA +627 -0
- couchbase_lm_cache-0.2.0.dist-info/RECORD +9 -0
- couchbase_lm_cache-0.2.0.dist-info/WHEEL +4 -0
- couchbase_lm_cache-0.2.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Couchbase remote storage connector plugin for LMCache."""
|
|
3
|
+
|
|
4
|
+
from couchbase_lm_cache.config import CouchbaseLMCacheConfig
|
|
5
|
+
from couchbase_lm_cache.connector import CouchbaseConnector
|
|
6
|
+
from couchbase_lm_cache.store import CouchbaseKVStore
|
|
7
|
+
|
|
8
|
+
__version__ = "0.2.0"
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"CouchbaseConnector",
|
|
12
|
+
"CouchbaseKVStore",
|
|
13
|
+
"CouchbaseLMCacheConfig",
|
|
14
|
+
"__version__",
|
|
15
|
+
]
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Configuration helpers for the LMCache Couchbase connector."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from os import environ
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
_PLUGIN_PREFIX = "remote_storage_plugin"
|
|
12
|
+
_ENV_PREFIX = "LMCACHE_COUCHBASE_"
|
|
13
|
+
# Payloads above this size are split across several documents. Couchbase Server
|
|
14
|
+
# caps documents at 20 MiB (one 256-token chunk of an 8B model is ~36 MiB), and
|
|
15
|
+
# ~1 MiB documents read about twice as fast as 16 MiB ones.
|
|
16
|
+
DEFAULT_SHARD_SIZE_BYTES = 1024 * 1024
|
|
17
|
+
DEFAULT_MAX_WORKERS = 32
|
|
18
|
+
# The SDK default (2.5 s) is tuned for small documents; a 16 MiB shard read
|
|
19
|
+
# from disk on a full-eviction bucket can take longer.
|
|
20
|
+
DEFAULT_KV_TIMEOUT_SECONDS = 10.0
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _as_int(value: Any, default: int | None = None) -> int | None:
|
|
24
|
+
if value is None or value == "":
|
|
25
|
+
return default
|
|
26
|
+
return int(value)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _as_float(value: Any, default: float) -> float:
|
|
30
|
+
if value is None or value == "":
|
|
31
|
+
return default
|
|
32
|
+
return float(value)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class CouchbaseLMCacheConfig:
|
|
37
|
+
"""Connection and behavior settings for Couchbase-backed LMCache storage.
|
|
38
|
+
|
|
39
|
+
Values can come from LMCache ``extra_config`` keys named
|
|
40
|
+
``remote_storage_plugin.<plugin_name>.<field>`` or from environment variables
|
|
41
|
+
with the ``LMCACHE_COUCHBASE_`` prefix.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
connection_string: str
|
|
45
|
+
username: str
|
|
46
|
+
password: str
|
|
47
|
+
bucket_name: str
|
|
48
|
+
scope_name: str = "_default"
|
|
49
|
+
collection_name: str = "_default"
|
|
50
|
+
key_prefix: str = "lmcache:"
|
|
51
|
+
ttl_seconds: int | None = None
|
|
52
|
+
kv_timeout_seconds: float = DEFAULT_KV_TIMEOUT_SECONDS
|
|
53
|
+
connect_timeout_seconds: float = 10.0
|
|
54
|
+
max_workers: int = DEFAULT_MAX_WORKERS
|
|
55
|
+
shard_size_bytes: int = DEFAULT_SHARD_SIZE_BYTES
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def from_env(cls) -> CouchbaseLMCacheConfig:
|
|
59
|
+
"""Build configuration from environment variables.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
ValueError: if required variables are absent.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
return cls._build(lambda field: None)
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def from_lmcache_extra_config(
|
|
69
|
+
cls,
|
|
70
|
+
extra_config: dict[str, Any] | None,
|
|
71
|
+
plugin_name: str = "couchbase",
|
|
72
|
+
) -> CouchbaseLMCacheConfig:
|
|
73
|
+
"""Build configuration from LMCache ``extra_config`` plus env fallback.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
extra_config: LMCache ``LMCacheEngineConfig.extra_config`` mapping.
|
|
77
|
+
plugin_name: Remote storage plugin name, for example ``couchbase`` or
|
|
78
|
+
``couchbase.primary``.
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
ValueError: if required connection fields are absent.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
extra_config = extra_config or {}
|
|
85
|
+
|
|
86
|
+
def get(field: str) -> Any:
|
|
87
|
+
specific = f"{_PLUGIN_PREFIX}.{plugin_name}.{field}"
|
|
88
|
+
if specific in extra_config:
|
|
89
|
+
return extra_config[specific]
|
|
90
|
+
plugin_type = plugin_name.split(".", 1)[0]
|
|
91
|
+
return extra_config.get(f"{_PLUGIN_PREFIX}.{plugin_type}.{field}")
|
|
92
|
+
|
|
93
|
+
return cls._build(get)
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def from_dict(cls, values: dict[str, Any] | None) -> CouchbaseLMCacheConfig:
|
|
97
|
+
"""Build configuration from plain field names plus env fallback.
|
|
98
|
+
|
|
99
|
+
Used by the LMCache multiprocess-mode L2 adapter, whose
|
|
100
|
+
``adapter_params`` use the same field names as ``extra_config``
|
|
101
|
+
without the ``remote_storage_plugin.<name>.`` prefix.
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
ValueError: if required connection fields are absent.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
values = values or {}
|
|
108
|
+
return cls._build(values.get)
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def _build(cls, lookup: Callable[[str], Any]) -> CouchbaseLMCacheConfig:
|
|
112
|
+
def get(field: str, env_name: str | None = None, default: Any = None) -> Any:
|
|
113
|
+
value = lookup(field)
|
|
114
|
+
if value is not None:
|
|
115
|
+
return value
|
|
116
|
+
return environ.get(_ENV_PREFIX + (env_name or field).upper(), default)
|
|
117
|
+
|
|
118
|
+
return cls(
|
|
119
|
+
connection_string=_required_value(
|
|
120
|
+
get("connection_string", "CONNSTR"), "connection_string"
|
|
121
|
+
),
|
|
122
|
+
username=_required_value(get("username"), "username"),
|
|
123
|
+
password=_required_value(get("password"), "password"),
|
|
124
|
+
bucket_name=_required_value(get("bucket_name", "BUCKET"), "bucket_name"),
|
|
125
|
+
scope_name=str(get("scope_name", "SCOPE", "_default")),
|
|
126
|
+
collection_name=str(get("collection_name", "COLLECTION", "_default")),
|
|
127
|
+
key_prefix=str(get("key_prefix", "KEY_PREFIX", "lmcache:")),
|
|
128
|
+
ttl_seconds=_as_int(get("ttl_seconds", "TTL_SECONDS")),
|
|
129
|
+
kv_timeout_seconds=_as_float(
|
|
130
|
+
get("kv_timeout_seconds", "KV_TIMEOUT_SECONDS"),
|
|
131
|
+
DEFAULT_KV_TIMEOUT_SECONDS,
|
|
132
|
+
),
|
|
133
|
+
connect_timeout_seconds=_as_float(
|
|
134
|
+
get("connect_timeout_seconds", "CONNECT_TIMEOUT_SECONDS"), 10.0
|
|
135
|
+
),
|
|
136
|
+
max_workers=_as_int(get("max_workers", "MAX_WORKERS"), DEFAULT_MAX_WORKERS)
|
|
137
|
+
or DEFAULT_MAX_WORKERS,
|
|
138
|
+
shard_size_bytes=_as_int(
|
|
139
|
+
get("shard_size_bytes", "SHARD_SIZE_BYTES"), DEFAULT_SHARD_SIZE_BYTES
|
|
140
|
+
)
|
|
141
|
+
or DEFAULT_SHARD_SIZE_BYTES,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def redacted(self) -> dict[str, Any]:
|
|
145
|
+
"""Return a safe dictionary for logging or support output."""
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
"connection_string": self.connection_string,
|
|
149
|
+
"username": self.username,
|
|
150
|
+
"password": "***" if self.password else "",
|
|
151
|
+
"bucket_name": self.bucket_name,
|
|
152
|
+
"scope_name": self.scope_name,
|
|
153
|
+
"collection_name": self.collection_name,
|
|
154
|
+
"key_prefix": self.key_prefix,
|
|
155
|
+
"ttl_seconds": self.ttl_seconds,
|
|
156
|
+
"kv_timeout_seconds": self.kv_timeout_seconds,
|
|
157
|
+
"connect_timeout_seconds": self.connect_timeout_seconds,
|
|
158
|
+
"max_workers": self.max_workers,
|
|
159
|
+
"shard_size_bytes": self.shard_size_bytes,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _required_value(value: Any, name: str) -> str:
|
|
164
|
+
if value is None or str(value) == "":
|
|
165
|
+
raise ValueError(f"Missing required Couchbase LMCache setting: {name}")
|
|
166
|
+
return str(value)
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""LMCache RemoteConnector implementation backed by Couchbase KV."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import builtins
|
|
8
|
+
import logging
|
|
9
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from couchbase_lm_cache.config import CouchbaseLMCacheConfig
|
|
13
|
+
from couchbase_lm_cache.store import CouchbaseKVStore
|
|
14
|
+
|
|
15
|
+
try: # pragma: no cover - exercised when installed alongside LMCache
|
|
16
|
+
from lmcache.logging import init_logger
|
|
17
|
+
from lmcache.utils import CacheEngineKey
|
|
18
|
+
from lmcache.v1.memory_management import MemoryObj
|
|
19
|
+
from lmcache.v1.protocol import RemoteMetadata
|
|
20
|
+
from lmcache.v1.storage_backend.connector.base_connector import RemoteConnector
|
|
21
|
+
|
|
22
|
+
_HAS_LMCACHE = True
|
|
23
|
+
except Exception: # pragma: no cover - unit tests run without heavy LMCache deps
|
|
24
|
+
CacheEngineKey = Any # type: ignore[misc,assignment]
|
|
25
|
+
MemoryObj = Any # type: ignore[misc,assignment]
|
|
26
|
+
RemoteMetadata = None # type: ignore[misc,assignment]
|
|
27
|
+
|
|
28
|
+
class RemoteConnector: # type: ignore[no-redef]
|
|
29
|
+
"""Fallback base so unit tests can run without LMCache installed."""
|
|
30
|
+
|
|
31
|
+
save_chunk_meta = False
|
|
32
|
+
remote_metadata_bytes = 0
|
|
33
|
+
|
|
34
|
+
def reshape_partial_chunk(self, memory_obj: Any, bytes_read: int) -> Any:
|
|
35
|
+
del bytes_read
|
|
36
|
+
return memory_obj
|
|
37
|
+
|
|
38
|
+
_HAS_LMCACHE = False
|
|
39
|
+
init_logger = logging.getLogger
|
|
40
|
+
|
|
41
|
+
logger = init_logger(__name__)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _byte_view(memory_obj: Any) -> memoryview:
|
|
45
|
+
"""Return a flat unsigned-byte view over a ``MemoryObj`` buffer."""
|
|
46
|
+
|
|
47
|
+
buffer = memory_obj.byte_array
|
|
48
|
+
view = buffer if isinstance(buffer, memoryview) else memoryview(buffer)
|
|
49
|
+
if view.format != "B":
|
|
50
|
+
view = view.cast("B")
|
|
51
|
+
return view
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _release(memory_obj: Any) -> None:
|
|
55
|
+
release = getattr(memory_obj, "ref_count_down", None)
|
|
56
|
+
if release is not None:
|
|
57
|
+
release()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class CouchbaseConnector(RemoteConnector):
|
|
61
|
+
"""LMCache remote storage connector that stores chunks in Couchbase KV.
|
|
62
|
+
|
|
63
|
+
This class is loaded by LMCache with ``remote_storage_plugins`` and implements
|
|
64
|
+
the ``RemoteConnector`` methods expected by ``RemoteBackend``.
|
|
65
|
+
|
|
66
|
+
Each LMCache chunk is one binary Couchbase document. When LMCache's
|
|
67
|
+
``save_chunk_meta`` is enabled (the LMCache default), the document starts with
|
|
68
|
+
LMCache's serialized ``RemoteMetadata`` header followed by the chunk bytes, so
|
|
69
|
+
header and payload are always written and expired atomically.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
loop: asyncio.AbstractEventLoop,
|
|
75
|
+
local_cpu_backend: Any,
|
|
76
|
+
config: Any,
|
|
77
|
+
plugin_name: str | None = None,
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Initialize the connector.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
loop: LMCache event loop.
|
|
83
|
+
local_cpu_backend: LMCache local CPU backend used to allocate buffers
|
|
84
|
+
for retrieved objects.
|
|
85
|
+
config: LMCache engine config containing ``extra_config``.
|
|
86
|
+
plugin_name: Name from ``remote_storage_plugins``; defaults to
|
|
87
|
+
``couchbase``.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
if _HAS_LMCACHE:
|
|
91
|
+
super().__init__(local_cpu_backend.config, local_cpu_backend.metadata)
|
|
92
|
+
self.loop = loop
|
|
93
|
+
self.local_cpu_backend = local_cpu_backend
|
|
94
|
+
self.config = config
|
|
95
|
+
configured_plugins = getattr(config, "remote_storage_plugins", None) or []
|
|
96
|
+
inferred_plugin_name = (
|
|
97
|
+
configured_plugins[0] if len(configured_plugins) == 1 else "couchbase"
|
|
98
|
+
)
|
|
99
|
+
self.plugin_name = plugin_name or inferred_plugin_name
|
|
100
|
+
extra_config = getattr(config, "extra_config", None)
|
|
101
|
+
self.cfg = CouchbaseLMCacheConfig.from_lmcache_extra_config(
|
|
102
|
+
extra_config, self.plugin_name
|
|
103
|
+
)
|
|
104
|
+
self.store = CouchbaseKVStore(self.cfg)
|
|
105
|
+
self.executor = ThreadPoolExecutor(
|
|
106
|
+
max_workers=self.cfg.max_workers, thread_name_prefix="couchbase-lmcache"
|
|
107
|
+
)
|
|
108
|
+
logger.info(
|
|
109
|
+
"Couchbase LMCache connector ready: %s (save_chunk_meta=%s)",
|
|
110
|
+
self.cfg.redacted(),
|
|
111
|
+
self.save_chunk_meta,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
async def _run_blocking(self, func: Any, *args: Any) -> Any:
|
|
115
|
+
return await self.loop.run_in_executor(self.executor, func, *args)
|
|
116
|
+
|
|
117
|
+
def _encode(self, memory_obj: MemoryObj) -> bytes:
|
|
118
|
+
payload = _byte_view(memory_obj)
|
|
119
|
+
if not self.save_chunk_meta:
|
|
120
|
+
return bytes(payload)
|
|
121
|
+
header = RemoteMetadata(
|
|
122
|
+
len(payload),
|
|
123
|
+
memory_obj.get_shapes(),
|
|
124
|
+
memory_obj.get_dtypes(),
|
|
125
|
+
memory_obj.get_memory_format(),
|
|
126
|
+
).serialize()
|
|
127
|
+
return header + bytes(payload)
|
|
128
|
+
|
|
129
|
+
def _decode(self, key: CacheEngineKey, document: bytes) -> MemoryObj | None:
|
|
130
|
+
if self.save_chunk_meta:
|
|
131
|
+
header_size = self.remote_metadata_bytes
|
|
132
|
+
if len(document) < header_size:
|
|
133
|
+
raise ValueError(
|
|
134
|
+
f"Couchbase document for {key} has {len(document)} bytes, "
|
|
135
|
+
f"shorter than the {header_size}-byte LMCache metadata header"
|
|
136
|
+
)
|
|
137
|
+
metadata = RemoteMetadata.deserialize(document[:header_size])
|
|
138
|
+
payload = memoryview(document)[header_size:]
|
|
139
|
+
if len(payload) != metadata.length:
|
|
140
|
+
raise ValueError(
|
|
141
|
+
f"Couchbase document for {key} has {len(payload)} payload "
|
|
142
|
+
f"bytes but its metadata header says {metadata.length}"
|
|
143
|
+
)
|
|
144
|
+
memory_obj = self.local_cpu_backend.allocate(
|
|
145
|
+
metadata.shapes, metadata.dtypes, metadata.fmt
|
|
146
|
+
)
|
|
147
|
+
else:
|
|
148
|
+
payload = memoryview(document)
|
|
149
|
+
memory_obj = self.local_cpu_backend.allocate(
|
|
150
|
+
self.meta_shapes, self.meta_dtypes, self.meta_fmt
|
|
151
|
+
)
|
|
152
|
+
if memory_obj is None:
|
|
153
|
+
logger.warning("LMCache local CPU backend could not allocate MemoryObj")
|
|
154
|
+
return None
|
|
155
|
+
try:
|
|
156
|
+
target = _byte_view(memory_obj)
|
|
157
|
+
if len(payload) > len(target):
|
|
158
|
+
raise ValueError(
|
|
159
|
+
f"Couchbase payload for {key} has {len(payload)} bytes but "
|
|
160
|
+
f"allocated LMCache buffer has {len(target)} bytes"
|
|
161
|
+
)
|
|
162
|
+
target[: len(payload)] = payload
|
|
163
|
+
if self.save_chunk_meta:
|
|
164
|
+
return memory_obj
|
|
165
|
+
return self.reshape_partial_chunk(memory_obj, len(payload))
|
|
166
|
+
except Exception:
|
|
167
|
+
_release(memory_obj)
|
|
168
|
+
raise
|
|
169
|
+
|
|
170
|
+
async def exists(self, key: CacheEngineKey) -> bool:
|
|
171
|
+
"""Return whether Couchbase contains the LMCache key."""
|
|
172
|
+
|
|
173
|
+
return bool(await self._run_blocking(self.store.exists, key))
|
|
174
|
+
|
|
175
|
+
def exists_sync(self, key: CacheEngineKey) -> bool:
|
|
176
|
+
"""Synchronous existence check used by LMCache's optional fast path."""
|
|
177
|
+
|
|
178
|
+
return self.store.exists(key)
|
|
179
|
+
|
|
180
|
+
async def get(self, key: CacheEngineKey) -> MemoryObj | None:
|
|
181
|
+
"""Fetch a chunk from Couchbase into an LMCache ``MemoryObj``."""
|
|
182
|
+
|
|
183
|
+
document = await self._run_blocking(self.store.get_bytes, key)
|
|
184
|
+
if document is None:
|
|
185
|
+
return None
|
|
186
|
+
return self._decode(key, document)
|
|
187
|
+
|
|
188
|
+
async def put(self, key: CacheEngineKey, memory_obj: MemoryObj) -> None:
|
|
189
|
+
"""Persist a chunk in Couchbase."""
|
|
190
|
+
|
|
191
|
+
await self._run_blocking(self.store.put_bytes, key, self._encode(memory_obj))
|
|
192
|
+
|
|
193
|
+
def remove_sync(self, key: CacheEngineKey) -> bool:
|
|
194
|
+
"""Remove a chunk; used by LMCache ``RemoteBackend.remove``."""
|
|
195
|
+
|
|
196
|
+
return self.store.remove(key)
|
|
197
|
+
|
|
198
|
+
async def list(self) -> builtins.list[str]:
|
|
199
|
+
"""List is intentionally unsupported for the remote connector contract."""
|
|
200
|
+
|
|
201
|
+
return []
|
|
202
|
+
|
|
203
|
+
def support_ping(self) -> bool:
|
|
204
|
+
"""Couchbase connectivity can be health-checked."""
|
|
205
|
+
|
|
206
|
+
return True
|
|
207
|
+
|
|
208
|
+
async def ping(self) -> int:
|
|
209
|
+
"""Return 0 when the Couchbase KV service answers, 1 otherwise."""
|
|
210
|
+
|
|
211
|
+
return 0 if await self._run_blocking(self.store.ping) else 1
|
|
212
|
+
|
|
213
|
+
async def close(self) -> None:
|
|
214
|
+
"""Close Couchbase and executor resources."""
|
|
215
|
+
|
|
216
|
+
await self._run_blocking(self.store.close)
|
|
217
|
+
self.executor.shutdown(wait=True, cancel_futures=False)
|
|
218
|
+
|
|
219
|
+
def support_batched_put(self) -> bool:
|
|
220
|
+
"""Couchbase writes are batched through concurrent executor jobs."""
|
|
221
|
+
|
|
222
|
+
return True
|
|
223
|
+
|
|
224
|
+
async def batched_put(
|
|
225
|
+
self, keys: builtins.list[CacheEngineKey], memory_objs: builtins.list[MemoryObj]
|
|
226
|
+
) -> None:
|
|
227
|
+
"""Persist multiple chunks concurrently."""
|
|
228
|
+
|
|
229
|
+
documents = [self._encode(memory_obj) for memory_obj in memory_objs]
|
|
230
|
+
await asyncio.gather(
|
|
231
|
+
*(
|
|
232
|
+
self._run_blocking(self.store.put_bytes, key, document)
|
|
233
|
+
for key, document in zip(keys, documents, strict=True)
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
def support_batched_get(self) -> bool:
|
|
238
|
+
"""Couchbase reads are batched through concurrent executor jobs."""
|
|
239
|
+
|
|
240
|
+
return True
|
|
241
|
+
|
|
242
|
+
async def batched_get(
|
|
243
|
+
self, keys: builtins.list[CacheEngineKey]
|
|
244
|
+
) -> builtins.list[MemoryObj | None]:
|
|
245
|
+
"""Fetch multiple chunks concurrently."""
|
|
246
|
+
|
|
247
|
+
return list(await asyncio.gather(*(self.get(key) for key in keys)))
|
|
248
|
+
|
|
249
|
+
def support_batched_contains(self) -> bool:
|
|
250
|
+
"""Enable LMCache prefix-hit counting over multiple keys."""
|
|
251
|
+
|
|
252
|
+
return True
|
|
253
|
+
|
|
254
|
+
def batched_contains(self, keys: builtins.list[CacheEngineKey]) -> int:
|
|
255
|
+
"""Return the number of consecutive existing keys from the start."""
|
|
256
|
+
|
|
257
|
+
count = 0
|
|
258
|
+
for key in keys:
|
|
259
|
+
if not self.store.exists(key):
|
|
260
|
+
return count
|
|
261
|
+
count += 1
|
|
262
|
+
return count
|
|
263
|
+
|
|
264
|
+
def support_batched_async_contains(self) -> bool:
|
|
265
|
+
"""Enable async prefix-hit counting."""
|
|
266
|
+
|
|
267
|
+
return True
|
|
268
|
+
|
|
269
|
+
async def batched_async_contains(
|
|
270
|
+
self,
|
|
271
|
+
lookup_id: str,
|
|
272
|
+
keys: builtins.list[CacheEngineKey],
|
|
273
|
+
pin: bool = False,
|
|
274
|
+
) -> int:
|
|
275
|
+
"""Return the async number of consecutive existing keys from the start."""
|
|
276
|
+
|
|
277
|
+
del lookup_id, pin
|
|
278
|
+
results = await asyncio.gather(
|
|
279
|
+
*(self._run_blocking(self.store.exists, key) for key in keys)
|
|
280
|
+
)
|
|
281
|
+
count = 0
|
|
282
|
+
for exists in results:
|
|
283
|
+
if not exists:
|
|
284
|
+
return count
|
|
285
|
+
count += 1
|
|
286
|
+
return count
|
|
287
|
+
|
|
288
|
+
def support_batched_get_non_blocking(self) -> bool:
|
|
289
|
+
"""Enable LMCache prefetch reads."""
|
|
290
|
+
|
|
291
|
+
return True
|
|
292
|
+
|
|
293
|
+
async def batched_get_non_blocking(
|
|
294
|
+
self,
|
|
295
|
+
lookup_id: str,
|
|
296
|
+
keys: builtins.list[CacheEngineKey],
|
|
297
|
+
) -> builtins.list[MemoryObj]:
|
|
298
|
+
"""Fetch the consecutive prefix of chunks for prefetch paths.
|
|
299
|
+
|
|
300
|
+
LMCache maps the returned objects positionally onto ``keys``, so results
|
|
301
|
+
stop at the first miss or error and any later objects are released.
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
del lookup_id
|
|
305
|
+
results = await asyncio.gather(
|
|
306
|
+
*(self.get(key) for key in keys), return_exceptions=True
|
|
307
|
+
)
|
|
308
|
+
prefix: builtins.list[MemoryObj] = []
|
|
309
|
+
broken = False
|
|
310
|
+
for result in results:
|
|
311
|
+
if result is None or isinstance(result, BaseException):
|
|
312
|
+
if isinstance(result, BaseException):
|
|
313
|
+
logger.warning("Couchbase prefetch failed: %s", result)
|
|
314
|
+
broken = True
|
|
315
|
+
elif broken:
|
|
316
|
+
_release(result)
|
|
317
|
+
else:
|
|
318
|
+
prefix.append(result)
|
|
319
|
+
return prefix
|