Skip to content

fix(connection): bound session/channel reconnect retry loops#182

Open
plusky wants to merge 1 commit into
openSUSE:masterfrom
plusky:fix/connection-bound-reconnect-loops
Open

fix(connection): bound session/channel reconnect retry loops#182
plusky wants to merge 1 commit into
openSUSE:masterfrom
plusky:fix/connection-bound-reconnect-loops

Conversation

@plusky

@plusky plusky commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

What

run() and __sftp_reconnect() retried session/channel creation in
unbounded while loops whose only recovery action, reconnect(), is a
no-op while the transport still reports active. A transport that stays
active but keeps refusing new sessions/channels (server MaxSessions
reached, half-broken transport) made both loops spin forever with no
sleep and no attempt cap, pinning a CPU.

Both loops are now bounded: _RECONNECT_MAX_ATTEMPTS (10) attempts with
a 1s backoff, mirroring wait_reconnect; once attempts reach
_RECONNECT_FORCE_AFTER (3) the transport is forcibly torn down and
reconnected even while is_active() is true, so a degraded-but-active
transport gets recycled. A reconnect that fails on the teardown path is
logged and consumes one attempt of the same budget (wait_reconnect's
idiom) instead of escaping as a raw connect error. When the budget is
exhausted, paramiko.SSHException is raised; run()'s docstring
documents the new raise.

Note: this is one of four independent single-concern fixes touching
nearby regions of repose/connection.py in this batch (bounded
reconnect loops, channel close on abort, prompt serialization,
accept-new persistence). They can merge in any order; later ones may
need a trivial rebase, which I'll provide.

Verification

Full gate green (ruff format/check, ty, pytest: 551 passed, coverage
83.92%). The recycle mechanism is mutation-verified: deleting the
force-teardown block, or unwrapping the reconnect try/except, fails the
new tests. Tests pin (a) recovery success — teardown at the force
threshold followed by a working reconnect lets run() complete and
asserts connect() was called, (b) budget consumption when reconnect
keeps failing ends in the cap-exhausted SSHException, not a raw
connect error, and (c) cap exhaustion on both the session and SFTP
loops. Reviewed by a fan-out adversarial code review; all confirmed
findings addressed.

AI-assisted.

run() and __sftp_reconnect() retried session/channel creation in an
unbounded while-loop calling reconnect(), which is a no-op while the
transport reports active. A transport that stays active but keeps
refusing new sessions/channels (server MaxSessions reached, half-broken
transport) made both loops spin with no sleep or attempt cap, pinning a
CPU forever.

Cap both loops at _RECONNECT_MAX_ATTEMPTS with a short backoff (mirroring
wait_reconnect) and force a full transport teardown/reconnect once
attempts reach _RECONNECT_FORCE_AFTER even while is_active() is True, so
a degraded-but-active transport is recycled. A reconnect that fails on
the teardown path is logged and swallowed (wait_reconnect's idiom) so it
consumes one attempt of the same budget instead of escaping as a raw
connect error. Raise SSHException when the cap is exhausted instead of
looping; run()'s docstring documents the new raise.

Tests pin the recycle path (teardown at the force threshold followed by
a successful reconnect lets run() complete), the budget consumption when
reconnect keeps failing, and the cap-exhausted SSHException on both the
session and SFTP loops.
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