Skip to content

Commit 2e2630c

Browse files
authored
Merge pull request #15 from aerospike-community/lyndon/l2-integration-e2e
Phase 2: storage/L2 plugins and live L2 integration test
2 parents d259f51 + 1037d1a commit 2e2630c

11 files changed

Lines changed: 469 additions & 22 deletions

File tree

.github/workflows/ci.yml

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ jobs:
5151
integration:
5252
name: Integration (Aerospike CE)
5353
runs-on: ubuntu-latest
54-
timeout-minutes: 25
54+
timeout-minutes: 30
5555
steps:
5656
- name: Harden the runner (Audit all outbound calls)
5757
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
@@ -60,6 +60,13 @@ jobs:
6060

6161
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
6262

63+
- name: Checkout LMCache (dev, L2 plugin APIs)
64+
uses: actions/checkout@v4
65+
with:
66+
repository: LMCache/LMCache
67+
ref: dev
68+
path: LMCache
69+
6370
- name: Set up Python
6471
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
6572
with:
@@ -69,10 +76,17 @@ jobs:
6976

7077
- name: Install dependencies
7178
run: |
72-
python -m pip install --upgrade pip
79+
python -m pip install --upgrade pip wheel
80+
# LMCache build-system pins setuptools 77–80 (see LMCache/pyproject.toml).
81+
pip install "setuptools>=77.0.3,<81.0.0"
7382
# CPU-only torch keeps the hosted runner under ~7 GiB RAM (lmcache pulls torch).
7483
pip install "torch" --index-url https://download.pytorch.org/whl/cpu
75-
pip install -e .
84+
# torch CPU can downgrade setuptools; restore LMCache's range before editable install.
85+
pip install "setuptools>=77.0.3,<81.0.0" --force-reinstall
86+
# PyPI 0.4.x lacks L2StoreResult; install dev (pure Python on CI via NO_NATIVE_EXT).
87+
NO_NATIVE_EXT=1 pip install -e ./LMCache --no-build-isolation
88+
pip install -e . --no-deps
89+
pip install "aerospike>=14.0.0,<19.0.0"
7690
pip install pytest pytest-asyncio
7791
7892
- name: Start Aerospike CE (Docker)

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,7 @@ htmlcov/
2323
*.swp
2424
.DS_Store
2525
.claude/
26+
# Local LMCache dev clone for L2 integration tests / CI layout (see README).
27+
LMCache/
2628
benchmarks/results/
2729
benchmarks/l2/.env.local

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ docker/ scripts/
3737
| ----- | ------- | ----- |
3838
| Preflight (S0) | `python scripts/preflight.py` | LMCache + Aerospike client symbols |
3939
| Unit | `pytest tests/unit -q` | No network |
40-
| Integration | `./scripts/start_aerospike_ce.sh` then `pytest tests/integration -q` | Live CE |
40+
| Integration | `./scripts/ci_integration_install.sh` then `./scripts/start_aerospike_ce.sh` and `pytest tests/integration -q` | Live CE + LMCache `dev` for L2 E2E (mirrors CI) |
4141
| Ecosystem bench | `pip install -r benchmarks/requirements.txt` then `python benchmarks/run.py --profile smoke` | Not in CI by default |
4242
| L2 bench | `./scripts/setup_l2_bench.sh` then `./benchmarks/l2/run.sh` (LMCache `dev` + live CE) | Not in CI by default |
4343
| Micro bench | `RUN_BENCH=1 pytest benchmarks/micro --benchmark-only` | FakeClient only |

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ pytest tests/integration -v
113113

114114
Large payloads (16–64 MiB): `RUN_LARGE_INTEGRATION=1`.
115115

