This is an automated email from the ASF dual-hosted git repository.
dataroaring pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new b9496b2a8f [feature](docker) regression test support run suite in
docker (#24508)
b9496b2a8f is described below
commit b9496b2a8f8bc8f0b3b7bcac3af53004def55a9b
Author: yujun <[email protected]>
AuthorDate: Sun Sep 24 21:44:16 2023 +0800
[feature](docker) regression test support run suite in docker (#24508)
---
docker/runtime/doris-compose/Dockerfile | 2 +-
docker/runtime/doris-compose/cluster.py | 51 ++-
docker/runtime/doris-compose/command.py | 162 +++++++--
docker/runtime/doris-compose/database.py | 38 ++-
docker/runtime/doris-compose/doris-compose.py | 37 ++-
docker/runtime/doris-compose/resource/init_be.sh | 7 +-
docker/runtime/doris-compose/resource/init_fe.sh | 44 ++-
docker/runtime/doris-compose/utils.py | 35 +-
regression-test/conf/regression-conf.groovy | 7 +
.../org/apache/doris/regression/Config.groovy | 39 ++-
.../apache/doris/regression/ConfigOptions.groovy | 11 +
.../org/apache/doris/regression/suite/Suite.groovy | 34 ++
.../doris/regression/suite/SuiteCluster.groovy | 363 +++++++++++++++++++++
.../suites/chaos/test_docker_example.groovy | 53 +++
14 files changed, 797 insertions(+), 86 deletions(-)
diff --git a/docker/runtime/doris-compose/Dockerfile
b/docker/runtime/doris-compose/Dockerfile
index 8f5d7e3376..b930b10668 100644
--- a/docker/runtime/doris-compose/Dockerfile
+++ b/docker/runtime/doris-compose/Dockerfile
@@ -25,6 +25,6 @@ ENV JAVA_HOME="/usr/local/openjdk-8/"
ADD output /opt/apache-doris/
RUN apt-get update && \
- apt-get install -y default-mysql-client python && \
+ apt-get install -y default-mysql-client python lsof && \
apt-get clean
diff --git a/docker/runtime/doris-compose/cluster.py
b/docker/runtime/doris-compose/cluster.py
index 2556809a3c..6c6ccf3953 100644
--- a/docker/runtime/doris-compose/cluster.py
+++ b/docker/runtime/doris-compose/cluster.py
@@ -91,8 +91,8 @@ class Group(object):
self.nodes = {} # id : NodeMeta
self.next_id = 1
- def add(self, id, image):
- assert image
+ def add(self, id, node_meta):
+ assert node_meta.image
if not id:
id = self.next_id
self.next_id += 1
@@ -104,7 +104,7 @@ class Group(object):
raise Exception(
"Failed to add {} with id {}, id exceeds {}".format(
self.node_type, id, ID_LIMIT))
- self.nodes[id] = NodeMeta(image)
+ self.nodes[id] = node_meta
return id
@@ -147,7 +147,7 @@ class Node(object):
else:
raise Exception("Unknown node type {}".format(node_type))
- def init_dir(self):
+ def init_conf(self, config):
path = self.get_path()
os.makedirs(path, exist_ok=True)
@@ -160,6 +160,12 @@ class Node(object):
conf_dir)
assert not utils.is_dir_empty(conf_dir), "conf directory {} is
empty, " \
"check doris path in image is correct".format(conf_dir)
+ utils.enable_dir_with_rw_perm(conf_dir)
+ if config:
+ with open(os.path.join(conf_dir, self.conf_file_name()),
+ "a") as f:
+ for item in config:
+ f.write(item + "\n")
for sub_dir in self.expose_sub_dirs():
os.makedirs(os.path.join(path, sub_dir), exist_ok=True)
@@ -169,6 +175,9 @@ class Node(object):
def is_be(self):
return self.node_type() == Node.TYPE_BE
+ def conf_file_name(self):
+ return self.node_type() + ".conf"
+
def node_type(self):
raise Exception("No implemented")
@@ -307,22 +316,38 @@ class BE(Node):
def expose_sub_dirs(self):
return super().expose_sub_dirs() + ["storage"]
+ def init_disk(self, be_disk_num):
+ if not be_disk_num or be_disk_num <= 1:
+ return
+ path = self.get_path()
+ for i in range(1, be_disk_num + 1):
+ os.makedirs("{}/storage/disk{}".format(path, i), exist_ok=True)
+ with open("{}/conf/{}".format(path, self.conf_file_name()), "a") as f:
+ f.write("storage_root_path = " + ";".join([
+ "${{DORIS_HOME}}/storage/disk{}".format(i)
+ for i in range(1, be_disk_num + 1)
+ ]) + "\n")
+
class Cluster(object):
- def __init__(self, name, subnet, image):
+ def __init__(self, name, subnet, image, fe_config, be_config, be_disk_num):
self.name = name
self.subnet = subnet
self.image = image
+ self.fe_config = fe_config
+ self.be_config = be_config
+ self.be_disk_num = be_disk_num
self.groups = {
node_type: Group(node_type)
for node_type in Node.TYPE_ALL
}
@staticmethod
- def new(name, image):
+ def new(name, image, fe_config, be_config, be_disk_num):
subnet = gen_subnet_prefix16()
- cluster = Cluster(name, subnet, image)
+ cluster = Cluster(name, subnet, image, fe_config, be_config,
+ be_disk_num)
os.makedirs(cluster.get_path(), exist_ok=True)
os.makedirs(get_status_path(name), exist_ok=True)
return cluster
@@ -393,9 +418,17 @@ class Cluster(object):
return num
def add(self, node_type, id=None):
- id = self.get_group(node_type).add(id, self.image)
+ node_meta = NodeMeta(self.image)
+ id = self.get_group(node_type).add(id, node_meta)
node = self.get_node(node_type, id)
- node.init_dir()
+ if not os.path.exists(node.get_path()):
+ if node.is_fe():
+ node.init_conf(self.fe_config)
+ elif node.is_be():
+ node.init_conf(self.be_config)
+ node.init_disk(self.be_disk_num)
+ else:
+ node.init_conf([])
return node
def remove(self, node_type, id):
diff --git a/docker/runtime/doris-compose/command.py
b/docker/runtime/doris-compose/command.py
index 2fa556d513..adb2d2b0d0 100644
--- a/docker/runtime/doris-compose/command.py
+++ b/docker/runtime/doris-compose/command.py
@@ -24,6 +24,7 @@ import os.path
import prettytable
import shutil
import sys
+import time
LOG = utils.get_logger()
@@ -73,6 +74,12 @@ class Command(object):
def run(self, args):
raise Exception("No implemented")
+ def _add_parser_output_json(self, parser):
+ parser.add_argument("--output-json",
+ default=False,
+ action=self._get_parser_bool_action(True),
+ help="output as json, and don't print log")
+
def _add_parser_ids_args(self, parser):
group = parser.add_argument_group("for existing nodes",
"apply to the existing nodes.")
@@ -100,6 +107,7 @@ class SimpleCommand(Command):
parser = args_parsers.add_parser(self.command, help=help)
parser.add_argument("NAME", help="Specify cluster name.")
self._add_parser_ids_args(parser)
+ self._add_parser_output_json(parser)
def run(self, args):
cluster = CLUSTER.Cluster.load(args.NAME)
@@ -127,6 +135,16 @@ class UpCommand(Command):
nargs="?",
help="Specify docker image.")
+ parser.add_argument(
+ "--wait-timeout",
+ type=int,
+ default=0,
+ help=
+ "Specify wait seconds for upping: 0(default) not wait, > 0 max
wait seconds, -1 wait unlimited."
+ )
+
+ self._add_parser_output_json(parser)
+
group1 = parser.add_argument_group("add new nodes",
"add cluster nodes.")
group1.add_argument(
@@ -137,6 +155,18 @@ class UpCommand(Command):
"--add-be-num",
type=int,
help="Specify add be num, default 3 for a new cluster.")
+ group1.add_argument("--fe-config",
+ nargs="*",
+ type=str,
+ help="Specify fe configs.")
+ group1.add_argument("--be-config",
+ nargs="*",
+ type=str,
+ help="Specify be configs.")
+ group1.add_argument("--be-disk-num",
+ default=None,
+ type=int,
+ help="Specify be disk num, default is 1.")
self._add_parser_ids_args(parser)
@@ -163,7 +193,7 @@ class UpCommand(Command):
except:
# a new cluster
if not args.IMAGE:
- raise Exception("New cluster must specific image")
+ raise Exception("New cluster must specific image") from None
if args.fe_id != None:
args.fe_id = None
LOG.warning(
@@ -172,7 +202,9 @@ class UpCommand(Command):
args.be_id = None
LOG.warning(
utils.render_yellow("Ignore --be-id for new cluster"))
- cluster = CLUSTER.Cluster.new(args.NAME, args.IMAGE)
+ cluster = CLUSTER.Cluster.new(args.NAME, args.IMAGE,
+ args.fe_config, args.be_config,
+ args.be_disk_num)
LOG.info("Create new cluster {} succ, cluster path is {}".format(
args.NAME, cluster.get_path()))
if not args.add_fe_num:
@@ -182,14 +214,20 @@ class UpCommand(Command):
_, related_nodes, _ = get_ids_related_nodes(cluster, args.fe_id,
args.be_id)
+ add_be_ids = []
+ add_fe_ids = []
if not related_nodes:
related_nodes = []
if args.add_fe_num:
for i in range(args.add_fe_num):
- related_nodes.append(cluster.add(CLUSTER.Node.TYPE_FE))
+ fe = cluster.add(CLUSTER.Node.TYPE_FE)
+ related_nodes.append(fe)
+ add_fe_ids.append(fe.id)
if args.add_be_num:
for i in range(args.add_be_num):
- related_nodes.append(cluster.add(CLUSTER.Node.TYPE_BE))
+ be = cluster.add(CLUSTER.Node.TYPE_BE)
+ related_nodes.append(be)
+ add_be_ids.append(be.id)
if args.IMAGE:
for node in related_nodes:
node.set_image(args.IMAGE)
@@ -218,11 +256,47 @@ class UpCommand(Command):
"Not up cluster cause specific --no-start, related node
num {}"
.format(related_node_num)))
else:
+ if args.wait_timeout != 0:
+ if args.wait_timeout == -1:
+ args.wait_timeout = 1000000000
+ expire_ts = time.time() + args.wait_timeout
+ while True:
+ db_mgr = database.get_db_mgr(args.NAME, False)
+ dead_frontends = []
+ for id in add_fe_ids:
+ fe_state = db_mgr.get_fe(id)
+ if not fe_state or not fe_state.alive:
+ dead_frontends.append(id)
+ dead_backends = []
+ for id in add_be_ids:
+ be_state = db_mgr.get_be(id)
+ if not be_state or not be_state.alive:
+ dead_backends.append(id)
+ if not dead_frontends and not dead_backends:
+ break
+ if time.time() >= expire_ts:
+ err = ""
+ if dead_frontends:
+ err += "dead fe: " + str(dead_frontends) + ". "
+ if dead_backends:
+ err += "dead be: " + str(dead_backends) + ". "
+ raise Exception(err)
+ time.sleep(1)
LOG.info(
utils.render_green(
"Up cluster {} succ, related node num {}".format(
args.NAME, related_node_num)))
+ db_mgr = database.get_db_mgr(args.NAME, False)
+ return {
+ "fe": {
+ "add_list": add_fe_ids,
+ },
+ "be": {
+ "add_list": add_be_ids,
+ },
+ }
+
class DownCommand(Command):
@@ -234,6 +308,7 @@ class DownCommand(Command):
"then apply to all containers.")
parser.add_argument("NAME", help="Specify cluster name")
self._add_parser_ids_args(parser)
+ self._add_parser_output_json(parser)
parser.add_argument(
"--clean",
default=False,
@@ -249,7 +324,11 @@ class DownCommand(Command):
"it will send dropp to fe, otherwise send decommission to
fe.")
def run(self, args):
- cluster = CLUSTER.Cluster.load(args.NAME)
+ cluster = None
+ try:
+ cluster = CLUSTER.Cluster.load(args.NAME)
+ except:
+ return "Cluster not exists or load failed"
for_all, related_nodes, related_node_num = get_ids_related_nodes(
cluster, args.fe_id, args.be_id, ignore_not_exists=True)
@@ -304,12 +383,15 @@ class DownCommand(Command):
"Down cluster {} succ, related node num {}".format(
args.NAME, related_node_num)))
+ return "down cluster succ"
+
class ListNode(object):
def __init__(self):
self.node_type = ""
self.id = 0
+ self.backend_id = ""
self.cluster_name = ""
self.ip = ""
self.status = ""
@@ -323,11 +405,18 @@ class ListNode(object):
self.last_heartbeat = ""
self.err_msg = ""
- def info(self):
- return (self.cluster_name, "{}-{}".format(self.node_type, self.id),
- self.ip, self.status, self.container_id, self.image,
- self.created, self.alive, self.is_master, self.query_port,
- self.tablet_num, self.last_heartbeat, self.err_msg)
+ def info(self, detail):
+ result = [
+ self.cluster_name, "{}-{}".format(self.node_type, self.id),
+ self.ip, self.status, self.container_id, self.image, self.created,
+ self.alive, self.is_master, self.query_port, self.backend_id,
+ self.tablet_num, self.last_heartbeat, self.err_msg
+ ]
+ if detail:
+ result += [
+ CLUSTER.FE_QUERY_PORT,
+ ]
+ return result
def update_db_info(self, db_mgr):
if self.node_type == CLUSTER.Node.TYPE_FE:
@@ -339,9 +428,11 @@ class ListNode(object):
self.last_heartbeat = fe.last_heartbeat
self.err_msg = fe.err_msg
elif self.node_type == CLUSTER.Node.TYPE_BE:
+ self.backend_id = -1
be = db_mgr.get_be(self.id)
if be:
self.alive = str(be.alive).lower()
+ self.backend_id = be.backend_id
self.tablet_num = be.tablet_num
self.last_heartbeat = be.last_heartbeat
self.err_msg = be.err_msg
@@ -358,12 +449,29 @@ class ListCommand(Command):
help=
"Specify multiple clusters, if specific, show all their
containers."
)
+ self._add_parser_output_json(parser)
parser.add_argument(
"-a",
"--all",
default=False,
action=self._get_parser_bool_action(True),
help="Show all stopped and bad doris compose projects")
+ parser.add_argument("--detail",
+ default=False,
+ action=self._get_parser_bool_action(True),
+ help="Print more detail fields.")
+
+ def _handle_data(self, header, datas):
+ if utils.is_enable_log():
+ table = prettytable.PrettyTable(
+ [utils.render_green(field) for field in header])
+ for row in datas:
+ table.add_row(row)
+ print(table)
+ return ""
+ else:
+ datas.insert(0, header)
+ return datas
def run(self, args):
COMPOSE_MISSING = "(missing)"
@@ -427,9 +535,8 @@ class ListCommand(Command):
TYPE_COMPOSESERVICE = type(ComposeService("", "", ""))
if not args.NAME:
- headers = (utils.render_green(field)
- for field in ("CLUSTER", "STATUS", "CONFIG FILES"))
- table = prettytable.PrettyTable(headers)
+ header = ("CLUSTER", "STATUS", "CONFIG FILES")
+ rows = []
for name in sorted(clusters.keys()):
cluster_info = clusters[name]
service_statuses = {}
@@ -445,19 +552,22 @@ class ListCommand(Command):
if not args.all and service_statuses.get("running", 0) == 0:
continue
compose_file = CLUSTER.get_compose_file(name)
- table.add_row(
+ rows.append(
(name, show_status, "{}{}".format(compose_file,
cluster_info["status"])))
- print(table)
- return
-
- headers = (utils.render_green(field)
- for field in ("CLUSTER", "NAME", "IP", "STATUS",
- "CONTAINER ID", "IMAGE", "CREATED", "alive",
- "is_master", "query_port", "tablet_num",
- "last_heartbeat", "err_msg"))
- table = prettytable.PrettyTable(headers)
-
+ return self._handle_data(header, rows)
+
+ header = [
+ "CLUSTER", "NAME", "IP", "STATUS", "CONTAINER ID", "IMAGE",
+ "CREATED", "alive", "is_master", "query_port", "backend_id",
+ "tablet_num", "last_heartbeat", "err_msg"
+ ]
+ if args.detail:
+ header += [
+ "query_port",
+ ]
+
+ rows = []
for cluster_name in sorted(clusters.keys()):
fe_ids = {}
be_ids = {}
@@ -526,9 +636,9 @@ class ListCommand(Command):
return key
for node in sorted(nodes, key=get_key):
- table.add_row(node.info())
+ rows.append(node.info(args.detail))
- print(table)
+ return self._handle_data(header, rows)
ALL_COMMANDS = [
diff --git a/docker/runtime/doris-compose/database.py
b/docker/runtime/doris-compose/database.py
index 57fc90275a..69b2cb50f2 100644
--- a/docker/runtime/doris-compose/database.py
+++ b/docker/runtime/doris-compose/database.py
@@ -38,9 +38,10 @@ class FEState(object):
class BEState(object):
- def __init__(self, id, decommissioned, alive, tablet_num, last_heartbeat,
- err_msg):
+ def __init__(self, id, backend_id, decommissioned, alive, tablet_num,
+ last_heartbeat, err_msg):
self.id = id
+ self.backend_id = backend_id
self.decommissioned = decommissioned
self.alive = alive
self.tablet_num = tablet_num
@@ -120,7 +121,7 @@ class DBManager(object):
except Exception as e:
if str(e).find("Backend does not exist") >= 0:
LOG.info("Decommission be {} with id {} from db succ " \
- "cause it does not exist in db.",format(be_endpoint,
id))
+ "cause it does not exist in db.".format(be_endpoint,
id))
return
raise e
@@ -142,12 +143,12 @@ class DBManager(object):
def _load_fe_states(self, query_ports):
fe_states = {}
alive_master_fe_port = None
- for record in self._exec_query("show frontends;"):
- ip = record[1]
- is_master = record[7] == "true"
- alive = record[10] == "true"
- last_heartbeat = record[12]
- err_msg = record[14]
+ for record in self._exec_query('''
+ select Host, IsMaster, Alive, LastHeartbeat, ErrMsg
+ from frontends()'''):
+ ip, is_master, alive, last_heartbeat, err_msg = record
+ is_master = utils.is_true(is_master)
+ alive = utils.is_true(alive)
id = CLUSTER.Node.get_id_from_ip(ip)
query_port = query_ports.get(id, None)
fe = FEState(id, query_port, is_master, alive, last_heartbeat,
@@ -162,16 +163,17 @@ class DBManager(object):
def _load_be_states(self):
be_states = {}
- for record in self._exec_query("show backends;"):
- ip = record[1]
- last_heartbeat = record[7]
- alive = record[8] == "true"
- decommissioned = record[9] == "true"
- tablet_num = int(record[10])
- err_msg = record[18]
+ for record in self._exec_query('''
+ select BackendId, Host, LastHeartbeat, Alive,
SystemDecommissioned, TabletNum, ErrMsg
+ from backends()'''):
+ backend_id, ip, last_heartbeat, alive, decommissioned, tablet_num,
err_msg = record
+ backend_id = int(backend_id)
+ alive = utils.is_true(alive)
+ decommissioned = utils.is_true(decommissioned)
+ tablet_num = int(tablet_num)
id = CLUSTER.Node.get_id_from_ip(ip)
- be = BEState(id, decommissioned, alive, tablet_num, last_heartbeat,
- err_msg)
+ be = BEState(id, backend_id, decommissioned, alive, tablet_num,
+ last_heartbeat, err_msg)
be_states[id] = be
self.be_states = be_states
diff --git a/docker/runtime/doris-compose/doris-compose.py
b/docker/runtime/doris-compose/doris-compose.py
index 77ceb736e3..c29449b116 100644
--- a/docker/runtime/doris-compose/doris-compose.py
+++ b/docker/runtime/doris-compose/doris-compose.py
@@ -17,6 +17,8 @@
import argparse
import command
+import sys
+import traceback
import utils
@@ -29,20 +31,35 @@ def parse_args():
return ap.parse_args(), ap.format_help()
-def run(args, help):
- timer = utils.Timer()
+def run(args, disable_log, help):
for cmd in command.ALL_COMMANDS:
if args.command == cmd.name:
- return cmd.run(args)
- timer.cancel()
+ timer = utils.Timer()
+ result = cmd.run(args)
+ if not disable_log:
+ timer.show()
+ return result
print(help)
- return -1
+ return ""
-def main():
+if __name__ == '__main__':
args, help = parse_args()
- run(args, help)
-
+ disable_log = getattr(args, "output_json", False)
+ if disable_log:
+ utils.set_enable_log(False)
-if __name__ == '__main__':
- main()
+ code = None
+ try:
+ data = run(args, disable_log, help)
+ if disable_log:
+ print(utils.pretty_json({"code": 0, "data": data}))
+ code = 0
+ except:
+ err = traceback.format_exc()
+ if disable_log:
+ print(utils.pretty_json({"code": 1, "err": err}))
+ else:
+ print(err)
+ code = 1
+ sys.exit(code)
diff --git a/docker/runtime/doris-compose/resource/init_be.sh
b/docker/runtime/doris-compose/resource/init_be.sh
index 9a59876382..d9f4abf125 100644
--- a/docker/runtime/doris-compose/resource/init_be.sh
+++ b/docker/runtime/doris-compose/resource/init_be.sh
@@ -28,6 +28,11 @@ add_backend() {
sleep 1
continue
fi
+ lsof -i:$BE_HEARTBEAT_PORT
+ if [ $? -ne 0 ]; then
+ sleep 1
+ continue
+ fi
output=`mysql -P $FE_QUERY_PORT -h $MASTER_FE_IP -u root --execute
"ALTER SYSTEM ADD BACKEND '$MY_IP:$BE_HEARTBEAT_PORT';" 2>&1`
res=$?
@@ -42,7 +47,7 @@ add_backend() {
main() {
if [ ! -f $REGISTER_FILE ]; then
- add_backend
+ add_backend &
fi
bash $DORIS_HOME/bin/start_be.sh
}
diff --git a/docker/runtime/doris-compose/resource/init_fe.sh
b/docker/runtime/doris-compose/resource/init_fe.sh
index fbbd335f37..463fa8c667 100644
--- a/docker/runtime/doris-compose/resource/init_fe.sh
+++ b/docker/runtime/doris-compose/resource/init_fe.sh
@@ -39,20 +39,42 @@ add_frontend() {
fe_daemon() {
set +e
while true; do
- output=`mysql -P $FE_QUERY_PORT -h $MY_IP -u root --execute "SHOW
FRONTENDS;" | grep -w $MY_IP | awk '{print $8}' 2>&1`
+ sleep 1
+ output=`mysql -P $FE_QUERY_PORT -h $MY_IP -u root --execute "SHOW
FRONTENDS;"`
+ code=$?
+ health_log "$output"
+ if [ $code -ne 0 ]; then
+ continue
+ fi
+ header=`grep IsMaster <<< $output`
if [ $? -ne 0 ]; then
- health_log "$output"
- else
- echo $output | grep true
- if [ $? -eq 0 ]; then
- echo $MY_IP > $MASTER_FE_IP_FILE
- if [ "$MASTER_FE_IP" != "$MY_IP" ]; then
- health_log "change to master, last master is $MASTER_FE_IP"
- MASTER_FE_IP=$MY_IP
- fi
+ health_log "not found header"
+ continue
+ fi
+ host_index=-1
+ is_master_index=-1
+ i=1
+ for field in $header; do
+ [[ "$field" = "Host" ]] && host_index=$i
+ [[ "$field" = "IsMaster" ]] && is_master_index=$i
+ ((i=i+1))
+ done
+ if [ $host_index -eq -1 ]; then
+ health_log "header not found Host"
+ continue
+ fi
+ if [ $is_master_index -eq -1 ]; then
+ health_log "header not found IsMaster"
+ continue
+ fi
+ echo "$output" | awk -v is_master="$is_master_index" -v
host="$host_index" '{print $is_master $host}' | grep $MY_IP | grep true 2>&1
+ if [ $? -eq 0 ]; then
+ echo $MY_IP > $MASTER_FE_IP_FILE
+ if [ "$MASTER_FE_IP" != "$MY_IP" ]; then
+ health_log "change to master, last master is $MASTER_FE_IP"
+ MASTER_FE_IP=$MY_IP
fi
fi
- sleep 3
done
}
diff --git a/docker/runtime/doris-compose/utils.py
b/docker/runtime/doris-compose/utils.py
index 7317715fe9..dfed11e8b0 100644
--- a/docker/runtime/doris-compose/utils.py
+++ b/docker/runtime/doris-compose/utils.py
@@ -16,6 +16,7 @@
# under the License.
import docker
+import json
import logging
import os
import subprocess
@@ -24,6 +25,10 @@ import yaml
DORIS_PREFIX = "doris-"
+LOG = None
+
+ENABLE_LOG = True
+
class Timer(object):
@@ -31,7 +36,7 @@ class Timer(object):
self.start = time.time()
self.canceled = False
- def __del__(self):
+ def show(self):
if not self.canceled:
LOG.info("=== Total run time: {} s".format(
int(time.time() - self.start)))
@@ -40,7 +45,21 @@ class Timer(object):
self.canceled = True
+def set_enable_log(enabled):
+ global ENABLE_LOG
+ ENABLE_LOG = enabled
+ get_logger().disabled = not enabled
+
+
+def is_enable_log():
+ return ENABLE_LOG
+
+
def get_logger(name=None):
+ global LOG
+ if LOG != None:
+ return LOG
+
logger = logging.getLogger(name)
if not logger.hasHandlers():
formatter = logging.Formatter(
@@ -52,10 +71,12 @@ def get_logger(name=None):
logger.addHandler(ch)
logger.setLevel(logging.INFO)
+ LOG = logger
+
return logger
-LOG = get_logger()
+get_logger()
def render_red(s):
@@ -159,7 +180,7 @@ def exec_shell_command(command, ignore_errors=False):
out = p.communicate()[0].decode('utf-8')
if not ignore_errors:
assert p.returncode == 0, out
- if out:
+ if ENABLE_LOG and out:
print(out)
return p.returncode, out
@@ -251,3 +272,11 @@ def read_compose_file(file):
def write_compose_file(file, compose):
with open(file, "w") as f:
f.write(yaml.dump(compose))
+
+
+def pretty_json(json_data):
+ return json.dumps(json_data, indent=4, sort_keys=True)
+
+
+def is_true(val):
+ return str(val) == "true" or str(val) == "1"
diff --git a/regression-test/conf/regression-conf.groovy
b/regression-test/conf/regression-conf.groovy
index 5b204b53d2..b5114f9dce 100644
--- a/regression-test/conf/regression-conf.groovy
+++ b/regression-test/conf/regression-conf.groovy
@@ -47,6 +47,13 @@ pluginPath = "${DORIS_HOME}/regression-test/plugins"
realDataPath = "${DORIS_HOME}/regression-test/realdata"
sslCertificatePath = "${DORIS_HOME}/regression-test/ssl_default_certificate"
+// docker image
+image = ""
+dockerEndDeleteFiles = false
+dorisComposePath =
"${DORIS_HOME}/docker/runtime/doris-compose/doris-compose.py"
+// do run docker test because pipeline not support build image now
+excludeDockerTest = true
+
// will test <group>/<suite>.groovy
// empty group will test all group
testGroups = ""
diff --git
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/Config.groovy
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/Config.groovy
index cff342889c..8fcba76f0f 100644
---
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/Config.groovy
+++
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/Config.groovy
@@ -60,6 +60,10 @@ class Config {
public boolean enableCacheData
public String pluginPath
public String sslCertificatePath
+ public String dorisComposePath
+ public String image
+ public Boolean dockerEndDeleteFiles
+ public Boolean excludeDockerTest
public String testGroups
public String excludeGroups
@@ -157,6 +161,8 @@ class Config {
config.enableCacheData =
Boolean.parseBoolean(cmd.getOptionValue(enableCacheDataOpt, "true"))
config.pluginPath =
FileUtils.getCanonicalPath(cmd.getOptionValue(pluginOpt, config.pluginPath))
config.sslCertificatePath =
FileUtils.getCanonicalPath(cmd.getOptionValue(sslCertificateOpt,
config.sslCertificatePath))
+ config.dorisComposePath =
FileUtils.getCanonicalPath(config.dorisComposePath)
+ config.image = cmd.getOptionValue(imageOpt, config.image)
config.suiteWildcard = cmd.getOptionValue(suiteOpt, config.testSuites)
.split(",")
.collect({s -> s.trim()})
@@ -303,6 +309,10 @@ class Config {
configToString(obj.sslCertificatePath)
)
+ config.image = configToString(obj.image)
+ config.dockerEndDeleteFiles = configToBoolean(obj.dockerEndDeleteFiles)
+ config.excludeDockerTest = configToBoolean(obj.excludeDockerTest)
+
def declareFileNames = config.getClass()
.getDeclaredFields()
.collect({f -> f.name})
@@ -323,7 +333,7 @@ class Config {
}
if (config.jdbcUrl == null) {
- //jdbcUrl needs parameter here. Refer to function: buildUrl(String
dbName)
+ //jdbcUrl needs parameter here. Refer to function:
buildUrlWithDb(String jdbcUrl, String dbName)
config.jdbcUrl =
"jdbc:mysql://127.0.0.1:9030/?useLocalSessionState=true&allowLoadLocalInfile=true"
log.info("Set jdbcUrl to '${config.jdbcUrl}' because not
specify.".toString())
}
@@ -418,6 +428,21 @@ class Config {
log.info("Set sslCertificatePath to '${config.sslCertificatePath}'
because not specify.".toString())
}
+ if (config.dockerEndDeleteFiles == null) {
+ config.dockerEndDeleteFiles = false
+ log.info("Set dockerEndDeleteFiles to
'${config.dockerEndDeleteFiles}' because not specify.".toString())
+ }
+
+ if (config.excludeDockerTest == null) {
+ config.excludeDockerTest = true
+ log.info("Set excludeDockerTest to '${config.excludeDockerTest}'
because not specify.".toString())
+ }
+
+ if (config.dorisComposePath == null) {
+ config.dorisComposePath =
"docker/runtime/doris-compose/doris-compose.py"
+ log.info("Set dorisComposePath to '${config.dorisComposePath}'
because not specify.".toString())
+ }
+
if (config.testGroups == null) {
config.testGroups = "default"
log.info("Set testGroups to '${config.testGroups}' because not
specify.".toString())
@@ -502,7 +527,7 @@ class Config {
}
Connection getConnectionByDbName(String dbName) {
- String dbUrl = buildUrl(dbName)
+ String dbUrl = buildUrlWithDb(jdbcUrl, dbName)
tryCreateDbIfNotExist(dbName)
log.info("connect to ${dbUrl}".toString())
return DriverManager.getConnection(dbUrl, jdbcUser, jdbcPassword)
@@ -556,12 +581,12 @@ class Config {
}
}
- private void buildUrlWithDefaultDb() {
- this.jdbcUrl = buildUrl(defaultDb)
+ public void buildUrlWithDefaultDb() {
+ this.jdbcUrl = buildUrlWithDb(jdbcUrl, defaultDb)
log.info("Reset jdbcUrl to ${jdbcUrl}".toString())
}
- private String buildUrl(String dbName) {
+ public static String buildUrlWithDb(String jdbcUrl, String dbName) {
String urlWithDb = jdbcUrl
String urlWithoutSchema = jdbcUrl.substring(jdbcUrl.indexOf("://") + 3)
if (urlWithoutSchema.indexOf("/") >= 0) {
@@ -584,7 +609,7 @@ class Config {
return urlWithDb
}
- private String addSslUrl(String url) {
+ private static String addSslUrl(String url) {
if (url.contains("TLS")) {
return url
}
@@ -606,7 +631,7 @@ class Config {
}
}
- private String addTimeoutUrl(String url) {
+ private static String addTimeoutUrl(String url) {
if (url.contains("connectTimeout=") || url.contains("socketTimeout="))
{
return url
diff --git
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/ConfigOptions.groovy
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/ConfigOptions.groovy
index 9c5db531ca..7ad0720bd1 100644
---
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/ConfigOptions.groovy
+++
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/ConfigOptions.groovy
@@ -48,6 +48,7 @@ class ConfigOptions {
static Option enableCacheDataOpt
static Option pluginOpt
static Option sslCertificateOpt
+ static Option imageOpt
static Option suiteOpt
static Option excludeSuiteOpt
static Option groupsOpt
@@ -172,6 +173,15 @@ class ConfigOptions {
.desc("the sslCertificate path")
.build()
+ imageOpt = Option.builder("image")
+ .argName("image")
+ .required(false)
+ .hasArg(true)
+ .type(String.class)
+ .longOpt("image")
+ .desc("the docker image")
+ .build()
+
suiteOpt = Option.builder("s")
.argName("suiteName")
.required(false)
@@ -381,6 +391,7 @@ class ConfigOptions {
.addOption(dataOpt)
.addOption(pluginOpt)
.addOption(sslCertificateOpt)
+ .addOption(imageOpt)
.addOption(confOpt)
.addOption(suiteOpt)
.addOption(excludeSuiteOpt)
diff --git
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/Suite.groovy
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/Suite.groovy
index 167a294d00..23ae47aa38 100644
---
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/Suite.groovy
+++
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/Suite.groovy
@@ -24,6 +24,7 @@ import com.google.common.util.concurrent.MoreExecutors
import com.google.gson.Gson
import groovy.json.JsonSlurper
import com.google.common.collect.ImmutableList
+import org.apache.doris.regression.Config
import org.apache.doris.regression.action.BenchmarkAction
import org.apache.doris.regression.util.DataUtils
import org.apache.doris.regression.util.OutputUtils
@@ -49,6 +50,7 @@ import java.util.stream.Collectors
import java.util.stream.LongStream
import static org.apache.doris.regression.util.DataUtils.sortByToString
+import java.sql.DriverManager
import java.sql.PreparedStatement
import java.sql.ResultSetMetaData
import org.junit.Assert
@@ -66,10 +68,13 @@ class Suite implements GroovyInterceptable {
final List<Throwable> lazyCheckExceptions = new Vector<>()
final List<Future> lazyCheckFutures = new Vector<>()
+ SuiteCluster cluster
+
Suite(String name, String group, SuiteContext context) {
this.name = name
this.group = group
this.context = context
+ this.cluster = null
}
String getConf(String key, String defaultValue = null) {
@@ -192,6 +197,35 @@ class Suite implements GroovyInterceptable {
return context.connect(user, password, url, actionSupplier)
}
+ public void docker(ClusterOptions options = new ClusterOptions(), Closure
actionSupplier) throws Exception {
+ if (context.config.excludeDockerTest) {
+ return
+ }
+
+ cluster = new SuiteCluster(name, context.config)
+ try {
+ cluster.destroy(true)
+ cluster.init(options)
+
+ def user = "root"
+ def password = ""
+ def masterFe = cluster.getMasterFe()
+ def url = String.format(
+
"jdbc:mysql://%s:%s/?useLocalSessionState=false&allowLoadLocalInfile=false",
+ masterFe.host, masterFe.queryPort)
+ def conn = DriverManager.getConnection(url, user, password)
+ def sql = "CREATE DATABASE IF NOT EXISTS " + context.dbName
+ logger.info("try create database if not exists {}", context.dbName)
+ JdbcUtils.executeToList(conn, sql)
+ url = Config.buildUrlWithDb(url, context.dbName)
+
+ logger.info("connect to docker cluster: suite={}, url={}", name,
url)
+ connect(user, password, url, actionSupplier)
+ } finally {
+ cluster.destroy(context.config.dockerEndDeleteFiles)
+ }
+ }
+
String get_ccr_body(String table) {
Gson gson = new Gson()
diff --git
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/SuiteCluster.groovy
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/SuiteCluster.groovy
new file mode 100644
index 0000000000..85dbea17b5
--- /dev/null
+++
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/SuiteCluster.groovy
@@ -0,0 +1,363 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.regression.suite
+
+import org.apache.doris.regression.Config
+
+import com.google.common.collect.Maps
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import groovy.json.JsonSlurper
+import groovy.transform.CompileStatic
+import groovy.util.logging.Slf4j
+import java.util.stream.Collectors
+
+class ClusterOptions {
+ int feNum = 1
+ int beNum = 3
+ int beDiskNum = 1
+ List<String> feConfigs
+ List<String> beConfigs
+}
+
+class ListHeader {
+ Map<String, Integer> fields;
+
+ ListHeader(List<Object> fieldList) {
+ this.fields = Maps.newHashMap()
+ for (int i = 0; i < fieldList.size(); i++) {
+ fields.put((String) fieldList.get(i), i)
+ }
+ }
+
+ int indexOf(String field) {
+ def index = fields.get(field)
+ assert index != null : "Not found field: " + field
+ return index
+ }
+}
+
+class Frontend {
+ int index
+ String host
+ int queryPort
+ boolean alive
+ boolean isMaster
+
+ static Frontend fromCompose(ListHeader header, int index, List<Object>
fields) {
+ Frontend fe = new Frontend()
+ fe.index = index
+ fe.host = (String) fields.get(header.indexOf("IP"))
+ fe.queryPort = (Integer) fields.get(header.indexOf("query_port"))
+ fe.alive = fields.get(header.indexOf("alive")) == "true"
+ fe.isMaster = fields.get(header.indexOf("is_master")) == "true"
+ return fe
+ }
+}
+
+class Backend {
+ int index
+ long backendId
+ String host
+ boolean alive
+ int tabletNum
+
+ static Backend fromCompose(ListHeader header, int index, List<Object>
fields) {
+ Backend be = new Backend()
+ be.index = index
+ be.backendId = (long) fields.get(header.indexOf("backend_id"))
+ be.host = (String) fields.get(header.indexOf("IP"))
+ be.alive = fields.get(header.indexOf("alive")) == "true"
+ be.tabletNum = (int) fields.get(header.indexOf("tablet_num"))
+ return be
+ }
+}
+
+@Slf4j
+@CompileStatic
+class SuiteCluster {
+ static final Logger logger = LoggerFactory.getLogger(this.class)
+
+ final String name
+ final Config config
+ private boolean inited
+
+ SuiteCluster(String name, Config config) {
+ this.name = name
+ this.config = config
+ this.inited = false
+ }
+
+ void init(ClusterOptions options) {
+ if (inited) {
+ return;
+ }
+
+ assert name != null && name != ""
+ assert options.feNum > 0 || options.beNum > 0
+ assert config.image != null && config.image != ""
+
+ def sb = new StringBuilder()
+ sb.append("up " + name + " ")
+ sb.append(config.image + " ")
+ if (options.feNum > 0) {
+ sb.append("--add-fe-num " + options.feNum + " ")
+ }
+ if (options.beNum > 0) {
+ sb.append("--add-be-num " + options.beNum + " ")
+ }
+ // TODO: need escape white space in config
+ if (options.feConfigs != null && options.feConfigs.size() > 0) {
+ sb.append("--fe-config ")
+ options.feConfigs.forEach(item -> sb.append(" " + item + " "))
+ }
+ if (options.beConfigs != null && options.beConfigs.size() > 0) {
+ sb.append("--be-config ")
+ options.beConfigs.forEach(item -> sb.append(" " + item + " "))
+ }
+ sb.append("--be-disk-num " + options.beDiskNum + " ")
+ sb.append("--wait-timeout 180")
+
+ runCmd(sb.toString(), -1)
+
+ // wait be report disk
+ Thread.sleep(5000)
+
+ inited = true;
+ }
+
+ Frontend getMasterFe() {
+ return getFrontends().stream().filter(fe ->
fe.isMaster).findFirst().orElse(null)
+ }
+
+ Frontend getFeByIndex(int index) {
+ return getFrontends().stream().filter(fe -> fe.index ==
index).findFirst().orElse(null)
+ }
+
+ Backend getBeByBackendId(long backendId) {
+ return getBackends().stream().filter(be -> be.backendId ==
backendId).findFirst().orElse(null)
+ }
+
+ Backend getBeByIndex(int index) {
+ return getBackends().stream().filter(be -> be.index ==
index).findFirst().orElse(null)
+ }
+
+ List<Frontend> getAllFrontends(boolean needAlive = false) {
+ return getFrontends().stream().filter(fe -> fe.alive ||
!needAlive).collect(Collectors.toList());
+ }
+
+ List<Backend> getAllBackends(boolean needAlive = false) {
+ return getBackends().stream().filter(be -> be.alive ||
!needAlive).collect(Collectors.toList());
+ }
+
+ private List<Frontend> getFrontends() {
+ List<Frontend> frontends = []
+ List<Backend> backends = []
+ getAllNodes(frontends, backends)
+ return frontends
+ }
+
+ private List<Backend> getBackends() {
+ List<Frontend> frontends = []
+ List<Backend> backends = []
+ getAllNodes(frontends, backends)
+ return backends
+ }
+
+ private void getAllNodes(List<Frontend> frontends, List<Backend> backends)
{
+ def cmd = "ls " + name + " --detail"
+ def data = runCmd(cmd)
+ assert data instanceof List
+ def rows = (List<List<Object>>) data
+ def header = new ListHeader(rows.get(0))
+ for (int i = 1; i < rows.size(); i++) {
+ def row = (List<Object>) rows.get(i)
+ def name = (String) row.get(header.indexOf("NAME"))
+ if (name.startsWith("be-")) {
+ int index = name.substring("be-".length()) as int
+ backends.add(Backend.fromCompose(header, index, row))
+ } else if (name.startsWith("fe-")) {
+ int index = name.substring("fe-".length()) as int
+ frontends.add(Frontend.fromCompose(header, index, row))
+ } else {
+ assert false : "Unknown node type with name: " + name
+ }
+ }
+ }
+
+ List<Integer> addFe(int num) throws Exception {
+ def result = add(num, 0)
+ return result.first
+ }
+
+ List<Integer> addBe(int num) throws Exception {
+ def result = add(0, num)
+ return result.second
+ }
+
+ Tuple2<List<Integer>, List<Integer>> add(int feNum, int beNum) throws
Exception {
+ assert feNum > 0 || beNum > 0
+
+ def sb = new StringBuilder()
+ sb.append("up " + name + " ")
+ if (feNum > 0) {
+ sb.append("--add-fe-num " + feNum + " ")
+ }
+ if (beNum > 0) {
+ sb.append("--add-be-num " + beNum + " ")
+ }
+ sb.append("--wait-timeout 60")
+
+ def data = (Map<String, Map<String, Object>>) runCmd(sb.toString(), -1)
+ def newFrontends = (List<Integer>) data.get("fe").get("add_list")
+ def newBackends = (List<Integer>) data.get("be").get("add_list")
+
+ // wait be report disk
+ Thread.sleep(5000)
+
+ return new Tuple2(newFrontends, newBackends)
+ }
+
+ void destroy(boolean clean) throws Exception {
+ def cmd = "down " + name
+ if (clean) {
+ cmd += " --clean"
+ }
+ runCmd(cmd)
+ inited = false
+ }
+
+ // if not specific fe indices, then start all frontends
+ void startFrontends(int... indices) {
+ runFrontendsCmd("start", indices)
+ }
+
+ // if not specific be indices, then start all backends
+ void startBackends(int... indices) {
+ runBackendsCmd("start", indices)
+ }
+
+ // if not specific fe indices, then stop all frontends
+ void stopFrontends(int... indices) {
+ runFrontendsCmd("stop", indices)
+ }
+
+ // if not specific be indices, then stop all backends
+ void stopBackends(int... indices) {
+ runBackendsCmd("stop", indices)
+ }
+
+ // if not specific fe indices, then restart all frontends
+ void restartFrontends(int... indices) {
+ runFrontendsCmd("restart", indices)
+ }
+
+ // if not specific be indices, then restart all backends
+ void restartBackends(int... indices) {
+ runBackendsCmd("restart", indices)
+ }
+
+ // if not specific fe indices, then drop all frontends
+ void dropFrontends(int... indices) {
+ runFrontendsCmd("down", indices)
+ }
+
+ // if not specific be indices, then decommission all backends
+ void decommissionBackends(int... indices) {
+ runBackendsCmdWithTimeout(300, "down", indices)
+ }
+
+ // if not specific be indices, then drop force all backends
+ void dropForceBackends(int... indices) {
+ runBackendsCmd("down --drop-force", indices)
+ }
+
+ void checkFeIsAlive(int index, boolean isAlive) {
+ def fe = getFeByIndex(index)
+ assert fe != null : "frontend with index " + index + " not exists!"
+ assert fe.alive == isAlive : (fe.alive ? "frontend with index " +
index + " still alive"
+ : "frontend with index " + index + " dead")
+ }
+
+ void checkBeIsAlive(int index, boolean isAlive) {
+ def be = getBeByIndex(index)
+ assert be != null : "backend with index " + index + " not exists!"
+ assert be.alive == isAlive : (be.alive ? "backend with index " + index
+ " still alive"
+ : "backend with index " + index + " dead")
+ }
+
+ void checkFeIsExists(int index, boolean isExists) {
+ def fe = getFeByIndex(index)
+ if (isExists) {
+ assert fe != null : "frontend with index " + index + " not exists!"
+ } else {
+ assert fe == null : "frontend with index " + index + " exists!"
+ }
+ }
+
+ void checkBeIsExists(int index, boolean isExists) {
+ def be = getBeByIndex(index)
+ if (isExists) {
+ assert be != null : "backend with index " + index + " not exists!"
+ } else {
+ assert be == null : "backend with index " + index + " exists!"
+ }
+ }
+
+ private void runFrontendsCmd(String op, int... indices) {
+ def cmd = op + " " + name + " --fe-id " + indices.join(" ")
+ runCmd(cmd)
+ }
+
+ private void runBackendsCmd(String op, int... indices) {
+ def cmd = op + " " + name + " --be-id " + indices.join(" ")
+ runCmd(cmd)
+ }
+
+ private void runBackendsCmdWithTimeout(int timeoutSecond, String op,
int... indices) {
+ def cmd = op + " " + name + " --be-id " + indices.join(" ")
+ runCmd(cmd, timeoutSecond)
+ }
+
+ private Object runCmd(String cmd, int timeoutSecond = 60) throws Exception
{
+ def fullCmd = String.format("python %s %s --output-json",
config.dorisComposePath, cmd)
+ logger.info("Run doris compose cmd: {}", fullCmd)
+ def proc = fullCmd.execute()
+ def outBuf = new StringBuilder()
+ def errBuf = new StringBuilder()
+ proc.consumeProcessOutput(outBuf, errBuf)
+ if (timeoutSecond > 0) {
+ proc.waitForOrKill(timeoutSecond * 1000)
+ } else {
+ proc.waitFor()
+ }
+ def out = outBuf.toString()
+ def err = errBuf.toString()
+ if (proc.exitValue()) {
+ throw new Exception(String.format("Exit value: %s != 0, stdout:
%s, stderr: %s",
+ proc.exitValue(), out, err))
+ }
+ def parser = new JsonSlurper()
+ def object = (Map<String, Object>) parser.parseText(out)
+ if (object.get("code") != 0) {
+ throw new Exception(String.format("Code: %s != 0, err: %s",
object.get("code"), object.get("err")))
+ }
+ return object.get("data")
+ }
+}
diff --git a/regression-test/suites/chaos/test_docker_example.groovy
b/regression-test/suites/chaos/test_docker_example.groovy
new file mode 100644
index 0000000000..5cb7238607
--- /dev/null
+++ b/regression-test/suites/chaos/test_docker_example.groovy
@@ -0,0 +1,53 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import org.apache.doris.regression.suite.ClusterOptions
+
+suite("test_docker_example") {
+ docker {
+ sql """create table tb1 (k int) DISTRIBUTED BY HASH(k) BUCKETS 10"""
+ sql """insert into tb1 values (1),(2),(3)"""
+
+ cluster.checkBeIsAlive(2, true)
+
+ // stop backend 2, 3
+ cluster.stopBackends(2, 3)
+ // wait be lost heartbeat
+ Thread.sleep(6000)
+
+ cluster.checkBeIsAlive(2, false)
+
+ test {
+ sql """insert into tb1 values (4),(5),(6)"""
+
+ // REPLICA_FEW_ERR
+ exception "errCode = 3,"
+ }
+ }
+
+ // run another docker
+ def options = new ClusterOptions()
+ // add fe config items
+ options.feConfigs = ["example_conf_k1=v1", "example_conf_k2=v2"]
+ // contains 5 backends
+ options.beNum = 5
+ // each backend has 3 disks
+ options.beDiskNum = 3
+ docker (options) {
+ sql """create table tb1 (k int) DISTRIBUTED BY HASH(k) BUCKETS 10
properties ("replication_num"="5")"""
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]