Skip to content

test: make TEST_TAP_TIMEOUT actually fire, and enable it by default - #6011

Merged
renecannao merged 2 commits into
v3.0from
ci/tap-test-timeout
Aug 9, 2026
Merged

test: make TEST_TAP_TIMEOUT actually fire, and enable it by default#6011
renecannao merged 2 commits into
v3.0from
ci/tap-test-timeout

Conversation

@renecannao

@renecannao renecannao commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

TEST_TAP_TIMEOUT could not catch the failure it exists for.

line = fop.stdout.readline()          # blocks
...
if tap_timeout > 0 and (time.time() - start_time) > tap_timeout:

readline() blocks until a full line arrives, so a test that hangs while producing no output never reached the deadline check. Verified directly: with tap_timeout=3 against sleep 300, the old loop was still blocked after 25 s; the new one raises at 3.0 s. That is the profile of the CI-mysql84-g9 stall on #5991.

Fix: select() on a bounded wait + raw os.read() chunking. select() guarantees the deadline check runs while the child is silent; chunking rather than line-reading means a test that stops mid-line can't wedge the loop either. Output order/content unchanged; a trailing partial line is now flushed instead of dropped; decoding uses errors='replace' so a stray non-UTF-8 byte no longer throws.

Verified against 4 cases: normal chatty test (all lines in order), silent hang (fires), partial-line-then-hang (fires), clean exit.

Also flips the default from 0 (disabled) to 1800s. Measured across 47 groups / 401 tests:

test max observed
reg_test_3765_ssl_pollout-t 12.5 min
test_cluster_sync-t 10.5 min
set_testing-240-t 7.8 min
test_auth_methods-t 7.7 min

Only 4 of 401 tests exceed 5 minutes, so 1800s (~2.4× the max) can't fire on a merely slow test.

Complements #6005/#6006: a step timeout bounds cost, but only this identifies which test hung and lets the run continue to its archive/coverage steps.

Summary by CodeRabbit

  • Bug Fixes
    • Added a default 30-minute timeout for test runs, while preserving custom timeout settings.
    • Improved test monitoring so timeouts are enforced even when no output is produced.
    • Improved handling of partial output and clean process completion during testing.

TEST_TAP_TIMEOUT could not catch the failure it exists for. The read loop
was:

    line = fop.stdout.readline()          # blocks
    ...
    if tap_timeout > 0 and (time.time() - start_time) > tap_timeout:

readline() blocks until a full line arrives, so a test that hangs while
producing no output never reached the deadline check at all. Verified
directly: with tap_timeout=3 against 'sleep 300', the old loop was still
blocked after 25 seconds; the new one raises at 3.0s.

That is the exact profile of the CI-mysql84-g9 stall on #5991, where
test_ssl_fast_forward-3_libmariadb-t ran for hours.

Replaces the blocking readline() with select() on a bounded wait plus raw
os.read() chunking. select() guarantees the deadline check runs even when
the child is silent; chunking rather than line-reading means a test that
stops mid-line cannot wedge the loop either. Output order and content are
unchanged, a trailing partial line is now flushed instead of dropped, and
decoding uses errors='replace' so a stray non-UTF-8 byte no longer throws.

Verified against four cases: normal chatty test (all lines, in order),
silent hang (timeout fires), partial-line-then-hang (timeout fires), and
clean exit.

Also flips the default from 0 (disabled) to 1800s. 1800 is ~2.4x the
slowest single test measured across 47 groups:

    reg_test_3765_ssl_pollout-t   12.5 min
    test_cluster_sync-t           10.5 min
    set_testing-240-t              7.8 min
    test_auth_methods-t            7.7 min

Only 4 of 401 tests exceed 5 minutes, so this cannot fire on a merely slow
test, while still stopping a hang well inside the 90-minute step budget
added in the companion CI PRs -- and, unlike a step or job timeout, it
identifies WHICH test hung and lets the run continue to its archive steps.
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@renecannao, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 14 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ebb0ee59-9012-403c-8c56-671bea17593f

📥 Commits

Reviewing files that changed from the base of the PR and between 81c5290 and 6b32b70.

📒 Files selected for processing (1)
  • test/scripts/bin/proxysql-tester.py
📝 Walkthrough

Walkthrough

The test harness now uses a 1,800-second default TAP timeout. The tester reads subprocess output with readiness checks and chunk buffering, so timeouts remain active during silent execution and partial output.

Changes

Test timeout monitoring

