Skip to content

Commit ba3943d

Browse files
authored
Merge pull request #7181 from roed-math/ci-test-statement-timeout
Stop CI jobs hanging for hours on a slow database query
2 parents f5785bd + f118274 commit ba3943d

3 files changed

Lines changed: 49 additions & 12 deletions

File tree

.github/workflows/python-package.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ jobs:
2828
test:
2929
runs-on: ubuntu-latest
3030
needs: matrix_prep
31+
# A healthy job finishes well inside half an hour; the slowest green ones
32+
# observed are around 25 minutes, and that includes building the conda
33+
# environment on a cache miss. GitHub's default is six hours, which is long
34+
# enough for a job stuck on the database to hold a runner all morning -- one
35+
# recently ran for five and a half. Fail such a job in an hour instead.
36+
timeout-minutes: 60
3137
strategy:
3238
fail-fast: false
3339
matrix: ${{fromJson(needs.matrix_prep.outputs.matrix)}}

lmfdb/characters/test_characters.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,29 @@ def test_modbrowse(self):
3535
assert '46.d' in W.get_data(as_text=True)
3636

3737
def test_search(self):
38-
W = self.tc.get('/Character/Dirichlet/?conductor=15&order=4')
39-
assert r'15.e' in W.get_data(as_text=True)
40-
W = self.tc.get('/Character/Dirichlet/?conductor=25-50&order=5-7')
41-
assert r'25.d' in W.get_data(as_text=True)
42-
W = self.tc.get('/Character/Dirichlet/?conductor=25-50&order=5-7&primitive=Yes')
43-
assert r'25.d' in W.get_data(as_text=True)
44-
W = self.tc.get('/Character/Dirichlet/?conductor=25-50&order=5-7&primitive=No')
45-
assert r'50.d' in W.get_data(as_text=True)
46-
W = self.tc.get('/Character/Dirichlet/?conductor=25-50&order=5-7&primitive=No&parity=Odd')
47-
assert r'56.n' in W.get_data(as_text=True)
48-
W = self.tc.get('/Character/Dirichlet/?conductor=25-50&order=5-7&primitive=No&parity=Even')
49-
assert r'50.d' in W.get_data(as_text=True)
38+
# Each filter is checked in both directions, so a parameter that stops
39+
# being parsed widens the search and trips a negative assertion.
40+
# Conductor 16, order 4 has a primitive even orbit (16.e), a primitive
41+
# odd one (16.f), and imprimitive counterparts at modulus 32. Labels
42+
# carry their tags, since a bare '16.e' also matches '416.e'.
43+
page = self.tc.get('/Character/Dirichlet/?conductor=15&order=4').get_data(as_text=True)
44+
assert '>15.e<' in page
45+
page = self.tc.get('/Character/Dirichlet/?conductor=16&order=4&is_primitive=no').get_data(as_text=True)
46+
assert '>32.e<' in page and '>32.f<' in page
47+
assert '>16.e<' not in page and '>16.f<' not in page
48+
page = self.tc.get('/Character/Dirichlet/?conductor=16&order=4&parity=even').get_data(as_text=True)
49+
assert '>16.e<' in page and '>32.e<' in page
50+
assert '>16.f<' not in page and '>32.f<' not in page
51+
page = self.tc.get('/Character/Dirichlet/?conductor=16&order=4&parity=odd').get_data(as_text=True)
52+
assert '>16.f<' in page and '>32.f<' in page
53+
assert '>16.e<' not in page and '>32.e<' not in page
54+
page = self.tc.get('/Character/Dirichlet/?conductor=16&order=4&is_primitive=no&parity=odd').get_data(as_text=True)
55+
assert '>32.f<' in page
56+
assert '>16.e<' not in page and '>16.f<' not in page and '>32.e<' not in page
57+
# A conductor range matches no index prefix, so this one reads every
58+
# orbit of conductor 25-50 before it can sort; allow the timeout page.
59+
self.check_args_with_timeout(
60+
'/Character/Dirichlet/?conductor=25-50&order=5-7', '>25.d<')
5061

5162
def test_condsearch(self):
5263
W = self.tc.get('/Character/Dirichlet/?conductor=111')

tox.ini

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,26 @@ passenv =
1313
SAGE_LOCAL
1414
HOME
1515

16+
# Bound how long a single database query may run. The suite talks to a shared
17+
# server (devmirror by default) that has no statement_timeout of its own, and
18+
# psycodict only hands one to connections made as the "webserver" role, so a
19+
# pathological query otherwise runs unbounded: a Dirichlet character search that
20+
# takes seconds on an idle server has held a CI job for over five hours.
21+
#
22+
# libpq reads PGOPTIONS when it opens a connection, including the ones psycodict
23+
# opens when it resets a broken one, so this needs no cooperation from the test
24+
# harness and cannot be lost on a reconnect. A query stopped this way raises
25+
# the same QueryCanceledError the search pages already handle, so a slow search
26+
# renders the usual timeout page instead of hanging.
27+
#
28+
# This is a hang catcher, not a performance budget. 120s is far above anything
29+
# the suite issues against a healthy server and far below the hangs it exists to
30+
# stop; the job-level timeout-minutes in .github/workflows/python-package.yml
31+
# backstops the case where many queries are slow at once. Export PGOPTIONS to
32+
# override, e.g. PGOPTIONS='-c statement_timeout=0' for the old behavior.
33+
setenv =
34+
PGOPTIONS = {env:PGOPTIONS:-c statement_timeout=120s}
35+
1636
commands =
1737
parallel --group 'echo "Running test on {}"; sage -python -m pytest -vv --durations=0 {}' ::: {posargs}
1838

0 commit comments

Comments
 (0)