Skip to content

Commit da5587f

Browse files
authored
fix(bench): retry the post-ingest database write across RDS restarts (#11018)
`post-ingest.py` connected to the benchmarks database only once. During the two RDS restarts on 2026-10-28, four ingest jobs failed. The script now repeats the write after 5, 10, 20, 40, 60, 60 and 60 seconds when the connection is lost or refused. Each try connects again with a new IAM token. A statement timeout or a validation error is not retried. A repeated write is safe. The commit row is an upsert, and the fact rows land in one transaction, so a lost connection leaves nothing half-written. The worst case is about 5.5 minutes, which fits the 10-minute `commit-metadata` job. New tests cover the retry behavior. Signed-off-by: Will Manning <will@willmanning.io>
1 parent 5d84290 commit da5587f

2 files changed

Lines changed: 102 additions & 5 deletions

File tree

‎scripts/post-ingest.py‎

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,15 @@
3030
import os
3131
import subprocess
3232
import sys
33+
import time
3334
import urllib.request
35+
from collections.abc import Callable
3436
from concurrent.futures import ThreadPoolExecutor
3537
from pathlib import Path
3638
from types import ModuleType
39+
from typing import TypeVar
40+
41+
T = TypeVar("T")
3742

3843
# MUST equal `bench/web/lib/schema-version.ts::SCHEMA_VERSION` in vortex-data/vortex.dev.
3944
# Bumping this is a coordinated change across the website contract, v3.rs, and
@@ -987,16 +992,57 @@ def refresh_site_cache(base_url: str, token: str, timeout: float) -> None:
987992
_warm_default_windows(base, timeout)
988993

989994

995+
# A restart of the RDS instance refuses connections for 30 seconds to a few minutes: on 2026-10-28
996+
# an engine upgrade took 33 seconds and an OS update about 3 minutes, and four ingest jobs failed.
997+
# These waits, about 4 minutes in all, span that window.
998+
_RETRY_DELAYS_S = (5, 10, 20, 40, 60, 60, 60)
999+
1000+
1001+
def _with_retries(
1002+
attempt: Callable[[], T],
1003+
*,
1004+
should_retry: Callable[[Exception], bool],
1005+
delays: tuple[float, ...] = _RETRY_DELAYS_S,
1006+
sleep: Callable[[float], None] = time.sleep,
1007+
) -> T:
1008+
"""Run `attempt`, and run it again after each delay while it raises an error that
1009+
`should_retry` accepts. The last error is raised once the delays run out."""
1010+
for delay in delays:
1011+
try:
1012+
return attempt()
1013+
except Exception as exc:
1014+
if not should_retry(exc):
1015+
raise
1016+
print(f"warning: database unavailable ({exc}); retrying in {delay:g}s", file=sys.stderr)
1017+
sleep(delay)
1018+
return attempt()
1019+
1020+
1021+
def _is_transient_db_error(exc: Exception) -> bool:
1022+
"""True for a lost or refused connection. A statement timeout is not retried: it is a slow
1023+
query, not an outage."""
1024+
import psycopg
1025+
1026+
return isinstance(exc, psycopg.OperationalError) and not isinstance(exc, psycopg.errors.QueryCanceled)
1027+
1028+
9901029
def _main_postgres(args: argparse.Namespace) -> int:
9911030
records = read_records(args.jsonl_path)
9921031
# `build_commit` runs `git show <commit_sha>`, so the SHA must be in the runner's local git
9931032
# history. A shallow checkout missing the SHA fails before opening a database connection.
9941033
commit = build_commit(args.commit_sha, args.branch, args.repo_url, args.git_dir)
995-
conn = connect_postgres(args.postgres, args.region)
996-
try:
997-
inserted, updated = ingest_postgres(conn, commit, records)
998-
finally:
999-
conn.close()
1034+
1035+
# Each attempt connects again, with a fresh IAM token, and writes the whole file. Retrying the
1036+
# write is safe: the commit row is an idempotent upsert, and the fact rows land in one
1037+
# transaction, so a connection lost mid-write leaves nothing half-written.
1038+
def write() -> tuple[int, int]:
1039+
conn = connect_postgres(args.postgres, args.region)
1040+
try:
1041+
return ingest_postgres(conn, commit, records)
1042+
finally:
1043+
conn.close()
1044+
1045+
inserted, updated = _with_retries(write, should_retry=_is_transient_db_error)
10001046
print(
10011047
json.dumps(
10021048
{"records": len(records), "inserted": inserted, "updated": updated},

‎scripts/tests/test_post_ingest.py‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,54 @@ def apply(connection, mid, record) -> bool:
9494
) == sorted(expected_records, key=lambda record: (record["kind"], record.get("query_idx", 0)))
9595
keys = [(kind, mid) for kind, mid, _ in applied]
9696
assert keys == sorted(keys)
97+
98+
99+
class _Transient(Exception):
100+
pass
101+
102+
103+
def test_retries_transient_errors_until_success(writer):
104+
calls: list[int] = []
105+
sleeps: list[float] = []
106+
107+
def attempt() -> str:
108+
calls.append(1)
109+
if len(calls) < 3:
110+
raise _Transient("connection refused")
111+
return "written"
112+
113+
result = writer._with_retries(
114+
attempt, should_retry=lambda e: isinstance(e, _Transient), delays=(5, 10, 20), sleep=sleeps.append
115+
)
116+
assert result == "written"
117+
assert len(calls) == 3
118+
assert sleeps == [5, 10]
119+
120+
121+
def test_raises_the_last_error_when_retries_run_out(writer):
122+
sleeps: list[float] = []
123+
124+
def attempt() -> None:
125+
raise _Transient("connection refused")
126+
127+
with pytest.raises(_Transient):
128+
writer._with_retries(
129+
attempt, should_retry=lambda e: isinstance(e, _Transient), delays=(5, 10), sleep=sleeps.append
130+
)
131+
assert sleeps == [5, 10]
132+
133+
134+
def test_does_not_retry_other_errors(writer):
135+
sleeps: list[float] = []
136+
137+
def attempt() -> None:
138+
raise ValueError("bad record")
139+
140+
with pytest.raises(ValueError):
141+
writer._with_retries(attempt, should_retry=lambda e: isinstance(e, _Transient), sleep=sleeps.append)
142+
assert sleeps == []
143+
144+
145+
def test_retry_schedule_spans_an_rds_restart(writer):
146+
# An OS update on the instance refused connections for about 3 minutes.
147+
assert sum(writer._RETRY_DELAYS_S) >= 180

0 commit comments

Comments
 (0)