fix: close qdrant archive restore race
This commit is contained in:
@@ -57,6 +57,9 @@ run_restore() {
|
|||||||
exit 17
|
exit 17
|
||||||
fi
|
fi
|
||||||
tar -C "$extract" -xf "$backup_dir/$input_name"
|
tar -C "$extract" -xf "$backup_dir/$input_name"
|
||||||
|
if [ -n "${RESTORE_CAPTURE_FILE:-}" ]; then
|
||||||
|
cp "$backup_dir/$input_name" "$RESTORE_CAPTURE_FILE"
|
||||||
|
fi
|
||||||
cp -R "$extract/payload"/. "$volume_dir"/
|
cp -R "$extract/payload"/. "$volume_dir"/
|
||||||
rm -rf "$snapshot" "$extract"
|
rm -rf "$snapshot" "$extract"
|
||||||
}
|
}
|
||||||
@@ -187,6 +190,33 @@ with tarfile.open(archive, "w") as tf:
|
|||||||
PY
|
PY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
make_duplicate_manifest_archive() {
|
||||||
|
archive_path=$1
|
||||||
|
python - "$archive_path" <<'PY'
|
||||||
|
import io, tarfile, sys
|
||||||
|
archive = sys.argv[1]
|
||||||
|
with tarfile.open(archive, "w") as tf:
|
||||||
|
manifest = b"""format=thothii-qdrant-backup-v1
|
||||||
|
project_name=thoth-task8
|
||||||
|
project_name=thoth-task8
|
||||||
|
volume_name=thoth-task8_qdrant-data
|
||||||
|
volume_role=qdrant-data
|
||||||
|
helper_image=qdrant/qdrant:v1.18.2@sha256:75eab8c4ba42096724fdcfde8b4de0b5713d529dde32f285a1f86fdcb2c9e50c
|
||||||
|
created_utc=2026-08-08T17:35:36Z
|
||||||
|
"""
|
||||||
|
info = tarfile.TarInfo("manifest.env")
|
||||||
|
info.size = len(manifest)
|
||||||
|
tf.addfile(info, io.BytesIO(manifest))
|
||||||
|
directory = tarfile.TarInfo("payload")
|
||||||
|
directory.type = tarfile.DIRTYPE
|
||||||
|
tf.addfile(directory)
|
||||||
|
payload = b"dup"
|
||||||
|
info = tarfile.TarInfo("payload/dup.txt")
|
||||||
|
info.size = len(payload)
|
||||||
|
tf.addfile(info, io.BytesIO(payload))
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
backup_output="$tmp/qdrant-backup.tar"
|
backup_output="$tmp/qdrant-backup.tar"
|
||||||
docker_log="$tmp/docker-backup.log"
|
docker_log="$tmp/docker-backup.log"
|
||||||
PATH="$fakebin:$PATH" DOCKER_LOG="$docker_log" PROJECT_NAME="$project" VOLUME_ROOT="$volume_root" \
|
PATH="$fakebin:$PATH" DOCKER_LOG="$docker_log" PROJECT_NAME="$project" VOLUME_ROOT="$volume_root" \
|
||||||
@@ -272,6 +302,27 @@ if grep -q "compose --project-name $project stop qdrant" "$symlink_log"; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
duplicate_archive="$tmp/duplicate.tar"
|
||||||
|
make_duplicate_manifest_archive "$duplicate_archive"
|
||||||
|
duplicate_log="$tmp/duplicate.log"
|
||||||
|
: >"$duplicate_log"
|
||||||
|
if PATH="$fakebin:$PATH" DOCKER_LOG="$duplicate_log" PROJECT_NAME="$project" VOLUME_ROOT="$volume_root" \
|
||||||
|
HELPER_IMAGE="qdrant/qdrant:v1.18.2@sha256:75eab8c4ba42096724fdcfde8b4de0b5713d529dde32f285a1f86fdcb2c9e50c" \
|
||||||
|
./scripts/vector-restore.sh --project-name "$project" --input "$duplicate_archive" --confirm-project "$project" \
|
||||||
|
>"$tmp/duplicate.out" 2>"$tmp/duplicate.err"; then
|
||||||
|
echo "restore accepted duplicate manifest keys" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
grep -Eq 'duplicate|manifest' "$tmp/duplicate.err"
|
||||||
|
if grep -q "compose --project-name $project stop qdrant" "$duplicate_log"; then
|
||||||
|
echo "restore stopped qdrant before duplicate-manifest rejection" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if grep -q '^run ' "$duplicate_log"; then
|
||||||
|
echo "restore ran helper before duplicate-manifest rejection" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
restore_source="$tmp/restore-source"
|
restore_source="$tmp/restore-source"
|
||||||
mkdir -p "$restore_source/payload/collections/demo"
|
mkdir -p "$restore_source/payload/collections/demo"
|
||||||
cat >"$restore_source/manifest.env" <<EOF
|
cat >"$restore_source/manifest.env" <<EOF
|
||||||
@@ -297,13 +348,25 @@ grep -q 'confirmation must match --project-name exactly' "$tmp/confirm.err"
|
|||||||
|
|
||||||
printf '%s' modified-live >"$volume_dir/collections/demo/state.json"
|
printf '%s' modified-live >"$volume_dir/collections/demo/state.json"
|
||||||
restore_log="$tmp/restore-ok.log"
|
restore_log="$tmp/restore-ok.log"
|
||||||
|
restore_capture="$tmp/restore-captured.tar"
|
||||||
PATH="$fakebin:$PATH" DOCKER_LOG="$restore_log" PROJECT_NAME="$project" VOLUME_ROOT="$volume_root" \
|
PATH="$fakebin:$PATH" DOCKER_LOG="$restore_log" PROJECT_NAME="$project" VOLUME_ROOT="$volume_root" \
|
||||||
|
RESTORE_CAPTURE_FILE="$restore_capture" \
|
||||||
HELPER_IMAGE="qdrant/qdrant:v1.18.2@sha256:75eab8c4ba42096724fdcfde8b4de0b5713d529dde32f285a1f86fdcb2c9e50c" \
|
HELPER_IMAGE="qdrant/qdrant:v1.18.2@sha256:75eab8c4ba42096724fdcfde8b4de0b5713d529dde32f285a1f86fdcb2c9e50c" \
|
||||||
./scripts/vector-restore.sh --project-name "$project" --input "$restore_input" --confirm-project "$project" >/dev/null
|
./scripts/vector-restore.sh --project-name "$project" --input "$restore_input" --confirm-project "$project" >/dev/null &
|
||||||
|
restore_pid=$!
|
||||||
|
sleep 1
|
||||||
|
printf '%s' swapped >"$restore_source/payload/collections/demo/state.json"
|
||||||
|
tar -C "$restore_source" -cf "$restore_input" manifest.env payload
|
||||||
|
wait "$restore_pid"
|
||||||
test "$(cat "$volume_dir/collections/demo/state.json")" = restored
|
test "$(cat "$volume_dir/collections/demo/state.json")" = restored
|
||||||
|
tar -xOf "$restore_capture" payload/collections/demo/state.json | grep -qx 'restored'
|
||||||
grep -q "compose --project-name $project stop qdrant" "$restore_log"
|
grep -q "compose --project-name $project stop qdrant" "$restore_log"
|
||||||
grep -q "compose --project-name $project start qdrant" "$restore_log"
|
grep -q "compose --project-name $project start qdrant" "$restore_log"
|
||||||
grep -q "run --rm --mount type=volume,src=$volume_name,dst=/qdrant-data --mount type=bind,src=$tmp,dst=/restore-backup,readonly" "$restore_log"
|
grep -Eq "run --rm --mount type=volume,src=$volume_name,dst=/qdrant-data --mount type=bind,src=.*/qdrant-archive\.[^,]*,dst=/restore-backup,readonly" "$restore_log"
|
||||||
|
if grep -q "src=$tmp,dst=/restore-backup,readonly" "$restore_log"; then
|
||||||
|
echo "restore mounted the original archive directory instead of a private copy" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
if grep -q "volume inspect --format {{ .Mountpoint }}" "$restore_log"; then
|
if grep -q "volume inspect --format {{ .Mountpoint }}" "$restore_log"; then
|
||||||
echo "restore consulted Docker mountpoints during normal restore" >&2
|
echo "restore consulted Docker mountpoints during normal restore" >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -312,6 +375,10 @@ if grep -q "prune" "$restore_log"; then
|
|||||||
echo "restore attempted global docker cleanup" >&2
|
echo "restore attempted global docker cleanup" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
if find "$tmp" -maxdepth 1 -type d -name 'qdrant-archive.*' | grep -q .; then
|
||||||
|
echo "restore left its private archive-copy directory behind" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
printf '%s' rollback-source >"$volume_dir/collections/demo/state.json"
|
printf '%s' rollback-source >"$volume_dir/collections/demo/state.json"
|
||||||
rollback_log="$tmp/restore-rollback.log"
|
rollback_log="$tmp/restore-rollback.log"
|
||||||
|
|||||||
+67
-21
@@ -29,9 +29,29 @@ done
|
|||||||
}
|
}
|
||||||
[ -r "$input" ] || { echo "backup input is not readable" >&2; exit 2; }
|
[ -r "$input" ] || { echo "backup input is not readable" >&2; exit 2; }
|
||||||
|
|
||||||
input_dir=$(dirname "$input")
|
|
||||||
input_name=$(basename "$input")
|
|
||||||
expected_volume_name="${project_name}_${volume_role}"
|
expected_volume_name="${project_name}_${volume_role}"
|
||||||
|
private_archive_dir=
|
||||||
|
private_archive_path=
|
||||||
|
cleanup() {
|
||||||
|
status=$?
|
||||||
|
if [ -n "${private_archive_dir:-}" ] && [ -d "${private_archive_dir:-}" ]; then
|
||||||
|
rm -rf "$private_archive_dir"
|
||||||
|
fi
|
||||||
|
if [ "${restart_qdrant:-0}" -eq 1 ]; then
|
||||||
|
docker compose --project-name "$project_name" start qdrant >/dev/null
|
||||||
|
fi
|
||||||
|
exit "$status"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT HUP INT TERM
|
||||||
|
|
||||||
|
private_archive_dir=$(mktemp -d "${TMPDIR:-/tmp}/qdrant-archive.XXXXXX")
|
||||||
|
private_archive_path="$private_archive_dir/archive.tar"
|
||||||
|
umask 077
|
||||||
|
cp "$input" "$private_archive_path"
|
||||||
|
chmod 0600 "$private_archive_path"
|
||||||
|
|
||||||
|
input_dir=$private_archive_dir
|
||||||
|
input_name=$(basename "$private_archive_path")
|
||||||
|
|
||||||
resolve_volume() {
|
resolve_volume() {
|
||||||
names=$(docker volume ls \
|
names=$(docker volume ls \
|
||||||
@@ -60,7 +80,7 @@ validate_volume_metadata() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
validate_archive_paths() {
|
validate_archive_paths() {
|
||||||
paths=$(tar -tf "$input") || {
|
paths=$(tar -tf "$private_archive_path") || {
|
||||||
echo "archive listing failed" >&2
|
echo "archive listing failed" >&2
|
||||||
exit 2
|
exit 2
|
||||||
}
|
}
|
||||||
@@ -102,7 +122,7 @@ EOF
|
|||||||
}
|
}
|
||||||
|
|
||||||
validate_archive_types() {
|
validate_archive_types() {
|
||||||
tar -tvf "$input" | while IFS= read -r entry; do
|
tar -tvf "$private_archive_path" | while IFS= read -r entry; do
|
||||||
[ -n "$entry" ] || continue
|
[ -n "$entry" ] || continue
|
||||||
type=$(printf '%.1s' "$entry")
|
type=$(printf '%.1s' "$entry")
|
||||||
case "$type" in
|
case "$type" in
|
||||||
@@ -124,7 +144,7 @@ validate_archive_types() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
validate_archive_manifest() {
|
validate_archive_manifest() {
|
||||||
manifest=$(tar -xOf "$input" manifest.env 2>/dev/null) || {
|
manifest=$(tar -xOf "$private_archive_path" manifest.env 2>/dev/null) || {
|
||||||
echo "archive manifest could not be read" >&2
|
echo "archive manifest could not be read" >&2
|
||||||
exit 2
|
exit 2
|
||||||
}
|
}
|
||||||
@@ -134,25 +154,59 @@ validate_archive_manifest() {
|
|||||||
manifest_role=
|
manifest_role=
|
||||||
manifest_helper=
|
manifest_helper=
|
||||||
created_utc=
|
created_utc=
|
||||||
seen=
|
format_count=0
|
||||||
|
project_count=0
|
||||||
|
volume_count=0
|
||||||
|
role_count=0
|
||||||
|
helper_count=0
|
||||||
|
created_count=0
|
||||||
while IFS='=' read -r key value; do
|
while IFS='=' read -r key value; do
|
||||||
[ -n "$key" ] || continue
|
[ -n "$key" ] || continue
|
||||||
case "$key" in
|
case "$key" in
|
||||||
format) format=$value ;;
|
format)
|
||||||
project_name) manifest_project=$value ;;
|
format_count=$((format_count + 1))
|
||||||
volume_name) manifest_volume=$value ;;
|
[ "$format_count" -eq 1 ] || { echo "archive manifest contains duplicate keys" >&2; exit 2; }
|
||||||
volume_role) manifest_role=$value ;;
|
format=$value
|
||||||
helper_image) manifest_helper=$value ;;
|
;;
|
||||||
created_utc) created_utc=$value ;;
|
project_name)
|
||||||
|
project_count=$((project_count + 1))
|
||||||
|
[ "$project_count" -eq 1 ] || { echo "archive manifest contains duplicate keys" >&2; exit 2; }
|
||||||
|
manifest_project=$value
|
||||||
|
;;
|
||||||
|
volume_name)
|
||||||
|
volume_count=$((volume_count + 1))
|
||||||
|
[ "$volume_count" -eq 1 ] || { echo "archive manifest contains duplicate keys" >&2; exit 2; }
|
||||||
|
manifest_volume=$value
|
||||||
|
;;
|
||||||
|
volume_role)
|
||||||
|
role_count=$((role_count + 1))
|
||||||
|
[ "$role_count" -eq 1 ] || { echo "archive manifest contains duplicate keys" >&2; exit 2; }
|
||||||
|
manifest_role=$value
|
||||||
|
;;
|
||||||
|
helper_image)
|
||||||
|
helper_count=$((helper_count + 1))
|
||||||
|
[ "$helper_count" -eq 1 ] || { echo "archive manifest contains duplicate keys" >&2; exit 2; }
|
||||||
|
manifest_helper=$value
|
||||||
|
;;
|
||||||
|
created_utc)
|
||||||
|
created_count=$((created_count + 1))
|
||||||
|
[ "$created_count" -eq 1 ] || { echo "archive manifest contains duplicate keys" >&2; exit 2; }
|
||||||
|
created_utc=$value
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
echo "archive manifest contains unexpected fields" >&2
|
echo "archive manifest contains unexpected fields" >&2
|
||||||
exit 2
|
exit 2
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
seen="$seen $key"
|
|
||||||
done <<EOF
|
done <<EOF
|
||||||
$manifest
|
$manifest
|
||||||
EOF
|
EOF
|
||||||
|
[ "$format_count" -eq 1 ] || { echo "archive manifest is incomplete" >&2; exit 2; }
|
||||||
|
[ "$project_count" -eq 1 ] || { echo "archive manifest is incomplete" >&2; exit 2; }
|
||||||
|
[ "$volume_count" -eq 1 ] || { echo "archive manifest is incomplete" >&2; exit 2; }
|
||||||
|
[ "$role_count" -eq 1 ] || { echo "archive manifest is incomplete" >&2; exit 2; }
|
||||||
|
[ "$helper_count" -eq 1 ] || { echo "archive manifest is incomplete" >&2; exit 2; }
|
||||||
|
[ "$created_count" -eq 1 ] || { echo "archive manifest is incomplete" >&2; exit 2; }
|
||||||
[ "$format" = "$archive_format" ] || { echo "archive format is unsupported" >&2; exit 2; }
|
[ "$format" = "$archive_format" ] || { echo "archive format is unsupported" >&2; exit 2; }
|
||||||
[ "$manifest_project" = "$project_name" ] || { echo "archive project does not match restore target" >&2; exit 2; }
|
[ "$manifest_project" = "$project_name" ] || { echo "archive project does not match restore target" >&2; exit 2; }
|
||||||
[ "$manifest_volume" = "$expected_volume_name" ] || { echo "archive volume does not match restore target" >&2; exit 2; }
|
[ "$manifest_volume" = "$expected_volume_name" ] || { echo "archive volume does not match restore target" >&2; exit 2; }
|
||||||
@@ -170,14 +224,6 @@ validate_volume_metadata "$volume_name"
|
|||||||
|
|
||||||
running_container=$(docker compose --project-name "$project_name" ps --status running -q qdrant)
|
running_container=$(docker compose --project-name "$project_name" ps --status running -q qdrant)
|
||||||
restart_qdrant=0
|
restart_qdrant=0
|
||||||
cleanup() {
|
|
||||||
status=$?
|
|
||||||
if [ "$restart_qdrant" -eq 1 ]; then
|
|
||||||
docker compose --project-name "$project_name" start qdrant >/dev/null
|
|
||||||
fi
|
|
||||||
exit "$status"
|
|
||||||
}
|
|
||||||
trap cleanup EXIT HUP INT TERM
|
|
||||||
|
|
||||||
if [ -n "$running_container" ]; then
|
if [ -n "$running_container" ]; then
|
||||||
docker compose --project-name "$project_name" stop qdrant >/dev/null
|
docker compose --project-name "$project_name" stop qdrant >/dev/null
|
||||||
|
|||||||
Reference in New Issue
Block a user