fix: close internal semantic review gaps

This commit is contained in:
2026-08-08 23:27:58 +02:00
parent 43d8063922
commit 2c4534d968
18 changed files with 806 additions and 77 deletions
+48
View File
@@ -0,0 +1,48 @@
#!/bin/sh
# Shared, daemon-scoped lock for operations that stop or replace one Compose Qdrant volume.
# The stopped container name is the atomic primitive; ownership labels prevent a cleanup trap
# from deleting a lock that it did not create.
vector_operation_lock_init() {
operation_lock_name="${expected_volume_name}-operation-lock"
operation_lock_owner="${project_name}-$$-$(date -u +%Y%m%dT%H%M%S)"
operation_lock_acquired=0
}
acquire_vector_operation_lock() {
if docker create \
--name "$operation_lock_name" \
--label "com.thothii.qdrant-operation-owner=$operation_lock_owner" \
--label "com.thothii.qdrant-operation-project=$project_name" \
--label "com.thothii.qdrant-operation-volume=$expected_volume_name" \
"$helper_image" /bin/true >/dev/null 2>&1; then
operation_lock_acquired=1
return 0
fi
if docker inspect "$operation_lock_name" >/dev/null 2>&1; then
echo "Qdrant operation already in progress for $expected_volume_name" >&2
else
echo "Unable to acquire Qdrant operation lock for $expected_volume_name" >&2
fi
return 2
}
release_vector_operation_lock() {
[ "${operation_lock_acquired:-0}" -eq 1 ] || return 0
metadata=$(docker inspect --format \
'{{ index .Config.Labels "com.thothii.qdrant-operation-owner" }} {{ index .Config.Labels "com.thothii.qdrant-operation-volume" }}' \
"$operation_lock_name" 2>/dev/null || true)
set -- $metadata
if [ "${1-}" != "$operation_lock_owner" ] || [ "${2-}" != "$expected_volume_name" ]; then
echo "Qdrant operation lock ownership changed; refusing to remove it" >&2
operation_lock_acquired=0
return 1
fi
if ! docker rm -f "$operation_lock_name" >/dev/null; then
echo "Unable to release Qdrant operation lock for $expected_volume_name" >&2
operation_lock_acquired=0
return 1
fi
operation_lock_acquired=0
}
+133 -1
View File
@@ -3,7 +3,17 @@ set -eu
cd "$(dirname "$0")/.."
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT HUP INT TERM
holder_pid=
holder_release="$tmp/holder-release"
cleanup() {
touch "$holder_release"
if [ -n "${holder_pid:-}" ]; then
kill "$holder_pid" 2>/dev/null || true
wait "$holder_pid" 2>/dev/null || true
fi
rm -rf "$tmp"
}
trap cleanup EXIT HUP INT TERM
fakebin="$tmp/bin"
mkdir "$fakebin"
@@ -28,6 +38,15 @@ run_backup() {
volume_name=$1
backup_dir=$2
output_name=$3
if [ -n "${RUN_BLOCK_READY:-}" ]; then
: >"$RUN_BLOCK_READY"
attempts=0
while [ ! -e "${RUN_BLOCK_RELEASE:?}" ]; do
attempts=$((attempts + 1))
[ "$attempts" -lt 400 ] || exit 24
sleep 0.05
done
fi
staging=$(mktemp -d "${TMPDIR:-/tmp}/fake-qdrant-backup.XXXXXX")
mkdir -p "$staging/payload"
cat >"$staging/manifest.env" <<EOF
@@ -36,12 +55,59 @@ project_name=${PROJECT_NAME:?}
volume_name=$volume_name
volume_role=qdrant-data
helper_image=${HELPER_IMAGE:?}
created_utc=2026-08-08T17:35:36Z
EOF
cp -R "$(volume_dir_for "$volume_name")"/. "$staging/payload"/
tar -C "$staging" -cf "$backup_dir/$output_name" manifest.env payload
rm -rf "$staging"
}
lock_state_root=${LOCK_ROOT:-$(dirname "${VOLUME_ROOT:?}")/locks}
mkdir -p "$lock_state_root"
if [ "$1" = create ]; then
shift
lock_name=
lock_owner=
lock_volume=
while [ "$#" -gt 0 ]; do
case "$1" in
--name) lock_name=$2; shift 2 ;;
--label)
case "$2" in
com.thothii.qdrant-operation-owner=*) lock_owner=${2#*=} ;;
com.thothii.qdrant-operation-volume=*) lock_volume=${2#*=} ;;
esac
shift 2
;;
*) shift ;;
esac
done
[ -n "$lock_name" ] && [ -n "$lock_owner" ] && [ -n "$lock_volume" ] || exit 25
state="$lock_state_root/$lock_name"
mkdir "$state" 2>/dev/null || exit 1
printf '%s\n' "$lock_owner" >"$state/owner"
printf '%s\n' "$lock_volume" >"$state/volume"
printf '%s\n' "$lock_name"
exit 0
fi
if [ "$1" = inspect ]; then
for lock_name in "$@"; do :; done
state="$lock_state_root/$lock_name"
[ -d "$state" ] || exit 1
printf '%s %s\n' "$(cat "$state/owner")" "$(cat "$state/volume")"
exit 0
fi
if [ "$1" = rm ]; then
for lock_name in "$@"; do :; done
state="$lock_state_root/$lock_name"
[ -d "$state" ] || exit 1
rm -rf "$state"
exit 0
fi
run_restore() {
volume_name=$1
backup_dir=$2
@@ -219,6 +285,7 @@ PY
backup_output="$tmp/qdrant-backup.tar"
docker_log="$tmp/docker-backup.log"
lock_name="${project}_qdrant-data-operation-lock"
PATH="$fakebin:$PATH" DOCKER_LOG="$docker_log" PROJECT_NAME="$project" VOLUME_ROOT="$volume_root" \
HELPER_IMAGE="qdrant/qdrant:v1.18.2@sha256:75eab8c4ba42096724fdcfde8b4de0b5713d529dde32f285a1f86fdcb2c9e50c" \
./scripts/vector-backup.sh --project-name "$project" --output "$backup_output" >/dev/null
@@ -231,6 +298,15 @@ tar -xOf "$backup_output" manifest.env | grep -qx "volume_name=$volume_name"
grep -q "run --rm --mount type=volume,src=$volume_name,dst=/qdrant-data,readonly --mount type=bind,src=$tmp,dst=/backup" "$docker_log"
grep -q "compose --project-name $project stop qdrant" "$docker_log"
grep -q "compose --project-name $project start qdrant" "$docker_log"
grep -q "create --name $lock_name" "$docker_log"
grep -q "rm -f $lock_name" "$docker_log"
test ! -e "$tmp/locks/$lock_name"
lock_line=$(grep -n "create --name $lock_name" "$docker_log" | sed -n '1s/:.*//p')
volume_line=$(grep -n '^volume ls ' "$docker_log" | sed -n '1s/:.*//p')
[ "$lock_line" -lt "$volume_line" ] || {
echo "backup resolved volume state before acquiring the operation lock" >&2
exit 1
}
if grep -q "volume inspect --format {{ .Mountpoint }}" "$docker_log"; then
echo "backup consulted Docker mountpoints" >&2
exit 1
@@ -240,6 +316,61 @@ if grep -q "prune" "$docker_log"; then
exit 1
fi
mkdir -p "$tmp/locks/$lock_name"
printf '%s\n' foreign-owner >"$tmp/locks/$lock_name/owner"
printf '%s\n' "$volume_name" >"$tmp/locks/$lock_name/volume"
foreign_lock_log="$tmp/foreign-lock.log"
if PATH="$fakebin:$PATH" DOCKER_LOG="$foreign_lock_log" PROJECT_NAME="$project" VOLUME_ROOT="$volume_root" \
HELPER_IMAGE="qdrant/qdrant:v1.18.2@sha256:75eab8c4ba42096724fdcfde8b4de0b5713d529dde32f285a1f86fdcb2c9e50c" \
./scripts/vector-backup.sh --project-name "$project" --output "$tmp/foreign-lock.tar" \
>"$tmp/foreign-lock.out" 2>"$tmp/foreign-lock.err"; then
echo "backup ignored a foreign operation lock" >&2
exit 1
fi
grep -q 'operation already in progress' "$tmp/foreign-lock.err"
test "$(cat "$tmp/locks/$lock_name/owner")" = foreign-owner
rm -rf "$tmp/locks/$lock_name"
holder_ready="$tmp/holder-ready"
holder_output="$tmp/holder.tar"
holder_log="$tmp/holder.log"
PATH="$fakebin:$PATH" DOCKER_LOG="$holder_log" PROJECT_NAME="$project" VOLUME_ROOT="$volume_root" \
RUN_BLOCK_READY="$holder_ready" RUN_BLOCK_RELEASE="$holder_release" \
HELPER_IMAGE="qdrant/qdrant:v1.18.2@sha256:75eab8c4ba42096724fdcfde8b4de0b5713d529dde32f285a1f86fdcb2c9e50c" \
./scripts/vector-backup.sh --project-name "$project" --output "$holder_output" >/dev/null 2>"$tmp/holder.err" &
holder_pid=$!
attempts=0
while [ ! -e "$holder_ready" ]; do
attempts=$((attempts + 1))
[ "$attempts" -lt 400 ] || { echo "lock holder did not reach copy phase" >&2; exit 1; }
sleep 0.05
done
for contender in backup restore; do
contender_log="$tmp/contender-$contender.log"
if [ "$contender" = backup ]; then
set -- ./scripts/vector-backup.sh --project-name "$project" --output "$tmp/contender.tar"
else
set -- ./scripts/vector-restore.sh --project-name "$project" --input "$backup_output" --confirm-project "$project"
fi
if PATH="$fakebin:$PATH" DOCKER_LOG="$contender_log" PROJECT_NAME="$project" VOLUME_ROOT="$volume_root" \
HELPER_IMAGE="qdrant/qdrant:v1.18.2@sha256:75eab8c4ba42096724fdcfde8b4de0b5713d529dde32f285a1f86fdcb2c9e50c" \
"$@" >"$tmp/contender-$contender.out" 2>"$tmp/contender-$contender.err"; then
echo "$contender interleaved with an active qdrant operation" >&2
exit 1
fi
grep -q 'operation already in progress' "$tmp/contender-$contender.err"
if grep -Eq '^volume (ls|inspect)|^compose .* (stop|start) qdrant|^run ' "$contender_log"; then
echo "$contender touched qdrant state after lock contention" >&2
exit 1
fi
done
touch "$holder_release"
wait "$holder_pid"
holder_pid=
test -s "$holder_output"
test ! -e "$tmp/locks/$lock_name"
existing="$tmp/existing.tar"
printf '%s' sentinel >"$existing"
if PATH="$fakebin:$PATH" DOCKER_LOG="$tmp/existing.log" PROJECT_NAME="$project" VOLUME_ROOT="$volume_root" \
@@ -392,5 +523,6 @@ fi
test "$(cat "$volume_dir/collections/demo/state.json")" = rollback-source
grep -q "compose --project-name $project stop qdrant" "$rollback_log"
grep -q "compose --project-name $project start qdrant" "$rollback_log"
test ! -e "$tmp/locks/$lock_name"
echo "qdrant backup/restore archive validation, scoped helper execution, and rollback safety passed."
+16 -5
View File
@@ -28,6 +28,9 @@ output_name=$(basename "$output")
[ -d "$output_dir" ] || { echo "backup destination directory does not exist" >&2; exit 2; }
expected_volume_name="${project_name}_${volume_role}"
script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd)
. "$script_dir/lib/vector-operation-lock.sh"
vector_operation_lock_init
resolve_volume() {
names=$(docker volume ls \
@@ -55,24 +58,32 @@ validate_volume_metadata() {
[ "${3-}" = "$volume_role" ] || { echo "volume metadata role label mismatch" >&2; exit 2; }
}
volume_name=$(resolve_volume)
validate_volume_metadata "$volume_name"
running_container=$(docker compose --project-name "$project_name" ps --status running -q qdrant)
restart_qdrant=0
temporary_output=
cleanup() {
status=$?
trap - EXIT HUP INT TERM
if [ -n "${temporary_output:-}" ] && [ -e "${temporary_output:-}" ]; then
rm -f "$temporary_output"
fi
if [ "$restart_qdrant" -eq 1 ]; then
docker compose --project-name "$project_name" start qdrant >/dev/null
if ! docker compose --project-name "$project_name" start qdrant >/dev/null; then
echo "Unable to restart Qdrant after backup" >&2
[ "$status" -ne 0 ] || status=1
fi
fi
if ! release_vector_operation_lock; then
[ "$status" -ne 0 ] || status=1
fi
exit "$status"
}
trap cleanup EXIT HUP INT TERM
acquire_vector_operation_lock
volume_name=$(resolve_volume)
validate_volume_metadata "$volume_name"
running_container=$(docker compose --project-name "$project_name" ps --status running -q qdrant)
if [ -n "$running_container" ]; then
docker compose --project-name "$project_name" stop qdrant >/dev/null
restart_qdrant=1
+13 -2
View File
@@ -30,15 +30,26 @@ done
[ -r "$input" ] || { echo "backup input is not readable" >&2; exit 2; }
expected_volume_name="${project_name}_${volume_role}"
script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd)
. "$script_dir/lib/vector-operation-lock.sh"
vector_operation_lock_init
private_archive_dir=
private_archive_path=
restart_qdrant=0
cleanup() {
status=$?
trap - EXIT HUP INT TERM
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
if ! docker compose --project-name "$project_name" start qdrant >/dev/null; then
echo "Unable to restart Qdrant after restore" >&2
[ "$status" -ne 0 ] || status=1
fi
fi
if ! release_vector_operation_lock; then
[ "$status" -ne 0 ] || status=1
fi
exit "$status"
}
@@ -219,11 +230,11 @@ validate_archive_paths
validate_archive_types
validate_archive_manifest
acquire_vector_operation_lock
volume_name=$(resolve_volume)
validate_volume_metadata "$volume_name"
running_container=$(docker compose --project-name "$project_name" ps --status running -q qdrant)
restart_qdrant=0
if [ -n "$running_container" ]; then
docker compose --project-name "$project_name" stop qdrant >/dev/null