Files
zeekay 9a90943b8f merge: ClickHouse 26.7.1.1315-stable into datastore — preserve debrand
13,838 upstream commits, forward from the 26.6.1.1 fork point. 129 conflicts:
37 resolved mechanically (take upstream's file, re-apply the brand sweep) and
92 by hand where our fork and upstream had both changed the same code.

The rule throughout: upstream owns the engine, we own the naming and a small set
of deliberate deviations. Never resolve by discarding an upstream change — a
dropped rename or a missed new parameter becomes a compile error a thousand
files away.

Deviations verified intact after the merge:
  ENABLE_XRAY still defaults OFF        (CMakeLists.txt:105)
  X-Datastore-User read by the server   (authenticateUserByHTTP.cpp)
  X-ClickHouse-* in src/                0
  hanzo/research                        present
  system.*_log retention bound          present
  ArrowFlight rip                       held against upstream re-adding it

Two things worth naming, because neither shows up as a conflict:

  src/CMakeLists.txt gained an UNGUARDED
  `add_object_library(datastore_storages_arrowflight Storages/ArrowFlight)` from
  upstream, for a directory our gRPC rip deleted. It sat outside every conflict
  hunk, so a resolver reading only the markers would sail straight past it into
  a CMake configure error. Removed; the Server/ArrowFlight sibling at line 474
  stays because it is inside `if (TARGET ch_contrib::arrow_flight)`.

  The brand sweep had rewritten github.com/ClickHouse/ClickHouse issue links to
  github.com/Datastore/Datastore — a dead host. Those point at upstream's own
  tracker and are restored, alongside the contributors list, sql.clickhouse.com
  and the ClickHouse Inc copyright: attribution is theirs, not ours to rename.

base/glibc-compatibility/memcpy/memcpy.h is deleted, not kept: upstream dropped
it for LLVM-libc's, and our only change to it was a comment rebrand.
2026-07-26 14:47:43 -07:00

436 lines
17 KiB
Bash
Executable File

#!/bin/bash
# shellcheck disable=SC2086,SC2001,SC2046,SC2030,SC2031,SC2010,SC2015
set -x
dmesg --clear ||:
set -e
set -u
set -o pipefail
stage=${stage:-}
repo_dir=/repo
CONFIG_DIR="/etc/datastore-server"
export PATH="$repo_dir/ci/tmp/:$PATH"
export PYTHONPATH=$repo_dir:$repo_dir/ci
cd /workspace
# Direct sanitizer reports to files instead of the server's stderr to avoid
# losing the report when the server aborts. The runtime appends ".<pid>"
# to `log_path`; reports are merged back in by collect_sanitizer_reports.
# Existing options from the environment/image are preserved.
SANITIZER_LOG_BASE="/workspace/sanitizer.log"
for _san in ASAN TSAN MSAN UBSAN LSAN; do
_var="${_san}_OPTIONS"
export "$_var"="${!_var:+${!_var} }log_path=${SANITIZER_LOG_BASE}"
done
unset _san _var
function collect_sanitizer_reports
{
# Merge sanitizer reports captured via log_path into stderr.log (for the
# failure parser) and server.log (for context and the OOM grep). Run from an
# EXIT trap so early `set -e` aborts are covered too; `|| true` keeps the
# exit code intact.
local report
for report in "${SANITIZER_LOG_BASE}".*; do
[ -e "$report" ] || continue
echo "Found sanitizer report: $report"
{
echo "=== sanitizer report from ${report} ==="
cat "$report"
echo
} | tee -a stderr.log >> server.log || true
done
}
trap collect_sanitizer_reports EXIT
function configure
{
chmod +x $repo_dir/ci/tmp/datastore
# datastore may be compressed - run once to decompress
$repo_dir/ci/tmp/datastore --query "SELECT 1" ||:
ln -sf $repo_dir/ci/tmp/datastore $repo_dir/ci/tmp/datastore-server
ln -sf $repo_dir/ci/tmp/datastore $repo_dir/ci/tmp/datastore-client
ln -sf $repo_dir/ci/tmp/datastore $repo_dir/ci/tmp/datastore-local
rm -rf $CONFIG_DIR ||:
mkdir -p $CONFIG_DIR ||:
cp -av --dereference "$repo_dir"/programs/server/config* $CONFIG_DIR
cp -av --dereference "$repo_dir"/programs/server/user* $CONFIG_DIR
# TODO figure out which ones are needed
cp -av --dereference "$repo_dir"/tests/config/config.d/listen.xml $CONFIG_DIR/config.d
cp -av --dereference "$repo_dir"/tests/config/users.d/ci_logs_sender.yaml $CONFIG_DIR/users.d
cp -av --dereference "$repo_dir"/ci/jobs/scripts/fuzzer/query-fuzzer-tweaks-users.xml $CONFIG_DIR/users.d
cp -av --dereference "$repo_dir"/ci/jobs/scripts/fuzzer/limit-recursion-settings.xml $CONFIG_DIR/users.d
cp -av --dereference "$repo_dir"/ci/jobs/scripts/fuzzer/fuzz-server-settings.xml $CONFIG_DIR/config.d
cat > $CONFIG_DIR/config.d/max_server_memory_usage_to_ram_ratio.xml <<EOL
<datastore>
<max_server_memory_usage_to_ram_ratio>0.75</max_server_memory_usage_to_ram_ratio>
</datastore>
EOL
(cd $repo_dir && python3 $repo_dir/ci/jobs/scripts/datastore_proc.py logs_export_config) || echo "Failed to create log export config"
}
function filter_exists_and_template
{
local path
for path in "$@"; do
if [ -e "$path" ]; then
# SC2001 shellcheck suggests:
# echo ${path//.sql.j2/.gen.sql}
# but it doesn't allow to use regex
echo "$path" | sed 's/\.sql\.j2$/.gen.sql/'
else
echo "'$path' does not exist" >&2
fi
done
}
function stop_server
{
datastore-client --query "select elapsed, query from system.processes" ||:
datastore stop
# Debug.
date
sleep 10
jobs
pstree -aspgT
}
function fuzz
{
$repo_dir/ci/jobs/scripts/fuzzer/generate-test-j2.py --path $repo_dir/tests/queries/0_stateless
# Obtain the list of newly added tests. They will be fuzzed in more extreme way than other tests.
# Don't overwrite the NEW_TESTS_OPT so that it can be set from the environment.
NEW_TESTS="$(sed -n 's!\(^tests/queries/0_stateless/.*\.sql\(\.j2\)\?\)$!ch/\1!p' /workspace/ci-changed-files.txt | sort -R)"
# ci-changed-files.txt contains also files that has been deleted/renamed, filter them out.
NEW_TESTS="$(filter_exists_and_template $NEW_TESTS)"
if [[ -n "$NEW_TESTS" ]]
then
NEW_TESTS_OPT="${NEW_TESTS_OPT:---interleave-queries-file ${NEW_TESTS}}"
else
NEW_TESTS_OPT="${NEW_TESTS_OPT:-}"
fi
mkdir -p /var/run/datastore-server
# server.log -> All server logs, including sanitizer
# stderr.log -> Process logs (sanitizer) only
( datastore-server \
--config-file $CONFIG_DIR/config.xml \
--pid-file /var/run/datastore-server/datastore-server.pid \
-- --path $CONFIG_DIR \
--logger.console=0 \
--logger.log=server.log 2>&1 | tee -a stderr.log >> server.log 2>&1
exit "${PIPESTATUS[0]}" ) &
server_bg_pid=$!
# A debug or sanitizer server can take over a minute from fork to listening;
# give it the same 120s budget as `wait_ready` in `datastore_proc.py`.
# A dead server is detected early, so the deadline only affects hung servers.
for _ in {1..120}
do
if datastore-client --receive_timeout=5 --query "select 1" || ! kill -0 $server_bg_pid
then
break
fi
sleep 1
done
server_pid=$(cat /var/run/datastore-server/datastore-server.pid)
kill -0 $server_pid
IS_ASAN=$(datastore-client --query "SELECT count() FROM system.build_options WHERE name = 'CXX_FLAGS' AND position('sanitize=address' IN value)")
if [[ "$IS_ASAN" = "1" ]];
then
echo "ASAN build detected. Not using gdb since it disables LeakSanitizer detections"
else
# Set follow-fork-mode to parent, because we attach to datastore-server, not to watchdog
# and datastore-server can do fork-exec, for example, to run some bridge.
# Do not set nostop noprint for all signals, because some it may cause gdb to hang,
# explicitly ignore non-fatal signals that are used by server.
# Number of SIGRTMIN can be determined only in runtime.
RTMIN=$(kill -l SIGRTMIN)
echo "
set follow-fork-mode parent
handle SIGHUP nostop noprint pass
handle SIGINT nostop noprint pass
handle SIGQUIT nostop noprint pass
handle SIGPIPE nostop noprint pass
handle SIGTERM nostop noprint pass
handle SIGUSR1 nostop noprint pass
handle SIGUSR2 nostop noprint pass
handle SIG$RTMIN nostop noprint pass
info signals
continue
backtrace full
thread apply all backtrace full
info registers
disassemble /s
up
disassemble /s
up
disassemble /s
p \"done\"
detach
quit
" > script.gdb
gdb -batch -command script.gdb -p $server_pid &
sleep 5
# gdb will send SIGSTOP, spend some time loading debug info, and then send SIGCONT, wait for it (up to send_timeout, 300s)
time datastore-client --query "SELECT 'Connected to datastore-server after attaching gdb'" ||:
# Check connectivity after we attach gdb, because it might cause the server
# to freeze, and the fuzzer will fail. In debug build, it can take a lot of time.
for _ in {1..180}
do
if datastore-client --receive_timeout=5 --query "select 1"
then
break
fi
sleep 1
done
kill -0 $server_pid # This checks that it is our server that is started and not some other one
fi
echo 'Server started and responded.'
(cd $repo_dir && python3 $repo_dir/ci/jobs/scripts/datastore_proc.py logs_export_start) || echo "Failed to start log exports"
# Setup arguments for the fuzzer
FUZZER_OUTPUT_SQL_FILE=''
if [[ "$FUZZER_TO_RUN" = "AST Fuzzer" ]];
then
if [[ -n "${TARGETED_QUERIES_FILE:-}" ]] && [[ -f "${TARGETED_QUERIES_FILE}" ]];
then
QUERIES_FILE="$(cat "${TARGETED_QUERIES_FILE}")"
echo "Using targeted AST fuzzer corpus from ${TARGETED_QUERIES_FILE}"
else
QUERIES_FILE=$(find /repo/tests/queries/0_stateless -type f -name "*.sql" | sort -R)
fi
if [[ -n "${FUZZER_COMPATIBILITY:-}" ]];
then
COMPAT_ARG="--compatibility=${FUZZER_COMPATIBILITY}"
echo "Using AST fuzzer compatibility setting: ${FUZZER_COMPATIBILITY}"
else
COMPAT_ARG=""
fi
FUZZER_ARGS="--query-fuzzer-runs=1000 --create-query-fuzzer-runs=50 $COMPAT_ARG --queries-file $QUERIES_FILE $NEW_TESTS_OPT"
elif [ "$FUZZER_TO_RUN" = "BuzzHouse" ]
then
FUZZER_ARGS="--buzz-house-config=fuzz.json"
else
>&2 echo "Fuzzer \"$FUZZER_TO_RUN\" unknown, provide either \"AST Fuzzer\" or \"BuzzHouse\""
exit 1
fi
# Convert FUZZ_TIME_LIMIT (e.g. "30m", "60m", "1800s" or plain seconds) into seconds
# so that the remaining budget can be recomputed between fuzzer passes.
fuzz_time_limit="${FUZZ_TIME_LIMIT:-30m}"
case "$fuzz_time_limit" in
*h) fuzz_budget_seconds=$(( ${fuzz_time_limit%h} * 3600 ));;
*m) fuzz_budget_seconds=$(( ${fuzz_time_limit%m} * 60 ));;
*s) fuzz_budget_seconds=$(( ${fuzz_time_limit%s} ));;
*) fuzz_budget_seconds=$(( fuzz_time_limit ));;
esac
fuzz_started_at=$SECONDS
fuzzer_exit_code=0
fuzz_pass=0
: > fuzzer.log
while :; do
fuzz_pass=$((fuzz_pass+1))
remaining_seconds=$(( fuzz_budget_seconds - (SECONDS - fuzz_started_at) ))
echo "=== Fuzzer pass $fuzz_pass, remaining time budget: ${remaining_seconds}s ==="
# Allow the fuzzer to run for some time, giving it a grace period of 5m to finish once the time
# out triggers. After that, it'll send a SIGKILL to the fuzzer to make sure it finishes within
# a reasonable time.
# Bound the parser/AST recursion on the client command line, matching the server-side caps in
# limit-recursion-settings.xml. The client parses every corpus query locally, and a corpus
# `SET compatibility=...` reverts max_parser_backtracks to its pre-24.3 default of 0 (unbounded);
# a command-line value survives that revert, unlike a profile value.
timeout --verbose --signal TERM --kill-after=5m --preserve-status "${remaining_seconds}s" datastore-client \
--max_memory_usage_in_client=1000000000 \
--receive_timeout=10 \
--receive_data_timeout_ms=10000 \
--stacktrace \
--max_parser_backtracks=1000000 \
--max_parser_depth=1000 \
$FUZZER_ARGS \
>> fuzzer.log \
2>&1 &
fuzzer_pid=$!
echo "Fuzzer pid is $fuzzer_pid"
# We need to give timeout some time to execute the underlying command with that many arguments
elapsed=0
maximum=50
while [[ $elapsed -lt $maximum ]]; do
if ps -o pid= --ppid "$fuzzer_pid"; then
echo "Found underlying PID!"
break;
else
echo "Not found. Trying again..."
fi
sleep 0.1
elapsed=$((elapsed+1))
done
# The fuzzer_pid belongs to the timeout process.
actual_fuzzer_pid=$(ps -o pid= --ppid "$fuzzer_pid")
if [[ "$IS_ASAN" = "1" ]];
then
echo "ASAN build detected. Not using gdb since it disables LeakSanitizer detections"
else
echo "Attaching gdb to the fuzzer itself"
gdb -batch -command script.gdb -p $actual_fuzzer_pid &
fi
# Wait for the fuzzer to complete.
# Note that the 'wait || ...' thing is required so that the script doesn't
# exit because of 'set -e' when 'wait' returns nonzero code.
fuzzer_exit_code=0
wait "$fuzzer_pid" || fuzzer_exit_code=$?
echo "Fuzzer exit code is $fuzzer_exit_code"
# A non-zero exit code is either the time limit (TERM/KILL sent by `timeout`) or a
# client/server failure — in both cases stop and let the code below classify it.
if [[ "$fuzzer_exit_code" != "0" ]]; then
break
fi
# BuzzHouse enforces its own internal time budget and exits 0 when it is reached.
if [[ "$FUZZER_TO_RUN" != "AST Fuzzer" ]]; then
break
fi
# Exit code 0 means the client walked the whole corpus and exited voluntarily. For the
# targeted AST fuzzer the corpus is small, so a pass can finish within a couple of
# minutes of a 30-minute budget. Each pass starts from a fresh random seed, so rerunning
# the corpus explores new mutations instead of throwing away the remaining budget.
remaining_seconds=$(( fuzz_budget_seconds - (SECONDS - fuzz_started_at) ))
if [[ "$remaining_seconds" -lt 60 ]]; then
echo "Fuzzer finished the corpus and only ${remaining_seconds}s of the budget remain, not restarting"
break
fi
# Make sure the server is still accepting queries before restarting: a fuzzer that
# exited with code 0 against a dead server would otherwise restart-loop until the budget
# runs out and hide the failure.
if ! datastore-client --receive_timeout=5 --query "SELECT 'fuzzer restart liveness check'"; then
echo "Server is not responding, not restarting the fuzzer"
break
fi
echo "Fuzzer finished the corpus with success, restarting it to use the remaining ${remaining_seconds}s of the budget"
done
# If the server dies, most often the fuzzer returns Code 210: Connetion
# refused, and sometimes also code 32: attempt to read after eof. For
# simplicity, check again whether the server is accepting connections using
# datastore-client. We don't check for the existence of the server process, because
# the process is still present while the server is terminating and not
# accepting the connections anymore.
# Default: the loop leaves this unset if it exhausts all retries via the
# "alive but busy" branches (TOO_MANY_SIMULTANEOUS_QUERIES /
# MEMORY_LIMIT_EXCEEDED / probe timeout); a dead server sets server_died=1
# and breaks. A receive/socket timeout means the server is alive but slow to
# answer (common right after a 30m ASAN fuzz run), not dead -- a dead server
# returns "Connection refused"/EOF instead -- so count repeated timeouts and
# only declare a hang once they persist, otherwise a single transient timeout
# turns a clean (exit 143) run into a bogus "server died" FAIL.
# BEGIN: server-liveness probe loop (exercised verbatim by
# ci/tests/test_fuzzer_liveness_loop.py)
server_died=0
timeouts=0
timeouts_max=12
for _ in {1..100}
do
if datastore-client --receive_timeout=5 --query "SELECT 1" 2> err
then
server_died=0
break
else
# There are legitimate queries leading to this error, example:
# SELECT * FROM remote('127.0.0.{1..255}', system, one)
if grep -F 'TOO_MANY_SIMULTANEOUS_QUERIES' err
then
# Give it some time to cool down. The SHOW PROCESSLIST is only a
# diagnostic and runs under `set -e`; if the same overload rejects
# it, do not abort the script (that would skip the status.tsv
# write below and surface as a missing-status job ERROR).
datastore-client --query "SHOW PROCESSLIST" ||:
timeouts=0
sleep 1
elif grep -F 'MEMORY_LIMIT_EXCEEDED' err
then
# Server is alive but at memory limit, give it time to reclaim
timeouts=0
sleep 1
elif grep -F 'Timeout exceeded while' err
then
# Alive but slow to answer: retry, and only treat it as a real
# hang once the timeouts persist (a dead server hits the branch
# below with "Connection refused"/EOF, not a timeout).
timeouts=$((timeouts + 1))
if [[ "$timeouts" -ge "$timeouts_max" ]]
then
echo "Server live check: probe timed out $timeouts times, treating server as hung"
cat err
server_died=1
break
fi
sleep 1
else
echo "Server live check returns $?"
cat err
server_died=1
break
fi
fi
done
# END: server-liveness probe loop
# Stop the server in background so we can wait for the subshell to
# finish in the foreground. We wait on server_bg_pid (the subshell running
# the server pipeline) rather than server_pid (from the PID file), because
# the PID file contains the forked server process which is not a direct
# child of this shell, so wait would fail with "not a child of this shell".
# The subshell exits with datastore-server's exit code via PIPESTATUS.
stop_server &
server_exit_code=0
wait $server_bg_pid || server_exit_code=$?
echo "Server exit code is $server_exit_code"
echo -e "$server_died\t$server_exit_code\t$fuzzer_exit_code" > status.tsv
if test -f core.*; then
zstd --threads=0 core.*
mv core.*.zst core.zst
fi
}
case "$stage" in
"")
;& # Did you know? This is "fallthrough" in bash. https://stackoverflow.com/questions/12010686/case-statement-fallthrough
"configure")
time configure
;&
"fuzz")
time fuzz
;&
esac
exit $server_exit_code