kgeisz commented on code in PR #8517:
URL: https://github.com/apache/hbase/pull/8517#discussion_r3925275145
##########
dev-support/read-replica/python/src/hbase_docker_client.py:
##########
@@ -0,0 +1,623 @@
+#!/usr/bin/env python3
+import ast
+import logging
+import re
+from concurrent.futures import ThreadPoolExecutor, TimeoutError as
FuturesTimeoutError
+
+import docker
+import requests
+import subprocess
+import time
+import xml.etree.ElementTree as ET
+
+from .logger_config import get_logger
+
+logger = get_logger(__name__)
+
+
+class DockerExecCommandError(Exception):
+ pass
+
+
+class HBaseShellCommandError(DockerExecCommandError):
+ pass
+
+
+class DockerExecCommandTimeoutError(DockerExecCommandError):
+ pass
+
+
+class HBaseDockerClient:
+ def __init__(self, container_name: str, local_conf: str, hbase_ui_port:
int = 16010,
+ cluster_name: str = "HBase Cluster", max_retries: int = 12,
sleep_time: int = 5,
+ hbase_host: str = "localhost") -> None:
+ self._container_name = container_name
+ self._local_conf = local_conf
+ self._hbase_ui_port = hbase_ui_port
+ self._cluster_name = cluster_name
+ self._max_retries = max_retries
+ self._sleep_time = sleep_time
+ self._hbase_host = hbase_host
+ self._docker_client = docker.from_env()
+
+ @property
+ def name(self) -> str:
+ return self._cluster_name
+
+ def run_docker_exec_command(self, bash_cmd: str, timeout: int | None =
None) -> str:
+ """
+ Uses the Docker SDK to exec a Bash command in the object's Docker
container.
+ Equivalent to: docker exec <container> bash -c <bash_cmd>
+ """
+ cmd = ["bash", "-c", bash_cmd]
+ cmd_str = f"docker exec {self._container_name} bash -c {bash_cmd}"
+ logger.debug(f"Running command on {self._cluster_name}: {cmd_str}")
+
+ try:
+ container =
self._docker_client.containers.get(self._container_name)
+
+ if timeout is not None:
+ with ThreadPoolExecutor(max_workers=1) as pool:
+ future = pool.submit(container.exec_run, cmd, demux=True)
+ try:
+ result = future.result(timeout=timeout)
+ except FuturesTimeoutError:
+ raise DockerExecCommandTimeoutError(
+ f"Command timed out after {timeout}s on
{self._cluster_name} "
+ f"({self._container_name}): {bash_cmd}\n"
+ f"The command used to run this was: {cmd_str}\n"
+ )
+ else:
+ result = container.exec_run(cmd, demux=True)
+ except DockerExecCommandError:
+ raise
+ except docker.errors.DockerException as e:
+ raise DockerExecCommandError(
+ f"The following command failed on {self._cluster_name}
({self._container_name}): {bash_cmd}\n"
+ f"The command used to run this was: {cmd_str}\n"
+ f"Docker error: {e}\n"
+ )
+
+ exit_code, (stdout, stderr) = result
+ stdout_str = (stdout or b'').decode('utf-8')
+ if exit_code != 0:
+ raise DockerExecCommandError(
+ f"The following command failed on {self._cluster_name}
({self._container_name}): {bash_cmd}\n"
+ f"The command used to run this was: {cmd_str}\n"
+ f"The command's STDERR was:\n{(stderr or
b'').decode('utf-8')}\n"
+ f"The command's STDOUT was:\n{stdout_str}\n"
+ )
+ return stdout_str
+
+ def run_hbase_shell_command(self, hbase_cmd: str, timeout: int | None =
None) -> str:
+ """
+ Uses 'docker exec' to run the provided HBase shell command in the
object's Docker container.
+ The command looks like: docker exec <container> bash -c hbase shell -n
<<< "<hbase_cmd>"
+ """
+ hbase_shell_cmd = f'''hbase shell -n <<< "{hbase_cmd}"'''
+ try:
+ return self.run_docker_exec_command(hbase_shell_cmd,
timeout=timeout)
+ except DockerExecCommandTimeoutError:
+ # DockerExecCommandTimeoutError is a subclass of
DockerExecCommandError, so we need to make sure
+ # it's specifically caught and re-raised. Otherwise, it's
swallowed when catching DockerExecCommandError
+ raise
+ except DockerExecCommandError as e:
+ raise HBaseShellCommandError(e)
+
+ def _get_pid_from_jps(self, process_name: str) -> int | None:
+ """Runs jps inside the container and returns the PID of the named
process, or None."""
+ try:
+ output = self.run_docker_exec_command("jps")
+ for line in output.strip().splitlines():
+ parts = line.split()
+ if len(parts) == 2 and parts[1] == process_name:
+ return int(parts[0])
+ except DockerExecCommandError:
+ pass
+ return None
+
+ def wait_for_hbase_ui(self) -> bool:
+ """Checks for a 200 OK on the HBase Master UI."""
+ # Read HBASE_HOST from environment, falling back to 'localhost' for
host-native execution
+ url = f"http://{self._hbase_host}:{self._hbase_ui_port}"
+ logger.info(f"Waiting for HBase UI: {self._cluster_name} on {url}")
+ last_exception = None
+ for attempt in range(1, self._max_retries + 1):
+ try:
+ response = requests.get(url)
+ if response.status_code == 200:
+ logger.info(f"SUCCESS: {self._cluster_name} UI is up.")
+ return True
+ except requests.exceptions.ConnectionError as e:
+ last_exception = e
+ logging.info(f"Waiting {self._sleep_time} seconds before
requesting HBase UI again")
+ time.sleep(self._sleep_time)
+
+ raise RuntimeError(f"\nTIMEOUT: {self._cluster_name} UI failed to
respond after "
+ f"{self._max_retries} attempts. "
+ f"Last raised exception was: {last_exception}")
+
+ def wait_for_master_initialization(self) -> bool:
+ """Waits for the current HMaster process to log 'Master has completed
initialization'."""
+ logger.info(f"Waiting for Master initialization: {self._cluster_name}
({self._container_name})")
+ for attempt in range(1, self._max_retries + 1):
+ pid = self._get_pid_from_jps("HMaster")
+ if pid is not None:
+ awk_cmd = (
+ f"awk '/env:JVM_PID={pid}/{{seen=1; found=0}} "
+ f"seen && /Master has completed initialization/{{found=1}}
"
+ f"END{{exit !found}}' /opt/hbase/logs/hbase-*-master-*.log"
+ )
+ try:
+ self.run_docker_exec_command(awk_cmd)
+ logger.info(f"SUCCESS: {self._cluster_name} Master has
completed initialization.")
+ return True
+ except DockerExecCommandError:
+ pass
+ logging.info(f"Waiting {self._sleep_time} seconds before checking
Master initialization again")
+ time.sleep(self._sleep_time)
+
+ raise RuntimeError(
+ f"\nTIMEOUT: {self._cluster_name} Master failed to initialize
after "
+ f"{self._max_retries} attempts.")
+
+ def wait_for_region_server_initialization(self) -> bool:
+ """Waits for the current HRegionServer process to log 'Serving as'
message."""
+ logger.info(f"Waiting for RegionServer initialization:
{self._cluster_name} ({self._container_name})")
+ for attempt in range(1, self._max_retries + 1):
+ pid = self._get_pid_from_jps("HRegionServer")
+ if pid is not None:
+ awk_cmd = (
+ f"awk '/env:JVM_PID={pid}/{{seen=1; found=0}} "
+ f"seen && /Serving as {self._container_name},/{{found=1}} "
+ f"END{{exit !found}}'
/opt/hbase/logs/hbase-*-regionserver-*.log"
+ )
+ try:
+ self.run_docker_exec_command(awk_cmd)
+ logger.info(f"SUCCESS: {self._cluster_name} RegionServer
is serving.")
+ return True
+ except DockerExecCommandError:
+ pass
+ logging.info(f"Waiting {self._sleep_time} seconds before checking
RegionServer initialization again")
+ time.sleep(self._sleep_time)
+
+ raise RuntimeError(
+ f"\nTIMEOUT: {self._cluster_name} RegionServer failed to
initialize after "
+ f"{self._max_retries} attempts.")
+
+ def check_server_status(self, desired_status: dict | None = None) -> bool:
+ """Runs 'status' inside the HBase shell and validates the output."""
+ if desired_status is None:
+ desired_status = {'masters': '1', 'region_servers': '1',
'dead_servers': '0'}
+ logger.info(f"Validating Cluster Status: {self._cluster_name}
({self._container_name})")
+ for attempt in range(1, self._max_retries + 1):
+ try:
+ output = self.get_hbase_status()
+
+ # The cluster's status should have 1 active master, 1 region
server,
+ # and no dead servers
+ validations = {
+ "Active Master": f"{desired_status['masters']} active
master" in output,
+ "Region Server": f"{desired_status['region_servers']}
servers" in output,
+ "No Dead Servers": f"{desired_status['dead_servers']}
dead" in output
+ }
+
+ if all(validations.values()):
+ for check, status in validations.items():
+ logger.info(f" [PASS] {check}")
+ logger.info(f"SUCCESS: {self._cluster_name} is fully
operational.")
+ return True
+ else:
+ logger.warning(f"{self._cluster_name} is responding, but
not all "
+ f"components are ready...")
+ logger.info(f"HBase 'status' command output:\n{output}")
+
+ except HBaseShellCommandError:
+ pass
+
+ logging.info(f"Waiting {self._sleep_time} seconds before getting
status on {self.name} again")
+ time.sleep(self._sleep_time)
+
+ raise RuntimeError(
+ f"\nTIMEOUT: {self._cluster_name} shell check failed after
{self._max_retries} attempts.")
+
+ def get_hbase_status(self) -> str:
+ logger.debug(f"Getting status of {self.name}")
+ return self.run_hbase_shell_command("status")
+
+ def wait_for_cluster_to_start(self) -> None:
+ """curls the cluster's HBase UI to make sure it is up and then makes
sure all desired servers are up"""
+ self.wait_for_hbase_ui()
+ self.wait_for_master_initialization()
+ self.wait_for_region_server_initialization()
+ self.check_server_status()
+
+ def create_table(self, table_name: str, column_family: str) -> bool:
+ logger.info(f"Creating table '{table_name}' on {self._cluster_name}")
+ create_cmd = f"create '{table_name}', '{column_family}'"
+ output = self.run_hbase_shell_command(create_cmd)
+
+ if f"Created table {table_name}" not in output:
+ logger.error(f"Could not create table '{table_name}' on
{self._cluster_name}")
+ return False
+ return True
+
+ def disable_table(self, table_name: str) -> None:
+ logger.debug(f"Disabling table '{table_name}' on {self.name}")
+ self.run_hbase_shell_command(f"disable '{table_name}'")
+
+ def drop_table(self, table_name: str) -> None:
+ logger.info(f"Dropping table '{table_name}' on {self.name}")
+ self.run_hbase_shell_command(f"drop '{table_name}'")
+
+ def list_tables(self) -> list:
Review Comment:
The empty list case has been handled. The regex grabs the `[...]` text, and
the final line in the return statement evaluates it and turns it into Python
code. If there are no tables, then we get `[]`, which is evaluated as an empty
list.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]