Layer / File(s) Summary
Timeout configuration and TAP output loop
test/infra/control/env-isolated.bash, test/scripts/bin/proxysql-tester.py
The harness sets TEST_TAP_TIMEOUT to 1,800 seconds while preserving explicit overrides. proxysql-tester.py uses select() and chunked reads to enforce timeouts, log complete lines, and handle partial trailing output.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ProxySQLTester
  participant select
  participant TAPProcess
  ProxySQLTester->>select: wait for TAP output with timeout
  select->>TAPProcess: check output readiness
  TAPProcess-->>ProxySQLTester: return output chunk or EOF
  ProxySQLTester->>ProxySQLTester: buffer and log complete lines
  ProxySQLTester->>ProxySQLTester: enforce timeout when no output arrives
Loading

Poem

A rabbit checks the TAP stream bright,
With timeout clocks through silent night.
Chunks arrive and lines take form,
Partial tails stay safe from storm.
Eighteen hundred seconds ring—
Tests now hop while watchdogs sing.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main changes: fixing timeout enforcement and enabling TEST_TAP_TIMEOUT by default.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/tap-test-timeout

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/scripts/bin/proxysql-tester.py (1)

869-889: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Preserve the timeout after EOF and reap killed processes.

At lines 888-889, EOF closes stdout, but the test process may still hang. Use fop.wait(timeout=remaining_timeout) instead of unconditional fop.wait() so TEST_TAP_TIMEOUT still applies. After fop.kill(), call fop.wait() before logging fop.returncode; returncode remains None until reap. Also keep the decode('utf-8', 'replace') fallback in the timeout path and avoid blocking for line in fop.stdout; drain only immediately available output instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/scripts/bin/proxysql-tester.py` around lines 869 - 889, Update the TAP
process handling around fop.wait() and the subprocess.TimeoutExpired handler:
preserve the remaining TEST_TAP_TIMEOUT after stdout EOF by using a bounded
wait, reap the process after fop.kill() before inspecting or logging its return
code, and replace the blocking fop.stdout iteration with a non-blocking drain of
immediately available output while retaining decode('utf-8', 'replace').
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@test/scripts/bin/proxysql-tester.py`:
- Around line 869-889: Update the TAP process handling around fop.wait() and the
subprocess.TimeoutExpired handler: preserve the remaining TEST_TAP_TIMEOUT after
stdout EOF by using a bounded wait, reap the process after fop.kill() before
inspecting or logging its return code, and replace the blocking fop.stdout
iteration with a non-blocking drain of immediately available output while
retaining decode('utf-8', 'replace').

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5645c09a-d7a0-42c8-8abd-df1e27ef1ba5

📥 Commits

Reviewing files that changed from the base of the PR and between 2bd3c17 and 81c5290.

📒 Files selected for processing (2)
  • test/infra/control/env-isolated.bash
  • test/scripts/bin/proxysql-tester.py
📜 Review details
🔇 Additional comments (2)
test/infra/control/env-isolated.bash (1)

107-114: LGTM!

test/scripts/bin/proxysql-tester.py (1)

8-8: LGTM!

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 81c52909cb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +863 to +866
wait = 1.0
if tap_timeout > 0:
wait = max(0.0, min(1.0, tap_timeout - (time.time() - start_time)))
ready, _, _ = select.select([fop.stdout], [], [], wait)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reap the timed-out test before using its return code

When this new non-blocking wait reaches the deadline for the intended silent-hang case, the TimeoutExpired handler only calls kill() and drains stdout; it never wait()s or poll()s the child. In Python, Popen.kill() does not populate returncode, so the later rc += abs(int(fop.returncode)) raises TypeError with returncode is None after the first timeout, causing the workdir to be reported as a Python exception and aborting the remaining TAP tests instead of recording a timed-out failure.

Useful? React with 👍 / 👎.

Review catch. The TimeoutExpired handler called kill() and drained stdout
but never wait()ed, so fop.returncode stayed None -- and the shared exit
path below evaluates:

    rc += abs(int(fop.returncode))

which raises TypeError on None. The first timed-out test would therefore
abort the whole group as a Python exception instead of failing one test.

Latent until now: the timeout could never fire while the read loop blocked
in readline(), so this path was unreachable. Enabling the timeout in the
previous commit makes it reachable, so it has to be fixed in the same PR.

wait() after the drain yields -SIGKILL, so abs(int(...)) contributes 9 and
the timed-out test is correctly scored as failed. Verified directly:
returncode None before wait(), -9 after.

Also switches the drain's decode to errors='replace', matching the main
read loop -- a hung test is exactly the case likely to emit a truncated
multi-byte sequence, which would otherwise throw inside the handler.
@sonarqubecloud

sonarqubecloud Bot commented Aug 9, 2026

Copy link
Copy Markdown

@renecannao
renecannao merged commit a7af833 into v3.0 Aug 9, 2026
34 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant