v0.6.11: Code Quality Hardening and Async Performance Overhaul#173
Merged
Conversation
- CR-02/08: Fix AsyncSFTPFile out-of-order response race — buffer unclaimed responses in dispatch loop; replace asyncio.shield with direct wait_for + explicit cancel on timeout - CR-03: Add configure_sanitizing_logging() helper to install SanitizingFilter on the root logger (bypasses propagation gap) - CR-04: WarningPolicy.missing_host_key() now stores key on first use (TOFU) - CR-05: Remove unconditional keyboard-interactive auth fallback in AsyncSSHClient._authenticate() - CR-06: put_recursive() only swallows SSH_FX_FAILURE, not all SFTPError - CR-07: Replace assert request.request_id is not None with if/raise - CR-09: Remove premature counter resets from _check_rekey(); counters now reset only after rekey thread succeeds (in _start_kex finally) - CR-10: Add SSH_FXF_APPEND to SFTPClient._mode_to_flags() for "a" mode - CR-11: listdir() try/finally guarantees directory handle closure - CR-12: Fix "ssh_library" → "spindlex" in HostKeyStorage.save() - CR-13: Document decrypt_length() as CTR-mode passthrough hook - CR-14: Narrow ChannelFile.read() exception catch from Exception to ChannelException; remove fragile "Timeout" string matching - CR-15: Add SSHClient.username property; add key_password param to AsyncSSHClient.connect() and thread through _authenticate - CR-16: HostKeyStorage.__init__ now distinguishes FileNotFoundError (silently ignored) from corrupt-file exceptions (warned) - CR-17: _initialize_sftp() already closes channel on failure (no-op) - CR-18: Remove dead `if TYPE_CHECKING: pass` block - CR-19: Replace misleading multi-line Bug comment in _recv_version() - CR-22: Remove all # Bug #N Fixed: inline comments across codebase - CR-23: _expect_message() raises TransportException instead of returning None when transport is inactive - CR-24: Remove S105/S106/S303/S324 from global ruff ignores; add noqa annotation at the one legitimate false-positive site All 1724 unit tests pass, ruff clean.
Five targeted optimisations in the async transport/SFTP stack: 1. Native async packet receive — replaces asyncio.to_thread(super()._read_message) with _recv_packet_async() that reads directly from the asyncio StreamReader, cutting two cross-thread context switches per SSH packet received. 2. Threshold-based write drain — _send_message_async() now calls drain() only when the write buffer exceeds 64 KB instead of after every packet, eliminating ~32 event-loop yields per 1 MB SFTP upload window. 3. Async-safe channel close — AsyncChannel._handle_close() schedules the EOF+CLOSE handshake as a task instead of calling _send_message() synchronously, preventing a deadlock when close is dispatched from the event-loop thread. 4. Doubled SFTP pipeline depth — _WINDOW and _PIPELINE_DEPTH raised from 32 to 64, keeping 2 MB of reads/writes in flight instead of 1 MB. 5. Tuned socket options — TCP_NODELAY disables Nagle coalescing on the async client; SO_SNDBUF/SO_RCVBUF raised to 1 MB each; DEFAULT_WINDOW_SIZE increased to 4 MB for deeper receive buffering. Also extracts _dispatch_packet() from _read_message() in the base Transport so the async path can reuse the same dispatch logic without the thread bridge.
Bump version to 0.6.11 and update changelog covering: - Async throughput overhaul (beats asyncssh on handshake, upload, download) - 23 code-review fixes (CR-02 through CR-24) - Benchmark script improvements
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
- async_channel: fix _handle_close sync fallback to use send_eof() and _close_channel() instead of non-existent _send_eof/_send_close/_close_sent attributes (mypy attr-defined errors) - async_channel: remove _close_sent guard from _async_close_handshake; add nosec B110 to best-effort except block (bandit B110) - async_transport: replace isinstance(msg, HandledMessage) with msg.msg_type != 0 to avoid mypy unreachable-code error; HandledMessage sentinel carries msg_type=0 which is not a valid SSH message type - async_transport: remove unused HandledMessage import and dead _read_single_packet method
adirothbuilds
requested changes
May 12, 2026
adirothbuilds
left a comment
Collaborator
There was a problem hiding this comment.
Fix the 2 comments i left
…d SFTP chunking - feat(async): implement robust retry logic in AsyncSSHClient.connect() covering the entire handshake phase to mitigate transient server-side resets (MaxStartups) - feat(sftp): implement automatic 64KB chunking in SFTPClient and AsyncSFTPClient write() to handle large data buffers and prevent protocol crashes - perf(protocol): increase MAX_MESSAGE_SIZE to 1MB and SFTP_MAX_PACKET_SIZE to 64KB for improved throughput - fix(transport): unify exception propagation in AsyncTransport to preserve specific SSHException subclasses - fix(forwarding): resolve NameError in PortForwarding by moving Channel import out of TYPE_CHECKING - chore: align protocol unit tests with updated message limits and perform project-wide code formatting
- Perform project-wide code formatting using Ruff to align with CI runner expectations - Ensure scripts/ and examples/ directories are correctly formatted - Fix minor indentation issues in AsyncSSHClient.connect() introduced during retry logic implementation
adirothbuilds
approved these changes
May 13, 2026
adirothbuilds
approved these changes
May 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
This release combines two parallel workstreams merged into
release/0.6.11:Type of Change
Performance Results (vs asyncssh 2.22.0, LAN)
SpindleX is also 2–7× faster than paramiko across all categories.
Key Changes
Performance (
async_transport.py,async_channel.py,async_sftp_client.py,async_ssh_client.py,constants.py)_recv_packet_async()reads directly fromasyncio.StreamReader, eliminating two cross-thread context switches per received SSH packet (previously usedasyncio.to_thread+run_coroutine_threadsafe)._send_message_async()only callsdrain()when the write buffer exceeds 64 KB, eliminating ~32 event-loop yields per 1 MiB SFTP upload window._WINDOWand_PIPELINE_DEPTHraised 32 → 64; 2 MiB in flight instead of 1 MiB.DEFAULT_WINDOW_SIZEraised 2 MiB → 4 MiB.TCP_NODELAYenabled;SO_SNDBUF/SO_RCVBUFset to 1 MiB.AsyncChannel._handle_close()schedules EOF+CLOSE as a task to prevent an event-loop deadlock in the native async receive path.Fixes (CR-02 through CR-24)
SSH_FXF_APPENDnow set for"a"open mode)key_passwordparameter added toSSHClient.connect()andAsyncSSHClient.connect()SSHClient.usernameproperty addedChannelFile.read()exception handling narrowed toChannelExceptionput_recursive()only suppressesSSH_FX_FAILURE(not allSFTPError)_expect_message()raisesTransportExceptioninstead of silently returningNoneon closed transport_check_rekey()HostKeyStoragedistinguishesFileNotFoundError(silent) from corrupt-file errors (warned); fixed library name insave()("ssh_library"→"spindlex")WarningPolicynow stores host key on first contact (TOFU)AsyncSSHClient._authenticate()Added
configure_sanitizing_logging()public API for attachingSanitizingFilterto any logger_dispatch_packet()extracted from_read_message()in baseTransportfor shared use by sync and async pathsImproved
# Bug #N Fixed:inline commentsif TYPE_CHECKING: passblockS105/S106/S303/S324from global ruff ignores; added precisenoqaat the one legitimate site_recv_version()How Has This Been Tested?
uv run pytest tests/) — ruff ✅ mypy ✅ bandit ✅pytest -m integrationChecklist