Skip to content

Commit 9b498de

Browse files
authored
acc: deflake dashboard read-after-deploy retry (#5895)
The `bundle/resources/dashboards` acceptance test flaked on cloud: after deploy, reads of a just-renamed/updated dashboard hit eventual consistency, and `retry.py`'s default ~2s window (5 × 500ms) wasn't enough. - **`retry.py`**: On exhaustion, the old loop passed the last attempt's stdout through as if valid even when a `--until`/`--until-not` condition never held — masking the flake as a downstream output diff. Now, an unmet content condition writes stale output to **stderr** and exits non-zero (`retry: condition not met after N attempts`); success and the no-condition case pass output through on stdout unchanged. - **`test.toml`**: Widen the window to `RETRY_MAX_ATTEMPTS=20` / `RETRY_INTERVAL_MS=1000` (~20s). `retry.py`'s loop isn't scaled by `TimeoutCIMultiplier`, and real cloud eventual consistency regularly exceeds the ~2s default. This pull request and its description were written by Isaac.
1 parent a152b60 commit 9b498de

2 files changed

Lines changed: 39 additions & 6 deletions

File tree

acceptance/bin/retry.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@
88
An attempt is considered successful when the command exits with code 0 and:
99
--until SUBSTR SUBSTR appears in stdout
1010
--until-not SUBSTR SUBSTR does not appear in stdout
11+
12+
The condition is checked on every attempt, including the last. If a
13+
--until/--until-not condition was given but never satisfied within
14+
RETRY_MAX_ATTEMPTS, retry.py writes the last (stale) output to stderr and exits
15+
non-zero, rather than passing the stale output through on stdout. When no
16+
condition is given, the final attempt's output and exit code pass through
17+
unchanged.
1118
"""
1219

1320
import argparse
@@ -32,18 +39,37 @@ def main():
3239
interval = float(os.environ.get("RETRY_INTERVAL_MS", "500")) / 1000.0
3340
max_attempts = int(os.environ.get("RETRY_MAX_ATTEMPTS", "5"))
3441

35-
result = subprocess.run(argv, capture_output=True)
36-
for _ in range(1, max_attempts):
37-
success = (
42+
def succeeded(result):
43+
return (
3844
result.returncode == 0
3945
and (until is None or until.encode() in result.stdout)
4046
and (until_not is None or until_not.encode() not in result.stdout)
4147
)
42-
if success:
43-
break
44-
time.sleep(interval)
48+
49+
ok = False
50+
result = None
51+
for attempt in range(max_attempts):
52+
if attempt > 0:
53+
time.sleep(interval)
4554
result = subprocess.run(argv, capture_output=True)
55+
if succeeded(result):
56+
ok = True
57+
break
58+
59+
if not ok and (until is not None or until_not is not None):
60+
# A content condition was requested but never held within max_attempts. Emit the
61+
# stale output to stderr (never stdout) so callers capturing stdout don't silently
62+
# ingest stale data, and fail loudly with a non-zero exit so the flake is
63+
# attributed here rather than surfacing later as an unexplained output diff.
64+
sys.stderr.buffer.write(result.stdout)
65+
sys.stderr.buffer.write(result.stderr)
66+
sys.stderr.buffer.flush()
67+
print(f"retry: condition not met after {max_attempts} attempts", file=sys.stderr)
68+
sys.exit(result.returncode or 1)
4669

70+
# Success, or no content condition was requested (retries only guarded the exit code).
71+
# Either way callers capture stdout (e.g. DASHBOARD=$(retry --until ...)), so pass the
72+
# final attempt's output and exit code through unchanged.
4773
sys.stdout.buffer.write(result.stdout)
4874
sys.stderr.buffer.write(result.stderr)
4975
sys.exit(result.returncode)

acceptance/bundle/resources/dashboards/test.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,10 @@ MSYS_NO_PATHCONV = "1"
1515
# returns 404, so reads right after deploy must be retried (retry.py). Skipped on
1616
# terraform, whose provider reads the dashboard back after create and fails on the 404.
1717
INJECT_STALE_ON_DIRECT = "1"
18+
19+
# Widen retry.py's window well beyond its ~2s default (5 attempts x 500ms). retry.py's
20+
# loop is not scaled by TimeoutCIMultiplier (that only scales the overall test timeout),
21+
# and real dashboard rename/update eventual consistency on cloud regularly exceeds 2s,
22+
# which flaked these read-after-deploy checks. 20 attempts x 1000ms gives a ~20s window.
23+
RETRY_MAX_ATTEMPTS = "20"
24+
RETRY_INTERVAL_MS = "1000"

0 commit comments

Comments
 (0)