2020-09-16 04:26:10 +00:00
|
|
|
import os
|
2017-05-19 18:54:05 +00:00
|
|
|
import subprocess as sp
|
2017-05-30 11:49:17 +00:00
|
|
|
import tempfile
|
2021-06-21 15:42:40 +00:00
|
|
|
import logging
|
2020-09-16 04:26:10 +00:00
|
|
|
from threading import Timer
|
2017-05-19 18:54:05 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Client:
|
|
|
|
def __init__(self, host, port=9000, command='/usr/bin/clickhouse-client'):
|
|
|
|
self.host = host
|
|
|
|
self.port = port
|
2018-08-23 15:31:20 +00:00
|
|
|
self.command = [command]
|
|
|
|
|
|
|
|
if os.path.basename(command) == 'clickhouse':
|
|
|
|
self.command.append('client')
|
|
|
|
|
|
|
|
self.command += ['--host', self.host, '--port', str(self.port), '--stacktrace']
|
2017-05-19 18:54:05 +00:00
|
|
|
|
2020-09-16 04:26:10 +00:00
|
|
|
def query(self, sql, stdin=None, timeout=None, settings=None, user=None, password=None, database=None,
|
|
|
|
ignore_error=False):
|
|
|
|
return self.get_query_request(sql, stdin=stdin, timeout=timeout, settings=settings, user=user,
|
|
|
|
password=password, database=database, ignore_error=ignore_error).get_answer()
|
2017-05-30 11:49:17 +00:00
|
|
|
|
2020-09-16 04:26:10 +00:00
|
|
|
def get_query_request(self, sql, stdin=None, timeout=None, settings=None, user=None, password=None, database=None,
|
|
|
|
ignore_error=False):
|
2017-07-24 20:12:59 +00:00
|
|
|
command = self.command[:]
|
2017-05-30 11:49:17 +00:00
|
|
|
|
2017-05-19 18:54:05 +00:00
|
|
|
if stdin is None:
|
2018-07-27 17:19:22 +00:00
|
|
|
command += ['--multiquery', '--testmode']
|
2017-05-19 18:54:05 +00:00
|
|
|
stdin = sql
|
|
|
|
else:
|
2017-05-30 11:49:17 +00:00
|
|
|
command += ['--query', sql]
|
2017-05-19 18:54:05 +00:00
|
|
|
|
2017-11-02 14:18:46 +00:00
|
|
|
if settings is not None:
|
2020-10-02 16:54:07 +00:00
|
|
|
for setting, value in settings.items():
|
2017-11-02 14:18:46 +00:00
|
|
|
command += ['--' + setting, str(value)]
|
|
|
|
|
2018-08-14 02:18:57 +00:00
|
|
|
if user is not None:
|
|
|
|
command += ['--user', user]
|
|
|
|
|
2020-02-29 12:57:52 +00:00
|
|
|
if password is not None:
|
|
|
|
command += ['--password', password]
|
|
|
|
|
2020-06-24 14:31:05 +00:00
|
|
|
if database is not None:
|
|
|
|
command += ['--database', database]
|
|
|
|
|
2018-08-22 15:42:27 +00:00
|
|
|
return CommandRequest(command, stdin, timeout, ignore_error)
|
2017-05-30 11:49:17 +00:00
|
|
|
|
2020-09-16 04:26:10 +00:00
|
|
|
def query_and_get_error(self, sql, stdin=None, timeout=None, settings=None, user=None, password=None,
|
|
|
|
database=None):
|
|
|
|
return self.get_query_request(sql, stdin=stdin, timeout=timeout, settings=settings, user=user,
|
|
|
|
password=password, database=database).get_error()
|
2017-05-30 11:49:17 +00:00
|
|
|
|
2020-09-16 04:26:10 +00:00
|
|
|
def query_and_get_answer_with_error(self, sql, stdin=None, timeout=None, settings=None, user=None, password=None,
|
|
|
|
database=None):
|
|
|
|
return self.get_query_request(sql, stdin=stdin, timeout=timeout, settings=settings, user=user,
|
|
|
|
password=password, database=database).get_answer_and_error()
|
2019-04-07 00:31:20 +00:00
|
|
|
|
2019-07-17 11:55:18 +00:00
|
|
|
|
2017-07-26 14:41:21 +00:00
|
|
|
class QueryTimeoutExceedException(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class QueryRuntimeException(Exception):
|
2021-01-07 03:42:39 +00:00
|
|
|
def __init__(self, message, returncode, stderr):
|
|
|
|
super(QueryRuntimeException, self).__init__(message)
|
|
|
|
self.returncode = returncode
|
|
|
|
self.stderr = stderr
|
2017-07-26 14:41:21 +00:00
|
|
|
|
|
|
|
|
2017-07-24 20:12:59 +00:00
|
|
|
class CommandRequest:
|
2018-08-22 15:42:27 +00:00
|
|
|
def __init__(self, command, stdin=None, timeout=None, ignore_error=False):
|
2017-05-30 11:49:17 +00:00
|
|
|
# Write data to tmp file to avoid PIPEs and execution blocking
|
2020-10-02 16:54:07 +00:00
|
|
|
stdin_file = tempfile.TemporaryFile(mode='w+')
|
2017-05-30 11:49:17 +00:00
|
|
|
stdin_file.write(stdin)
|
|
|
|
stdin_file.seek(0)
|
|
|
|
self.stdout_file = tempfile.TemporaryFile()
|
|
|
|
self.stderr_file = tempfile.TemporaryFile()
|
2018-08-22 15:42:27 +00:00
|
|
|
self.ignore_error = ignore_error
|
2017-05-19 18:54:05 +00:00
|
|
|
|
2020-09-07 08:25:14 +00:00
|
|
|
# print " ".join(command)
|
2017-05-30 11:49:17 +00:00
|
|
|
|
2020-01-28 10:58:37 +00:00
|
|
|
# we suppress stderror on client becase sometimes thread sanitizer
|
|
|
|
# can print some debug information there
|
|
|
|
env = {}
|
|
|
|
env["TSAN_OPTIONS"] = "verbosity=0"
|
2020-10-02 16:54:07 +00:00
|
|
|
self.process = sp.Popen(command, stdin=stdin_file, stdout=self.stdout_file, stderr=self.stderr_file, env=env, universal_newlines=True)
|
2017-05-30 11:49:17 +00:00
|
|
|
|
|
|
|
self.timer = None
|
|
|
|
self.process_finished_before_timeout = True
|
2017-05-19 18:54:05 +00:00
|
|
|
if timeout is not None:
|
|
|
|
def kill_process():
|
2017-05-30 11:49:17 +00:00
|
|
|
if self.process.poll() is None:
|
|
|
|
self.process_finished_before_timeout = False
|
2017-07-24 20:12:59 +00:00
|
|
|
self.process.kill()
|
2017-05-30 11:49:17 +00:00
|
|
|
|
|
|
|
self.timer = Timer(timeout, kill_process)
|
|
|
|
self.timer.start()
|
2017-05-19 18:54:05 +00:00
|
|
|
|
2017-05-30 11:49:17 +00:00
|
|
|
def get_answer(self):
|
|
|
|
self.process.wait()
|
|
|
|
self.stdout_file.seek(0)
|
|
|
|
self.stderr_file.seek(0)
|
2017-05-19 18:54:05 +00:00
|
|
|
|
2021-02-17 15:40:01 +00:00
|
|
|
stdout = self.stdout_file.read().decode('utf-8', errors='replace')
|
|
|
|
stderr = self.stderr_file.read().decode('utf-8', errors='replace')
|
2017-05-19 18:54:05 +00:00
|
|
|
|
2018-08-22 15:42:27 +00:00
|
|
|
if self.timer is not None and not self.process_finished_before_timeout and not self.ignore_error:
|
2021-06-21 15:42:40 +00:00
|
|
|
logging.debug(f"Timed out. Last stdout:{stdout}, stderr:{stderr}")
|
2017-07-26 14:41:21 +00:00
|
|
|
raise QueryTimeoutExceedException('Client timed out!')
|
|
|
|
|
2018-08-22 15:42:27 +00:00
|
|
|
if (self.process.returncode != 0 or stderr) and not self.ignore_error:
|
2020-09-16 04:26:10 +00:00
|
|
|
raise QueryRuntimeException(
|
2021-01-07 03:42:39 +00:00
|
|
|
'Client failed! Return code: {}, stderr: {}'.format(self.process.returncode, stderr), self.process.returncode, stderr)
|
2017-05-19 18:54:05 +00:00
|
|
|
|
|
|
|
return stdout
|
2019-04-07 00:31:20 +00:00
|
|
|
|
|
|
|
def get_error(self):
|
|
|
|
self.process.wait()
|
|
|
|
self.stdout_file.seek(0)
|
|
|
|
self.stderr_file.seek(0)
|
|
|
|
|
2021-02-17 15:40:01 +00:00
|
|
|
stdout = self.stdout_file.read().decode('utf-8', errors='replace')
|
|
|
|
stderr = self.stderr_file.read().decode('utf-8', errors='replace')
|
2019-04-07 00:31:20 +00:00
|
|
|
|
|
|
|
if self.timer is not None and not self.process_finished_before_timeout and not self.ignore_error:
|
|
|
|
raise QueryTimeoutExceedException('Client timed out!')
|
|
|
|
|
|
|
|
if (self.process.returncode == 0):
|
2021-01-07 03:42:39 +00:00
|
|
|
raise QueryRuntimeException('Client expected to be failed but succeeded! stdout: {}'.format(stdout), self.process.returncode, stderr)
|
2019-04-07 00:31:20 +00:00
|
|
|
|
|
|
|
return stderr
|
2019-07-17 11:55:18 +00:00
|
|
|
|
|
|
|
def get_answer_and_error(self):
|
|
|
|
self.process.wait()
|
|
|
|
self.stdout_file.seek(0)
|
|
|
|
self.stderr_file.seek(0)
|
|
|
|
|
2021-02-17 15:40:01 +00:00
|
|
|
stdout = self.stdout_file.read().decode('utf-8', errors='replace')
|
|
|
|
stderr = self.stderr_file.read().decode('utf-8', errors='replace')
|
2019-07-17 11:55:18 +00:00
|
|
|
|
|
|
|
if self.timer is not None and not self.process_finished_before_timeout and not self.ignore_error:
|
|
|
|
raise QueryTimeoutExceedException('Client timed out!')
|
|
|
|
|
|
|
|
return (stdout, stderr)
|