Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,10 @@ jobs:
fi
echo "Using base benchmark config: ${CONFIG_FILE}"
(cd "${BENCH_BASE_DIR}" && uv sync --dev)
# Older main lacks --json-out / json_out support on bench scripts.
# Overlay HEAD's tests/performance harness into the base worktree so
# measurements still import base ccbt (__file__ under base) but emit
# CI JSON artifacts the suite runner expects.
mkdir -p "${BENCH_BASE_DIR}/tests/performance"
cp -a "${{ github.workspace }}/tests/performance/." "${BENCH_BASE_DIR}/tests/performance/"
# Use HEAD's runner script (has current CLI), but execute benchmarks in base workdir.
# Do not overlay HEAD's tests/performance onto base — newer harnesses call APIs
# (e.g. derive_encryption_key(direction=...)) that older main does not provide.
# Legacy scripts emit JSON via --output-dir; the suite runner falls back to that.
uv run python dev/scripts/run_benchmark_suite.py \
--output-dir "${BENCH_BASE_DIR}" \
--workdir "${BENCH_BASE_DIR}" \
Expand Down
5 changes: 5 additions & 0 deletions ccbt/discovery/tracker_udp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,11 @@ async def start(self) -> None:
"""
self._stopping = False
self._refresh_udp_pending_settings_from_config()
# Tests inject a mock transport; never bind a real UDP socket in test mode
# (Windows CI often rejects binding to the configured tracker port).
if self._test_mode and self.transport is not None:
self._socket_ready = True
return
# Note: Assert socket should never be recreated during runtime
# If socket is already initialized and healthy, return immediately
# Socket recreation breaks session logic and causes WinError 10022 on Windows
Expand Down
32 changes: 25 additions & 7 deletions ccbt/storage/xet_deduplication.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,25 @@ def __init__(
self.dht_client = dht_client
# Serialize DB access: SQLite connection is not thread-safe; run blocking
# DB and disk I/O in thread pool so the event loop stays responsive.
self._db_lock = asyncio.Lock()
# Lazily bound to the running loop — Python 3.9 locks capture the loop at
# construction, which breaks under pytest-asyncio's per-test loops.
self._db_lock: Optional[asyncio.Lock] = None

def _get_db_lock(self) -> asyncio.Lock:
"""Return an ``asyncio.Lock`` bound to the current running loop."""
try:
loop = asyncio.get_running_loop()
except RuntimeError:
if self._db_lock is None:
self._db_lock = asyncio.Lock()
return self._db_lock

lock = self._db_lock
bound_loop = getattr(lock, "_loop", None) if lock is not None else None
if lock is None or (bound_loop is not None and bound_loop is not loop):
self._db_lock = asyncio.Lock()
return self._db_lock
return lock

def _init_database(self) -> sqlite3.Connection:
"""Initialize SQLite cache database.
Expand Down Expand Up @@ -250,7 +268,7 @@ async def check_chunk_exists(self, chunk_hash: bytes) -> Optional[Path]:
Path to stored chunk if exists, None otherwise

