修bug,完善功能
This commit is contained in:
1 parent
30364b1c4b
commit
6996e19694
125 files changed
+15343
-3746
No files matched your search
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
# 通过既有引导程序初始化目录,随后以当前非特权用户启动单个服务。
|
||||
case "${1:-server}" in
|
||||
server)
|
||||
if [ "$#" -gt 0 ]; then shift; fi
|
||||
config=${CHUSQL_CONFIG:-/etc/chusql/server.toml}
|
||||
password_file=${CHUSQL_ADMIN_PASSWORD_FILE:-/run/secrets/admin_password}
|
||||
if [ ! -r "$password_file" ]; then
|
||||
echo "Cannot read administrator password file: $password_file" >&2
|
||||
exit 1
|
||||
fi
|
||||
password=$(head -n 1 "$password_file")
|
||||
case "$password" in
|
||||
*[![:space:]]*) ;;
|
||||
*)
|
||||
echo "Administrator password file must start with a non-blank password." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
printf '%s\n' "$password" | csql-bootstrap --config "$config" \
|
||||
--user "${CHUSQL_ADMIN_USER:-root}" --password-stdin
|
||||
unset password
|
||||
exec chusql-server --config "$config" --user "${CHUSQL_ADMIN_USER:-root}" "$@"
|
||||
;;
|
||||
web)
|
||||
shift
|
||||
exec chusql-web --config "${CHUSQL_CONFIG:-/etc/chusql/web.toml}" \
|
||||
--user "${CHUSQL_ADMIN_USER:-root}" "$@"
|
||||
;;
|
||||
*)
|
||||
exec "$@"
|
||||
;;
|
||||
esac
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
# 检查 TCP 协议响应或 Web 健康接口,不读取账号口令。
|
||||
case "${1:-server}" in
|
||||
server)
|
||||
response=$(printf '{"method":"ping"}\n{"method":"quit"}\n' | nc -w 3 127.0.0.1 7777)
|
||||
printf '%s\n' "$response" | grep -Eq '"status"[[:space:]]*:[[:space:]]*"pong"'
|
||||
;;
|
||||
web)
|
||||
curl --fail --silent --show-error --max-time 3 http://127.0.0.1:7778/api/health >/dev/null
|
||||
;;
|
||||
*)
|
||||
echo "Unknown healthcheck service: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,12 @@
|
||||
# 容器内固定端口与持久化目录,其余参数沿用内置默认值。
|
||||
[storage]
|
||||
data_dir = "/var/lib/chusql"
|
||||
log_files = "/var/log/chusql"
|
||||
|
||||
[server]
|
||||
listen_host = "0.0.0.0"
|
||||
port = 7777
|
||||
sudo_auth_user = ""
|
||||
|
||||
[buffer]
|
||||
pool_size = 256
|
||||
Executable
+81
@@ -0,0 +1,81 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
# 使用临时替身验证入口分派、口令文件及引导失败,不启动编译或数据库。
|
||||
repo=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
work=$(mktemp -d)
|
||||
trap 'rm -rf "$work"' EXIT HUP INT TERM
|
||||
export TEST_CALLS="$work/calls"
|
||||
export TEST_PASSWORD="$work/received"
|
||||
export CHUSQL_ADMIN_PASSWORD_FILE="$work/password"
|
||||
export CHUSQL_CONFIG="$work/settings.toml"
|
||||
export CHUSQL_ADMIN_USER=operator
|
||||
mkdir "$work/bin"
|
||||
export PATH="$work/bin:$PATH"
|
||||
|
||||
cat >"$work/bin/csql-bootstrap" <<'STUB'
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
printf 'bootstrap %s\n' "$*" >> "$TEST_CALLS"
|
||||
cat > "$TEST_PASSWORD"
|
||||
exit "${TEST_BOOTSTRAP_EXIT:-0}"
|
||||
STUB
|
||||
cat >"$work/bin/chusql-server" <<'STUB'
|
||||
#!/bin/sh
|
||||
printf 'server %s\n' "$*" >> "$TEST_CALLS"
|
||||
STUB
|
||||
cat >"$work/bin/chusql-web" <<'STUB'
|
||||
#!/bin/sh
|
||||
printf 'web %s\n' "$*" >> "$TEST_CALLS"
|
||||
STUB
|
||||
chmod +x "$work/bin/"*
|
||||
|
||||
# 缺失或空白口令应在启动程序前拒绝。
|
||||
for password_kind in missing empty blank; do
|
||||
rm -f "$CHUSQL_ADMIN_PASSWORD_FILE" "$TEST_CALLS"
|
||||
case "$password_kind" in
|
||||
empty) : >"$CHUSQL_ADMIN_PASSWORD_FILE" ;;
|
||||
blank) printf ' \r\n' >"$CHUSQL_ADMIN_PASSWORD_FILE" ;;
|
||||
esac
|
||||
if sh "$repo/docker/entrypoint.sh" server >"$work/output" 2>&1; then
|
||||
echo "Unexpected success for $password_kind password" >&2
|
||||
exit 1
|
||||
fi
|
||||
test ! -e "$TEST_CALLS"
|
||||
done
|
||||
|
||||
# 保留空格与反斜杠,引导后按原样传参。
|
||||
printf 'test password\\value' >"$CHUSQL_ADMIN_PASSWORD_FILE"
|
||||
sh "$repo/docker/entrypoint.sh" server --max-rows 42
|
||||
printf 'test password\\value\n' >"$work/expected"
|
||||
cmp "$work/expected" "$TEST_PASSWORD"
|
||||
grep -Fq "bootstrap --config $CHUSQL_CONFIG --user operator --password-stdin" "$TEST_CALLS"
|
||||
grep -Fq "server --config $CHUSQL_CONFIG --user operator --max-rows 42" "$TEST_CALLS"
|
||||
test "$(wc -l <"$TEST_CALLS")" -eq 2
|
||||
|
||||
# 引导失败必须阻止启动,Web 不读取口令。
|
||||
rm "$TEST_CALLS"
|
||||
if TEST_BOOTSTRAP_EXIT=7 sh "$repo/docker/entrypoint.sh" server; then exit 1; fi
|
||||
test "$(wc -l <"$TEST_CALLS")" -eq 1
|
||||
rm "$CHUSQL_ADMIN_PASSWORD_FILE" "$TEST_CALLS"
|
||||
sh "$repo/docker/entrypoint.sh" web
|
||||
grep -Fq "web --config $CHUSQL_CONFIG --user operator" "$TEST_CALLS"
|
||||
test "$(sh "$repo/docker/entrypoint.sh" printf passthrough)" = passthrough
|
||||
# 健康检查必须识别协议状态和 HTTP 失败。
|
||||
cat >"$work/bin/nc" <<'STUB'
|
||||
#!/bin/sh
|
||||
cat > /dev/null
|
||||
printf '%s\n' "$TEST_TCP_RESPONSE"
|
||||
exit "${TEST_NC_EXIT:-0}"
|
||||
STUB
|
||||
cat >"$work/bin/curl" <<'STUB'
|
||||
#!/bin/sh
|
||||
exit "${TEST_CURL_EXIT:-0}"
|
||||
STUB
|
||||
chmod +x "$work/bin/nc" "$work/bin/curl"
|
||||
TEST_TCP_RESPONSE='{"status":"pong"}' sh "$repo/docker/healthcheck.sh" server
|
||||
if TEST_TCP_RESPONSE='{"status":"error"}' sh "$repo/docker/healthcheck.sh" server; then exit 1; fi
|
||||
if TEST_TCP_RESPONSE='{"status":"pong"}' TEST_NC_EXIT=1 sh "$repo/docker/healthcheck.sh" server; then exit 1; fi
|
||||
sh "$repo/docker/healthcheck.sh" web
|
||||
if TEST_CURL_EXIT=22 sh "$repo/docker/healthcheck.sh" web; then exit 1; fi
|
||||
echo "entrypoint and healthcheck checks passed"
|
||||
@@ -0,0 +1,375 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import concurrent.futures
|
||||
import json
|
||||
import pathlib
|
||||
import secrets
|
||||
import socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
|
||||
# 在隔离容器和数据卷验证持久化、冲突、崩溃、磁盘满及备份恢复。
|
||||
|
||||
|
||||
# 执行有超时的 Docker 命令并保留失败诊断。
|
||||
def docker(*args, **kwargs):
|
||||
try:
|
||||
return subprocess.run(
|
||||
["docker", *args], check=True, capture_output=True, timeout=60, **kwargs
|
||||
).stdout
|
||||
except subprocess.CalledProcessError as error:
|
||||
diagnostic = error.stderr.decode(errors="replace") if isinstance(error.stderr, bytes) else error.stderr
|
||||
raise RuntimeError(diagnostic) from error
|
||||
|
||||
|
||||
class Client:
|
||||
# 建立独立会话并验证协议及登录。
|
||||
def __init__(self, port, password, user="root"):
|
||||
self.socket = socket.create_connection(("127.0.0.1", port), timeout=15)
|
||||
self.stream = self.socket.makefile("rwb")
|
||||
try:
|
||||
assert self.request(method="hello", protocol=1)["status"] == "hello"
|
||||
assert self.request(method="login", user=user, password=password)["status"] == "ok"
|
||||
except BaseException:
|
||||
self.close()
|
||||
raise
|
||||
|
||||
# 发送一条请求,不重试结果未知的写入。
|
||||
def request(self, **request):
|
||||
self.stream.write(json.dumps(request).encode() + b"\n")
|
||||
self.stream.flush()
|
||||
raw = self.stream.readline(4 * 1024 * 1024)
|
||||
if not raw:
|
||||
raise ConnectionError("server closed the connection")
|
||||
return json.loads(raw)
|
||||
|
||||
# 执行 SQL,保留错误码以识别事务冲突。
|
||||
def sql(self, sql):
|
||||
reply = self.request(method="query", sql=sql)
|
||||
if reply["status"] == "error":
|
||||
raise QueryError(reply)
|
||||
assert reply["status"] == "result", reply
|
||||
return reply
|
||||
|
||||
# 关闭流及底层套接字。
|
||||
def close(self):
|
||||
try:
|
||||
self.stream.close()
|
||||
finally:
|
||||
self.socket.close()
|
||||
|
||||
|
||||
class QueryError(Exception):
|
||||
# 保存服务端结构化错误。
|
||||
def __init__(self, reply):
|
||||
self.reply = reply
|
||||
super().__init__(str(reply))
|
||||
|
||||
|
||||
# 仅等待真实服务登录成功,不用固定启动延时。
|
||||
def ready(container, password):
|
||||
deadline = time.monotonic() + 45
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
port = int(docker("port", container, "7777/tcp", text=True).strip().rsplit(":", 1)[1])
|
||||
client = Client(port, password)
|
||||
client.close()
|
||||
return port
|
||||
except (OSError, ValueError, AssertionError, subprocess.CalledProcessError):
|
||||
time.sleep(0.2)
|
||||
raise TimeoutError(f"server did not become ready: {container}")
|
||||
|
||||
|
||||
# 连接验收库。
|
||||
def connect(port, password):
|
||||
client = Client(port, password)
|
||||
try:
|
||||
client.sql("USE acceptance")
|
||||
return client
|
||||
except BaseException:
|
||||
client.close()
|
||||
raise
|
||||
|
||||
|
||||
# 验证事务冲突不会丢失更新,失败后显式回滚。
|
||||
def conflict(port, password):
|
||||
first = connect(port, password)
|
||||
second = connect(port, password)
|
||||
try:
|
||||
for client in (first, second):
|
||||
client.sql("BEGIN")
|
||||
client.sql("UPDATE counters SET value = value + 1 WHERE id = 1")
|
||||
first.sql("COMMIT")
|
||||
try:
|
||||
second.sql("COMMIT")
|
||||
except QueryError as error:
|
||||
assert error.reply["code"] == "serialization_failure", error.reply
|
||||
second.sql("ROLLBACK")
|
||||
else:
|
||||
raise AssertionError("conflicting update unexpectedly committed")
|
||||
assert first.sql("SELECT value FROM counters WHERE id = 1")["rows"] == [[1]]
|
||||
first.sql("UPDATE counters SET value = 0 WHERE id = 1")
|
||||
finally:
|
||||
first.close()
|
||||
second.close()
|
||||
|
||||
|
||||
# 在独立会话中持续提交关联写入并核对已确认提交。
|
||||
def workload(port, password, seconds, stop, acknowledged, lock, worker):
|
||||
client = connect(port, password)
|
||||
deadline = time.monotonic() + seconds
|
||||
sequence = 0
|
||||
conflicts = 0
|
||||
try:
|
||||
while not stop.is_set() and time.monotonic() < deadline:
|
||||
sequence += 1
|
||||
identity = worker * 1_000_000 + sequence
|
||||
client.sql("BEGIN")
|
||||
client.sql(f"INSERT INTO events (id, actor) VALUES ({identity}, {worker})")
|
||||
client.sql("UPDATE counters SET value = value + 1 WHERE id = 1")
|
||||
try:
|
||||
client.sql("COMMIT")
|
||||
except QueryError as error:
|
||||
if error.reply["code"] != "serialization_failure":
|
||||
raise
|
||||
conflicts += 1
|
||||
client.sql("ROLLBACK")
|
||||
continue
|
||||
with lock:
|
||||
acknowledged.add(identity)
|
||||
except (OSError, ConnectionError, ValueError):
|
||||
if not stop.is_set():
|
||||
raise
|
||||
finally:
|
||||
try:
|
||||
client.close()
|
||||
except OSError:
|
||||
if not stop.is_set():
|
||||
raise
|
||||
return conflicts
|
||||
|
||||
|
||||
# 核对事务组完整性及已确认提交,允许断线提交结果未知。
|
||||
def verify(port, password, acknowledged):
|
||||
client = connect(port, password)
|
||||
try:
|
||||
rows = []
|
||||
last = -1
|
||||
while True:
|
||||
page = client.sql(f"SELECT id FROM events WHERE id > {last} ORDER BY id LIMIT 500")
|
||||
assert not page["truncated"], page
|
||||
rows.extend(page["rows"])
|
||||
if len(page["rows"]) < 500:
|
||||
break
|
||||
last = page["rows"][-1][0]
|
||||
identities = {row[0] for row in rows}
|
||||
assert len(identities) == len(rows), "duplicate committed event"
|
||||
assert acknowledged <= identities, "acknowledged commits were lost"
|
||||
assert client.sql("SELECT value FROM counters WHERE id = 1")["rows"] == [[len(rows)]]
|
||||
return len(rows)
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
|
||||
# 验证恢复后的账号和最小授权,不授予写权限。
|
||||
def verify_reader(port, password):
|
||||
client = Client(port, password, user="backup_reader")
|
||||
try:
|
||||
client.sql("USE acceptance")
|
||||
client.sql("SELECT id FROM events")
|
||||
reply = client.request(method="query", sql="DELETE FROM events")
|
||||
assert reply["status"] == "error" and reply["code"] == "forbidden", reply
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
|
||||
# 运行有资源上限的容器,并登记待清理名称。
|
||||
def launch(image, name, volume, secret, containers):
|
||||
containers.append(name)
|
||||
docker(
|
||||
"run", "-d", "--name", name, "--cpus", "1", "--memory", "512m",
|
||||
"--memory-swap", "512m", "--pids-limit", "128",
|
||||
"--tmpfs", "/fault:rw,size=16m,uid=10001,gid=10001,mode=0700",
|
||||
"-p", "127.0.0.1::7777", "-v", f"{volume}:/var/lib/chusql",
|
||||
"-v", f"{secret}:/run/secrets/admin_password:ro", image,
|
||||
)
|
||||
|
||||
|
||||
# 清理所有测试资源,缺失对象除外均明确报错。
|
||||
def cleanup(containers, volumes):
|
||||
errors = []
|
||||
for kind, names in (("container", containers), ("volume", volumes)):
|
||||
for name in reversed(names):
|
||||
try:
|
||||
docker(kind, "rm", *(["-f"] if kind == "container" else []), name)
|
||||
except RuntimeError as error:
|
||||
if f"No such {kind}" not in str(error):
|
||||
errors.append(str(error))
|
||||
if errors:
|
||||
raise RuntimeError("test resource cleanup failed: " + "; ".join(errors))
|
||||
|
||||
|
||||
# 注入真实 ENOSPC,验证提交后故障封闭与重放。
|
||||
def disk_full(container, port, password):
|
||||
directory = "/var/lib/chusql/databases/acceptance"
|
||||
docker("exec", container, "sh", "-ec",
|
||||
f"touch /fault/checkpoint; ln -s /fault/checkpoint {directory}/wal.checkpoint.pending")
|
||||
filled = subprocess.run(
|
||||
["docker", "exec", container, "sh", "-c", "LC_ALL=C dd if=/dev/zero of=/fault/fill bs=4096"],
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
assert filled.returncode != 0 and "No space left on device" in filled.stderr, filled.stderr
|
||||
client = connect(port, password)
|
||||
try:
|
||||
reply = client.request(method="query", sql="INSERT INTO faults (id) VALUES (1)")
|
||||
assert reply["status"] == "error" and "space" in reply["message"].lower(), reply
|
||||
blocked = client.request(method="query", sql="SELECT id FROM faults")
|
||||
assert blocked["status"] == "error" and "recovery" in blocked["message"].lower(), blocked
|
||||
docker("exec", container, "sh", "-ec", f"test -s {directory}/wal.log")
|
||||
finally:
|
||||
client.close()
|
||||
docker("exec", container, "sh", "-ec",
|
||||
f"rm -f /fault/fill {directory}/wal.checkpoint.pending")
|
||||
docker("restart", "-t", "10", container)
|
||||
port = ready(container, password)
|
||||
client = connect(port, password)
|
||||
try:
|
||||
assert client.sql("SELECT id FROM faults")["rows"] == [[1]]
|
||||
client.sql("INSERT INTO faults (id) VALUES (2)")
|
||||
finally:
|
||||
client.close()
|
||||
return port
|
||||
|
||||
|
||||
# 验收完整镜像,清理仅限本次创建的容器及卷。
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Validate an already built ChuSQL image in isolated containers.")
|
||||
parser.add_argument("--image", default="chusql:local")
|
||||
parser.add_argument("--seconds", type=int, default=60)
|
||||
args = parser.parse_args()
|
||||
if args.seconds < 5:
|
||||
parser.error("--seconds must be at least 5")
|
||||
name = "chusql-acceptance-" + secrets.token_hex(4)
|
||||
containers = []
|
||||
volumes = []
|
||||
password = "Aa1!" + secrets.token_urlsafe(30)
|
||||
reader_password = "Aa1!" + secrets.token_urlsafe(30)
|
||||
acknowledged = set()
|
||||
lock = threading.Lock()
|
||||
stop = threading.Event()
|
||||
with tempfile.TemporaryDirectory(prefix="chusql-acceptance-") as temporary:
|
||||
secret = pathlib.Path(temporary) / "password"
|
||||
secret.write_text(password + "\n")
|
||||
secret.chmod(0o644)
|
||||
try:
|
||||
image = json.loads(docker("image", "inspect", args.image))[0]
|
||||
assert image["Config"]["User"] == "10001:10001", image["Config"]["User"]
|
||||
print(f"image={image['Id']} repo_digests={image['RepoDigests']}", flush=True)
|
||||
for suffix in ("data", "restore"):
|
||||
volume = name + "-" + suffix
|
||||
docker("volume", "create", volume)
|
||||
volumes.append(volume)
|
||||
server = name + "-server"
|
||||
launch(args.image, server, volumes[0], secret, containers)
|
||||
port = ready(server, password)
|
||||
client = Client(port, password)
|
||||
try:
|
||||
for sql in (
|
||||
"CREATE DATABASE acceptance", "USE acceptance",
|
||||
"CREATE TABLE counters (id INT PRIMARY KEY, value INT)",
|
||||
"CREATE TABLE events (id INT PRIMARY KEY, actor INT)",
|
||||
"CREATE TABLE faults (id INT PRIMARY KEY)",
|
||||
"INSERT INTO counters (id, value) VALUES (1, 0)",
|
||||
f"CREATE USER backup_reader IDENTIFIED BY '{reader_password}'",
|
||||
"GRANT SELECT ON acceptance.events TO backup_reader",
|
||||
):
|
||||
client.sql(sql)
|
||||
finally:
|
||||
client.close()
|
||||
conflict(port, password)
|
||||
verify_reader(port, reader_password)
|
||||
print("conflicting transactions and rollback: passed", flush=True)
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as pool:
|
||||
tasks = [pool.submit(workload, port, password, args.seconds, stop, acknowledged, lock, i)
|
||||
for i in range(1, 4)]
|
||||
conflicts = sum(task.result(timeout=args.seconds + 30) for task in tasks)
|
||||
assert len(acknowledged) >= 3, "insufficient successful load transactions"
|
||||
count = verify(port, password, acknowledged)
|
||||
print(f"load: {args.seconds}s, {count} committed transactions, {conflicts} conflicts", flush=True)
|
||||
uncommitted = connect(port, password)
|
||||
uncommitted.sql("BEGIN")
|
||||
uncommitted.sql("INSERT INTO faults (id) VALUES (99)")
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as pool:
|
||||
tasks = [pool.submit(workload, port, password, 30, stop, acknowledged, lock, i)
|
||||
for i in (10, 11)]
|
||||
deadline = time.monotonic() + 20
|
||||
while time.monotonic() < deadline:
|
||||
with lock:
|
||||
progressed = len(acknowledged) >= count + 5
|
||||
if progressed:
|
||||
break
|
||||
if any(task.done() for task in tasks):
|
||||
for task in tasks:
|
||||
if task.done():
|
||||
task.result()
|
||||
raise AssertionError("crash workload ended before progress")
|
||||
time.sleep(0.02)
|
||||
assert progressed, "crash workload made no progress"
|
||||
stop.set()
|
||||
docker("kill", "--signal", "KILL", server)
|
||||
for task in tasks:
|
||||
task.result(timeout=20)
|
||||
uncommitted.close()
|
||||
docker("start", server)
|
||||
port = ready(server, password)
|
||||
count = verify(port, password, acknowledged)
|
||||
client = connect(port, password)
|
||||
try:
|
||||
assert client.sql("SELECT id FROM faults")["rows"] == [], "uncommitted data survived crash"
|
||||
finally:
|
||||
client.close()
|
||||
print(f"SIGKILL recovery: {count} complete transactions, all acknowledged commits retained", flush=True)
|
||||
port = disk_full(server, port, password)
|
||||
verify(port, password, acknowledged)
|
||||
print("ENOSPC after WAL commit, recovery fence and replay: passed", flush=True)
|
||||
secret.write_text(secrets.token_urlsafe(30) + "\n")
|
||||
docker("restart", "-t", "10", server)
|
||||
port = ready(server, password)
|
||||
verify(port, password, acknowledged)
|
||||
print("idempotent bootstrap preserves existing password and data: passed", flush=True)
|
||||
docker("stop", "-t", "10", server)
|
||||
archive = docker("run", "--rm", "--network", "none", "--memory", "128m", "--cpus", "1",
|
||||
"-v", f"{volumes[0]}:/var/lib/chusql:ro", "--entrypoint", "tar",
|
||||
args.image, "-C", "/var/lib/chusql", "-czf", "-", ".")
|
||||
docker("run", "--rm", "-i", "--network", "none", "--memory", "128m", "--cpus", "1",
|
||||
"-v", f"{volumes[1]}:/var/lib/chusql", "--entrypoint", "tar", args.image,
|
||||
"-C", "/var/lib/chusql", "-xzf", "-", input=archive)
|
||||
restored = name + "-restored"
|
||||
launch(args.image, restored, volumes[1], secret, containers)
|
||||
port = ready(restored, password)
|
||||
assert verify(port, password, acknowledged) == count
|
||||
verify_reader(port, reader_password)
|
||||
client = connect(port, password)
|
||||
try:
|
||||
assert client.sql("SELECT id FROM faults ORDER BY id")["rows"] == [[1], [2]]
|
||||
finally:
|
||||
client.close()
|
||||
print("cold backup restores into an empty volume with original credentials: passed", flush=True)
|
||||
for container in containers:
|
||||
state = json.loads(docker("inspect", container))[0]
|
||||
assert not state["State"]["OOMKilled"], container
|
||||
assert state["HostConfig"]["Memory"] == 512 * 1024 * 1024
|
||||
print("runtime acceptance: passed (1 CPU / 512 MiB per server)", flush=True)
|
||||
except BaseException:
|
||||
for container in containers:
|
||||
result = subprocess.run(["docker", "logs", "--tail", "40", container], capture_output=True, text=True, timeout=15)
|
||||
print(result.stdout, result.stderr, flush=True)
|
||||
raise
|
||||
finally:
|
||||
cleanup(containers, volumes)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,9 @@
|
||||
# Web 经 Compose 网络连接数据库,不挂载或打开数据库目录。
|
||||
[server]
|
||||
listen_host = "server"
|
||||
port = 7777
|
||||
|
||||
[web]
|
||||
listen_host = "0.0.0.0"
|
||||
port = 7778
|
||||
cookie_secure = false
|
||||
Reference in new issue
Block a user