Files
Alexey MilovidovandClaude Opus 4.8 295ced78d1 Enable more Ruff checks (F401/F403/F405/F541/F841) and clean up tests
Follow-up to #105171, which added `ruff` with a deliberately narrow,
bug-class-only rule set and left the unused-name and star-import rules
off pending a dedicated cleanup pass. This enables that second batch in
`pyproject.toml` and fixes every violation across the tree (the bulk in
integration tests):

- `F401` unused-import and `F841` unused-variable: removed, preserving
  any side-effecting right-hand side (e.g. `result = node.query(...)`
  becomes `node.query(...)`). Two `try: import ...` feature probes are
  kept with `# noqa: F401`.
- `F541` f-string-missing-placeholders: dropped the redundant `f` prefix.
- `F403`/`F405` star imports: every `from module import *` is expanded
  into explicit imports of the names actually used, taken from each
  name's real origin, so stdlib/third-party names import from their real
  source rather than through a test "convenience bundle". The now-empty
  `tests/integration/helpers/kafka/common_direct.py` bundle is removed and
  its three remaining consumers import `_VarintBytes`/`KafkaAdminClient`
  directly.

Eliminating the star imports surfaced a real bug the wildcard had masked
from `F821`: `generate_presets.py` in `test_prometheus_protocols` called
an undefined `execute_instant_query_with_http_api`; fixed to the existing
`execute_query_via_http_api`.

No `black`/`isort` reformatting was applied (neither is enforced in the
style check), so the diff is limited to the lint fixes themselves.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-18 23:22:25 +00:00

231 lines
8.6 KiB
Python

