fix(test): stabilize bgprocess polling in backup/restore/IE/maintenance tests (#9958)

The shared polling helpers in:

  - web/pgadmin/tools/backup/tests/test_backup_utils.py
  - web/pgadmin/tools/import_export/tests/test_import_export_utils.py
  - web/pgadmin/tools/maintenance/tests/test_create_maintenance_job.py
  - web/pgadmin/tools/restore/tests/test_create_restore_job.py

all share the same race that surfaced on macos-latest / pg16 in
PR #9955's CI run:

  - Wait budget was 2.5s (5 iterations x 0.5s; maintenance used 5s).
  - The break condition was `execution_time' in the_process`, but
    `execution_time` is the elapsed time of a *running* bgprocess --
    it is set before the wrapped pg_dump / pg_restore / psql / COPY
    actually finishes. The completion signal is `exit_code` becoming
    non-None.
  - So the helper could return control while the wrapped command was
    still running, and the next assertion -- e.g.
    `assert_equal(the_process['exit_code'] in [0, 1], True)` -- would
    fire on `None in [0, 1]`, i.e. `False != True`.

Some scenarios masked the bug by listing `None` in their
`expected_exit_code` set (a tell that someone noticed the polling was
unreliable and worked around it by widening accepted exit codes).
Scenarios that didn't include `None` were the ones that flaked.

Fix all four helpers identically:

  - Poll for up to 60 iterations x 0.5s = 30s, generous enough for
    the slowest CI runner.
  - Break only when `the_process.get('exit_code') is not None`, the
    actual completion signal.
  - Narrow `except Exception` to `except StopIteration`, which is the
    only thing `next(...)` here can raise.

No call-site changes needed; the helper contract (returns once the
job is done; raises if the bgprocess never finished) is unchanged in
spirit and strictly more reliable in practice.

Verified:

  - pycodestyle on the four files: 0 violations.

This fixes the failure observed in the macos-latest / pg16 leg of
PR #9955's CI run (run 26154521710, job 76930277702), which was
unrelated to that PR's lockfile-only changes.
This commit is contained in:
Ashesh Vashi
2026-05-20 18:58:32 +05:30
committed by GitHub
parent 25f0d853c8
commit ae6dc4ba18
4 changed files with 28 additions and 28 deletions
@@ -25,11 +25,12 @@ def create_backup_job(tester, url, params, assert_equal):
def run_backup_job(tester, job_id, expected_params, assert_in, assert_not_in,
assert_equal):
cnt = 0
the_process = None
while True:
if cnt >= 5:
break
# Wait up to 30s for the background pg_dump to actually finish.
# The completion signal is `exit_code` becoming non-None — NOT the
# mere presence of `execution_time`, which is set while the process
# is still running.
for _ in range(60):
# Check the process list
response1 = tester.get('/misc/bgprocess/?_={0}'.format(
secrets.choice(range(1, 9999999))))
@@ -39,13 +40,12 @@ def run_backup_job(tester, job_id, expected_params, assert_in, assert_not_in,
try:
the_process = next(
p for p in process_list if p['id'] == job_id)
except Exception:
except StopIteration:
the_process = None
if the_process and 'execution_time' in the_process:
if the_process and the_process.get('exit_code') is not None:
break
time.sleep(0.5)
cnt += 1
assert_equal('execution_time' in the_process, True)
assert_equal('stime' in the_process, True)
@@ -38,11 +38,12 @@ def create_import_export_job(tester, url, params, assert_equal):
def run_import_export_job(tester, job_id, expected_params, assert_in,
assert_not_in, assert_equal):
cnt = 0
the_process = None
while True:
if cnt >= 5:
break
# Wait up to 30s for the background psql/COPY job to actually finish.
# The completion signal is `exit_code` becoming non-None — NOT the
# mere presence of `execution_time`, which is set while the process
# is still running.
for _ in range(60):
# Check the process list
response1 = tester.get('/misc/bgprocess/?_={0}'.format(
secrets.choice(range(1, 9999999))))
@@ -52,13 +53,12 @@ def run_import_export_job(tester, job_id, expected_params, assert_in,
try:
the_process = next(
p for p in process_list if p['id'] == job_id)
except Exception:
except StopIteration:
the_process = None
if the_process and 'execution_time' in the_process:
if the_process and the_process.get('exit_code') is not None:
break
time.sleep(0.5)
cnt += 1
assert_equal('execution_time' in the_process, True)
assert_equal('stime' in the_process, True)
@@ -73,11 +73,12 @@ class MaintenanceJobTest(BaseTestGenerator):
response_data = json.loads(response.data.decode('utf-8'))
job_id = response_data['data']['job_id']
cnt = 0
the_process = None
while True:
if cnt >= 10:
break
# Wait up to 30s for the background maintenance command to
# actually finish. The completion signal is `exit_code` becoming
# non-None — NOT the mere presence of `execution_time`, which is
# set while the process is still running.
for _ in range(60):
# Check the process list
response1 = self.tester.get('/misc/bgprocess/?_={0}'.format(
secrets.choice(range(1, 9999999))))
@@ -87,13 +88,12 @@ class MaintenanceJobTest(BaseTestGenerator):
try:
the_process = next(
p for p in process_list if p['id'] == job_id)
except Exception:
except StopIteration:
the_process = None
if the_process and 'execution_time' in the_process:
if the_process and the_process.get('exit_code') is not None:
break
time.sleep(0.5)
cnt += 1
self.assertTrue('execution_time' in the_process)
self.assertTrue('stime' in the_process)
@@ -165,11 +165,12 @@ class RestoreJobTest(BaseTestGenerator):
response_data = json.loads(response.data.decode('utf-8'))
job_id = response_data['data']['job_id']
cnt = 0
the_process = None
while True:
if cnt >= 5:
break
# Wait up to 30s for the background pg_restore to actually
# finish. The completion signal is `exit_code` becoming non-None
# — NOT the mere presence of `execution_time`, which is set
# while the process is still running.
for _ in range(60):
# Check the process list
response1 = self.tester.get('/misc/bgprocess/?_={0}'.format(
secrets.choice(range(1, 9999999))))
@@ -179,13 +180,12 @@ class RestoreJobTest(BaseTestGenerator):
try:
the_process = next(
p for p in process_list if p['id'] == job_id)
except Exception:
except StopIteration:
the_process = None
if the_process and 'execution_time' in the_process:
if the_process and the_process.get('exit_code') is not None:
break
time.sleep(0.5)
cnt += 1
self.assertTrue('execution_time' in the_process)
self.assertTrue('stime' in the_process)