Skip to content

Guard tune_timeout_for_session_needs_pause from stale pause_until#5784

Open
rahim-kanji wants to merge 3 commits into
v3.0from
v3.0_fix-stale-pause-until
Open

Guard tune_timeout_for_session_needs_pause from stale pause_until#5784
rahim-kanji wants to merge 3 commits into
v3.0from
v3.0_fix-stale-pause-until

Conversation

@rahim-kanji

@rahim-kanji rahim-kanji commented May 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

tune_timeout_for_session_needs_pause computed (pause_until - curtime) as unsigned subtraction without checking if pause_until was still in the future. When pause_until <= curtime (stale), the result underflowed, which was then assigned to a signed int poll_timeout, becoming a large negative integer.

The downstream ttw calculation compares this against (unsigned int)pgsql_thread___poll_timeout. Signed-to-unsigned promotion turned the negative value into a huge unsigned, the comparison took the wrong branch, and ttw fell back to the default poll_timeout (2000 ms). The worker thread then blocked in poll() for up to 2 seconds, freezing all sessions on that thread.

Summary by CodeRabbit

  • Bug Fixes
    • Clear session pause state when advancing connection-handshake steps so stale pauses don't carry forward and delay new connections.
    • Preserve existing shorter poll timeouts when applying a future pause: only shorten poll timeout if the pending pause genuinely requires it, preventing expired or irrelevant pauses from corrupting scheduling.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d53bbc22-66ca-4cbd-8e86-907aa9450acb

📥 Commits

Reviewing files that changed from the base of the PR and between 9887933 and ee1793d.

📒 Files selected for processing (2)
  • lib/MySQL_Session.cpp
  • lib/PgSQL_Session.cpp
📜 Recent review details
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{cpp,h,hpp}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{cpp,h,hpp}: Feature tiers are controlled via flags: PROXYSQL31=1 for v3.1.x features (FFTO, TSDB), PROXYSQL40=1 for v4.0.x features (plugin loader). PROXYSQL40=1 implies both PROXYSQL31=1 and PROXYSQLFFTO=1 and PROXYSQLTSDB=1. Use conditional compilation with #ifdef PROXYSQL31, #ifdef PROXYSQL40, #ifdef PROXYSQLFFTO, #ifdef PROXYSQLTSDB, #ifdef PROXYSQLCLICKHOUSE.
Class names use PascalCase with protocol prefixes: MySQL_, PgSQL_, or ProxySQL_ (e.g., MySQL_Protocol, PgSQL_Session).
Member variables use snake_case.
Constants and macros use UPPER_SNAKE_CASE.
Use C++17; conditional compilation for feature tiers via #ifdef PROXYSQL31, #ifdef PROXYSQL40, #ifdef PROXYSQLFFTO, #ifdef PROXYSQLTSDB, #ifdef PROXYSQLCLICKHOUSE.
Use RAII for resource management; use jemalloc for memory allocation.
Use pthread mutexes for synchronization; use std::atomic<> for counters.

Files:

  • lib/PgSQL_Session.cpp
  • lib/MySQL_Session.cpp
{lib,src}/**/*.{cpp,h,hpp}

📄 CodeRabbit inference engine (CLAUDE.md)

GenAI/MCP/RAG/LLM features live entirely in plugins/genai/ and load as a .so at runtime via dlopen. Do not guard with PROXYSQLGENAI in core code — that flag no longer guards any core code as of Step 7 of the GenAI plugin carve-out.

Files:

  • lib/PgSQL_Session.cpp
  • lib/MySQL_Session.cpp
🔇 Additional comments (3)
lib/MySQL_Session.cpp (1)

1866-1874: LGTM!

Also applies to: 2613-2621, 2627-2631, 6393-6404, 6552-6565

lib/PgSQL_Session.cpp (2)

4506-4516: LGTM!

Also applies to: 4548-4559, 4681-4697


6383-6391: LGTM!


📝 Walkthrough

Walkthrough

Three files update pause-until delay handling: Base_Thread.cpp refines poll timeout calculation to guard against expired pauses, while MySQL_Session.cpp and PgSQL_Session.cpp reset pause_until at two checkpoints during server connection transitions to avoid carrying stale backoffs forward.

Changes

Pause delay handling across thread polling and session connections