"""
async with self._db_lock:
async with self._get_db_lock():
return await to_thread_compat(
self._check_chunk_exists_sync,
chunk_hash,
Expand Down Expand Up @@ -326,7 +344,7 @@ async def store_chunk(
Path to stored chunk (may be existing or new)

"""
async with self._db_lock:
async with self._get_db_lock():
existing = await to_thread_compat(
self._check_chunk_exists_sync,
chunk_hash,
Expand Down Expand Up @@ -408,7 +426,7 @@ async def add_file_chunk_reference(

"""
try:
async with self._db_lock:
async with self._get_db_lock():
skipped = await to_thread_compat(
self._add_file_chunk_reference_sync,
file_path,
Expand Down Expand Up @@ -656,7 +674,7 @@ async def store_file_metadata(self, metadata: XetFileMetadata) -> None:
metadata_dict["xorb_refs"] = [h.hex() for h in metadata.xorb_refs]
metadata_json = json.dumps(metadata_dict)

async with self._db_lock:
async with self._get_db_lock():
await to_thread_compat(
self._store_file_metadata_sync,
metadata,
Expand Down Expand Up @@ -698,7 +716,7 @@ async def get_file_metadata(self, file_path: str) -> Optional[XetFileMetadata]:

"""
try:
async with self._db_lock:
async with self._get_db_lock():
metadata_dict = await to_thread_compat(
self._get_file_metadata_sync,
file_path,
Expand Down Expand Up @@ -1112,7 +1130,7 @@ def close(self) -> None:

async def aclose(self) -> None:
"""Close database connection under the DB lock (idempotent)."""
async with self._db_lock:
async with self._get_db_lock():
self.close()

def __enter__(self):
Expand Down
35 changes: 29 additions & 6 deletions dev/scripts/run_benchmark_suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,17 @@ def _find_legacy_artifact(workdir: Path, benchmark_key: str) -> Path | None:
return matches[-1] if matches else None


def _find_output_dir_artifact(output_dir: Path, benchmark_key: str) -> Path | None:
"""Find a JSON file written via legacy ``--output-dir``."""
if not output_dir.is_dir():
return None
matches = sorted(output_dir.glob(f"{benchmark_key}-*.json"))
if matches:
return matches[-1]
matches = sorted(output_dir.glob("*.json"))
return matches[-1] if matches else None


def _run_benchmark(
spec: BenchmarkSpec,
*,
Expand Down Expand Up @@ -131,10 +142,19 @@ def _run_benchmark(
if quick:
cmd.append("--quick")

def _invoke(with_json_out: bool) -> subprocess.CompletedProcess[str]:
legacy_dir = output_dir / f"_legacy_{spec.benchmark_key}"

def _invoke(
*,
with_json_out: bool,
with_legacy_output_dir: bool = False,
) -> subprocess.CompletedProcess[str]:
run_cmd = [*cmd]
if with_json_out:
run_cmd.extend(["--json-out", str(output_path)])
elif with_legacy_output_dir:
legacy_dir.mkdir(parents=True, exist_ok=True)
run_cmd.extend(["--output-dir", str(legacy_dir)])
return subprocess.run(
run_cmd,
cwd=workdir,
Expand All @@ -144,13 +164,14 @@ def _invoke(with_json_out: bool) -> subprocess.CompletedProcess[str]:
)

completed = _invoke(with_json_out=True)
if completed.returncode != 0:
# Older scripts may reject --json-out; retry without it and look for
# legacy artifact paths or an explicit --output-dir write.
completed = _invoke(with_json_out=False)
if completed.returncode != 0 or not output_path.is_file():
# Older scripts reject --json-out; ask them to write via --output-dir.
completed = _invoke(with_json_out=False, with_legacy_output_dir=True)

if completed.returncode != 0:
legacy = _find_legacy_artifact(workdir, spec.benchmark_key)
if legacy is None:
legacy = _find_output_dir_artifact(legacy_dir, spec.benchmark_key)
if legacy is None:
stderr = completed.stderr.strip() or completed.stdout.strip()
msg = f"Benchmark {spec.benchmark_key} failed ({completed.returncode}): {stderr}"
Expand All @@ -159,7 +180,9 @@ def _invoke(with_json_out: bool) -> subprocess.CompletedProcess[str]:
elif output_path.is_file():
payload = _normalize_payload(_load_json(output_path), spec.benchmark_key, config_name)
else:
legacy = _find_legacy_artifact(workdir, spec.benchmark_key)
legacy = _find_output_dir_artifact(legacy_dir, spec.benchmark_key)
if legacy is None:
legacy = _find_legacy_artifact(workdir, spec.benchmark_key)
if legacy is None:
detail = (completed.stderr or completed.stdout or "").strip()
msg = f"Benchmark {spec.benchmark_key} produced no JSON artifact"
Expand Down
Loading
Loading