This is an automated email from the ASF dual-hosted git repository.
imbajin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hugegraph.git
The following commit(s) were added to refs/heads/master by this push:
new 8b2932c76 fix(server): retry all PD peers while waiting for storage
(#3129)
8b2932c76 is described below
commit 8b2932c7644e621026c9ee20cbb3533062386ec5
Author: KAI <[email protected]>
AuthorDate: Fri Jul 31 19:22:47 2026 +0530
fix(server): retry all PD peers while waiting for storage (#3129)
- cap each PD connection attempt at 2 seconds
- cap each PD request at 3 seconds
- cover failover after a hanging first peer
---------
Co-authored-by: imbajin <[email protected]>
---
.github/workflows/server-ci.yml | 9 +
.../src/assembly/static/bin/wait-storage.sh | 23 +--
.../src/assembly/travis/test-wait-storage.sh | 213 +++++++++++++++++++++
3 files changed, 232 insertions(+), 13 deletions(-)
diff --git a/.github/workflows/server-ci.yml b/.github/workflows/server-ci.yml
index 0d6c54b51..72f28b669 100644
--- a/.github/workflows/server-ci.yml
+++ b/.github/workflows/server-ci.yml
@@ -9,6 +9,15 @@ on:
pull_request:
jobs:
+ wait-storage-shell-test:
+ runs-on: ubuntu-22.04
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Run wait-storage.sh peer failover tests
+ run:
hugegraph-server/hugegraph-dist/src/assembly/travis/test-wait-storage.sh
+
build-server:
# TODO: we need test & replace it to ubuntu-24.04 or ubuntu-latest
runs-on: ubuntu-22.04
diff --git
a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/wait-storage.sh
b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/wait-storage.sh
index d4e9e278f..93c3a19dd 100755
--- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/wait-storage.sh
+++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/wait-storage.sh
@@ -30,6 +30,8 @@ BIN=$(abs_path)
TOP="$(cd "$BIN"/../ && pwd)"
GRAPH_CONF="$TOP/conf/graphs/hugegraph.properties"
WAIT_STORAGE_TIMEOUT_S=300
+WAIT_STORAGE_PD_CONNECT_TIMEOUT_S=2
+WAIT_STORAGE_PD_MAX_TIMEOUT_S=3
. "$BIN"/util.sh
@@ -97,9 +99,13 @@ if env | grep '^hugegraph\.' > /dev/null; then
log() { echo '[wait-storage] '\"\$1\"; }
- check_any_pd() {
+ check_any_pd_stores() {
for peer in \$(echo \"\$PD_REST_LIST\" | tr ',' ' '); do
- if curl ${PD_AUTH_ARGS} -f -s http://\${peer}/v1/health
>/dev/null 2>&1; then
+ if curl ${PD_AUTH_ARGS} -f -s \
+ --connect-timeout ${WAIT_STORAGE_PD_CONNECT_TIMEOUT_S} \
+ --max-time ${WAIT_STORAGE_PD_MAX_TIMEOUT_S} \
+ http://\${peer}/v1/stores 2>/dev/null | \
+ grep -qi '\"state\"[[:space:]]*:[[:space:]]*\"Up\"'; then
echo \"\$peer\"
return 0
fi
@@ -107,20 +113,11 @@ if env | grep '^hugegraph\.' > /dev/null; then
return 1
}
- until PD_REST=\$(check_any_pd); do
- log 'No PD peer ready yet, retrying in 5s'
- sleep 5
- done
- log \"PD health check PASSED via \$PD_REST\"
-
- until curl ${PD_AUTH_ARGS} -f -s \
- http://\${PD_REST}/v1/stores 2>/dev/null | \
- grep -qi '\"state\"[[:space:]]*:[[:space:]]*\"Up\"'; do
+ until PD_REST=\$(check_any_pd_stores); do
log 'No Up store yet, retrying in 5s'
sleep 5
done
-
- log 'Store registration check PASSED'
+ log \"Store registration check PASSED via \$PD_REST\"
log 'Storage backend is VIABLE'
" || { echo "[wait-storage] ERROR: Timeout waiting for storage
backend"; exit 1; }
diff --git
a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-wait-storage.sh
b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-wait-storage.sh
new file mode 100755
index 000000000..da6a008f0
--- /dev/null
+++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-wait-storage.sh
@@ -0,0 +1,213 @@
+#!/bin/bash
+#
+# 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.
+#
+
+set -uo pipefail
+
+SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
+SOURCE_BIN="${1:-$(cd "${SCRIPT_DIR}/../static/bin" && pwd)}"
+TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/wait-storage-test.XXXXXX")
+DIST_ROOT="${TMP_DIR}/dist"
+MOCK_BIN="${TMP_DIR}/mock-bin"
+CALL_LOG="${TMP_DIR}/curl-calls"
+ARGS_LOG="${TMP_DIR}/curl-args"
+COUNT_FILE="${TMP_DIR}/store-call-count"
+TIMEOUT_LOG="${TMP_DIR}/timeout-arg"
+CASE_OUTPUT=""
+CASE_RC=0
+
+trap 'rm -rf "${TMP_DIR}"' EXIT
+
+fail() {
+ echo "FAIL: $1" >&2
+ [[ -z "${CASE_OUTPUT}" ]] || printf '%s\n' "${CASE_OUTPUT}" >&2
+ exit 1
+}
+
+assert_equal() {
+ local name="$1" expected="$2" actual="$3"
+ [[ "${actual}" == "${expected}" ]] || \
+ fail "${name}: expected '${expected}', got '${actual}'"
+}
+
+assert_output() {
+ local expected="$1"
+ [[ "${CASE_OUTPUT}" == *"${expected}"* ]] || \
+ fail "missing output '${expected}'"
+}
+
+assert_contract() {
+ ! grep -q '/v1/health' "${CALL_LOG}" || \
+ fail "/v1/health must not gate readiness"
+ [[ -s "${ARGS_LOG}" ]] || fail "curl was not called"
+ if grep -Fv -- '-u test-user:test-password' "${ARGS_LOG}" | grep -q .; then
+ fail "authentication arguments were not preserved"
+ fi
+ if grep -Fv -- '--connect-timeout 2' "${ARGS_LOG}" | grep -q .; then
+ fail "per-peer connect timeout was not preserved"
+ fi
+ if grep -Fv -- '--max-time 3' "${ARGS_LOG}" | grep -q .; then
+ fail "per-peer maximum timeout was not preserved"
+ fi
+ assert_equal "outer timeout" "300s" "$(cat "${TIMEOUT_LOG}")"
+}
+
+run_case() {
+ local scenario="$1" peers="$2" abort_after="$3"
+ : > "${CALL_LOG}"
+ : > "${ARGS_LOG}"
+ : > "${COUNT_FILE}"
+ : > "${TIMEOUT_LOG}"
+ : > "${DIST_ROOT}/conf/graphs/hugegraph.properties"
+
+ CASE_OUTPUT=$(env \
+ PATH="${MOCK_BIN}:${PATH}" \
+ MOCK_SCENARIO="${scenario}" \
+ MOCK_ABORT_AFTER="${abort_after}" \
+ MOCK_CALL_LOG="${CALL_LOG}" \
+ MOCK_ARGS_LOG="${ARGS_LOG}" \
+ MOCK_COUNT_FILE="${COUNT_FILE}" \
+ MOCK_TIMEOUT_LOG="${TIMEOUT_LOG}" \
+ HG_SERVER_PD_REST_ENDPOINT="${peers}" \
+ PD_AUTH_USER="test-user" \
+ PD_AUTH_PASSWORD="test-password" \
+ 'hugegraph.backend=hstore' \
+ 'hugegraph.pd.peers=config-only:8686' \
+ "${DIST_ROOT}/bin/wait-storage.sh" 2>&1)
+ CASE_RC=$?
+}
+
+if [[ ! -f "${SOURCE_BIN}/wait-storage.sh" || ! -f "${SOURCE_BIN}/util.sh" ]];
then
+ fail "wait-storage.sh or util.sh not found under ${SOURCE_BIN}"
+fi
+
+mkdir -p "${DIST_ROOT}/bin" "${DIST_ROOT}/conf/graphs" "${MOCK_BIN}"
+cp "${SOURCE_BIN}/wait-storage.sh" "${SOURCE_BIN}/util.sh" "${DIST_ROOT}/bin/"
+: > "${DIST_ROOT}/conf/graphs/hugegraph.properties"
+
+cat > "${MOCK_BIN}/timeout" <<'EOF'
+#!/bin/bash
+printf '%s\n' "$1" > "${MOCK_TIMEOUT_LOG}"
+shift
+"$@" &
+command_pid=$!
+ticks=0
+while kill -0 "${command_pid}" 2>/dev/null; do
+ count=$(cat "${MOCK_COUNT_FILE}" 2>/dev/null || true)
+ count=${count:-0}
+ ticks=$((ticks + 1))
+ if [[ "${count}" -ge "${MOCK_ABORT_AFTER}" || "${ticks}" -ge 500 ]]; then
+ kill -TERM "${command_pid}" 2>/dev/null || true
+ wait "${command_pid}" 2>/dev/null || true
+ exit 124
+ fi
+ /bin/sleep 0.01
+done
+wait "${command_pid}"
+EOF
+
+cat > "${MOCK_BIN}/sleep" <<'EOF'
+#!/bin/bash
+/bin/sleep 0.02
+EOF
+
+cat > "${MOCK_BIN}/curl" <<'EOF'
+#!/bin/bash
+set -u
+url="${!#}"
+printf '%s\n' "$*" >> "${MOCK_ARGS_LOG}"
+printf '%s\n' "${url}" >> "${MOCK_CALL_LOG}"
+
+if [[ "${url}" == */v1/health ]]; then
+ printf '{}\n'
+ exit 0
+fi
+
+count=$(cat "${MOCK_COUNT_FILE}" 2>/dev/null || true)
+count=$((${count:-0} + 1))
+printf '%s\n' "${count}" > "${MOCK_COUNT_FILE}"
+
+if [[ "${MOCK_SCENARIO}" == "pd1-up" && \
+ "${url}" == "http://pd1:8620/v1/stores" ]]; then
+ printf '{"stores":[{"state":"Up"}]}\n'
+elif [[ "${MOCK_SCENARIO}" == "hanging-first" && \
+ "${url}" == "http://pd0:8620/v1/stores" ]]; then
+ if [[ " $* " == *" --connect-timeout 2 "* && \
+ " $* " == *" --max-time 3 "* ]]; then
+ /bin/sleep 0.05
+ else
+ /bin/sleep 10
+ fi
+ exit 28
+elif [[ "${MOCK_SCENARIO}" == "hanging-first" && \
+ "${url}" == "http://pd1:8620/v1/stores" ]]; then
+ printf '{"stores":[{"state":"Up"}]}\n'
+elif [[ "${MOCK_SCENARIO}" == "retry" && "${count}" -eq 3 && \
+ "${url}" == "http://pd0:8620/v1/stores" ]]; then
+ exit 7
+elif [[ "${MOCK_SCENARIO}" == "retry" && "${count}" -eq 4 && \
+ "${url}" == "http://pd1:8620/v1/stores" ]]; then
+ printf '{"stores":[{"state":"Up"}]}\n'
+else
+ printf '{"stores":[]}\n'
+fi
+EOF
+
+chmod +x "${MOCK_BIN}/timeout" "${MOCK_BIN}/sleep" "${MOCK_BIN}/curl"
+
+PD0='http://pd0:8620/v1/stores'
+PD1='http://pd1:8620/v1/stores'
+TWO_CALLS="${PD0}"$'\n'"${PD1}"
+FOUR_CALLS="${TWO_CALLS}"$'\n'"${TWO_CALLS}"
+
+echo "wait-storage.sh peer failover tests"
+
+run_case "pd1-up" "pd0:8620,pd1:8620" 6
+assert_equal "storeless first peer rc" "0" "${CASE_RC}"
+assert_equal "configured peer order" "${TWO_CALLS}" "$(cat "${CALL_LOG}")"
+assert_output "Store registration check PASSED via pd1:8620"
+assert_contract
+echo " PASS storeless first peer"
+
+run_case "pd1-up" "pd1:8620,pd0:8620" 6
+assert_equal "reversed peer order rc" "0" "${CASE_RC}"
+assert_equal "stop after first Up peer" "${PD1}" "$(cat "${CALL_LOG}")"
+assert_contract
+echo " PASS reversed peer order"
+
+run_case "hanging-first" "pd0:8620,pd1:8620" 4
+assert_equal "hanging first peer rc" "0" "${CASE_RC}"
+assert_equal "healthy peer after per-peer timeout" "${TWO_CALLS}" "$(cat
"${CALL_LOG}")"
+assert_output "Store registration check PASSED via pd1:8620"
+assert_contract
+echo " PASS hanging first peer"
+
+run_case "retry" "pd0:8620,pd1:8620" 8
+assert_equal "retry rc" "0" "${CASE_RC}"
+assert_equal "complete peer rescan" "${FOUR_CALLS}" "$(cat "${CALL_LOG}")"
+assert_output "Storage backend is VIABLE"
+assert_contract
+echo " PASS unavailable peer retry"
+
+run_case "none" "pd0:8620,pd1:8620" 4
+[[ "${CASE_RC}" -ne 0 ]] || fail "all-unready peers must fail closed"
+assert_equal "all-unready rescan" "${FOUR_CALLS}" "$(cat "${CALL_LOG}")"
+assert_output "ERROR: Timeout waiting for storage backend"
+assert_contract
+echo " PASS all-unready timeout"
+
+echo "5 passed, 0 failed"