Layer / File(s) Summary
Poll timeout calculation guard for pause delays
lib/Base_Thread.cpp
Base_Thread::tune_timeout_for_session_needs_pause now checks pause_until > curtime before computing poll_timeout and only updates thr->mypolls.poll_timeout when it's unset (0) or larger than the new value; comments explain avoiding unsigned underflow from expired pauses.
Session pause state resets during connection transitions
lib/MySQL_Session.cpp, lib/PgSQL_Session.cpp
Both session handlers set pause_until = 0 after popping previous_status and again after successful backend connection/setup (before capability handling) in handler_again___status_CONNECTING_SERVER(), preventing stale pause scheduling from persisting across transitions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I nudge the pauses, clear and neat,
Old waits expire — no ticking beat,
Threads wake softly when time is due,
Sessions reset, connections renew,
Hop, hop — the rabbit checks the queue.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly summarizes the main change: guarding tune_timeout_for_session_needs_pause from stale pause_until values, which is the core fix addressed across all modified files.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 v3.0_fix-stale-pause-until

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 and usage tips.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces resets for pause_until during server connection status changes in MySQL and PostgreSQL sessions and refactors the timeout tuning logic in Base_Thread.cpp. Review feedback highlights that the commented-out logic for expired pauses should be enabled to prevent thread blocking. Additionally, the reviewer pointed out a unit mismatch where microsecond values were being assigned to a millisecond-based timeout and provided a code suggestion to correct the calculation and ensure proper polling behavior.

Comment thread lib/Base_Thread.cpp
Comment on lines +351 to 367
if (myds->sess->pause_until > curtime) {
// Future pause: align poll_timeout to the pause expiration.
if (thr->mypolls.poll_timeout == 0 || (myds->sess->pause_until - curtime < thr->mypolls.poll_timeout)) {
thr->mypolls.poll_timeout = myds->sess->pause_until - curtime;
proxy_debug(PROXY_DEBUG_MYSQL_CONNECTION, 7, "Session=%p , poll_timeout=%u , pause_until=%llu , curtime=%llu\n", myds->sess, thr->mypolls.poll_timeout,
myds->sess->pause_until, curtime);
}
}
/* Do we need immediately wake up poll() because of an already expired pause?
else {
// pause_until > 0 (caller checked) but <= curtime: pause has already expired.
// Wake poll() immediately rather than computing (pause_until - curtime)
if (thr->mypolls.poll_timeout == 0 || thr->mypolls.poll_timeout > 1) {
thr->mypolls.poll_timeout = 1;
}
}*/
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The logic for handling expired pauses should be enabled, and a unit mismatch in the timeout calculation needs to be addressed:

  1. Expired Pauses: The commented-out else block should be active. If pause_until <= curtime, the session is ready for processing. By not updating poll_timeout to a small value (like 1ms), the thread may block for the default timeout (2000ms), causing the freeze described in the PR summary.
  2. Unit Mismatch: pause_until - curtime is in microseconds, but poll_timeout is compared downstream against a millisecond value (2000 ms). If poll_timeout is set to microseconds, the comparison timeout < 2000 will fail for any pause longer than 2ms, causing the tuning to be ignored. The difference should be converted to milliseconds.
	if (myds->sess->pause_until > curtime) {
		// Future pause: align poll_timeout to the pause expiration.
		// Convert microseconds to milliseconds (ceiling division)
		unsigned int timeout_ms = (myds->sess->pause_until - curtime + 999) / 1000;
		if (thr->mypolls.poll_timeout == 0 || timeout_ms < (unsigned int)thr->mypolls.poll_timeout) {
			thr->mypolls.poll_timeout = timeout_ms;
			proxy_debug(PROXY_DEBUG_MYSQL_CONNECTION, 7, "Session=%p , poll_timeout=%u , pause_until=%llu , curtime=%llu\n", myds->sess, thr->mypolls.poll_timeout, 
				myds->sess->pause_until, curtime);
		}
	} else {
		// pause_until > 0 (caller checked) but <= curtime: pause has already expired.
		// Wake poll() immediately rather than computing (pause_until - curtime)
		if (thr->mypolls.poll_timeout == 0 || thr->mypolls.poll_timeout > 1) {
			thr->mypolls.poll_timeout = 1;
		}
	}