#!/usr/bin/env python3
"""
ClickHouse Memory Leak Detection Script
Monitors ClickHouse memory usage over multiple iterations to detect potential leaks.
Uses system.asynchronous_metrics for accurate memory tracking.
"""
import logging
import time
from datetime import datetime
from typing import Dict, List, Optional
from dataclasses import dataclass
from integration.helpers.client import Client, CommandRequest
from integration.helpers.cluster import ClickHouseCluster, ClickHouseInstance
@dataclass
class MemorySnapshot:
"""Represents a memory measurement at a point in time."""
timestamp: datetime
memory_resident: int
phase: str
class ElOracloDeLeaks:
"""Detects memory leaks in ClickHouse by monitoring metrics over iterations."""
def __init__(self):
"""Initialize the detector with ClickHouse connection."""
self.snapshots: List[MemorySnapshot] = []
self.logger = logging.getLogger(__name__)
self.counter: int = 0
def _get_memory_snapshot(
self, phase: str, cluster: ClickHouseCluster
) -> Optional[MemorySnapshot]:
"""Capture current memory metrics from ClickHouse."""
try:
next_node: ClickHouseInstance = cluster.instances["node0"]
client = Client(
host=next_node.ip_address, port=9000, command=cluster.client_bin_path
)
metrics_str = client.query("""
SELECT metric, value
FROM system.asynchronous_metrics
WHERE metric = 'MemoryResident';
""")
if not isinstance(metrics_str, str) or metrics_str == "":
self.logger.warning(
f"No tables found to fetch on node {next_node.name}"
)
return None
fetched_metrics: list[tuple[str, ...]] = [
tuple(line.split("\t")) for line in metrics_str.split("\n") if line
]
metrics = {row[0]: int(row[1]) for row in fetched_metrics}
return MemorySnapshot(
timestamp=datetime.now(),
memory_resident=metrics.get("MemoryResident", 0),
phase=phase,
)
except Exception as ex:
self.logger.warning(
f"Error occurred while fetching metrics from the server: {ex}"
)
return None
def reset_and_capture_baseline(self, cluster: ClickHouseCluster):
"""Capture baseline memory before starting tests."""
self.snapshots.clear()
baseline = self._get_memory_snapshot("baseline", cluster)
if baseline is None:
self.logger.error("Could not get baseline capture")
return
self.snapshots.append(baseline)
self._print_snapshot(baseline)
def _print_snapshot(self, snapshot: MemorySnapshot):
"""Pretty print a memory snapshot."""
self.logger.info(f"Iteration ({snapshot.phase}):")
self.logger.info(
f"MemoryResident: {snapshot.memory_resident / 1024 / 1024:.2f} MB"
)
def calculate_growth(self, snapshot: MemorySnapshot) -> Dict[str, float]:
"""Calculate memory growth percentage from baseline."""
if len(self.snapshots) == 0:
return {}
prev_resident = self.snapshots[-1].memory_resident
resident_growth = (
(snapshot.memory_resident - prev_resident) / prev_resident * 100
if prev_resident != 0
else 0.0
)
return {
"resident_growth": resident_growth,
"resident_mb_delta": (snapshot.memory_resident - prev_resident)
/ 1024
/ 1024,
}
def _detect_leak(
self, cluster: ClickHouseCluster, leak_threshold_percent: float = 10.0
):
"""
Run leak detection test.
Args:
leak_threshold_percent: Alert if growth exceeds this percentage
"""
# Check baseline
if len(self.snapshots) == 0:
self.logger.error("No previous captures exist")
return
leak_detected = False
try:
snapshot = self._get_memory_snapshot("Generator iteration", cluster)
if snapshot is None:
return
growth = self.calculate_growth(snapshot)
self.snapshots.append(snapshot)
self._print_snapshot(snapshot)
# Print progress
self.logger.info(
f"RSS: {growth['resident_growth']:+6.2f}% "
f"({growth['resident_mb_delta']:+7.2f} MB)"
)
leak_detected = growth["resident_growth"] > leak_threshold_percent
except Exception as ex:
self.logger.warning(
f"Error occurred while fetching metrics from the server: {ex}"
)
if leak_detected:
message: str = (
f"⚠️ WARNING: Potential leak detected: MemoryResident grew by {growth['resident_growth']:.2f}%"
)
self.logger.warning(message)
raise ValueError(message)
def _pause_or_resume_clickhouse_background_tasks(
self, cmd: str, backupn: int, cluster: ClickHouseCluster, timeout: int = 30
) -> bool:
"""Stop all background operations that could affect memory."""
try:
active_merges = 1
active_mutations = 1
next_node: ClickHouseInstance = cluster.instances["node0"]
client = Client(
host=next_node.ip_address, port=9000, command=cluster.client_bin_path
)
for c in ["MERGES", "MUTATIONS", "FETCHES", "MOVES", "TTL MERGES"]:
client.query(f"SYSTEM {cmd} {c};")
if cmd == "START":
active_merges = 0
active_mutations = 0
# Restore everything
client.query(
f"RESTORE ALL FROM File('/var/lib/clickhouse/user_files/leaks{backupn}');"
)
else:
start_time = time.time()
while (
active_merges != 0 or active_mutations != 0
) and time.time() - start_time < timeout:
# Check for active merges and mutations
result1 = client.query("SELECT count() FROM system.merges;")
result2 = client.query(
"SELECT count() FROM system.mutations WHERE is_done = 0;"
)
if (
not isinstance(result1, str)
or result1 == ""
or not isinstance(result2, str)
or result2 == ""
):
self.logger.warning(
"Could not fetch number of merges or mutations"
)
return False
active_merges = int(result1)
active_mutations = int(result2)
for c in ["MARK CACHE", "UNCOMPRESSED CACHE"]:
client.query(f"SYSTEM CLEAR {c};")
# Backup then drop all databases
client.query(
f"BACKUP ALL TO File('/var/lib/clickhouse/user_files/leaks{backupn}');"
)
dbs_str = client.query(
"SELECT name FROM system.databases WHERE name NOT IN ('system', 'information_schema', 'INFORMATION_SCHEMA');"
)
if not isinstance(dbs_str, str) or dbs_str == "":
self.logger.warning("Could not fetch databases")
return False
fetched_dbs: list[str] = [line for line in dbs_str.split("\n") if line]
for db_name in fetched_dbs:
client.query(f"DROP DATABASE IF EXISTS {db_name};")
return active_merges == 0 and active_mutations == 0
except Exception as ex:
self.logger.warning(
f"Error occurred while setting {cmd} operations in the server: {ex}"
)
return False
def run_next_leak_detection(
self,
cluster: ClickHouseCluster,
generator: CommandRequest,
leak_threshold_percent: float = 10.0,
):
backupn = self.counter
self.counter += 1
generator.pause_process()
time.sleep(1) # wait for process to stop?
if self._pause_or_resume_clickhouse_background_tasks("STOP", backupn, cluster):
self._detect_leak(cluster, leak_threshold_percent)
self._pause_or_resume_clickhouse_background_tasks("START", backupn, cluster)
generator.resume_process()