From eb815b26f1899f5ab80b6aa261131c05cf84e289 Mon Sep 17 00:00:00 2001 From: Huanyi Xie <74809554+xiehuanyi@users.noreply.github.com> Date: Tue, 6 Oct 2026 19:51:58 +0300 Subject: [PATCH] fix(pool): keep label key and value identities distinct --- sandboxes/pool.py | 10 +++++----- tests/test_pool_label_keys.py | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 tests/test_pool_label_keys.py diff --git a/sandboxes/pool.py b/sandboxes/pool.py index e92e36a..4aa8b6f 100644 --- a/sandboxes/pool.py +++ b/sandboxes/pool.py @@ -88,7 +88,7 @@ def __init__(self, pool_config: PoolConfig | None = None): self._busy_sandboxes: set[str] = set() # Label index for fast lookup - self._label_index: dict[str, set[str]] = {} + self._label_index: dict[tuple[str, str], set[str]] = {} # Locks for thread-safe operations self._lock = asyncio.Lock() @@ -288,7 +288,7 @@ async def _create_sandbox(self, provider: Any, config: SandboxConfig) -> Sandbox # Update label index for key, value in entry.labels.items(): - label_key = f"{key}:{value}" + label_key = (key, value) if label_key not in self._label_index: self._label_index[label_key] = set() self._label_index[label_key].add(sandbox.id) @@ -304,7 +304,7 @@ async def _create_sandbox(self, provider: Any, config: SandboxConfig) -> Sandbox async def _find_reusable_sandbox(self, labels: dict[str, str]) -> Sandbox | None: """Find an idle sandbox with matching labels.""" # Build label keys - label_keys = [f"{k}:{v}" for k, v in labels.items()] + label_keys = list(labels.items()) # Find sandboxes with all matching labels matching_ids = None @@ -333,7 +333,7 @@ async def find_by_labels(self, labels: dict[str, str]) -> list[Sandbox]: return [] async with self._lock: - label_keys = [f"{k}:{v}" for k, v in labels.items()] + label_keys = list(labels.items()) matching_ids: set[str] | None = None for label_key in label_keys: @@ -435,7 +435,7 @@ def _remove_from_pool_locked(self, sandbox_id: str) -> SandboxPoolEntry | None: entry = self._pool.pop(sandbox_id) for key, value in entry.labels.items(): - label_key = f"{key}:{value}" + label_key = (key, value) if label_key in self._label_index: self._label_index[label_key].discard(sandbox_id) if not self._label_index[label_key]: diff --git a/tests/test_pool_label_keys.py b/tests/test_pool_label_keys.py new file mode 100644 index 0000000..d33f82c --- /dev/null +++ b/tests/test_pool_label_keys.py @@ -0,0 +1,25 @@ +import pytest + +from sandboxes.base import SandboxConfig +from sandboxes.pool import PoolConfig, SandboxPool +from tests.test_pool import MockProvider + + +@pytest.mark.asyncio +async def test_colons_do_not_collapse_distinct_label_pairs(): + provider = MockProvider() + pool = SandboxPool(PoolConfig(auto_cleanup=False)) + try: + first = await pool.acquire(provider, SandboxConfig(labels={"scope:project": "a"})) + await pool.release(first.id) + assert await pool.find_by_labels({"scope": "project:a"}) == [] + second = await pool.acquire(provider, SandboxConfig(labels={"scope": "project:a"})) + assert second.id != first.id + assert [sandbox.id for sandbox in await pool.find_by_labels({"scope:project": "a"})] == [ + first.id + ] + assert [sandbox.id for sandbox in await pool.find_by_labels({"scope": "project:a"})] == [ + second.id + ] + finally: + await pool.stop()