tune_timeout_for_session_needs_pause computed (pause_until - curtime) as unsigned subtraction without checking if pause_until was still in the future. When pause_until <= curtime (stale), the result underflowed to ~1.8e19, which was then assigned to a signed int poll_timeout, becoming a large negative integer.

The downstream ttw calculation at PgSQL_Thread.cpp:3210 compares this against (unsigned int)pgsql_thread___poll_timeout. Signed-to-unsigned promotion turned the negative value into a huge unsigned, the comparison took the wrong branch, and ttw fell back to the default poll_timeout (2000 ms). The worker thread then blocked in poll() for up to 2 seconds, freezing all sessions on that thread.
@rahim-kanji rahim-kanji force-pushed the v3.0_fix-stale-pause-until branch from 81f4798 to 1430f50 Compare May 13, 2026 10:02
@renecannao renecannao marked this pull request as ready for review May 25, 2026 21:33
@sonarqubecloud

Copy link
Copy Markdown

@renecannao

Copy link
Copy Markdown
Contributor

Stale-pause analysis — the condition is reachable, but low severity

Reviewing whether pause_until <= curtime can actually reach tune_timeout_for_session_needs_pause against current v3.0.

Entry guard (lib/Base_Thread.cpp:611):

if (unlikely(myds->sess->pause_until > 0)) {
    tune_timeout_for_session_needs_pause<T>(myds);
}

So the question reduces to: can a session reach BeforePoll with pause_until > 0 and pause_until <= curtime?

Yes — there is a real lifecycle gap

In MySQL_Session::handler_again___status_CONNECTING_SERVER (lib/MySQL_Session.cpp):

// 2994: connect attempt fails → set a FUTURE pause and return
if (mybe->server_myds->myconn==NULL) {
    pause_until = thread->curtime + mysql_thread___connect_retries_delay*1000;  // future
    *_rc=1;
    return false;
}
...
// 3008: NEXT iteration — connection is now ASYNC_IDLE (succeeded)
if (mybe->server_myds->myconn->async_state_machine==ASYNC_IDLE) {
    st = previous_status.top(); previous_status.pop();
    NEXT_IMMEDIATE_NEW(st);   // == set_status(st); return true;   ← pause_until NOT cleared
}

NEXT_IMMEDIATE_NEW (lib/MySQL_Session.cpp:1814) is set_status(st); return true; — it returns to the caller without re-entering the state machine, so the pause_until set for the retry delay at 2994/3010 is never cleared on the success transition. The mirror path exists in PgSQL_Session.cpp:~1555-1638.

The usual "AfterPoll clears it via the handler" reasoning does not cover this case, because the handler's own terminal transition is precisely what leaves pause_until stale — which is exactly what the pause_until = 0 additions in this PR target.

But the impact is smaller than the PR description claims

The PR's "huge unsigned → poll() frozen" chain is not quite what happens, because mypolls.poll_timeout is declared int (signed) in include/MySQL_Thread.h:601:

  1. (pause_until - curtime) underflows to ~1.8e19 (unsigned long long).
  2. Assigned to int poll_timeout → implementation-defined (typically a large negative on common platforms).
  3. At run_ComputePollTimeout (lib/MySQL_Thread.cpp:3582):
    int ttw = (mypolls.poll_timeout
               ? (mypolls.poll_timeout/1000 < (unsigned int) mysql_thread___poll_timeout
                  ? mypolls.poll_timeout/1000 : mysql_thread___poll_timeout)
               : mysql_thread___poll_timeout);
    The signed poll_timeout/1000 is promoted to unsigned for the comparison → becomes huge → the comparison takes the wrong branch → ttw falls back to the default mysql_thread___poll_timeout (~2000ms).

Real-world effect: a backend connect-retry that succeeds at the retry-delay boundary leaves a stale pause_until, causing the next poll() on that worker to block for up to ~2s instead of waking immediately for the ready session. A transient latency hiccup on one worker thread — recoverable, not a hard hang that "freezes all sessions."

Verdict

  • The bug is real and reachable (concrete path above), so the guard + pause_until = 0 resets are legitimate hardening.
  • But I'd downgrade the severity from the PR's framing: it is a transient up-to-2s stall, not an indefinite freeze.
  • Keeping this PR open as low-priority hardening. No rebase performed in this pass.

@renecannao renecannao mentioned this pull request Jul 8, 2026
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.

2 participants