Skip to content

Add the ability to send app-layer keepalive pings#1553

Merged
rolandwalker merged 1 commit intomainfrom
RW/app-layer-keepalives
Feb 16, 2026
Merged

Add the ability to send app-layer keepalive pings#1553
rolandwalker merged 1 commit intomainfrom
RW/app-layer-keepalives

Conversation

@rolandwalker
Copy link
Contributor

Description

The config option is named with default_ under the [connection] section on the theory that it might one day be configurable by individual connections.

It may be hard to prove when this works/does something useful. But I have the issue of frequent reconnections with some servers, and this feature seems to be provided by MySQL Workbench:

image

Checklist

  • I added this contribution to the changelog.md file.
  • I added my name to the AUTHORS file (or it's already there).
  • To lint and format the code, I ran
    uv run ruff check && uv run ruff format && uv run mypy --install-types .

@rolandwalker rolandwalker self-assigned this Feb 14, 2026
@rolandwalker rolandwalker force-pushed the RW/app-layer-keepalives branch from b5e6ea6 to 3097a8f Compare February 14, 2026 12:55
Copy link
Contributor

@scottnemes scottnemes left a comment

Choose a reason for hiding this comment

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

Works with the basic use case of running queries in the CLI. Can test by setting wait_timeout to something low, waiting that long, then running a query and it will reconnect. But if you enable the keepalive to less than the wait_timeout, then you should not see a reconnect after that time.

@rolandwalker rolandwalker force-pushed the RW/app-layer-keepalives branch from 3097a8f to 39bb577 Compare February 16, 2026 09:22
@github-actions
Copy link

Review summary: Small, useful feature, but there are a couple of correctness/edge-case gaps and no tests around the new behavior.

Findings

  1. Potential crash if ping() raises while idle
    keepalive_hook calls self.sqlexecute.conn.ping(reconnect=False) without guarding against disconnection or transport errors. If the server drops the connection (the exact scenario this targets), this can raise and potentially tear down the prompt loop.
    File: mycli/main.py:992-1009
    Action: wrap the ping in a narrow try/except for driver exceptions (e.g., OperationalError, InterfaceError) and log+ignore, or check self.sqlexecute/conn is present and open before ping.

  2. Keepalive not active during editor loop
    The prompt used in handle_editor_command does not pass the inputhook, so keepalive won’t run while the user is in the edit-then-return loop. This is acknowledged in a comment, but it means the feature is incomplete for a common path.
    File: mycli/main.py:784-789
    Action: pass the same inputhook there (or refactor to a shared prompt helper).

Tests / edge cases

  • No tests for default_keepalive_ticks behavior. Consider at least a small unit test that ensures prompt_app.prompt receives the inputhook when the setting is non-zero, and that ping() is invoked after N ticks (with the hook called in a loop and ping mocked).
    Files: mycli/main.py, test additions in test/.

If you want, I can draft a test with a mocked PromptSession and sqlexecute.conn.

@rolandwalker rolandwalker force-pushed the RW/app-layer-keepalives branch from 39bb577 to 45444e6 Compare February 16, 2026 09:26
@github-actions
Copy link

Findings

  1. Global counter shared across instances: KEEPALIVE_COUNTER is module‑global, so multiple MyCli instances (tests, embedded usage) will share the same counter and ping cadence. This can cause unexpected pings in one instance based on another’s activity. Consider moving the counter to an instance attribute (e.g., self._keepalive_counter) in mycli/main.py.
  2. Counter does not reset on user activity: The counter increments on every inputhook tick but never resets when the user types. This means a ping can fire even while the user is actively typing, defeating the “idle” goal. If prompt_toolkit doesn’t expose a clean input event, you could reset on each prompt call or on context.input_is_ready() transitions. At least document that it’s “time since prompt shown,” not idle time.
  3. Possible attribute/connection state errors: self.sqlexecute.conn may be None or closed when inputhook fires (e.g., not connected yet, after disconnect). This will throw AttributeError or other exceptions not caught. Consider guarding with if getattr(self.sqlexecute, "conn", None) and/or checking connection state before ping. The current except pymysql.OperationalError won’t catch missing/closed connection errors.

Missing tests / edge cases

  1. Add a test for config parsing when default_keepalive_ticks is missing from user config (ensure sane default and no exception).
  2. Add a unit test covering keepalive hook behavior: when disabled (0), no ping; when enabled, ping is attempted and exceptions are handled without crashing; and verify it doesn’t fire if there is no connection.

If you want, I can draft a small unit test and a safe guard for connection state in mycli/main.py.

@rolandwalker rolandwalker force-pushed the RW/app-layer-keepalives branch from 45444e6 to 0b43960 Compare February 16, 2026 09:28
@github-actions
Copy link

Findings (ordered by severity)

  1. UI hang risk during idle ping
    mycli/main.py
    keepalive_hook calls self.sqlexecute.conn.ping(reconnect=False) on the prompt-toolkit inputhook path. That hook runs on the UI event loop; a slow or stalled network can freeze the prompt while idle. Consider making this non-blocking (e.g., short socket timeout, or offload to a background thread/timer) or at least guard with a very small timeout if the driver supports it.

  2. Keepalive not applied to all prompt paths
    mycli/main.py
    There’s an explicit comment: # buglet: this prompt() invocation doesn't have an inputhook for keepalive pings on the prompt used in the reconnect loop. That means keepalive won’t run in that state, which is exactly when idle timeouts may happen. If the feature is intended to cover all idle prompts, wire inputhook into this prompt too.

Missing tests / edge cases

  1. Add a unit test to verify keepalive behavior
    Suggestions: simulate the inputhook being called N times and assert conn.ping is invoked when default_keepalive_ticks > 0, and not invoked when default_keepalive_ticks = 0. This could be a small test with a mocked connection.

  2. Add a test that keepalive doesn’t run when no active connection
    Ensure self.sqlexecute or self.sqlexecute.conn is None doesn’t raise or log excessively; currently it’s caught but not tested.

If you want, I can sketch a minimal test scaffold for this behavior.

The config option is named with "default_" under the [connection]
section on the theory that it might one day be configurable by
individual connections.
@rolandwalker rolandwalker force-pushed the RW/app-layer-keepalives branch from 0b43960 to c03ab3a Compare February 16, 2026 09:32
@github-actions
Copy link

Findings (ordered by severity)

  1. Correctness: Keepalive is not active in the editor re‑prompt loop, so idle time spent re‑editing can still drop the connection even with keepalive enabled. The code even flags this as a “buglet”. Consider passing the same inputhook into this prompt or routing through a shared prompt helper. mycli/main.py:777
  2. Correctness: The keepalive tick counter never resets on actual user input. That means pings can fire while the user is actively typing (not just idle), and the effective interval depends on how frequently inputhook is called. If the intent is “idle only,” reset the counter on any input event or use time-based tracking. mycli/main.py:992

Missing tests / edge cases

  1. No coverage for keepalive enabling/disabling and the ping call threshold (mocking prompt_toolkit inputhook + SQLExecute.conn.ping). A unit test could validate “no ping when default_keepalive_ticks=0” and “ping after N ticks.”

No security concerns found.

If you want, I can sketch a minimal test harness for this without spinning up a real DB connection.

@rolandwalker rolandwalker merged commit c7b4c4b into main Feb 16, 2026
10 checks passed
@rolandwalker rolandwalker deleted the RW/app-layer-keepalives branch February 16, 2026 09:36
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