Skip to content

Commit 6669125

Browse files
committed
fix: interrupt PostgreSQL cache subprocesses
1 parent c736fc5 commit 6669125

2 files changed

Lines changed: 110 additions & 28 deletions

File tree

scripts/pg_compat/fetch_libpg_query.sh

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ LOCK_RECLAIM_DIR=
1313
LOCK_RECLAIM_FILE=
1414
LOCK_HELD=0
1515
FILE_CONTENT=
16+
ACTIVE_CHILD_PID=
1617

1718
usage() {
1819
echo "Usage: $0 [--with-postgres-source]" >&2
@@ -166,10 +167,51 @@ acquire_lock() {
166167
exit 1
167168
}
168169

170+
handle_signal() {
171+
local signal_name="$1"
172+
local exit_status="$2"
173+
local child_pid="$ACTIVE_CHILD_PID"
174+
175+
trap - HUP INT TERM
176+
ACTIVE_CHILD_PID=
177+
if [[ -n "$child_pid" ]]; then
178+
if ! kill "-${signal_name}" -- "-${child_pid}" 2>/dev/null; then
179+
kill "-${signal_name}" "$child_pid" 2>/dev/null || true
180+
fi
181+
wait "$child_pid" 2>/dev/null || true
182+
fi
183+
exit "$exit_status"
184+
}
185+
186+
run_interruptible() {
187+
local child_pid
188+
local child_status
189+
190+
python3 -c '
191+
import os
192+
import signal
193+
import sys
194+
195+
os.setsid()
196+
for signal_number in (signal.SIGHUP, signal.SIGINT, signal.SIGTERM):
197+
signal.signal(signal_number, signal.SIG_DFL)
198+
os.execvp(sys.argv[1], sys.argv[1:])
199+
' "$@" &
200+
child_pid=$!
201+
ACTIVE_CHILD_PID="$child_pid"
202+
if wait "$child_pid"; then
203+
child_status=0
204+
else
205+
child_status=$?
206+
fi
207+
ACTIVE_CHILD_PID=
208+
return "$child_status"
209+
}
210+
169211
trap release_lock EXIT
170-
trap 'exit 129' HUP
171-
trap 'exit 130' INT
172-
trap 'exit 143' TERM
212+
trap 'handle_signal HUP 129' HUP
213+
trap 'handle_signal INT 130' INT
214+
trap 'handle_signal TERM 143' TERM
173215

174216
LIBPG_QUERY_URL="$(
175217
python3 - "$PINS" <<'PY'
@@ -248,13 +290,13 @@ fetch_libpg_query() {
248290
echo "Cache path exists but is not a Git checkout: ${checkout}" >&2
249291
exit 1
250292
fi
251-
git clone --no-checkout "$LIBPG_QUERY_URL" "$checkout"
293+
run_interruptible git clone --no-checkout "$LIBPG_QUERY_URL" "$checkout"
252294
fi
253295

254-
git -C "$checkout" remote set-url origin "$LIBPG_QUERY_URL"
296+
run_interruptible git -C "$checkout" remote set-url origin "$LIBPG_QUERY_URL"
255297
echo "Fetching libpg_query ${role}: ${branch} at ${commit}"
256-
git -C "$checkout" fetch --force --no-tags origin "$commit"
257-
git -C "$checkout" checkout --detach --force "$commit"
298+
run_interruptible git -C "$checkout" fetch --force --no-tags origin "$commit"
299+
run_interruptible git -C "$checkout" checkout --detach --force "$commit"
258300

259301
actual_head="$(git -C "$checkout" rev-parse HEAD)"
260302
if [[ "$actual_head" != "$commit" ]]; then
@@ -301,7 +343,7 @@ fetch_postgres_source() {
301343

302344
if [[ ! -f "$archive" ]]; then
303345
download_tmp="$(mktemp "${postgres_root}/.postgresql-${pg_version}.download.XXXXXX")"
304-
if ! curl --fail --location --retry 3 --output "$download_tmp" "$archive_url"; then
346+
if ! run_interruptible curl --fail --location --retry 3 --output "$download_tmp" "$archive_url"; then
305347
rm -f "$download_tmp"
306348
return 1
307349
fi
@@ -316,7 +358,7 @@ fetch_postgres_source() {
316358

317359
if [[ ! -d "$source_dir" ]]; then
318360
extract_tmp="$(mktemp -d "${postgres_root}/.postgresql-${pg_version}.extract.XXXXXX")"
319-
if ! tar -xjf "$archive" -C "$extract_tmp"; then
361+
if ! run_interruptible tar -xjf "$archive" -C "$extract_tmp"; then
320362
rm -rf "$extract_tmp"
321363
return 1
322364
fi

tests/pg_compat/test_common.py

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,15 @@ def create_harness(self, directory, pins):
236236
printf 'PG_VERSION = 18.4\\nPG_VERSION_NUM = 180004\\n' > "$checkout/Makefile"
237237
fi
238238
if [[ "${STUB_BLOCK_STARTED:-}" != "" && "$(basename "$checkout")" == "previous" ]]; then
239+
if [[ "${STUB_BLOCK_PID:-}" != "" ]]; then
240+
printf '%s\n' "$$" > "$STUB_BLOCK_PID"
241+
fi
239242
touch "$STUB_BLOCK_STARTED"
243+
if [[ "${STUB_BLOCK_FOREVER:-}" != "" ]]; then
244+
while true; do
245+
sleep 60
246+
done
247+
fi
240248
while [[ ! -e "$STUB_BLOCK_RELEASE" ]]; do
241249
sleep 0.02
242250
done
@@ -335,6 +343,19 @@ def create_lock(self, cache, owner=None):
335343
(lock_dir / "owner").write_text(owner, encoding="utf-8")
336344
return lock_dir
337345

346+
def kill_process_safely(self, pid):
347+
try:
348+
process_group = os.getpgid(pid)
349+
except ProcessLookupError:
350+
return
351+
try:
352+
if process_group == pid:
353+
os.killpg(process_group, signal.SIGKILL)
354+
else:
355+
os.kill(pid, signal.SIGKILL)
356+
except ProcessLookupError:
357+
pass
358+
338359
def test_bad_download_is_not_published_and_poisoned_cache_recovers(self):
339360
good_content = b"good archive"
340361
expected_hash = hashlib.sha256(good_content).hexdigest()
@@ -484,9 +505,10 @@ def test_terminated_fetch_releases_lock_without_continuing(self):
484505
directory, valid_pins()
485506
)
486507
started = root / "started"
487-
release = root / "release"
508+
child_pid_path = root / "child-pid"
488509
environment["STUB_BLOCK_STARTED"] = str(started)
489-
environment["STUB_BLOCK_RELEASE"] = str(release)
510+
environment["STUB_BLOCK_PID"] = str(child_pid_path)
511+
environment["STUB_BLOCK_FOREVER"] = "1"
490512

491513
process = subprocess.Popen(
492514
[str(FETCH_SCRIPT)],
@@ -495,24 +517,42 @@ def test_terminated_fetch_releases_lock_without_continuing(self):
495517
text=True,
496518
stdout=subprocess.PIPE,
497519
stderr=subprocess.PIPE,
520+
start_new_session=True,
498521
)
499-
deadline = time.monotonic() + 5
500-
while not started.exists() and time.monotonic() < deadline:
501-
time.sleep(0.02)
502-
self.assertTrue(started.exists(), "fetch did not reach locked operation")
503-
504-
os.kill(process.pid, signal.SIGTERM)
505-
release.touch()
506-
process.communicate(timeout=5)
507-
508-
self.assertNotEqual(process.returncode, 0)
509-
self.assertFalse((cache / ".pg_compat.lock").exists())
510-
511-
retry_environment = environment.copy()
512-
retry_environment.pop("STUB_BLOCK_STARTED")
513-
retry_environment.pop("STUB_BLOCK_RELEASE")
514-
retry = self.run_fetch(retry_environment)
515-
self.assertEqual(retry.returncode, 0, retry.stderr)
522+
child_pid = None
523+
try:
524+
deadline = time.monotonic() + 5
525+
while (
526+
(not started.exists() or not child_pid_path.exists())
527+
and time.monotonic() < deadline
528+
):
529+
time.sleep(0.02)
530+
self.assertTrue(
531+
started.exists(), "fetch did not reach locked operation"
532+
)
533+
self.assertTrue(
534+
child_pid_path.exists(), "blocked child did not record its PID"
535+
)
536+
child_pid = int(
537+
child_pid_path.read_text(encoding="utf-8").strip()
538+
)
539+
540+
os.kill(process.pid, signal.SIGTERM)
541+
try:
542+
process.communicate(timeout=2)
543+
except subprocess.TimeoutExpired:
544+
self.fail("terminated fetch did not exit promptly")
545+
546+
self.assertEqual(process.returncode, 143)
547+
with self.assertRaises(ProcessLookupError):
548+
os.kill(child_pid, 0)
549+
self.assertFalse((cache / ".pg_compat.lock").exists())
550+
finally:
551+
if process.poll() is None:
552+
self.kill_process_safely(process.pid)
553+
process.communicate(timeout=5)
554+
if child_pid is not None:
555+
self.kill_process_safely(child_pid)
516556

517557

518558
if __name__ == "__main__":

0 commit comments

Comments
 (0)