116+
**L2 plugin** (`test_l2_plugin_*.py`): live tests against Aerospike CE via `PluginL2AdapterConfig`, aligned with LMCache’s meaningful upstream patterns (`test_mock_l2_adapter`, RESP L2 integration, `lmc_external_l2_adapter`) — not LMCache’s full mocked suite. Requires [LMCache](https://github.com/LMCache/LMCache) `dev` in `LMCache/` (or `LMCACHE_SRC`). Install: `./scripts/ci_integration_install.sh`.
117+
116118
## Benchmarks
117119

118120
Benchmark code lives under **`benchmarks/`** (not included in the PyPI wheel).

scripts/ci_integration_install.sh

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/usr/bin/env bash
2+
# Mirror the integration job dependency install (for local CI reproduction).
3+
#
4+
# Usage (from repo root):
5+
# ./scripts/ci_integration_install.sh
6+
# ./scripts/start_aerospike_ce.sh && source .aerospike-ci.env && pytest tests/integration -v
7+
8+
set -euo pipefail
9+
10+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
11+
cd "$ROOT"
12+
13+
if [[ ! -d LMCache ]]; then
14+
echo "LMCache/ not found; clone dev: git clone --depth 1 -b dev https://github.com/LMCache/LMCache.git LMCache" >&2
15+
exit 1
16+
fi
17+
18+
python -m pip install --upgrade pip wheel
19+
pip install "setuptools>=77.0.3,<81.0.0"
20+
pip install "torch" --index-url https://download.pytorch.org/whl/cpu
21+
pip install "setuptools>=77.0.3,<81.0.0" --force-reinstall
22+
NO_NATIVE_EXT=1 pip install -e ./LMCache --no-build-isolation
23+
pip install -e . --no-deps
24+
# --no-deps skips aerospike; required for start_aerospike_ce.sh host probe and tests.
25+
pip install "aerospike>=14.0.0,<19.0.0"
26+
pip install pytest pytest-asyncio
27+
28+
echo "CI integration install complete."

scripts/start_aerospike_ce.sh

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,14 @@ if ! command -v docker >/dev/null 2>&1; then
2929
exit 1
3030
fi
3131

32+
PYTHON="${PYTHON:-python}"
33+
if ! command -v "$PYTHON" >/dev/null 2>&1; then
34+
PYTHON=python3
35+
fi
36+
3237
HOST_PORT="${AEROSPIKE_TEST_PORT:-}"
3338
if [[ -z "$HOST_PORT" ]]; then
34-
HOST_PORT="$(python3 -c 'import socket; s=socket.socket(); s.bind(("", 0)); print(s.getsockname()[1]); s.close()')"
39+
HOST_PORT="$("$PYTHON" -c 'import socket; s=socket.socket(); s.bind(("", 0)); print(s.getsockname()[1]); s.close()')"
3540
fi
3641

3742
CONF_DIR="$(mktemp -d)"
@@ -60,21 +65,28 @@ done
6065

6166
echo "Waiting for host-side client on 127.0.0.1:${HOST_PORT} (up to 90s) ..."
6267
host_deadline=$((SECONDS + 90))
63-
until python3 -c "
68+
_host_probe_err="$(mktemp)"
69+
until "$PYTHON" -c "
6470
import aerospike
6571
c = aerospike.client({'hosts': [('127.0.0.1', ${HOST_PORT})]})
6672
c.connect()
6773
info = c.info_random_node('namespace/lmcache')
6874
c.close()
6975
assert 'nsup-period=120' in info
70-
" 2>/dev/null; do
76+
" 2>"$_host_probe_err"; do
7177
if (( SECONDS >= host_deadline )); then
7278
echo "Aerospike CE did not accept host connections in time." >&2
79+
if [[ -s "$_host_probe_err" ]]; then
80+
echo "Last host probe error:" >&2
81+
tail -20 "$_host_probe_err" >&2
82+
fi
7383
docker logs "$CONTAINER_NAME" 2>&1 | tail -80 >&2 || true
84+
rm -f "$_host_probe_err"
7485
exit 1
7586
fi
7687
sleep 1
7788
done
89+
rm -f "$_host_probe_err"
7890

7991
cat >"$ENV_FILE" <<EOF
8092
AEROSPIKE_TEST_HOST=127.0.0.1

tests/integration/helpers.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@
33
from __future__ import annotations
44

55
import asyncio
6+
import importlib
7+
import importlib.util
68
import os
9+
import sys
10+
import types
11+
from pathlib import Path
712
from typing import Any
813

914
import torch
@@ -26,6 +31,78 @@ def on_github_actions() -> bool:
2631
return os.environ.get("GITHUB_ACTIONS", "").lower() == "true"
2732

2833

34+
def lmcache_source_root() -> Path | None:
35+
"""LMCache source tree (CI: ``LMCache/`` in repo; local: sibling ``../LMCache``)."""
36+
env = os.environ.get("LMCACHE_SRC", "").strip()
37+
if env:
38+
root = Path(env)
39+
return root if root.is_dir() else None
40+
repo = Path(__file__).resolve().parents[2]
41+
for candidate in (repo / "LMCache", repo.parent / "LMCache"):
42+
if candidate.is_dir():
43+
return candidate
44+
return None
45+
46+
47+
def install_lmcache_test_native_storage_ops_fallback() -> bool:
48+
"""Use LMCache's test Bitmap stub when ``native_storage_ops`` is not built."""
49+
try:
50+
mod = importlib.import_module("lmcache.native_storage_ops")
51+
if hasattr(mod, "Bitmap") and hasattr(mod, "TTLLock"):
52+
return True
53+
except Exception:
54+
pass
55+
56+
root = lmcache_source_root()
57+
raw_utils = (
58+
None
59+
if root is None
60+
else root / "tests/v1/storage_backend/raw_block_test_utils.py"
61+
)
62+
if raw_utils is None or not raw_utils.is_file():
63+
return False
64+
65+
spec = importlib.util.spec_from_file_location(
66+
"lmcache_raw_block_test_utils", raw_utils
67+
)
68+
raw_mod = importlib.util.module_from_spec(spec)
69+
assert spec.loader is not None
70+
spec.loader.exec_module(raw_mod)
71+
raw_mod.install_native_storage_ops_fallback()
72+
return True
73+
74+
75+
def _minimal_native_storage_ops_fallback() -> None:
76+
"""Tiny Bitmap/TTLLock stub when LMCache source is unavailable."""
77+
78+
class Bitmap:
79+
def __init__(self, size: int, first_n: int = 0) -> None:
80+
self._size = int(size)
81+
self._bits = {i for i in range(min(int(first_n), self._size))}
82+
83+
def set(self, index: int) -> None:
84+
index = int(index)
85+
if index < 0 or index >= self._size:
86+
raise IndexError(index)
87+
self._bits.add(index)
88+
89+
def test(self, index: int) -> bool:
90+
return int(index) in self._bits
91+
92+
class TTLLock:
93+
pass
94+
95+
fallback = types.ModuleType("lmcache.native_storage_ops")
96+
fallback.Bitmap = Bitmap # type: ignore[attr-defined]
97+
fallback.TTLLock = TTLLock # type: ignore[attr-defined]
98+
sys.modules["lmcache.native_storage_ops"] = fallback
99+
100+
101+
def ensure_native_storage_ops_for_l2_tests() -> None:
102+
if not install_lmcache_test_native_storage_ops_fallback():
103+
_minimal_native_storage_ops_fallback()
104+
105+
29106
def aerospike_hosts() -> tuple[tuple[str, int], ...]:
30107
host = os.environ.get("AEROSPIKE_TEST_HOST", "127.0.0.1")
31108
port = int(os.environ.get("AEROSPIKE_TEST_PORT", "3000"))
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""Shared helpers for live AerospikeL2Plugin integration tests (LMCache L2 contract)."""
2+
3+
from __future__ import annotations
4+
5+
import importlib
6+
import os
7+
import select
8+
from collections.abc import Iterator
9+
10+
import pytest
11+
import torch
12+
13+
from tests.integration.helpers import (
14+
aerospike_hosts,
15+
ensure_native_storage_ops_for_l2_tests,
16+
)
17+
18+
ensure_native_storage_ops_for_l2_tests()
19+
20+
l2_mod = importlib.import_module("lmcache_aerospike.l2_plugin")
21+
if not l2_mod.L2_MP_AVAILABLE:
22+
pytest.skip(
23+
"LMCache multiprocess L2 APIs (L2StoreResult) not in this lmcache build",
24+
allow_module_level=True,
25+
)
26+
27+
from lmcache.v1.distributed.api import ObjectKey # noqa: E402
28+
from lmcache.v1.distributed.l2_adapters.factory import ( # noqa: E402
29+
create_l2_adapter_from_registry,
30+
)
31+
from lmcache.v1.distributed.l2_adapters.plugin_l2_adapter import ( # noqa: E402
32+
PluginL2AdapterConfig,
33+
)
34+
from lmcache.v1.memory_management import ( # noqa: E402
35+
MemoryFormat,
36+
MemoryObjMetadata,
37+
TensorMemoryObj,
38+
)
39+
from lmcache.v1.platform import consume_fd # noqa: E402
40+
from lmcache.v1.protocol import init_remote_metadata_info # noqa: E402
41+
42+
init_remote_metadata_info(1)
43+
44+
RUN_INTEGRATION = os.environ.get("RUN_INTEGRATION") == "1"
45+
L2_IT_SET = os.environ.get("AEROSPIKE_L2_TEST_SET", "kv_chunks_l2_it")
46+
47+
48+
def wait_for_event_fd(event_fd: int, timeout: float = 30.0) -> bool:
49+
poll = select.poll()
50+
poll.register(event_fd, select.POLLIN)
51+
if not poll.poll(timeout * 1000):
52+
return False
53+
try:
54+
consume_fd(event_fd)
55+
except BlockingIOError:
56+
pass
57+
return True
58+
59+
60+
def object_key(chunk_id: int, model_name: str = "lmcache_aerospike_it") -> ObjectKey:
61+
return ObjectKey(
62+
chunk_hash=ObjectKey.IntHash2Bytes(chunk_id),
63+
model_name=model_name,
64+
kv_rank=0,
65+
)
66+
67+
68+
def memory_obj(size: int = 256, fill_value: float = 1.0) -> TensorMemoryObj:
69+
"""TensorMemoryObj with shapes/dtypes (LMCache dev metadata for serde)."""
70+
raw = torch.empty(size, dtype=torch.float32)
71+
raw.fill_(fill_value)
72+
meta = MemoryObjMetadata(
73+
shape=torch.Size([size]),
74+
dtype=torch.float32,
75+
address=0,
76+
phy_size=size * 4,
77+
fmt=MemoryFormat.KV_2LTD,
78+
ref_count=1,
79+
shapes=[torch.Size([size])],
80+
dtypes=[torch.float32],
81+
)
82+
return TensorMemoryObj(raw, meta, parent_allocator=None)
83+
84+
85+
def plugin_config() -> PluginL2AdapterConfig:
86+
host, port = aerospike_hosts()[0]
87+
return PluginL2AdapterConfig(
88+
module_path="lmcache_aerospike.l2_plugin",
89+
class_name="AerospikeL2Plugin",
90+
adapter_params={
91+
"hosts": f"{host}:{port}",
92+
"namespace": os.environ.get("AEROSPIKE_TEST_NAMESPACE", "lmcache"),
93+
"set_name": L2_IT_SET,
94+
},
95+
)
96+
97+
98+
@pytest.fixture
99+
def aerospike_l2_adapter() -> Iterator:
100+
adapter = create_l2_adapter_from_registry(plugin_config())
101+
try:
102+
yield adapter
103+
finally:
104+
adapter.close()

0 commit comments

Comments
 (0)