Skip to content

Commit 7e93461

Browse files
feat(tools): added ClickhouseService
1 parent 4901782 commit 7e93461

3 files changed

Lines changed: 190 additions & 0 deletions

File tree

‎asap-tools/experiments/experiment_utils/services/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
DumbKafkaConsumerService,
4040
)
4141
from .grafana import GrafanaService
42+
from .clickhouse_service import ClickHouseService
4243

4344

4445
def create_prometheus_service(cfg, provider, num_nodes: int, node_offset: int):
@@ -153,4 +154,5 @@ def create_prometheus_service(cfg, provider, num_nodes: int, node_offset: int):
153154
"DumbKafkaConsumerService",
154155
"GrafanaService",
155156
"create_prometheus_service",
157+
"ClickHouseService",
156158
]
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
"""
2+
ClickHouse Docker service management for SQL experiment infrastructure.
3+
"""
4+
5+
import os
6+
import subprocess
7+
from typing import Optional
8+
from jinja2 import Template
9+
10+
from .base import DockerServiceBase
11+
from experiment_utils.providers.base import InfrastructureProvider
12+
import constants
13+
import utils
14+
15+
16+
class ClickHouseService(DockerServiceBase):
17+
"""Manages a ClickHouse Docker container on a remote CloudLab node."""
18+
19+
CONTAINER_NAME = "clickhouse-server"
20+
DEFAULT_HTTP_PORT = 8123
21+
DEFAULT_NATIVE_PORT = 9000
22+
DEFAULT_IMAGE_TAG = "latest"
23+
DEFAULT_DATABASE = "default"
24+
25+
def __init__(
26+
self, provider: InfrastructureProvider, num_nodes: int, node_offset: int
27+
):
28+
super().__init__(provider, num_nodes, node_offset)
29+
self.compose_file: Optional[str] = None
30+
self._http_port = self.DEFAULT_HTTP_PORT
31+
32+
def get_container_name(self) -> str:
33+
return self.CONTAINER_NAME
34+
35+
def get_service_url(self) -> str:
36+
return f"http://localhost:{self._http_port}"
37+
38+
def get_health_endpoint(self) -> str:
39+
# ClickHouse HTTP interface responds with "Ok." on /ping when ready
40+
return "/ping"
41+
42+
def get_http_port(self) -> int:
43+
return self._http_port
44+
45+
def is_healthy(self) -> bool:
46+
"""ClickHouse is ready only when /ping returns exactly 'Ok.'"""
47+
result = self.provider.execute_command(
48+
node_idx=self.node_offset,
49+
cmd=f"curl -s http://localhost:{self._http_port}/ping",
50+
cmd_dir=None,
51+
nohup=False,
52+
popen=False,
53+
ignore_errors=True,
54+
)
55+
if not isinstance(result, subprocess.CompletedProcess):
56+
return False
57+
return result.returncode == 0 and result.stdout.strip() == "Ok."
58+
59+
def start(
60+
self,
61+
experiment_output_dir: str,
62+
local_experiment_dir: str,
63+
http_port: int = DEFAULT_HTTP_PORT,
64+
native_port: int = DEFAULT_NATIVE_PORT,
65+
database: str = DEFAULT_DATABASE,
66+
image_tag: str = DEFAULT_IMAGE_TAG,
67+
cpu_limit: Optional[float] = None,
68+
memory_limit: Optional[str] = None,
69+
**kwargs,
70+
) -> None:
71+
"""
72+
Render the Jinja2 compose template, rsync it to the node, and start ClickHouse.
73+
74+
Args:
75+
experiment_output_dir: Remote directory for data and config storage
76+
local_experiment_dir: Local directory to write the rendered compose file
77+
http_port: ClickHouse HTTP interface port (default 8123)
78+
native_port: ClickHouse native TCP port (default 9000)
79+
database: Default database name
80+
image_tag: Docker image tag for clickhouse/clickhouse-server
81+
cpu_limit: Optional CPU limit (e.g. 4.0)
82+
memory_limit: Optional memory limit (e.g. "8g")
83+
"""
84+
self._http_port = http_port
85+
self._force_cleanup_container()
86+
87+
data_dir = os.path.join(experiment_output_dir, "clickhouse_data")
88+
log_dir = os.path.join(experiment_output_dir, "clickhouse_logs")
89+
90+
self.provider.execute_command(
91+
node_idx=self.node_offset,
92+
cmd=f"mkdir -p {data_dir} {log_dir} && chmod 777 {data_dir} {log_dir}",
93+
cmd_dir=None,
94+
nohup=False,
95+
popen=False,
96+
)
97+
98+
template_path = os.path.join(
99+
os.path.dirname(__file__), "docker-compose.clickhouse.yml.j2"
100+
)
101+
with open(template_path, "r") as f:
102+
template = Template(f.read())
103+
104+
compose_content = template.render(
105+
container_name=self.CONTAINER_NAME,
106+
image_tag=image_tag,
107+
database=database,
108+
data_dir=data_dir,
109+
log_dir=log_dir,
110+
cpu_limit=str(cpu_limit) if cpu_limit is not None else None,
111+
memory_limit=memory_limit,
112+
)
113+
114+
local_compose_file = os.path.join(
115+
local_experiment_dir, "docker-compose.clickhouse.yml"
116+
)
117+
os.makedirs(os.path.dirname(local_compose_file), exist_ok=True)
118+
with open(local_compose_file, "w") as f:
119+
f.write(compose_content)
120+
121+
remote_compose_file = os.path.join(
122+
experiment_output_dir, "docker-compose.clickhouse.yml"
123+
)
124+
self.compose_file = remote_compose_file
125+
126+
hostname = f"node{self.node_offset}.{self.provider.hostname_suffix}"
127+
rsync_cmd = 'rsync -azh -e "ssh {}" {} {}@{}:{}'.format(
128+
constants.SSH_OPTIONS,
129+
local_compose_file,
130+
self.provider.username,
131+
hostname,
132+
remote_compose_file,
133+
)
134+
utils.run_cmd_with_retry(rsync_cmd, popen=False, ignore_errors=False)
135+
136+
self.provider.execute_command(
137+
node_idx=self.node_offset,
138+
cmd=f"docker compose -f {remote_compose_file} up -d",
139+
cmd_dir=None,
140+
nohup=False,
141+
popen=False,
142+
)
143+
144+
self._wait_for_service_ready()
145+
146+
def stop(self, **kwargs) -> None:
147+
"""Stop and remove the ClickHouse container."""
148+
if self.compose_file:
149+
self.provider.execute_command(
150+
node_idx=self.node_offset,
151+
cmd=f"docker compose -f {self.compose_file} down",
152+
cmd_dir=None,
153+
nohup=False,
154+
popen=False,
155+
ignore_errors=True,
156+
)
157+
self.compose_file = None
158+
else:
159+
self._force_cleanup_container()
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
version: '3.8'
2+
3+
services:
4+
clickhouse:
5+
image: clickhouse/clickhouse-server:{{ image_tag }}
6+
container_name: {{ container_name }}
7+
network_mode: host
8+
volumes:
9+
- {{ data_dir }}:/var/lib/clickhouse
10+
- {{ log_dir }}:/var/log/clickhouse-server
11+
environment:
12+
- CLICKHOUSE_DB={{ database }}
13+
- CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1
14+
ulimits:
15+
nofile:
16+
soft: 262144
17+
hard: 262144
18+
{% if cpu_limit or memory_limit -%}
19+
deploy:
20+
resources:
21+
limits:
22+
{% if cpu_limit -%}
23+
cpus: '{{ cpu_limit }}'
24+
{% endif -%}
25+
{% if memory_limit -%}
26+
memory: {{ memory_limit }}
27+
{% endif -%}
28+
{% endif -%}
29+
restart: "no"

0 commit comments

Comments
 (0)